mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] add prevalence expanded section to expandable flyout (#158606)
This commit is contained in:
parent
eaf2d33c59
commit
39c68b52b5
43 changed files with 1486 additions and 457 deletions
|
@ -5,24 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createRule } from '../../../../tasks/api_calls/rules';
|
||||
import { getNewRule } from '../../../../objects/rule';
|
||||
import { DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_BUTTON } from '../../../../screens/expandable_flyout/alert_details_left_panel_prevalence_tab';
|
||||
import {
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_left_panel';
|
||||
import { openPrevalenceTab } from '../../../../tasks/expandable_flyout/alert_details_left_panel_prevalence_tab';
|
||||
import { openInsightsTab } from '../../../../tasks/expandable_flyout/alert_details_left_panel';
|
||||
import { expandDocumentDetailsExpandableFlyoutLeftSection } from '../../../../tasks/expandable_flyout/alert_details_right_panel';
|
||||
import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common';
|
||||
import {
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_left_panel';
|
||||
import {
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_BUTTON,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_left_panel_prevalence_tab';
|
||||
import { cleanKibana } from '../../../../tasks/common';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
import { login, visit } from '../../../../tasks/login';
|
||||
import { createRule } from '../../../../tasks/api_calls/rules';
|
||||
import { getNewRule } from '../../../../objects/rule';
|
||||
import { ALERTS_URL } from '../../../../urls/navigation';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
|
||||
describe(
|
||||
'Expandable flyout left panel prevalence',
|
||||
'Alert details expandable flyout left panel prevalence',
|
||||
{ env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
|
@ -37,7 +46,7 @@ describe(
|
|||
openPrevalenceTab();
|
||||
});
|
||||
|
||||
it('should show prevalence table', () => {
|
||||
it('should display prevalence tab', () => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Insights');
|
||||
|
@ -48,7 +57,29 @@ describe(
|
|||
.should('be.visible')
|
||||
.and('have.text', 'Prevalence');
|
||||
|
||||
// TODO actual test
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE).should('be.visible');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL)
|
||||
.should('contain.text', 'host.name')
|
||||
.and('contain.text', 'user.name');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL)
|
||||
.should('contain.text', 'siem-kibana')
|
||||
.and('contain.text', 'test');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL).should(
|
||||
'contain.text',
|
||||
2
|
||||
);
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL).should(
|
||||
'contain.text',
|
||||
2
|
||||
);
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL).should(
|
||||
'contain.text',
|
||||
100
|
||||
);
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL).should(
|
||||
'contain.text',
|
||||
100
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,9 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
} from '../../../public/flyout/left/components/test_ids';
|
||||
import { getDataTestSubjectSelector } from '../../helpers/common';
|
||||
import { INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID } from '../../../public/flyout/left/tabs/test_ids';
|
||||
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_BUTTON = getDataTestSubjectSelector(
|
||||
INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE = getDataTestSubjectSelector(
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL =
|
||||
getDataTestSubjectSelector(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID);
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Story } from '@storybook/react';
|
||||
import { Provider as ReduxStoreProvider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { sourcererReducer } from '../../../common/store/sourcerer';
|
||||
import { inputsReducer } from '../../../common/store/inputs';
|
||||
import type { LeftPanelContext } from '../context';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
import { AnalyzeGraph } from './analyze_graph';
|
||||
|
||||
export default {
|
||||
component: AnalyzeGraph,
|
||||
title: 'Flyout/AnalyzeGraph',
|
||||
};
|
||||
|
||||
// TODO to get this working, we need to spent some time getting all the foundation items for storybook
|
||||
// (ReduxStoreProvider, CellActionsProvider...) similarly to how it was done for the TestProvidersComponent
|
||||
// see ticket https://github.com/elastic/security-team/issues/6223
|
||||
// export const Default: Story<void> = () => {
|
||||
// const contextValue = {
|
||||
// eventId: 'some_id',
|
||||
// } as unknown as LeftPanelContext;
|
||||
//
|
||||
// return (
|
||||
// <LeftFlyoutContext.Provider value={contextValue}>
|
||||
// <AnalyzeGraph />
|
||||
// </LeftFlyoutContext.Provider>
|
||||
// );
|
||||
// };
|
||||
|
||||
export const Error: Story<void> = () => {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
inputs: inputsReducer,
|
||||
sourcerer: sourcererReducer,
|
||||
},
|
||||
});
|
||||
const services = {
|
||||
data: {},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addError: () => {},
|
||||
addSuccess: () => {},
|
||||
addWarning: () => {},
|
||||
remove: () => {},
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext({ ...services });
|
||||
|
||||
const contextValue = {
|
||||
eventId: null,
|
||||
} as unknown as LeftPanelContext;
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<ReduxStoreProvider store={store}>
|
||||
<KibanaReactContext.Provider>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<AnalyzeGraph />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</KibanaReactContext.Provider>
|
||||
</ReduxStoreProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
|
@ -8,8 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import type { LeftPanelContext } from '../context';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { AnalyzeGraph } from './analyze_graph';
|
||||
import { ANALYZE_GRAPH_ERROR_TEST_ID, ANALYZER_GRAPH_TEST_ID } from './test_ids';
|
||||
|
@ -39,9 +38,9 @@ describe('<AnalyzeGraph />', () => {
|
|||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<AnalyzeGraph />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId(ANALYZER_GRAPH_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -54,9 +53,9 @@ describe('<AnalyzeGraph />', () => {
|
|||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<AnalyzeGraph />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId(ANALYZE_GRAPH_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { EntitiesDetails } from './entities_details';
|
||||
import { ENTITIES_DETAILS_TEST_ID, HOST_DETAILS_TEST_ID, USER_DETAILS_TEST_ID } from './test_ids';
|
||||
|
@ -35,9 +35,9 @@ describe('<EntitiesDetails />', () => {
|
|||
it('renders entities details correctly', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={mockContextValue}>
|
||||
<LeftPanelContext.Provider value={mockContextValue}>
|
||||
<EntitiesDetails />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId(ENTITIES_DETAILS_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -48,7 +48,7 @@ describe('<EntitiesDetails />', () => {
|
|||
it('does not render user and host details if user name and host name are not available', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider
|
||||
<LeftPanelContext.Provider
|
||||
value={{
|
||||
...mockContextValue,
|
||||
getFieldsData: (fieldName) =>
|
||||
|
@ -56,7 +56,7 @@ describe('<EntitiesDetails />', () => {
|
|||
}}
|
||||
>
|
||||
<EntitiesDetails />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId(USER_DETAILS_TEST_ID)).not.toBeInTheDocument();
|
||||
|
@ -66,7 +66,7 @@ describe('<EntitiesDetails />', () => {
|
|||
it('does not render user and host details if @timestamp is not available', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider
|
||||
<LeftPanelContext.Provider
|
||||
value={{
|
||||
...mockContextValue,
|
||||
getFieldsData: (fieldName) => {
|
||||
|
@ -82,7 +82,7 @@ describe('<EntitiesDetails />', () => {
|
|||
}}
|
||||
>
|
||||
<EntitiesDetails />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId(USER_DETAILS_TEST_ID)).not.toBeInTheDocument();
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { PrevalenceDetails } from './prevalence_details';
|
||||
import {
|
||||
PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useFetchFieldValuePairByEventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('../../../common/components/event_details/get_alert_summary_rows');
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_by_event_type');
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
|
||||
const panelContextValue = {
|
||||
eventId: 'event id',
|
||||
indexName: 'indexName',
|
||||
browserFields: {},
|
||||
dataFormattedForFieldBrowser: [],
|
||||
} as unknown as LeftPanelContext;
|
||||
|
||||
describe('PrevalenceDetails', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
it('should render the table', () => {
|
||||
const mockSummaryRow = {
|
||||
title: 'test',
|
||||
description: {
|
||||
data: {
|
||||
field: 'field',
|
||||
},
|
||||
values: ['value'],
|
||||
},
|
||||
};
|
||||
(getSummaryRows as jest.Mock).mockReturnValue([mockSummaryRow]);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={panelContextValue}>
|
||||
<PrevalenceDetails />
|
||||
</LeftPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the error message if no highlighted fields', () => {
|
||||
jest.mocked(getSummaryRows).mockReturnValue([]);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LeftPanelContext.Provider value={panelContextValue}>
|
||||
<PrevalenceDetails />
|
||||
</LeftPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -5,17 +5,166 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { PREVALENCE_DETAILS_TEST_ID } from './test_ids';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations';
|
||||
import {
|
||||
PREVALENCE_ERROR_MESSAGE,
|
||||
PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_HOST_PREVALENCE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_NAME_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_TYPE_COLUMN_TITLE,
|
||||
PREVALENCE_TABLE_USER_PREVALENCE_COLUMN_TITLE,
|
||||
} from './translations';
|
||||
import {
|
||||
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID,
|
||||
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { PrevalenceDetailsCountCell } from './prevalence_details_count_cell';
|
||||
import type { AlertSummaryRow } from '../../../common/components/event_details/helpers';
|
||||
import { PrevalenceDetailsPrevalenceCell } from './prevalence_details_prevalence_cell';
|
||||
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
import { useLeftPanelContext } from '../context';
|
||||
import { EventKind } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
|
||||
interface PrevalenceDetailsTableCell {
|
||||
highlightedField: { name: string; values: string[] };
|
||||
scopeId: string;
|
||||
}
|
||||
|
||||
export const PREVALENCE_TAB_ID = 'prevalence-details';
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<unknown>> = [
|
||||
{
|
||||
field: 'type',
|
||||
name: PREVALENCE_TABLE_TYPE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: PREVALENCE_TABLE_NAME_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID,
|
||||
},
|
||||
{
|
||||
field: 'alertCount',
|
||||
name: PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={data.highlightedField}
|
||||
scopeId={data.scopeId}
|
||||
type={{
|
||||
eventKind: EventKind.signal,
|
||||
include: true,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'docCount',
|
||||
name: PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={data.highlightedField}
|
||||
scopeId={data.scopeId}
|
||||
type={{
|
||||
eventKind: EventKind.signal,
|
||||
exclude: true,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'hostPrevalence',
|
||||
name: PREVALENCE_TABLE_HOST_PREVALENCE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={data.highlightedField}
|
||||
scopeId={data.scopeId}
|
||||
aggregationField={'host.name'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'userPrevalence',
|
||||
name: PREVALENCE_TABLE_USER_PREVALENCE_COLUMN_TITLE,
|
||||
'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
|
||||
render: (data: PrevalenceDetailsTableCell) => (
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={data.highlightedField}
|
||||
scopeId={data.scopeId}
|
||||
aggregationField={'user.name'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Prevalence displayed in the document details expandable flyout left section under the Insights tab
|
||||
* Prevalence table displayed in the document details expandable flyout left section under the Insights tab
|
||||
*/
|
||||
export const PrevalenceDetails: React.FC = () => {
|
||||
return <EuiText data-test-subj={PREVALENCE_DETAILS_TEST_ID}>{'Prevalence'}</EuiText>;
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = useLeftPanelContext();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const summaryRows = getSummaryRows({
|
||||
browserFields: browserFields || {},
|
||||
data: dataFormattedForFieldBrowser || [],
|
||||
eventId,
|
||||
scopeId,
|
||||
isReadOnly: false,
|
||||
});
|
||||
|
||||
const getCellRenderFields = (summaryRow: AlertSummaryRow): PrevalenceDetailsTableCell => ({
|
||||
highlightedField: {
|
||||
name: summaryRow.description.data.field,
|
||||
values: summaryRow.description.values || [],
|
||||
},
|
||||
scopeId,
|
||||
});
|
||||
|
||||
return (summaryRows || []).map((summaryRow) => {
|
||||
const fields = getCellRenderFields(summaryRow);
|
||||
return {
|
||||
type: summaryRow.description.data.field,
|
||||
name: summaryRow.description.values,
|
||||
alertCount: fields,
|
||||
docCount: fields,
|
||||
hostPrevalence: fields,
|
||||
userPrevalence: fields,
|
||||
};
|
||||
});
|
||||
}, [browserFields, dataFormattedForFieldBrowser, eventId, scopeId]);
|
||||
|
||||
if (!eventId || !dataFormattedForFieldBrowser || !browserFields || !data || data.length === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="error"
|
||||
color="danger"
|
||||
title={<h2>{ERROR_TITLE(PREVALENCE_ERROR_MESSAGE)}</h2>}
|
||||
body={<p>{ERROR_MESSAGE(PREVALENCE_ERROR_MESSAGE)}</p>}
|
||||
data-test-subj={PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
tableCaption=""
|
||||
items={data}
|
||||
columns={columns}
|
||||
data-test-subj={PREVALENCE_DETAILS_TABLE_TEST_ID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PrevalenceDetails.displayName = 'PrevalenceDetails';
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EventKind,
|
||||
useFetchFieldValuePairByEventType,
|
||||
} from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import {
|
||||
PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { PrevalenceDetailsCountCell } from './prevalence_details_count_cell';
|
||||
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_by_event_type');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const scopeId = 'scopeId';
|
||||
const type = {
|
||||
eventKind: EventKind.signal,
|
||||
include: true,
|
||||
};
|
||||
|
||||
describe('PrevalenceDetailsAlertCountCell', () => {
|
||||
it('should show loading spinner', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return error icon', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show count value with eventKind undefined', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toHaveTextContent('1');
|
||||
});
|
||||
|
||||
it('should show count value with eventKind passed via props', () => {
|
||||
jest.mocked(useFetchFieldValuePairByEventType).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsCountCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toHaveTextContent('1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { VFC } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import {
|
||||
PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import type { EventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import { useFetchFieldValuePairByEventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
|
||||
export interface PrevalenceDetailsCountCellProps {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* The scope id
|
||||
*/
|
||||
scopeId: string;
|
||||
/**
|
||||
* Limit the search to include or exclude a specific value for the event.kind field
|
||||
* (alert, asset, enrichment, event, metric, state, pipeline_error, signal)
|
||||
*/
|
||||
type: EventType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component displaying a value in many PrevalenceDetails table cells. It is used for the third and fourth columns,
|
||||
* which display the number of alerts and documents for a given field/value pair.
|
||||
* For the doc columns, type should "signal" for its eventKind property, and exclude set to true.
|
||||
* For the alert columns, type should have "signal" for its eventKind property, and include should be true.
|
||||
*/
|
||||
export const PrevalenceDetailsCountCell: VFC<PrevalenceDetailsCountCellProps> = ({
|
||||
highlightedField,
|
||||
scopeId,
|
||||
type,
|
||||
}) => {
|
||||
const { loading, error, count } = useFetchFieldValuePairByEventType({
|
||||
highlightedField,
|
||||
isActiveTimelines: scopeId === TimelineId.active,
|
||||
type,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <EuiLoadingSpinner data-test-subj={PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EuiIcon
|
||||
data-test-subj={PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID}
|
||||
type="error"
|
||||
color="danger"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div data-test-subj={PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID}>{count}</div>;
|
||||
};
|
||||
|
||||
PrevalenceDetailsCountCell.displayName = 'PrevalenceDetailsCountCell';
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { PrevalenceDetailsPrevalenceCell } from './prevalence_details_prevalence_cell';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const scopeId = 'scopeId';
|
||||
const aggregationField = 'aggregationField';
|
||||
|
||||
describe('PrevalenceDetailsAlertCountCell', () => {
|
||||
it('should show loading spinner when useFetchFieldValuePairWithAggregation loading', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show loading spinner when useFetchUniqueByField loading', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if useFetchFieldValuePairWithAggregation errors out', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if useFetchUniqueByField errors out', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null if prevalence is infinite', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show prevalence value', () => {
|
||||
jest.mocked(useFetchFieldValuePairWithAggregation).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
jest.mocked(useFetchUniqueByField).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceDetailsPrevalenceCell
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
aggregationField={aggregationField}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID)).toHaveTextContent('1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { VFC } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import {
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID,
|
||||
PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
export interface PrevalenceDetailsPrevalenceCellProps {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* The scope id
|
||||
*/
|
||||
scopeId: string;
|
||||
/**
|
||||
* The aggregation field
|
||||
*/
|
||||
aggregationField: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component displaying a value in many PrevalenceDetails table cells. It is used for the fifth and sixth columns,
|
||||
* which displays the prevalence percentage for host.name and user.name fields.
|
||||
*/
|
||||
export const PrevalenceDetailsPrevalenceCell: VFC<PrevalenceDetailsPrevalenceCellProps> = ({
|
||||
highlightedField,
|
||||
scopeId,
|
||||
aggregationField,
|
||||
}) => {
|
||||
const {
|
||||
loading: aggregationLoading,
|
||||
error: aggregationError,
|
||||
count: aggregationCount,
|
||||
} = useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
isActiveTimelines: scopeId === TimelineId.active,
|
||||
aggregationField,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: uniqueLoading,
|
||||
error: uniqueError,
|
||||
count: uniqueCount,
|
||||
} = useFetchUniqueByField({ field: aggregationField });
|
||||
|
||||
if (aggregationLoading || uniqueLoading) {
|
||||
return (
|
||||
<EuiLoadingSpinner data-test-subj={PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID} />
|
||||
);
|
||||
}
|
||||
|
||||
const prevalence = aggregationCount / uniqueCount;
|
||||
if (aggregationError || uniqueError || !isFinite(prevalence)) {
|
||||
return (
|
||||
<EuiIcon
|
||||
data-test-subj={PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID}
|
||||
type="error"
|
||||
color="danger"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj={PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID}>
|
||||
{Math.round(prevalence * 100)}
|
||||
{'%'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PrevalenceDetailsPrevalenceCell.displayName = 'PrevalenceDetailsPrevalenceCell';
|
|
@ -8,8 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import type { LeftPanelContext } from '../context';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { rawEventData, TestProviders } from '../../../common/mock';
|
||||
import { RESPONSE_DETAILS_TEST_ID, RESPONSE_EMPTY_TEST_ID } from './test_ids';
|
||||
import { ResponseDetails } from './response_details';
|
||||
|
@ -56,12 +55,12 @@ const defaultContextValue = {
|
|||
dataAsNestedObject: {
|
||||
_id: 'test',
|
||||
},
|
||||
data: rawEventData,
|
||||
searchHit: rawEventData,
|
||||
} as unknown as LeftPanelContext;
|
||||
|
||||
const contextWithResponseActions = {
|
||||
...defaultContextValue,
|
||||
data: {
|
||||
searchHit: {
|
||||
...rawEventData,
|
||||
fields: {
|
||||
...rawEventData.fields,
|
||||
|
@ -80,9 +79,9 @@ const contextWithResponseActions = {
|
|||
const renderSUT = (contextValue: LeftPanelContext) =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<ResponseDetails />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -36,23 +36,23 @@ const InlineBlock = styled.div`
|
|||
* Automated response actions results, displayed in the document details expandable flyout left section under the Insights tab, Response tab
|
||||
*/
|
||||
export const ResponseDetails: React.FC = () => {
|
||||
const { data, dataAsNestedObject } = useLeftPanelContext();
|
||||
const { searchHit, dataAsNestedObject } = useLeftPanelContext();
|
||||
const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled(
|
||||
'endpointResponseActionsEnabled'
|
||||
);
|
||||
const expandedEventFieldsObject = data
|
||||
? (expandDottedObject((data as RawEventData).fields) as ExpandedEventFieldsObject)
|
||||
const expandedEventFieldsObject = searchHit
|
||||
? (expandDottedObject((searchHit as RawEventData).fields) as ExpandedEventFieldsObject)
|
||||
: undefined;
|
||||
|
||||
const responseActions =
|
||||
expandedEventFieldsObject?.kibana?.alert?.rule?.parameters?.[0].response_actions;
|
||||
|
||||
const responseActionsView = useResponseActionsView({
|
||||
rawEventData: data,
|
||||
rawEventData: searchHit,
|
||||
ecsData: dataAsNestedObject,
|
||||
});
|
||||
const osqueryView = useOsqueryTab({
|
||||
rawEventData: data,
|
||||
rawEventData: searchHit,
|
||||
ecsData: dataAsNestedObject,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Story } from '@storybook/react';
|
||||
import { SessionView } from './session_view';
|
||||
import type { LeftPanelContext } from '../context';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
|
||||
export default {
|
||||
component: SessionView,
|
||||
title: 'Flyout/SessionView',
|
||||
};
|
||||
|
||||
// TODO to get this working, we need to spent some time getting all the foundation items for storybook
|
||||
// (ReduxStoreProvider, CellActionsProvider...) similarly to how it was done for the TestProvidersComponent
|
||||
// see ticket https://github.com/elastic/security-team/issues/6223
|
||||
// export const Default: Story<void> = () => {
|
||||
// const contextValue = {
|
||||
// getFieldsData: () => {},
|
||||
// } as unknown as LeftPanelContext;
|
||||
//
|
||||
// return (
|
||||
// <LeftFlyoutContext.Provider value={contextValue}>
|
||||
// <SessionView />
|
||||
// </LeftFlyoutContext.Provider>
|
||||
// );
|
||||
// };
|
||||
|
||||
export const Error: Story<void> = () => {
|
||||
const contextValue = {
|
||||
getFieldsData: () => {},
|
||||
} as unknown as LeftPanelContext;
|
||||
|
||||
return (
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<SessionView />
|
||||
</LeftFlyoutContext.Provider>
|
||||
);
|
||||
};
|
|
@ -8,8 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import type { LeftPanelContext } from '../context';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids';
|
||||
import {
|
||||
|
@ -56,9 +55,9 @@ describe('<SessionView />', () => {
|
|||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<SessionView />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId(SESSION_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -72,9 +71,9 @@ describe('<SessionView />', () => {
|
|||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<SessionView />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId(SESSION_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -87,9 +86,9 @@ describe('<SessionView />', () => {
|
|||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<SessionView />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId(SESSION_VIEW_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
|
|
|
@ -15,6 +15,35 @@ export const SESSION_VIEW_ERROR_TEST_ID = `${PREFIX}SessionViewError` as const;
|
|||
|
||||
/* Insights tab */
|
||||
|
||||
/* Prevalence */
|
||||
|
||||
export const PREVALENCE_DETAILS_TABLE_TEST_ID = `${PREFIX}PrevalenceDetailsTable` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_TYPE_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableTypeCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_NAME_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableNameCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableAlertCountCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableDocCountCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableHostPrevalenceCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsTableUserPrevalenceCell` as const;
|
||||
export const PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID = `${PREFIX}PrevalenceDetailsTable` as const;
|
||||
export const PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsCountCellLoading` as const;
|
||||
export const PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsCountCellError` as const;
|
||||
export const PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsCountCellValue` as const;
|
||||
export const PREVALENCE_DETAILS_PREVALENCE_CELL_LOADING_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsPrevalenceCellLoading` as const;
|
||||
export const PREVALENCE_DETAILS_PREVALENCE_CELL_ERROR_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsPrevalenceCellError` as const;
|
||||
export const PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID =
|
||||
`${PREFIX}PrevalenceDetailsPrevalenceCellValue` as const;
|
||||
|
||||
/* Entities */
|
||||
export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const;
|
||||
export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const;
|
||||
|
@ -26,6 +55,7 @@ export const HOST_DETAILS_INFO_TEST_ID = 'host-overview';
|
|||
export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID =
|
||||
`${PREFIX}HostsDetailsRelatedUsersTable` as const;
|
||||
|
||||
export const THREAT_INTELLIGENCE_DETAILS_TEST_ID = `${PREFIX}ThreatIntelligenceDetails` as const;
|
||||
export const PREVALENCE_DETAILS_TEST_ID = `${PREFIX}PrevalenceDetails` as const;
|
||||
export const CORRELATIONS_DETAILS_TEST_ID = `${PREFIX}CorrelationsDetails` as const;
|
||||
|
||||
|
|
|
@ -8,15 +8,14 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import type { LeftPanelContext } from '../context';
|
||||
import { LeftFlyoutContext } from '../context';
|
||||
import { LeftPanelContext } from '../context';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import {
|
||||
THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID,
|
||||
THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { ThreatIntelligenceDetails } from './threat_intelligence_details';
|
||||
import { useThreatIntelligenceDetails } from './hooks/use_threat_intelligence_details';
|
||||
import { useThreatIntelligenceDetails } from '../hooks/use_threat_intelligence_details';
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../common/lib/kibana');
|
||||
|
@ -32,7 +31,7 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('./hooks/use_threat_intelligence_details');
|
||||
jest.mock('../hooks/use_threat_intelligence_details');
|
||||
|
||||
const defaultContextValue = {
|
||||
getFieldsData: () => 'id',
|
||||
|
@ -42,9 +41,9 @@ const defaultContextValue = {
|
|||
const renderSUT = (contextValue: LeftPanelContext) =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<LeftFlyoutContext.Provider value={contextValue}>
|
||||
<LeftPanelContext.Provider value={contextValue}>
|
||||
<ThreatIntelligenceDetails />
|
||||
</LeftFlyoutContext.Provider>
|
||||
</LeftPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elasti
|
|||
import isEmpty from 'lodash/isEmpty';
|
||||
import { EnrichmentRangePicker } from '../../../common/components/event_details/cti_details/enrichment_range_picker';
|
||||
import { ThreatDetailsView } from '../../../common/components/event_details/cti_details/threat_details_view';
|
||||
import { useThreatIntelligenceDetails } from './hooks/use_threat_intelligence_details';
|
||||
import { useThreatIntelligenceDetails } from '../hooks/use_threat_intelligence_details';
|
||||
import { THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID } from './test_ids';
|
||||
|
||||
export const THREAT_INTELLIGENCE_TAB_ID = 'threat-intelligence-details';
|
||||
|
|
|
@ -85,6 +85,55 @@ export const RELATED_USERS_TOOL_TIP = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceErrorMessage',
|
||||
{
|
||||
defaultMessage: 'prevalence',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_TYPE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableTypeColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Type',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_NAME_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableNameColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_ALERT_COUNT_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Alert count',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_DOC_COUNT_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Doc count',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_HOST_PREVALENCE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableHostPrevalenceColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Host prevalence',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TABLE_USER_PREVALENCE_COLUMN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.prevalenceTableUserPrevalenceColumnTitle',
|
||||
{
|
||||
defaultMessage: 'User prevalence',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESPONSE_TITLE = i18n.translate('xpack.securitySolution.flyout.response.title', {
|
||||
defaultMessage: 'Responses',
|
||||
});
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { css } from '@emotion/react';
|
||||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import type { Ecs } from '@kbn/cases-plugin/common';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import type { SearchHit } from '../../../common/search_strategy';
|
||||
import type { LeftPanelProps } from '.';
|
||||
import type { GetFieldsData } from '../../common/hooks/use_get_fields_data';
|
||||
import { useGetFieldsData } from '../../common/hooks/use_get_fields_data';
|
||||
import { useTimelineEventsDetails } from '../../timelines/containers/details';
|
||||
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
|
||||
|
@ -31,20 +32,32 @@ export interface LeftPanelContext {
|
|||
*/
|
||||
indexName: string;
|
||||
/**
|
||||
* Retrieves searchHit values for the provided field
|
||||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
getFieldsData: (field: string) => unknown | unknown[];
|
||||
scopeId: string;
|
||||
/**
|
||||
* An object containing fields by type
|
||||
*/
|
||||
browserFields: BrowserFields | null;
|
||||
/**
|
||||
* An object with top level fields from the ECS object
|
||||
*/
|
||||
dataAsNestedObject: Ecs | null;
|
||||
/**
|
||||
* An array of field objects with category and value
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
|
||||
|
||||
data: SearchHit | undefined;
|
||||
|
||||
dataAsNestedObject: Ecs | null;
|
||||
/**
|
||||
* The actual raw document object
|
||||
*/
|
||||
searchHit: SearchHit | undefined;
|
||||
/**
|
||||
* Retrieves searchHit values for the provided field
|
||||
*/
|
||||
getFieldsData: GetFieldsData;
|
||||
}
|
||||
|
||||
export const LeftFlyoutContext = createContext<LeftPanelContext | undefined>(undefined);
|
||||
export const LeftPanelContext = createContext<LeftPanelContext | undefined>(undefined);
|
||||
|
||||
export type LeftPanelProviderProps = {
|
||||
/**
|
||||
|
@ -53,7 +66,7 @@ export type LeftPanelProviderProps = {
|
|||
children: React.ReactNode;
|
||||
} & Partial<LeftPanelProps['params']>;
|
||||
|
||||
export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProviderProps) => {
|
||||
export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPanelProviderProps) => {
|
||||
const currentSpaceId = useSpaceId();
|
||||
const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : '';
|
||||
const [{ pageName }] = useRouteSpy();
|
||||
|
@ -73,17 +86,28 @@ export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProvider
|
|||
|
||||
const contextValue = useMemo(
|
||||
() =>
|
||||
id && indexName
|
||||
id && indexName && scopeId
|
||||
? {
|
||||
eventId: id,
|
||||
indexName,
|
||||
getFieldsData,
|
||||
data: searchHit,
|
||||
dataFormattedForFieldBrowser,
|
||||
scopeId,
|
||||
browserFields: sourcererDataView.browserFields,
|
||||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit,
|
||||
getFieldsData,
|
||||
}
|
||||
: undefined,
|
||||
[id, indexName, getFieldsData, searchHit, dataFormattedForFieldBrowser, dataAsNestedObject]
|
||||
[
|
||||
id,
|
||||
indexName,
|
||||
scopeId,
|
||||
sourcererDataView.browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
getFieldsData,
|
||||
dataAsNestedObject,
|
||||
searchHit,
|
||||
]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
|
@ -99,11 +123,11 @@ export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProvider
|
|||
);
|
||||
}
|
||||
|
||||
return <LeftFlyoutContext.Provider value={contextValue}>{children}</LeftFlyoutContext.Provider>;
|
||||
return <LeftPanelContext.Provider value={contextValue}>{children}</LeftPanelContext.Provider>;
|
||||
};
|
||||
|
||||
export const useLeftPanelContext = () => {
|
||||
const contextValue = useContext(LeftFlyoutContext);
|
||||
const contextValue = useContext(LeftPanelContext);
|
||||
|
||||
if (!contextValue) {
|
||||
throw new Error('LeftPanelContext can only be used within LeftPanelContext provider');
|
||||
|
|
|
@ -8,24 +8,24 @@
|
|||
import { useThreatIntelligenceDetails } from './use_threat_intelligence_details';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useTimelineEventsDetails } from '../../../../timelines/containers/details';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
|
||||
import { useLeftPanelContext } from '../../context';
|
||||
import { useInvestigationTimeEnrichment } from '../../../../common/containers/cti/event_enrichment';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
import type { RouteSpyState } from '../../../../common/utils/route/types';
|
||||
import { useTimelineEventsDetails } from '../../../timelines/containers/details';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useRouteSpy } from '../../../common/utils/route/use_route_spy';
|
||||
import { useLeftPanelContext } from '../context';
|
||||
import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment';
|
||||
import { SecurityPageName } from '../../../../common/constants';
|
||||
import type { RouteSpyState } from '../../../common/utils/route/types';
|
||||
import {
|
||||
type GetBasicDataFromDetailsData,
|
||||
useBasicDataFromDetailsData,
|
||||
} from '../../../../timelines/components/side_panel/event_details/helpers';
|
||||
} from '../../../timelines/components/side_panel/event_details/helpers';
|
||||
|
||||
jest.mock('../../../../timelines/containers/details');
|
||||
jest.mock('../../../../common/containers/sourcerer');
|
||||
jest.mock('../../../../common/utils/route/use_route_spy');
|
||||
jest.mock('../../context');
|
||||
jest.mock('../../../../common/containers/cti/event_enrichment');
|
||||
jest.mock('../../../../timelines/components/side_panel/event_details/helpers');
|
||||
jest.mock('../../../timelines/containers/details');
|
||||
jest.mock('../../../common/containers/sourcerer');
|
||||
jest.mock('../../../common/utils/route/use_route_spy');
|
||||
jest.mock('../context');
|
||||
jest.mock('../../../common/containers/cti/event_enrichment');
|
||||
jest.mock('../../../timelines/components/side_panel/event_details/helpers');
|
||||
|
||||
describe('useThreatIntelligenceDetails', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -66,9 +66,11 @@ describe('useThreatIntelligenceDetails', () => {
|
|||
jest.mocked(useLeftPanelContext).mockReturnValue({
|
||||
indexName: 'test-index',
|
||||
eventId: 'test-event-id',
|
||||
getFieldsData: () => {},
|
||||
getFieldsData: () => null,
|
||||
dataFormattedForFieldBrowser: null,
|
||||
data: {
|
||||
scopeId: 'test-scope-id',
|
||||
browserFields: null,
|
||||
searchHit: {
|
||||
_id: 'testId',
|
||||
_index: 'testIndex',
|
||||
},
|
|
@ -6,22 +6,22 @@
|
|||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { CtiEnrichment, EventFields } from '../../../../../common/search_strategy';
|
||||
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
|
||||
import type { CtiEnrichment, EventFields } from '../../../../common/search_strategy';
|
||||
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
|
||||
import {
|
||||
filterDuplicateEnrichments,
|
||||
getEnrichmentFields,
|
||||
parseExistingEnrichments,
|
||||
timelineDataToEnrichment,
|
||||
} from '../../../../common/components/event_details/cti_details/helpers';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
} from '../../../common/components/event_details/cti_details/helpers';
|
||||
import { SecurityPageName } from '../../../../common/constants';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
|
||||
import { useInvestigationTimeEnrichment } from '../../../../common/containers/cti/event_enrichment';
|
||||
import { useTimelineEventsDetails } from '../../../../timelines/containers/details';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
|
||||
import { useLeftPanelContext } from '../../context';
|
||||
import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment';
|
||||
import { useTimelineEventsDetails } from '../../../timelines/containers/details';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useRouteSpy } from '../../../common/utils/route/use_route_spy';
|
||||
import { useLeftPanelContext } from '../context';
|
||||
|
||||
export interface ThreatIntelligenceDetailsValue {
|
||||
enrichments: CtiEnrichment[];
|
|
@ -29,12 +29,13 @@ export interface LeftPanelProps extends FlyoutPanel {
|
|||
params?: {
|
||||
id: string;
|
||||
indexName: string;
|
||||
scopeId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
const { eventId, indexName } = useLeftPanelContext();
|
||||
const { eventId, indexName, scopeId } = useLeftPanelContext();
|
||||
|
||||
const selectedTabId = useMemo(() => {
|
||||
const defaultTab = tabs[0].id;
|
||||
|
@ -49,6 +50,7 @@ export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {
|
|||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -36,9 +36,11 @@ export const mockGetFieldsData = (field: string): string[] => {
|
|||
export const mockContextValue: LeftPanelContext = {
|
||||
eventId: 'eventId',
|
||||
indexName: 'index',
|
||||
getFieldsData: mockGetFieldsData,
|
||||
scopeId: 'scopeId',
|
||||
browserFields: null,
|
||||
dataFormattedForFieldBrowser: null,
|
||||
data: {
|
||||
getFieldsData: mockGetFieldsData,
|
||||
searchHit: {
|
||||
_id: 'testId',
|
||||
_index: 'testIndex',
|
||||
},
|
||||
|
|
|
@ -143,6 +143,7 @@ describe('<ThreatIntelligenceOverview />', () => {
|
|||
params: {
|
||||
id: panelContextValue.eventId,
|
||||
indexName: panelContextValue.indexName,
|
||||
scopeId: panelContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,9 +33,10 @@ export const CorrelationsOverview: React.FC = () => {
|
|||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
}, [eventId, openLeftPanel, indexName]);
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
|
||||
const { loading, error, data } = useCorrelations({
|
||||
eventId,
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('<ExpandDetailButton />', () => {
|
|||
const panelContextValue = {
|
||||
eventId: 'eventId',
|
||||
indexName: 'indexName',
|
||||
scopeId: 'scopeId',
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const { getByTestId } = render(
|
||||
|
@ -40,6 +41,7 @@ describe('<ExpandDetailButton />', () => {
|
|||
params: {
|
||||
id: panelContextValue.eventId,
|
||||
indexName: panelContextValue.indexName,
|
||||
scopeId: panelContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import type { FC } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { COLLAPSE_DETAILS_BUTTON_TEST_ID, EXPAND_DETAILS_BUTTON_TEST_ID } from './test_ids';
|
||||
import { LeftPanelKey } from '../../left';
|
||||
|
@ -21,19 +21,20 @@ export const ExpandDetailButton: FC = memo(() => {
|
|||
const { closeLeftPanel, openLeftPanel, panels } = useExpandableFlyoutContext();
|
||||
const isExpanded: boolean = panels.left != null;
|
||||
|
||||
const { eventId, indexName } = useRightPanelContext();
|
||||
const { eventId, indexName, scopeId } = useRightPanelContext();
|
||||
|
||||
const expandDetails = () => {
|
||||
const expandDetails = useCallback(() => {
|
||||
openLeftPanel({
|
||||
id: LeftPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
};
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
|
||||
const collapseDetails = () => closeLeftPanel();
|
||||
const collapseDetails = useCallback(() => closeLeftPanel(), [closeLeftPanel]);
|
||||
|
||||
return isExpanded ? (
|
||||
<EuiButtonEmpty
|
||||
|
|
|
@ -15,15 +15,17 @@ import React from 'react';
|
|||
import { PrevalenceOverview } from './prevalence_overview';
|
||||
import { usePrevalence } from '../hooks/use_prevalence';
|
||||
import { PrevalenceOverviewRow } from './prevalence_overview_row';
|
||||
import { useFetchUniqueHostsWithFieldPair } from '../hooks/use_fetch_unique_hosts_with_field_value_pair';
|
||||
import { useFetchUniqueHosts } from '../hooks/use_fetch_unique_hosts';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('../hooks/use_fetch_unique_hosts_with_field_value_pair');
|
||||
jest.mock('../hooks/use_fetch_unique_hosts');
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
jest.mock('../hooks/use_prevalence');
|
||||
|
||||
const field = 'field';
|
||||
const values = ['value'];
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const scopeId = 'scopeId';
|
||||
const callbackIfNull = jest.fn();
|
||||
|
||||
|
@ -44,12 +46,12 @@ const renderPrevalenceOverview = (contextValue: RightPanelContext) => (
|
|||
|
||||
describe('<PrevalenceOverview />', () => {
|
||||
it('should render PrevalenceOverviewRows', () => {
|
||||
(useFetchUniqueHostsWithFieldPair as jest.Mock).mockReturnValue({
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueHosts as jest.Mock).mockReturnValue({
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
|
@ -58,8 +60,7 @@ describe('<PrevalenceOverview />', () => {
|
|||
empty: false,
|
||||
prevalenceRows: [
|
||||
<PrevalenceOverviewRow
|
||||
field={field}
|
||||
values={values}
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={callbackIfNull}
|
||||
data-test-subj={'test'}
|
||||
|
@ -90,12 +91,12 @@ describe('<PrevalenceOverview />', () => {
|
|||
});
|
||||
|
||||
it('should navigate to left section Insights tab when clicking on button', () => {
|
||||
(useFetchUniqueHostsWithFieldPair as jest.Mock).mockReturnValue({
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueHosts as jest.Mock).mockReturnValue({
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
|
@ -104,8 +105,7 @@ describe('<PrevalenceOverview />', () => {
|
|||
empty: false,
|
||||
prevalenceRows: [
|
||||
<PrevalenceOverviewRow
|
||||
field={field}
|
||||
values={values}
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={callbackIfNull}
|
||||
data-test-subj={'test'}
|
||||
|
|
|
@ -8,14 +8,16 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { PrevalenceOverviewRow } from './prevalence_overview_row';
|
||||
import { useFetchUniqueHostsWithFieldPair } from '../hooks/use_fetch_unique_hosts_with_field_value_pair';
|
||||
import { useFetchUniqueHosts } from '../hooks/use_fetch_unique_hosts';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('../hooks/use_fetch_unique_hosts_with_field_value_pair');
|
||||
jest.mock('../hooks/use_fetch_unique_hosts');
|
||||
jest.mock('../../shared/hooks/use_fetch_field_value_pair_with_aggregation');
|
||||
jest.mock('../../shared/hooks/use_fetch_unique_by_field');
|
||||
|
||||
const field = 'field';
|
||||
const values = ['values'];
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const scopeId = 'scopeId';
|
||||
const dataTestSubj = 'test';
|
||||
const iconDataTestSubj = 'testIcon';
|
||||
|
@ -25,12 +27,12 @@ const loadingDataTestSubj = 'testLoading';
|
|||
|
||||
describe('<PrevalenceOverviewRow />', () => {
|
||||
it('should display row if prevalence is below or equal threshold', () => {
|
||||
(useFetchUniqueHostsWithFieldPair as jest.Mock).mockReturnValue({
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueHosts as jest.Mock).mockReturnValue({
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
|
@ -38,27 +40,28 @@ describe('<PrevalenceOverviewRow />', () => {
|
|||
|
||||
const { getByTestId, getAllByText, queryByTestId } = render(
|
||||
<PrevalenceOverviewRow
|
||||
field={field}
|
||||
values={values}
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={() => {}}
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
);
|
||||
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
expect(getByTestId(iconDataTestSubj)).toBeInTheDocument();
|
||||
expect(getByTestId(valueDataTestSubj)).toBeInTheDocument();
|
||||
expect(getAllByText(`${field}, ${values} is uncommon`)).toHaveLength(1);
|
||||
expect(getAllByText(`${name}, ${values} is uncommon`)).toHaveLength(1);
|
||||
expect(queryByTestId(colorDataTestSubj)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display row if prevalence is higher than threshold', () => {
|
||||
(useFetchUniqueHostsWithFieldPair as jest.Mock).mockReturnValue({
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueHosts as jest.Mock).mockReturnValue({
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 2,
|
||||
|
@ -67,8 +70,7 @@ describe('<PrevalenceOverviewRow />', () => {
|
|||
|
||||
const { queryAllByAltText } = render(
|
||||
<PrevalenceOverviewRow
|
||||
field={field}
|
||||
values={values}
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={callbackIfNull}
|
||||
data-test-subj={dataTestSubj}
|
||||
|
@ -80,12 +82,12 @@ describe('<PrevalenceOverviewRow />', () => {
|
|||
});
|
||||
|
||||
it('should not display row if error retrieving data', () => {
|
||||
(useFetchUniqueHostsWithFieldPair as jest.Mock).mockReturnValue({
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
});
|
||||
(useFetchUniqueHosts as jest.Mock).mockReturnValue({
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: true,
|
||||
count: 0,
|
||||
|
@ -94,8 +96,7 @@ describe('<PrevalenceOverviewRow />', () => {
|
|||
|
||||
const { queryAllByAltText } = render(
|
||||
<PrevalenceOverviewRow
|
||||
field={field}
|
||||
values={values}
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={callbackIfNull}
|
||||
data-test-subj={dataTestSubj}
|
||||
|
@ -107,12 +108,12 @@ describe('<PrevalenceOverviewRow />', () => {
|
|||
});
|
||||
|
||||
it('should display loading', () => {
|
||||
(useFetchUniqueHostsWithFieldPair as jest.Mock).mockReturnValue({
|
||||
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
|
||||
loading: true,
|
||||
error: false,
|
||||
count: 1,
|
||||
});
|
||||
(useFetchUniqueHosts as jest.Mock).mockReturnValue({
|
||||
(useFetchUniqueByField as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
count: 10,
|
||||
|
@ -120,8 +121,7 @@ describe('<PrevalenceOverviewRow />', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<PrevalenceOverviewRow
|
||||
field={field}
|
||||
values={values}
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={() => {}}
|
||||
data-test-subj={dataTestSubj}
|
||||
|
|
|
@ -8,22 +8,19 @@
|
|||
import type { VFC } from 'react';
|
||||
import React from 'react';
|
||||
import { PREVALENCE_ROW_UNCOMMON } from './translations';
|
||||
import { useFetchUniqueHostsWithFieldPair } from '../hooks/use_fetch_unique_hosts_with_field_value_pair';
|
||||
import { useFetchUniqueHosts } from '../hooks/use_fetch_unique_hosts';
|
||||
import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field';
|
||||
import { InsightsSummaryRow } from './insights_summary_row';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
|
||||
const HOST_FIELD = 'host.name';
|
||||
const PERCENTAGE_THRESHOLD = 0.1; // we show the prevalence if its value is below 10%
|
||||
|
||||
export interface PrevalenceOverviewRowProps {
|
||||
/**
|
||||
* Highlighted field
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Highlighted field value
|
||||
*/
|
||||
values: string[];
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
|
@ -44,8 +41,7 @@ export interface PrevalenceOverviewRowProps {
|
|||
* the row will render null.
|
||||
*/
|
||||
export const PrevalenceOverviewRow: VFC<PrevalenceOverviewRowProps> = ({
|
||||
field,
|
||||
values,
|
||||
highlightedField,
|
||||
scopeId,
|
||||
callbackIfNull,
|
||||
'data-test-subj': dataTestSubj,
|
||||
|
@ -56,23 +52,25 @@ export const PrevalenceOverviewRow: VFC<PrevalenceOverviewRowProps> = ({
|
|||
loading: hostsLoading,
|
||||
error: hostsError,
|
||||
count: hostsCount,
|
||||
} = useFetchUniqueHostsWithFieldPair({
|
||||
field,
|
||||
values,
|
||||
} = useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
isActiveTimelines,
|
||||
aggregationField: HOST_FIELD,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: uniqueHostsLoading,
|
||||
error: uniqueHostsError,
|
||||
count: uniqueHostsCount,
|
||||
} = useFetchUniqueHosts();
|
||||
} = useFetchUniqueByField({ field: HOST_FIELD });
|
||||
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
// prevalence is number of host(s) where the field/value pair was found divided by the total number of hosts in the environment
|
||||
const prevalence = hostsCount / uniqueHostsCount;
|
||||
const loading = hostsLoading || uniqueHostsLoading;
|
||||
const error = hostsError || uniqueHostsError;
|
||||
const text = `${field}, ${values} ${PREVALENCE_ROW_UNCOMMON}`;
|
||||
const text = `${name}, ${values} ${PREVALENCE_ROW_UNCOMMON}`;
|
||||
|
||||
// we do not want to render the row is the prevalence is Infinite, 0 or above the decided threshold
|
||||
const shouldNotRender =
|
||||
|
|
|
@ -31,7 +31,7 @@ import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left';
|
|||
* and the SummaryPanel component for data rendering.
|
||||
*/
|
||||
export const ThreatIntelligenceOverview: FC = () => {
|
||||
const { eventId, indexName, dataFormattedForFieldBrowser } = useRightPanelContext();
|
||||
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser } = useRightPanelContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
|
||||
const goToThreatIntelligenceTab = useCallback(() => {
|
||||
|
@ -41,9 +41,10 @@ export const ThreatIntelligenceOverview: FC = () => {
|
|||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
}, [eventId, openLeftPanel, indexName]);
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
|
||||
const {
|
||||
loading: threatIntelLoading,
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import type { SearchHit } from '@kbn/es-types';
|
||||
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import type { SearchHit } from '../../../common/search_strategy';
|
||||
import { useTimelineEventsDetails } from '../../timelines/containers/details';
|
||||
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
|
@ -50,9 +50,9 @@ export interface RightPanelContext {
|
|||
/**
|
||||
* The actual raw document object
|
||||
*/
|
||||
searchHit: SearchHit<object> | undefined;
|
||||
searchHit: SearchHit | undefined;
|
||||
/**
|
||||
*
|
||||
* Promise to trigger a data refresh
|
||||
*/
|
||||
refetchFlyoutData: () => Promise<void>;
|
||||
/**
|
||||
|
@ -101,9 +101,9 @@ export const RightPanelProvider = ({
|
|||
indexName,
|
||||
scopeId,
|
||||
browserFields: sourcererDataView.browserFields,
|
||||
dataAsNestedObject: dataAsNestedObject as unknown as Ecs,
|
||||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit: searchHit as SearchHit<object>,
|
||||
searchHit,
|
||||
refetchFlyoutData,
|
||||
getFieldsData,
|
||||
}
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/common';
|
||||
import { createFetchAggregatedData } from '../utils/fetch_aggregated_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
const AGG_KEY = 'uniqueHosts';
|
||||
const QUERY_KEY = 'useFetchUniqueHosts';
|
||||
|
||||
interface RawAggregatedDataResponse {
|
||||
aggregations: {
|
||||
[AGG_KEY]: {
|
||||
buckets: unknown[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const searchRequest: IEsSearchRequest = {
|
||||
params: {
|
||||
body: {
|
||||
aggs: {
|
||||
[AGG_KEY]: {
|
||||
terms: {
|
||||
field: 'host.name',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface UseUniqueValuesValue {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Number of unique hosts found in the environment
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all unique hosts in the environment, using ReactQuery.
|
||||
* The query uses an aggregation by unique hosts.
|
||||
*/
|
||||
export const useFetchUniqueHosts = (): UseUniqueValuesValue => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY],
|
||||
() =>
|
||||
createFetchAggregatedData<RawAggregatedDataResponse>(searchService, searchRequest, AGG_KEY),
|
||||
{
|
||||
select: (res) => res.aggregations[AGG_KEY].buckets.length,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
count: data || 0,
|
||||
};
|
||||
};
|
|
@ -70,16 +70,22 @@ export const usePrevalence = ({
|
|||
|
||||
const prevalenceRows = useMemo(
|
||||
() =>
|
||||
summaryRows.map((row) => (
|
||||
<PrevalenceOverviewRow
|
||||
field={row.description.data.field}
|
||||
values={row.description.values || []}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={() => setCount((prevCount) => prevCount + 1)}
|
||||
data-test-subj={INSIGHTS_PREVALENCE_TEST_ID}
|
||||
key={row.description.data.field}
|
||||
/>
|
||||
)),
|
||||
summaryRows.map((row) => {
|
||||
const highlightedField = {
|
||||
name: row.description.data.field,
|
||||
values: row.description.values || [],
|
||||
};
|
||||
|
||||
return (
|
||||
<PrevalenceOverviewRow
|
||||
highlightedField={highlightedField}
|
||||
scopeId={scopeId}
|
||||
callbackIfNull={() => setCount((prevCount) => prevCount + 1)}
|
||||
data-test-subj={INSIGHTS_PREVALENCE_TEST_ID}
|
||||
key={row.description.data.field}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
[summaryRows, scopeId]
|
||||
);
|
||||
|
||||
|
|
|
@ -9,38 +9,46 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
UseFetchUniqueHostWithFieldPairParams,
|
||||
UseFetchUniqueHostWithFieldPairResult,
|
||||
} from './use_fetch_unique_hosts_with_field_value_pair';
|
||||
import { useFetchUniqueHostsWithFieldPair } from './use_fetch_unique_hosts_with_field_value_pair';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import type {
|
||||
UseFetchFieldValuePairByEventTypeParams,
|
||||
UseFetchFieldValuePairByEventTypeResult,
|
||||
} from './use_fetch_field_value_pair_by_event_type';
|
||||
import {
|
||||
EventKind,
|
||||
useFetchFieldValuePairByEventType,
|
||||
} from './use_fetch_field_value_pair_by_event_type';
|
||||
|
||||
jest.mock('@tanstack/react-query');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../../common/hooks/use_selector');
|
||||
jest.mock('../../../common/containers/use_global_time');
|
||||
|
||||
const field = 'field';
|
||||
const values = ['values'];
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const isActiveTimelines = true;
|
||||
const type = {
|
||||
eventKind: EventKind.alert,
|
||||
include: true,
|
||||
};
|
||||
|
||||
describe('useFetchUniqueHostsWithFieldPair', () => {
|
||||
describe('useFetchFieldValuePairByEventType', () => {
|
||||
let hookResult: RenderHookResult<
|
||||
UseFetchUniqueHostWithFieldPairParams,
|
||||
UseFetchUniqueHostWithFieldPairResult
|
||||
UseFetchFieldValuePairByEventTypeParams,
|
||||
UseFetchFieldValuePairByEventTypeResult
|
||||
>;
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: { search: jest.fn() },
|
||||
},
|
||||
});
|
||||
(useDeepEqualSelector as jest.Mock).mockReturnValue({ to: '', from: '' });
|
||||
jest.mocked(useDeepEqualSelector).mockReturnValue({ to: '', from: '' });
|
||||
(useGlobalTime as jest.Mock).mockReturnValue({ to: '', from: '' });
|
||||
|
||||
it('should return loading true while data is being fetched', async () => {
|
||||
it('should return loading true while data is being fetched', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
|
@ -48,7 +56,7 @@ describe('useFetchUniqueHostsWithFieldPair', () => {
|
|||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchUniqueHostsWithFieldPair({ field, values, isActiveTimelines })
|
||||
useFetchFieldValuePairByEventType({ highlightedField, isActiveTimelines, type })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeTruthy();
|
||||
|
@ -56,7 +64,7 @@ describe('useFetchUniqueHostsWithFieldPair', () => {
|
|||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return error true when data fetching has errored out', async () => {
|
||||
it('should return error true when data fetching has errored out', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
|
@ -64,7 +72,7 @@ describe('useFetchUniqueHostsWithFieldPair', () => {
|
|||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchUniqueHostsWithFieldPair({ field, values, isActiveTimelines })
|
||||
useFetchFieldValuePairByEventType({ highlightedField, isActiveTimelines, type })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
|
@ -72,7 +80,7 @@ describe('useFetchUniqueHostsWithFieldPair', () => {
|
|||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count on success', async () => {
|
||||
it('should return count on success', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
@ -80,7 +88,7 @@ describe('useFetchUniqueHostsWithFieldPair', () => {
|
|||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchUniqueHostsWithFieldPair({ field, values, isActiveTimelines })
|
||||
useFetchFieldValuePairByEventType({ highlightedField, isActiveTimelines, type })
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import type { RawResponse } from '../utils/fetch_data';
|
||||
|
||||
const QUERY_KEY = 'FetchFieldValuePairByEventType';
|
||||
|
||||
export enum EventKind {
|
||||
alert = 'alert',
|
||||
asset = 'asset',
|
||||
enrichment = 'enrichment',
|
||||
event = 'event',
|
||||
metric = 'metric',
|
||||
state = 'state',
|
||||
pipeline_error = 'pipeline_error',
|
||||
signal = 'signal',
|
||||
}
|
||||
|
||||
export interface EventType {
|
||||
eventKind: EventKind;
|
||||
include?: boolean;
|
||||
exclude?: boolean;
|
||||
}
|
||||
|
||||
export interface UseFetchFieldValuePairByEventTypeParams {
|
||||
/**
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
* True is the current timeline is active ('timeline-1')
|
||||
*/
|
||||
isActiveTimelines: boolean;
|
||||
/**
|
||||
* Limit the search to include or exclude a specific value for the event.kind field
|
||||
* (alert, asset, enrichment, event, metric, state, pipeline_error, signal)
|
||||
*/
|
||||
type: EventType;
|
||||
}
|
||||
|
||||
export interface UseFetchFieldValuePairByEventTypeResult {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Number of unique hosts found for the field/value pair
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all the unique hosts in the environment that have the field/value pair, using ReactQuery.
|
||||
*/
|
||||
export const useFetchFieldValuePairByEventType = ({
|
||||
highlightedField,
|
||||
isActiveTimelines,
|
||||
type,
|
||||
}: UseFetchFieldValuePairByEventTypeParams): UseFetchFieldValuePairByEventTypeResult => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const timelineTime = useDeepEqualSelector((state) =>
|
||||
inputsSelectors.timelineTimeRangeSelector(state)
|
||||
);
|
||||
const globalTime = useGlobalTime();
|
||||
const { to, from } = isActiveTimelines ? timelineTime : globalTime;
|
||||
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
const req: IEsSearchRequest = buildSearchRequest(name, values, from, to, type);
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, name, values, from, to, type],
|
||||
() => createFetchData<RawResponse>(searchService, req),
|
||||
{
|
||||
select: (res) => res.hits.total,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
count: data || 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the search request for the field/values pair, for a date range from/to.
|
||||
* We set the size to 0 as we only care about the total number of documents.
|
||||
* Passing signalEventKind as true will return only alerts (event.kind === "signal"), otherwise return all other documents (event.kind !== "signal")
|
||||
*/
|
||||
const buildSearchRequest = (
|
||||
field: string,
|
||||
values: string[],
|
||||
from: string,
|
||||
to: string,
|
||||
type: EventType
|
||||
): IEsSearchRequest => {
|
||||
const query = buildEsQuery(
|
||||
undefined,
|
||||
[],
|
||||
[
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[field]: values[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(type.include
|
||||
? [
|
||||
{
|
||||
match: {
|
||||
'event.kind': type.eventKind,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
...(type.exclude
|
||||
? {
|
||||
must_not: [
|
||||
{
|
||||
match: {
|
||||
'event.kind': type.eventKind,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
params: {
|
||||
body: {
|
||||
query,
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
UseFetchFieldValuePairWithAggregationParams,
|
||||
UseFetchFieldValuePairWithAggregationResult,
|
||||
} from './use_fetch_field_value_pair_with_aggregation';
|
||||
import { useFetchFieldValuePairWithAggregation } from './use_fetch_field_value_pair_with_aggregation';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
|
||||
jest.mock('@tanstack/react-query');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../../common/hooks/use_selector');
|
||||
jest.mock('../../../common/containers/use_global_time');
|
||||
|
||||
const highlightedField = {
|
||||
name: 'field',
|
||||
values: ['values'],
|
||||
};
|
||||
const isActiveTimelines = true;
|
||||
const aggregationField = 'aggregationField';
|
||||
|
||||
describe('useFetchFieldValuePairWithAggregation', () => {
|
||||
let hookResult: RenderHookResult<
|
||||
UseFetchFieldValuePairWithAggregationParams,
|
||||
UseFetchFieldValuePairWithAggregationResult
|
||||
>;
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: { search: jest.fn() },
|
||||
},
|
||||
});
|
||||
jest.mocked(useDeepEqualSelector).mockReturnValue({ to: '', from: '' });
|
||||
(useGlobalTime as jest.Mock).mockReturnValue({ to: '', from: '' });
|
||||
|
||||
it('should return loading true while data is being fetched', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
isActiveTimelines,
|
||||
aggregationField,
|
||||
})
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeTruthy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return error true when data fetching has errored out', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
isActiveTimelines,
|
||||
aggregationField,
|
||||
})
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeTruthy();
|
||||
expect(hookResult.result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count on success', () => {
|
||||
(useQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: 1,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() =>
|
||||
useFetchFieldValuePairWithAggregation({
|
||||
highlightedField,
|
||||
isActiveTimelines,
|
||||
aggregationField,
|
||||
})
|
||||
);
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
expect(hookResult.result.current.count).toBe(1);
|
||||
});
|
||||
});
|
|
@ -6,41 +6,34 @@
|
|||
*/
|
||||
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/common';
|
||||
import { createFetchAggregatedData } from '../utils/fetch_aggregated_data';
|
||||
import { buildAggregationSearchRequest } from '../utils/build_requests';
|
||||
import type { RawAggregatedDataResponse } from '../utils/fetch_data';
|
||||
import { AGG_KEY, createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
|
||||
const AGG_KEY = 'hostsWithSameFieldValuePair';
|
||||
const QUERY_KEY = 'useFetchUniqueHostsWithFieldPair';
|
||||
const QUERY_KEY = 'useFetchFieldValuePairWithAggregation';
|
||||
|
||||
interface RawAggregatedDataResponse {
|
||||
aggregations: {
|
||||
[AGG_KEY]: {
|
||||
buckets: unknown[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseFetchUniqueHostWithFieldPairParams {
|
||||
export interface UseFetchFieldValuePairWithAggregationParams {
|
||||
/**
|
||||
* Highlighted field
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Highlighted field value
|
||||
*/
|
||||
values: string[];
|
||||
* The highlighted field name and values
|
||||
* */
|
||||
highlightedField: { name: string; values: string[] };
|
||||
/**
|
||||
*
|
||||
*/
|
||||
isActiveTimelines: boolean;
|
||||
/**
|
||||
* Field to aggregate value by
|
||||
*/
|
||||
aggregationField: string;
|
||||
}
|
||||
|
||||
export interface UseFetchUniqueHostWithFieldPairResult {
|
||||
export interface UseFetchFieldValuePairWithAggregationResult {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
|
@ -56,14 +49,15 @@ export interface UseFetchUniqueHostWithFieldPairResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all the unique hosts in the environment that have the field/value pair, using ReactQuery.
|
||||
* The query uses an aggregation by unique hosts.
|
||||
* Hook to retrieve all the unique documents for the aggregationField in the environment that have the field/value pair, using ReactQuery.
|
||||
*
|
||||
* Foe example, passing 'host.name' via the aggregationField props will return the number of unique hosts in the environment that have the field/value pair.
|
||||
*/
|
||||
export const useFetchUniqueHostsWithFieldPair = ({
|
||||
field,
|
||||
values,
|
||||
export const useFetchFieldValuePairWithAggregation = ({
|
||||
highlightedField,
|
||||
isActiveTimelines,
|
||||
}: UseFetchUniqueHostWithFieldPairParams): UseFetchUniqueHostWithFieldPairResult => {
|
||||
aggregationField,
|
||||
}: UseFetchFieldValuePairWithAggregationParams): UseFetchFieldValuePairWithAggregationResult => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
|
@ -76,12 +70,13 @@ export const useFetchUniqueHostsWithFieldPair = ({
|
|||
const globalTime = useGlobalTime();
|
||||
const { to, from } = isActiveTimelines ? timelineTime : globalTime;
|
||||
|
||||
const searchRequest = buildSearchRequest(field, values, from, to);
|
||||
const { name, values } = highlightedField;
|
||||
|
||||
const searchRequest = buildSearchRequest(name, values, from, to, aggregationField);
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, field, values],
|
||||
() =>
|
||||
createFetchAggregatedData<RawAggregatedDataResponse>(searchService, searchRequest, AGG_KEY),
|
||||
[QUERY_KEY, name, values, from, to, aggregationField],
|
||||
() => createFetchData<RawAggregatedDataResponse>(searchService, searchRequest),
|
||||
{
|
||||
select: (res) => res.aggregations[AGG_KEY].buckets.length,
|
||||
keepPreviousData: true,
|
||||
|
@ -97,13 +92,14 @@ export const useFetchUniqueHostsWithFieldPair = ({
|
|||
|
||||
/**
|
||||
* Build the search request for the field/values pair, for a date range from/to.
|
||||
* The request contains aggregation by host.name field.
|
||||
* The request contains aggregation by aggregationField.
|
||||
*/
|
||||
const buildSearchRequest = (
|
||||
field: string,
|
||||
values: string[],
|
||||
from: string,
|
||||
to: string
|
||||
to: string,
|
||||
aggregationField: string
|
||||
): IEsSearchRequest => {
|
||||
const query = buildEsQuery(
|
||||
undefined,
|
||||
|
@ -133,21 +129,5 @@ const buildSearchRequest = (
|
|||
},
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
params: {
|
||||
body: {
|
||||
query,
|
||||
aggs: {
|
||||
[AGG_KEY]: {
|
||||
terms: {
|
||||
field: 'host.name',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
return buildAggregationSearchRequest(aggregationField, AGG_KEY, query);
|
||||
};
|
|
@ -8,16 +8,20 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import type { UseUniqueValuesValue } from './use_fetch_unique_hosts';
|
||||
import { useFetchUniqueHosts } from './use_fetch_unique_hosts';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
UseFetchUniqueByFieldParams,
|
||||
UseFetchUniqueByFieldValue,
|
||||
} from './use_fetch_unique_by_field';
|
||||
import { useFetchUniqueByField } from './use_fetch_unique_by_field';
|
||||
|
||||
jest.mock('@tanstack/react-query');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
describe('useFetchUniqueHosts', () => {
|
||||
let hookResult: RenderHookResult<unknown, UseUniqueValuesValue>;
|
||||
const field = 'host.name';
|
||||
|
||||
describe('useFetchUniqueByField', () => {
|
||||
let hookResult: RenderHookResult<UseFetchUniqueByFieldParams, UseFetchUniqueByFieldValue>;
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: { search: jest.fn() },
|
||||
|
@ -31,7 +35,7 @@ describe('useFetchUniqueHosts', () => {
|
|||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchUniqueHosts());
|
||||
hookResult = renderHook(() => useFetchUniqueByField({ field }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeTruthy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
||||
|
@ -45,7 +49,7 @@ describe('useFetchUniqueHosts', () => {
|
|||
data: 0,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchUniqueHosts());
|
||||
hookResult = renderHook(() => useFetchUniqueByField({ field }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeTruthy();
|
||||
|
@ -59,7 +63,7 @@ describe('useFetchUniqueHosts', () => {
|
|||
data: 1,
|
||||
});
|
||||
|
||||
hookResult = renderHook(() => useFetchUniqueHosts());
|
||||
hookResult = renderHook(() => useFetchUniqueByField({ field }));
|
||||
|
||||
expect(hookResult.result.current.loading).toBeFalsy();
|
||||
expect(hookResult.result.current.error).toBeFalsy();
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/common';
|
||||
import type { RawAggregatedDataResponse } from '../utils/fetch_data';
|
||||
import { AGG_KEY, createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { buildAggregationSearchRequest } from '../utils/build_requests';
|
||||
|
||||
const QUERY_KEY = 'useFetchUniqueByField';
|
||||
|
||||
export interface UseFetchUniqueByFieldParams {
|
||||
/**
|
||||
* Field to aggregate by
|
||||
*/
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface UseFetchUniqueByFieldValue {
|
||||
/**
|
||||
* Returns true if data is being loaded
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Returns true if fetching data has errored out
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Number of unique document by field found in the environment
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve all unique documents by field in the environment, using ReactQuery.
|
||||
*
|
||||
* For example, passing 'host.name' via the field props will return the number of unique hosts in the environment.
|
||||
*/
|
||||
export const useFetchUniqueByField = ({
|
||||
field,
|
||||
}: UseFetchUniqueByFieldParams): UseFetchUniqueByFieldValue => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
},
|
||||
} = useKibana();
|
||||
const searchRequest: IEsSearchRequest = buildAggregationSearchRequest(field, AGG_KEY);
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, field],
|
||||
() => createFetchData<RawAggregatedDataResponse>(searchService, searchRequest),
|
||||
{
|
||||
select: (res) => res.aggregations[AGG_KEY].buckets.length,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
count: data || 0,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IEsSearchRequest } from '@kbn/data-plugin/common';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
/**
|
||||
* Builds a search request for an aggregation.
|
||||
* We're setting the query size to 0 as we only care about the aggregation result here.
|
||||
*
|
||||
* @param field aggregation field
|
||||
* @param key aggregation key
|
||||
* @param query optional query
|
||||
*/
|
||||
export const buildAggregationSearchRequest = (
|
||||
field: string,
|
||||
key: string,
|
||||
query?: QueryDslQueryContainer
|
||||
): IEsSearchRequest => ({
|
||||
params: {
|
||||
body: {
|
||||
query,
|
||||
aggs: {
|
||||
[key]: {
|
||||
terms: {
|
||||
field,
|
||||
size: 1000, // setting a high size to get as close as possible to all unique values
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -8,13 +8,34 @@
|
|||
import type { IEsSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type { ISearchStart } from '@kbn/data-plugin/public';
|
||||
|
||||
export const AGG_KEY = 'aggregation';
|
||||
|
||||
/**
|
||||
* Interface for aggregation responses
|
||||
*/
|
||||
export interface RawAggregatedDataResponse {
|
||||
aggregations: {
|
||||
[AGG_KEY]: {
|
||||
buckets: unknown[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for non-aggregated responses
|
||||
*/
|
||||
export interface RawResponse {
|
||||
hits: {
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable method that returns a promise wrapping the search functionality of Kibana search service
|
||||
*/
|
||||
export const createFetchAggregatedData = async <TResponse, T = {}>(
|
||||
export const createFetchData = async <TResponse, T = {}>(
|
||||
searchService: ISearchStart,
|
||||
req: IEsSearchRequest,
|
||||
aggregationKey: string
|
||||
req: IEsSearchRequest
|
||||
): Promise<TResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
searchService.search<IEsSearchRequest, IKibanaSearchResponse<TResponse>>(req).subscribe({
|
Loading…
Add table
Add a link
Reference in a new issue