mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[TIP] Add to timeline (#138836)
* [TIP] Add to timeline - add button to all cells in the indicators table - add button to a new actions column for the indicator flyout table - add button to barchart legend - create new providers (kibana context and security context) for unit tests and storybook - change all Storybook console.log to window.alert to follow the EUI pattern - fix broken Storybook indicator table story https://github.com/elastic/security-team/issues/4557
This commit is contained in:
parent
1b541c4d33
commit
e3181e65f3
48 changed files with 1321 additions and 372 deletions
|
@ -18,6 +18,10 @@ Verify your node version [here](https://github.com/elastic/kibana/blob/main/.nod
|
|||
|
||||
**Run Kibana:**
|
||||
|
||||
> **Important:**
|
||||
>
|
||||
> See here to get your `kibana.yaml` to enable the Threat Intelligence plugin.
|
||||
|
||||
```
|
||||
yarn kbn reset && yarn kbn bootstrap
|
||||
yarn start --no-base-path
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum of indicator fields supported by the Threat Intelligence plugin.
|
||||
*/
|
||||
export enum RawIndicatorFieldId {
|
||||
Type = 'threat.indicator.type',
|
||||
FirstSeen = 'threat.indicator.first_seen',
|
||||
|
@ -21,11 +24,17 @@ export enum RawIndicatorFieldId {
|
|||
TimeStamp = '@timestamp',
|
||||
}
|
||||
|
||||
/**
|
||||
* Threat Intelligence Indicator interface.
|
||||
*/
|
||||
export interface Indicator {
|
||||
_id?: unknown;
|
||||
fields: Partial<Record<RawIndicatorFieldId, unknown[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create new Indicators, used mainly in jest unit tests and Storybook stories.
|
||||
*/
|
||||
export const generateMockIndicator = (): Indicator => ({
|
||||
fields: {
|
||||
'@timestamp': ['2022-01-01T01:01:01.000Z'],
|
||||
|
@ -37,6 +46,9 @@ export const generateMockIndicator = (): Indicator => ({
|
|||
_id: Math.random(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Used to create new url-type Indicators, used mainly in jest unit tests and Storybook stories.
|
||||
*/
|
||||
export const generateMockUrlIndicator = (): Indicator => {
|
||||
const indicator = generateMockIndicator();
|
||||
|
||||
|
@ -46,6 +58,9 @@ export const generateMockUrlIndicator = (): Indicator => {
|
|||
return indicator;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to create new file-type Indicators, used mainly in jest unit tests and Storybook stories.
|
||||
*/
|
||||
export const generateMockFileIndicator = (): Indicator => {
|
||||
const indicator = generateMockIndicator();
|
||||
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { login } from '../../tasks/login';
|
||||
import { login } from '../tasks/login';
|
||||
import {
|
||||
EMPTY_PAGE_BODY,
|
||||
EMPTY_PAGE_DOCS_LINK,
|
||||
EMPTY_PAGE_INTEGRATIONS_LINK,
|
||||
} from '../../screens/empty_page';
|
||||
} from '../screens/empty_page';
|
||||
|
||||
const THREAT_INTEL_PATH = '/app/security/threat_intelligence/';
|
||||
|
|
@ -25,10 +25,10 @@ import {
|
|||
ENDING_BREADCRUMB,
|
||||
FIELD_BROWSER,
|
||||
FIELD_BROWSER_MODAL,
|
||||
} from '../../screens/indicators';
|
||||
import { login } from '../../tasks/login';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
|
||||
import { selectRange } from '../../tasks/select_range';
|
||||
} from '../screens/indicators';
|
||||
import { login } from '../tasks/login';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||
import { selectRange } from '../tasks/select_range';
|
||||
|
||||
before(() => {
|
||||
login();
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 {
|
||||
BARCHART_TIMELINE_BUTTON,
|
||||
FLYOUT_CLOSE_BUTTON,
|
||||
FLYOUT_TABLE_ROW_TIMELINE_BUTTON,
|
||||
INDICATOR_TYPE_CELL,
|
||||
INDICATORS_TABLE_CELL_TIMELINE_BUTTON,
|
||||
TIMELINE_DRAGGABLE_ITEM,
|
||||
TOGGLE_FLYOUT_BUTTON,
|
||||
UNTITLED_TIMELINE_BUTTON,
|
||||
} from '../screens/indicators';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||
import { login } from '../tasks/login';
|
||||
import { selectRange } from '../tasks/select_range';
|
||||
|
||||
const THREAT_INTELLIGENCE = '/app/security/threat_intelligence/indicators';
|
||||
|
||||
before(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
describe('Indicators', () => {
|
||||
before(() => {
|
||||
esArchiverLoad('threat_intelligence');
|
||||
});
|
||||
after(() => {
|
||||
esArchiverUnload('threat_intelligence');
|
||||
});
|
||||
|
||||
describe('Indicators timeline interactions', () => {
|
||||
before(() => {
|
||||
cy.visit(THREAT_INTELLIGENCE);
|
||||
|
||||
selectRange();
|
||||
});
|
||||
|
||||
it('should add entry in timeline when clicking in the barchart legend', () => {
|
||||
cy.get(BARCHART_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
|
||||
it('should add entry in timeline when clicking in an indicator table cell', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover');
|
||||
cy.get(INDICATORS_TABLE_CELL_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
|
||||
it('should add entry in timeline when clicking in an indicators flyout row', () => {
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_TABLE_ROW_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(UNTITLED_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,6 +17,8 @@ export const INDICATORS_TABLE = `[data-test-subj="tiIndicatorsTable"]`;
|
|||
|
||||
export const TOGGLE_FLYOUT_BUTTON = `[data-test-subj="tiToggleIndicatorFlyoutButton"]`;
|
||||
|
||||
export const FLYOUT_CLOSE_BUTTON = `[data-test-subj="euiFlyoutCloseButton"]`;
|
||||
|
||||
export const FLYOUT_TITLE = `[data-test-subj="tiIndicatorFlyoutTitle"]`;
|
||||
|
||||
export const FLYOUT_TABS = `[data-test-subj="tiIndicatorFlyoutTabs"]`;
|
||||
|
@ -46,3 +48,14 @@ export const FIELD_BROWSER_MODAL = `[data-test-subj="fields-browser-container"]`
|
|||
export const FIELD_BROWSER_MODAL_SOURCE_CHECKBOX = `[data-test-subj="field-_source-checkbox"]`;
|
||||
|
||||
export const FIELD_BROWSER_CLOSE = `[data-test-subj="close"]`;
|
||||
|
||||
export const BARCHART_TIMELINE_BUTTON = '[data-test-subj="tiTimelineButton"]';
|
||||
|
||||
export const INDICATORS_TABLE_CELL_TIMELINE_BUTTON =
|
||||
'[data-test-subj="tiIndicatorsTableCellTimelineButton"]';
|
||||
|
||||
export const FLYOUT_TABLE_ROW_TIMELINE_BUTTON = '[data-test-subj="tiFlyoutTableRowTimelineButton"]';
|
||||
|
||||
export const UNTITLED_TIMELINE_BUTTON = '[data-test-subj="flyoutOverlay"]';
|
||||
|
||||
export const TIMELINE_DRAGGABLE_ITEM = '[data-test-subj="providerContainer"]';
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock to map an indicator field to its type.
|
||||
*/
|
||||
export const generateFieldTypeMap = (): { [id: string]: string } => ({
|
||||
'@timestamp': 'date',
|
||||
'threat.indicator.ip': 'ip',
|
||||
'threat.indicator.first_seen': 'date',
|
||||
'threat.feed.name': 'string',
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { HoverActionsConfig } from '@kbn/timelines-plugin/public/components/hover_actions';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
|
||||
/**
|
||||
* Returns a default object to mock the timelines plugin for our unit tests and storybook stories.
|
||||
* The button mocks a window.alert onClick event.
|
||||
*/
|
||||
export const mockKibanaTimelinesService: TimelinesUIStart = {
|
||||
getHoverActions(): HoverActionsConfig {
|
||||
return {
|
||||
getAddToTimelineButton: () => (
|
||||
<EuiButtonIcon
|
||||
iconType="timeline"
|
||||
iconSize="s"
|
||||
onClick={() => window.alert('Add to Timeline button clicked')}
|
||||
/>
|
||||
),
|
||||
} as unknown as HoverActionsConfig;
|
||||
},
|
||||
} as unknown as TimelinesUIStart;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { EuiText } from '@elastic/eui';
|
||||
import { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
/**
|
||||
* Returns a default object to mock the triggers actions ui plugin for our unit tests and storybook stories.
|
||||
*/
|
||||
export const mockTriggersActionsUiService: TriggersActionsStart = {
|
||||
getFieldBrowser: () => (
|
||||
<EuiText style={{ display: 'inline' }} size="xs">
|
||||
Fields
|
||||
</EuiText>
|
||||
),
|
||||
} as unknown as TriggersActionsStart;
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants';
|
||||
|
||||
/**
|
||||
* Creates an object to pass to the uiSettings property when creating a KibanaReacrContext (see src/plugins/kibana_react/public/context/context.tsx).
|
||||
* @param dateFormat defaults to ''
|
||||
* @param timezone defaults to 'UTC
|
||||
* @returns the object {@link IUiSettingsClient}
|
||||
*/
|
||||
export const mockUiSettingsService = (dateFormat: string = '', timezone: string = 'UTC') =>
|
||||
({
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: dateFormat,
|
||||
[DEFAULT_DATE_FORMAT_TZ]: timezone,
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
} as unknown as IUiSettingsClient);
|
||||
|
||||
/**
|
||||
* Mocks date format or timezone for testing.
|
||||
* @param key dateFormat | dateFormat:tz
|
||||
* @returns string
|
||||
*/
|
||||
export function mockUiSetting(key: string): string | undefined {
|
||||
if (key === 'dateFormat') {
|
||||
return 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
}
|
||||
if (key === 'dateFormat:tz') {
|
||||
return 'America/New_York';
|
||||
}
|
||||
}
|
|
@ -8,7 +8,8 @@
|
|||
import React, { ReactNode, VFC } from 'react';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { CoreStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
import { SecuritySolutionContext } from '../../containers/security_solution_context';
|
||||
import { getSecuritySolutionContextMock } from './mock_security_context';
|
||||
|
||||
|
@ -21,6 +22,10 @@ export interface KibanaContextMock {
|
|||
* For the core ui-settings package (see {@link IUiSettingsClient})
|
||||
*/
|
||||
uiSettings?: IUiSettingsClient;
|
||||
/**
|
||||
* For the timelines plugin
|
||||
*/
|
||||
timelines: TimelinesUIStart;
|
||||
}
|
||||
|
||||
export interface StoryProvidersComponentProps {
|
||||
|
@ -42,7 +47,7 @@ export const StoryProvidersComponent: VFC<StoryProvidersComponentProps> = ({
|
|||
children,
|
||||
kibana,
|
||||
}) => {
|
||||
const KibanaReactContext = createKibanaReactContext(kibana as CoreStart);
|
||||
const KibanaReactContext = createKibanaReactContext(kibana);
|
||||
const securitySolutionContextMock = getSecuritySolutionContextMock();
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,17 +7,18 @@
|
|||
|
||||
import moment from 'moment/moment';
|
||||
import React, { FC } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import type { IStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { getSecuritySolutionContextMock } from './mock_security_context';
|
||||
import { mockUiSetting } from './mock_kibana_ui_setting';
|
||||
import { createTGridMocks } from '@kbn/timelines-plugin/public/mock';
|
||||
import { KibanaContext } from '../../hooks/use_kibana';
|
||||
import { SecuritySolutionPluginContext } from '../../types';
|
||||
import { getSecuritySolutionContextMock } from './mock_security_context';
|
||||
import { mockUiSetting } from './mock_kibana_ui_settings_service';
|
||||
import { SecuritySolutionContext } from '../../containers/security_solution_context';
|
||||
|
||||
export const localStorageMock = (): IStorage => {
|
||||
|
@ -90,6 +91,8 @@ const dataServiceMock = {
|
|||
},
|
||||
};
|
||||
|
||||
const timelinesServiceMock = createTGridMocks();
|
||||
|
||||
const core = coreMock.createStart();
|
||||
const coreServiceMock = {
|
||||
...core,
|
||||
|
@ -106,6 +109,7 @@ export const mockedServices = {
|
|||
triggersActionsUi: {
|
||||
getFieldBrowser: jest.fn().mockReturnValue(null),
|
||||
},
|
||||
timelines: timelinesServiceMock,
|
||||
};
|
||||
|
||||
export const TestProvidersComponent: FC = ({ children }) => (
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants';
|
||||
import { mockUiSettingsService } from '../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { DateFormatter } from './date_formatter';
|
||||
|
||||
const mockValidStringDate = '1 Jan 2022 00:00:00 GMT';
|
||||
|
@ -21,19 +21,9 @@ export default {
|
|||
};
|
||||
|
||||
export function Default() {
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService(),
|
||||
} as unknown as CoreStart);
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
|
@ -43,19 +33,9 @@ export function Default() {
|
|||
}
|
||||
|
||||
export function UserTimeZone() {
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'America/New_York',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService('', 'America/New York'),
|
||||
});
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
|
@ -65,19 +45,9 @@ export function UserTimeZone() {
|
|||
}
|
||||
|
||||
export function UserDateFormat() {
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: 'MMM Do YY',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService('MMM Do YY', 'UTC'),
|
||||
});
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
|
@ -87,18 +57,9 @@ export function UserDateFormat() {
|
|||
}
|
||||
|
||||
export function CustomDateFormat() {
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService('', 'UTC'),
|
||||
});
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
|
@ -108,19 +69,9 @@ export function CustomDateFormat() {
|
|||
}
|
||||
|
||||
export function InvalidStringDate() {
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService(),
|
||||
});
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants';
|
||||
import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map';
|
||||
import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { generateMockIndicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorField } from './indicator_field';
|
||||
|
||||
|
@ -18,13 +18,12 @@ export default {
|
|||
};
|
||||
|
||||
const mockIndicator = generateMockIndicator();
|
||||
const mockFieldTypesMap: { [id: string]: string } = {
|
||||
'threat.indicator.ip': 'ip',
|
||||
'threat.indicator.first_seen': 'date',
|
||||
};
|
||||
|
||||
const mockFieldTypesMap = generateFieldTypeMap();
|
||||
|
||||
export function Default() {
|
||||
const mockField = 'threat.indicator.ip';
|
||||
|
||||
return (
|
||||
<IndicatorField indicator={mockIndicator} field={mockField} fieldTypesMap={mockFieldTypesMap} />
|
||||
);
|
||||
|
@ -32,27 +31,16 @@ export function Default() {
|
|||
|
||||
export function IncorrectField() {
|
||||
const mockField = 'abc';
|
||||
|
||||
return (
|
||||
<IndicatorField indicator={mockIndicator} field={mockField} fieldTypesMap={mockFieldTypesMap} />
|
||||
);
|
||||
}
|
||||
|
||||
export function HandlesDates() {
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService() });
|
||||
const mockField = 'threat.indicator.first_seen';
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorField
|
||||
|
|
|
@ -11,12 +11,10 @@ import { IndicatorField } from './indicator_field';
|
|||
import { generateMockIndicator } from '../../../../../common/types/indicator';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map';
|
||||
|
||||
const mockIndicator = generateMockIndicator();
|
||||
const mockFieldTypesMap: { [id: string]: string } = {
|
||||
'threat.indicator.ip': 'ip',
|
||||
'threat.indicator.first_seen': 'date',
|
||||
};
|
||||
const mockFieldTypesMap = generateFieldTypeMap();
|
||||
|
||||
describe('<IndicatorField />', () => {
|
||||
beforeEach(() => {});
|
||||
|
|
|
@ -12,17 +12,23 @@ import { DateFormatter } from '../../../../components/date_formatter';
|
|||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
|
||||
export interface IndicatorFieldProps {
|
||||
/**
|
||||
* Indicator to display the field value from (see {@link Indicator}).
|
||||
*/
|
||||
indicator: Indicator;
|
||||
/**
|
||||
* The field to get the indicator's value for.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* An object to know what type the field is ('file', 'date', ...).
|
||||
*/
|
||||
fieldTypesMap: { [id: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an indicator object, a field and a field => type object to returns the correct value to display
|
||||
* @param indicator see {@link Indicator}
|
||||
* @param field the field to get the indicator's value for
|
||||
* @param fieldTypesMap an object to know what type ('file', 'date', ...) the field is
|
||||
* @returns If the type is a 'date', returns the {@link DateFormatter} component, else returns the value or {@link EMPTY_VALUE}
|
||||
* Takes an indicator object, a field and a field => type object to returns the correct value to display.
|
||||
* @returns If the type is a 'date', returns the {@link DateFormatter} component, else returns the value or {@link EMPTY_VALUE}.
|
||||
*/
|
||||
export const IndicatorField: VFC<IndicatorFieldProps> = ({ indicator, field, fieldTypesMap }) => {
|
||||
const value = unwrapValue(indicator, field as RawIndicatorFieldId);
|
||||
|
|
|
@ -9,6 +9,9 @@ import moment from 'moment';
|
|||
import React from 'react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { TimeRangeBounds } from '@kbn/data-plugin/common';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { ChartSeries } from '../../hooks/use_aggregated_indicators';
|
||||
import { IndicatorsBarChart } from './indicators_barchart';
|
||||
|
||||
|
@ -45,12 +48,19 @@ const mockIndicators: ChartSeries[] = [
|
|||
},
|
||||
];
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
|
||||
const numberOfDays = 1;
|
||||
|
||||
const mockDateRange: TimeRangeBounds = {
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(numberOfDays, 'days'),
|
||||
};
|
||||
const mockHeight = '500px';
|
||||
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
timelines: mockKibanaTimelinesService,
|
||||
} as unknown as CoreStart);
|
||||
|
||||
export default {
|
||||
component: IndicatorsBarChart,
|
||||
|
@ -58,13 +68,28 @@ export default {
|
|||
};
|
||||
|
||||
export const Default: Story<void> = () => (
|
||||
<IndicatorsBarChart indicators={mockIndicators} dateRange={mockDateRange} />
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsBarChart indicators={mockIndicators} field={mockField} dateRange={mockDateRange} />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
|
||||
export const NoData: Story<void> = () => (
|
||||
<IndicatorsBarChart indicators={[]} dateRange={mockDateRange} />
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsBarChart indicators={[]} field={''} dateRange={mockDateRange} />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
|
||||
export const CustomHeight: Story<void> = () => (
|
||||
<IndicatorsBarChart indicators={mockIndicators} dateRange={mockDateRange} height={mockHeight} />
|
||||
);
|
||||
export const CustomHeight: Story<void> = () => {
|
||||
const mockHeight = '500px';
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsBarChart
|
||||
indicators={mockIndicators}
|
||||
field={mockField}
|
||||
dateRange={mockDateRange}
|
||||
height={mockHeight}
|
||||
/>
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,39 +16,44 @@ import { IndicatorsBarChart } from './indicators_barchart';
|
|||
moment.suppressDeprecationWarnings = true;
|
||||
moment.tz.setDefault('UTC');
|
||||
|
||||
const mockIndicators: ChartSeries[] = [
|
||||
{
|
||||
x: '1 Jan 2022 00:00:00 GMT',
|
||||
y: 0,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 00:00:00 GMT',
|
||||
y: 10,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 12:00:00 GMT',
|
||||
y: 25,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 18:00:00 GMT',
|
||||
y: 15,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
];
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
const mockDateRange: TimeRangeBounds = {
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(1, 'days'),
|
||||
};
|
||||
|
||||
describe('<IndicatorsBarChart />', () => {
|
||||
it('should render barchart', () => {
|
||||
const mockIndicators: ChartSeries[] = [
|
||||
{
|
||||
x: '1 Jan 2022 00:00:00 GMT',
|
||||
y: 0,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 00:00:00 GMT',
|
||||
y: 10,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 12:00:00 GMT',
|
||||
y: 25,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 18:00:00 GMT',
|
||||
y: 15,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
];
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
const mockDateRange: TimeRangeBounds = {
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(1, 'days'),
|
||||
};
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsBarChart indicators={mockIndicators} dateRange={mockDateRange} />
|
||||
<IndicatorsBarChart
|
||||
indicators={mockIndicators}
|
||||
dateRange={mockDateRange}
|
||||
field={mockField}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,28 +9,55 @@ import React, { VFC } from 'react';
|
|||
import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '@elastic/charts';
|
||||
import { EuiThemeProvider } from '@elastic/eui';
|
||||
import { TimeRangeBounds } from '@kbn/data-plugin/common';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { barChartTimeAxisLabelFormatter } from '../../../../common/utils/dates';
|
||||
import { ChartSeries } from '../../hooks/use_aggregated_indicators';
|
||||
|
||||
export const TIMELINE_BUTTON_TEST_ID = 'tiTimelineButton';
|
||||
|
||||
const ID = 'tiIndicator';
|
||||
const DEFAULT_CHART_HEIGHT = '200px';
|
||||
const DEFAULT_CHART_WIDTH = '100%';
|
||||
|
||||
export interface IndicatorsBarChartProps {
|
||||
/**
|
||||
* Array of indicators already processed to be consumed by the BarSeries component from the @elastic/charts library.
|
||||
*/
|
||||
indicators: ChartSeries[];
|
||||
/**
|
||||
* Min and max dates to nicely format the label in the @elastic/charts Axis component.
|
||||
*/
|
||||
dateRange: TimeRangeBounds;
|
||||
/**
|
||||
* Indicator field selected in the IndicatorFieldSelector component, passed to the {@link AddToTimeline} to populate the timeline.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Option height value to override the default {@link DEFAULT_CHART_HEIGHT} default barchart height.
|
||||
*/
|
||||
height?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a barchart of aggregated indicators using the @elastic/charts library.
|
||||
*/
|
||||
export const IndicatorsBarChart: VFC<IndicatorsBarChartProps> = ({
|
||||
indicators,
|
||||
dateRange,
|
||||
field,
|
||||
height = DEFAULT_CHART_HEIGHT,
|
||||
}) => {
|
||||
return (
|
||||
<EuiThemeProvider>
|
||||
<Chart size={{ width: DEFAULT_CHART_WIDTH, height }}>
|
||||
<Settings showLegend showLegendExtra legendPosition={Position.Right} />
|
||||
<Settings
|
||||
showLegend
|
||||
showLegendExtra
|
||||
legendPosition={Position.Right}
|
||||
legendAction={({ label }) => (
|
||||
<AddToTimeline data={label} field={field} testId={TIMELINE_BUTTON_TEST_ID} />
|
||||
)}
|
||||
/>
|
||||
<Axis
|
||||
id={`${ID}TimeAxis`}
|
||||
position={Position.Bottom}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { TimeRange } from '@kbn/es-query';
|
|||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { StoryProvidersComponent } from '../../../../common/mocks/story_providers';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { Aggregation, AGGREGATION_NAME } from '../../hooks/use_aggregated_indicators';
|
||||
import { DEFAULT_TIME_RANGE } from '../../hooks/use_filters/utils';
|
||||
import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper';
|
||||
|
@ -24,93 +25,101 @@ export default {
|
|||
title: 'IndicatorsBarChartWrapper',
|
||||
};
|
||||
|
||||
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
|
||||
const mockIndexPattern: DataView = {
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
} as DataViewField,
|
||||
{
|
||||
name: 'threat.feed.name',
|
||||
type: 'string',
|
||||
} as DataViewField,
|
||||
],
|
||||
} as DataView;
|
||||
export const Default: Story<void> = () => {
|
||||
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
|
||||
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
const numberOfDays: number = 1;
|
||||
const aggregation1: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
const mockIndexPattern: DataView = {
|
||||
fields: [
|
||||
{
|
||||
doc_count: 0,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
} as DataViewField,
|
||||
{
|
||||
doc_count: 10,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
name: 'threat.feed.name',
|
||||
type: 'string',
|
||||
} as DataViewField,
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH Malware',
|
||||
};
|
||||
const aggregation2: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 20,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
doc_count: 8,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
};
|
||||
const mockData = {
|
||||
search: {
|
||||
search: () =>
|
||||
of({
|
||||
rawResponse: {
|
||||
aggregations: {
|
||||
[AGGREGATION_NAME]: {
|
||||
buckets: [aggregation1, aggregation2],
|
||||
} as DataView;
|
||||
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
const numberOfDays: number = 1;
|
||||
const aggregation1: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 0,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
doc_count: 10,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH Malware',
|
||||
};
|
||||
const aggregation2: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 20,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
doc_count: 8,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
};
|
||||
|
||||
const dataServiceMock = {
|
||||
search: {
|
||||
search: () =>
|
||||
of({
|
||||
rawResponse: {
|
||||
aggregations: {
|
||||
[AGGREGATION_NAME]: {
|
||||
buckets: [aggregation1, aggregation2],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
calculateBounds: () => ({
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(numberOfDays, 'days'),
|
||||
}),
|
||||
},
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
calculateBounds: () => ({
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(numberOfDays, 'days'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
filterManager: {
|
||||
getFilters: () => {},
|
||||
setFilters: () => {},
|
||||
getUpdates$: () => of(),
|
||||
},
|
||||
},
|
||||
filterManager: {
|
||||
getFilters: () => {},
|
||||
setFilters: () => {},
|
||||
getUpdates$: () => of(),
|
||||
},
|
||||
},
|
||||
} as unknown as DataPublicPluginStart;
|
||||
} as unknown as DataPublicPluginStart;
|
||||
|
||||
const mockUiSettings = { get: () => {} } as unknown as IUiSettingsClient;
|
||||
const uiSettingsMock = {
|
||||
get: () => {},
|
||||
} as unknown as IUiSettingsClient;
|
||||
|
||||
const timelinesMock = mockKibanaTimelinesService;
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return (
|
||||
<StoryProvidersComponent kibana={{ data: mockData, uiSettings: mockUiSettings }}>
|
||||
<StoryProvidersComponent
|
||||
kibana={{ data: dataServiceMock, uiSettings: uiSettingsMock, timelines: timelinesMock }}
|
||||
>
|
||||
<IndicatorsBarChartWrapper timeRange={mockTimeRange} indexPattern={mockIndexPattern} />
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
|
|
|
@ -18,13 +18,25 @@ import { IndicatorsBarChart } from '../indicators_barchart/indicators_barchart';
|
|||
const DEFAULT_FIELD = RawIndicatorFieldId.Feed;
|
||||
|
||||
export interface IndicatorsBarChartWrapperProps {
|
||||
/**
|
||||
* From and to values received from the KQL bar and passed down to the hook to query data.
|
||||
*/
|
||||
timeRange?: TimeRange;
|
||||
/**
|
||||
* List of fields coming from the Security Solution sourcerer data view, passed down to the {@link IndicatorFieldSelector} to populate the dropdown.
|
||||
*/
|
||||
indexPattern: SecuritySolutionDataViewBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the {@link IndicatorsBarChart} and {@link IndicatorsFieldSelector} components,
|
||||
* and handles retrieving aggregated indicator data.
|
||||
*/
|
||||
export const IndicatorsBarChartWrapper = memo<IndicatorsBarChartWrapperProps>(
|
||||
({ timeRange, indexPattern }) => {
|
||||
const { dateRange, indicators, onFieldChange } = useAggregatedIndicators({ timeRange });
|
||||
const { dateRange, indicators, selectedField, onFieldChange } = useAggregatedIndicators({
|
||||
timeRange,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -47,7 +59,11 @@ export const IndicatorsBarChartWrapper = memo<IndicatorsBarChartWrapperProps>(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{timeRange ? <IndicatorsBarChart indicators={indicators} dateRange={dateRange} /> : <></>}
|
||||
{timeRange ? (
|
||||
<IndicatorsBarChart indicators={indicators} dateRange={dateRange} field={selectedField} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,8 +33,7 @@ export const Default: Story<void> = () => {
|
|||
return (
|
||||
<IndicatorsFieldSelector
|
||||
indexPattern={mockIndexPattern}
|
||||
// eslint-disable-next-line no-console
|
||||
valueChange={(value: string) => console.log(value)}
|
||||
valueChange={(value: string) => window.alert(`${value} selected`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -43,19 +42,12 @@ export const WithDefaultValue: Story<void> = () => {
|
|||
return (
|
||||
<IndicatorsFieldSelector
|
||||
indexPattern={mockIndexPattern}
|
||||
// eslint-disable-next-line no-console
|
||||
valueChange={(value: string) => console.log(value)}
|
||||
valueChange={(value: string) => window.alert(`${value} selected`)}
|
||||
defaultStackByValue={RawIndicatorFieldId.LastSeen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoData: Story<void> = () => {
|
||||
return (
|
||||
<IndicatorsFieldSelector
|
||||
indexPattern={{ fields: [] } as any}
|
||||
// eslint-disable-next-line no-console
|
||||
valueChange={(value: string) => console.log(value)}
|
||||
/>
|
||||
);
|
||||
return <IndicatorsFieldSelector indexPattern={{ fields: [] } as any} valueChange={() => {}} />;
|
||||
};
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map';
|
||||
import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyout } from './indicators_flyout';
|
||||
|
||||
|
@ -18,47 +20,34 @@ export default {
|
|||
title: 'IndicatorsFlyout',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
const mockFieldTypesMap: { [id: string]: string } = {
|
||||
'threat.indicator.ip': 'ip',
|
||||
'threat.indicator.first_seen': 'date',
|
||||
};
|
||||
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
uiSettings: mockUiSettingsService(),
|
||||
timelines: mockKibanaTimelinesService,
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
const mockFieldTypesMap = generateFieldTypeMap();
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsFlyout
|
||||
indicator={mockIndicator}
|
||||
fieldTypesMap={mockFieldTypesMap}
|
||||
// eslint-disable-next-line no-console
|
||||
closeFlyout={() => console.log('closing')}
|
||||
closeFlyout={() => window.alert('Closing flyout')}
|
||||
/>
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmtpyIndicator: Story<void> = () => {
|
||||
export const EmptyIndicator: Story<void> = () => {
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsFlyout
|
||||
indicator={{ fields: {} } as Indicator}
|
||||
fieldTypesMap={{}}
|
||||
// eslint-disable-next-line no-console
|
||||
closeFlyout={() => console.log('closing')}
|
||||
closeFlyout={() => window.alert('Closing flyout')}
|
||||
/>
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
|
|
|
@ -11,16 +11,14 @@ import { IndicatorsFlyout, SUBTITLE_TEST_ID, TITLE_TEST_ID } from './indicators_
|
|||
import { generateMockIndicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { dateFormatter } from '../../../../common/utils/dates';
|
||||
import { mockUiSetting } from '../../../../common/mocks/mock_kibana_ui_setting';
|
||||
import { mockUiSetting } from '../../../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map';
|
||||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
import { displayValue } from '../../lib/display_value';
|
||||
|
||||
const mockIndicator = generateMockIndicator();
|
||||
const mockFieldTypesMap: { [id: string]: string } = {
|
||||
'@timestamp': 'date',
|
||||
'threat.feed.name': 'string',
|
||||
};
|
||||
const mockFieldTypesMap = generateFieldTypeMap();
|
||||
|
||||
describe('<IndicatorsFlyout />', () => {
|
||||
it('should render ioc id in title and first_seen in subtitle', () => {
|
||||
|
|
|
@ -36,11 +36,23 @@ const enum TAB_IDS {
|
|||
}
|
||||
|
||||
export interface IndicatorsFlyoutProps {
|
||||
/**
|
||||
* Indicator passed down to the different tabs (table and json views).
|
||||
*/
|
||||
indicator: Indicator;
|
||||
/**
|
||||
* Object mapping each field with their type to ease display in the {@link IndicatorsFlyoutTable} component.
|
||||
*/
|
||||
fieldTypesMap: { [id: string]: string };
|
||||
/**
|
||||
* Event to close flyout (used by {@link EuiFlyout}).
|
||||
*/
|
||||
closeFlyout: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leverages the {@link EuiFlyout} from the @elastic/eui library to dhow the details of a specific {@link Indicator}.
|
||||
*/
|
||||
export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({
|
||||
indicator,
|
||||
fieldTypesMap,
|
||||
|
|
|
@ -15,9 +15,9 @@ export default {
|
|||
title: 'IndicatorsFlyoutJson',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
return <IndicatorsFlyoutJson indicator={mockIndicator} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -14,9 +14,16 @@ export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutJsonEmptyPrompt';
|
|||
export const CODE_BLOCK_TEST_ID = 'tiFlyoutJsonCodeBlock';
|
||||
|
||||
export interface IndicatorsFlyoutJsonProps {
|
||||
/**
|
||||
* Indicator to display in json format.
|
||||
*/
|
||||
indicator: Indicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays all the properties and values of an {@link Indicator} in json view,
|
||||
* using the {@link EuiCodeBlock} from the @elastic/eui library.
|
||||
*/
|
||||
export const IndicatorsFlyoutJson: VFC<IndicatorsFlyoutJsonProps> = ({ indicator }) => {
|
||||
return Object.keys(indicator).length === 0 ? (
|
||||
<EuiEmptyPrompt
|
||||
|
|
|
@ -9,36 +9,26 @@ import React from 'react';
|
|||
import { Story } from '@storybook/react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants';
|
||||
import { IndicatorsFlyoutTable } from './indicators_flyout_table';
|
||||
import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map';
|
||||
import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyoutTable } from './indicators_flyout_table';
|
||||
|
||||
export default {
|
||||
component: IndicatorsFlyoutTable,
|
||||
title: 'IndicatorsFlyoutTable',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
const mockFieldTypesMap: { [id: string]: string } = {
|
||||
'threat.indicator.ip': 'ip',
|
||||
'threat.indicator.first_seen': 'date',
|
||||
};
|
||||
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
const mockFieldTypesMap = generateFieldTypeMap();
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService(),
|
||||
timelines: mockKibanaTimelinesService,
|
||||
} as unknown as CoreStart);
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsFlyoutTable indicator={mockIndicator} fieldTypesMap={mockFieldTypesMap} />
|
||||
|
@ -48,11 +38,6 @@ export const Default: Story<void> = () => {
|
|||
|
||||
export const EmptyIndicator: Story<void> = () => {
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsFlyoutTable
|
||||
indicator={{ fields: {} } as unknown as Indicator}
|
||||
fieldTypesMap={{}}
|
||||
/>
|
||||
</KibanaReactContext.Provider>
|
||||
<IndicatorsFlyoutTable indicator={{ fields: {} } as unknown as Indicator} fieldTypesMap={{}} />
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Indicator,
|
||||
RawIndicatorFieldId,
|
||||
} from '../../../../../common/types/indicator';
|
||||
import { generateFieldTypeMap } from '../../../../common/mocks/mock_field_type_map';
|
||||
import {
|
||||
EMPTY_PROMPT_TEST_ID,
|
||||
IndicatorsFlyoutTable,
|
||||
|
@ -22,10 +23,7 @@ import { unwrapValue } from '../../lib/unwrap_value';
|
|||
import { displayValue } from '../../lib/display_value';
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
const mockFieldTypesMap: { [id: string]: string } = {
|
||||
'@timestamp': 'date',
|
||||
'threat.feed.name': 'string',
|
||||
};
|
||||
const mockFieldTypesMap = generateFieldTypeMap();
|
||||
|
||||
describe('<IndicatorsFlyoutTable />', () => {
|
||||
it('should render fields and values in table', () => {
|
||||
|
|
|
@ -9,10 +9,12 @@ import { EuiEmptyPrompt, EuiInMemoryTable } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { VFC } from 'react';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { IndicatorField } from '../indicator_field/indicator_field';
|
||||
|
||||
export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutTableEmptyPrompt';
|
||||
export const TABLE_TEST_ID = 'tiFlyoutTableMemoryTable';
|
||||
export const TIMELINE_BUTTON_TEST_ID = 'tiFlyoutTableRowTimelineButton';
|
||||
|
||||
const search = {
|
||||
box: {
|
||||
|
@ -22,16 +24,42 @@ const search = {
|
|||
};
|
||||
|
||||
export interface IndicatorsFlyoutTableProps {
|
||||
/**
|
||||
* Indicator to display in table view.
|
||||
*/
|
||||
indicator: Indicator;
|
||||
/**
|
||||
* Object mapping each field with their type to ease display in the {@link IndicatorField} component.
|
||||
*/
|
||||
fieldTypesMap: { [id: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays all the properties and values of an {@link Indicator} in table view,
|
||||
* using the {@link EuiInMemoryTable} from the @elastic/eui library.
|
||||
*/
|
||||
export const IndicatorsFlyoutTable: VFC<IndicatorsFlyoutTableProps> = ({
|
||||
indicator,
|
||||
fieldTypesMap,
|
||||
}) => {
|
||||
const items: string[] = Object.keys(indicator.fields);
|
||||
const columns = [
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutTable.actionsColumnLabel"
|
||||
defaultMessage="Actions"
|
||||
/>
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
render: (field: string) => (
|
||||
<AddToTimeline data={indicator} field={field} testId={TIMELINE_BUTTON_TEST_ID} />
|
||||
),
|
||||
},
|
||||
],
|
||||
width: '72px',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
|
|
|
@ -5,18 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
|
||||
import React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { RawIndicatorFieldId } from '../../../../../../common/types/indicator';
|
||||
import { EuiDataGridColumn, EuiText } from '@elastic/eui';
|
||||
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
|
||||
import { IndicatorsFieldBrowser } from '../../indicators_field_browser';
|
||||
import { ComputedIndicatorFieldId } from '../cell_renderer';
|
||||
|
||||
export interface Column {
|
||||
id: RawIndicatorFieldId | ComputedIndicatorFieldId;
|
||||
displayAsText: string;
|
||||
}
|
||||
|
||||
export const useToolbarOptions = ({
|
||||
browserFields,
|
||||
|
@ -31,7 +24,7 @@ export const useToolbarOptions = ({
|
|||
start: number;
|
||||
end: number;
|
||||
indicatorCount: number;
|
||||
columns: Column[];
|
||||
columns: EuiDataGridColumn[];
|
||||
onResetColumns: () => void;
|
||||
onToggleColumn: (columnId: string) => void;
|
||||
}) =>
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import React from 'react';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants';
|
||||
import { mockTriggersActionsUiService } from '../../../../common/mocks/mock_kibana_triggers_actions_ui_service';
|
||||
import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsTable } from './indicators_table';
|
||||
|
||||
|
@ -19,34 +20,19 @@ export default {
|
|||
title: 'IndicatorsTable',
|
||||
};
|
||||
|
||||
const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator());
|
||||
const mockIndexPattern: DataView = undefined as unknown as DataView;
|
||||
|
||||
const stub = () => void 0;
|
||||
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
triggersActionsUi: {
|
||||
getFieldBrowser: () => (
|
||||
<EuiText style={{ display: 'inline' }} size="xs">
|
||||
Fields
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
|
||||
export function WithIndicators() {
|
||||
const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator());
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
uiSettings: mockUiSettingsService(),
|
||||
timelines: mockKibanaTimelinesService,
|
||||
triggersActionsUi: mockTriggersActionsUiService,
|
||||
} as unknown as CoreStart);
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<IndicatorsTable
|
||||
|
|
|
@ -6,20 +6,29 @@
|
|||
*/
|
||||
|
||||
import React, { VFC, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { EuiDataGrid, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridColumnCellActionProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiDataGridColumn } from '@elastic/eui/src/components/datagrid/data_grid_types';
|
||||
import { BrowserFields, SecuritySolutionDataViewBase } from '../../../../types';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { cellRendererFactory, ComputedIndicatorFieldId } from './cell_renderer';
|
||||
import { EmptyState } from '../../../../components/empty_state';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { IndicatorsTableContext, IndicatorsTableContextValue } from './context';
|
||||
import { IndicatorsFlyout } from '../indicators_flyout/indicators_flyout';
|
||||
import { Pagination } from '../../hooks/use_indicators';
|
||||
import { Column, useToolbarOptions } from './hooks/use_toolbar_options';
|
||||
import { useToolbarOptions } from './hooks/use_toolbar_options';
|
||||
|
||||
const defaultColumns: Column[] = [
|
||||
const defaultColumns: EuiDataGridColumn[] = [
|
||||
{
|
||||
id: RawIndicatorFieldId.TimeStamp,
|
||||
displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.timestampColumnTitle', {
|
||||
|
@ -73,6 +82,7 @@ export interface IndicatorsTableProps {
|
|||
}
|
||||
|
||||
export const TABLE_TEST_ID = 'tiIndicatorsTable';
|
||||
export const CELL_TIMELINE_BUTTON_TEST_ID = 'tiIndicatorsTableCellTimelineButton';
|
||||
|
||||
const gridStyle = {
|
||||
border: 'horizontal',
|
||||
|
@ -91,9 +101,9 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
|||
indexPattern,
|
||||
browserFields,
|
||||
}) => {
|
||||
const [columns, setColumns] = useState<Column[]>(defaultColumns);
|
||||
const [columns, setColumns] = useState<EuiDataGridColumn[]>(defaultColumns);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Array<Column['id']>>(
|
||||
const [visibleColumns, setVisibleColumns] = useState<Array<EuiDataGridColumn['id']>>(
|
||||
columns.map((column) => column.id)
|
||||
);
|
||||
|
||||
|
@ -152,8 +162,8 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
|||
|
||||
const handleToggleColumn = useCallback((columnId: string) => {
|
||||
setColumns((currentColumns) => {
|
||||
const columnsMatchingId = ({ id }: Column) => id === columnId;
|
||||
const columnsNotMatchingId = (column: Column) => !columnsMatchingId(column);
|
||||
const columnsMatchingId = ({ id }: EuiDataGridColumn) => id === columnId;
|
||||
const columnsNotMatchingId = (column: EuiDataGridColumn) => !columnsMatchingId(column);
|
||||
|
||||
const enabled = Boolean(currentColumns.find(columnsMatchingId));
|
||||
|
||||
|
@ -174,6 +184,22 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
|||
setVisibleColumns(columns.map(({ id }) => id));
|
||||
}, [columns]);
|
||||
|
||||
useMemo(() => {
|
||||
columns.map(
|
||||
(col: EuiDataGridColumn) =>
|
||||
(col.cellActions = [
|
||||
({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => (
|
||||
<AddToTimeline
|
||||
data={indicators[rowIndex % pagination.pageSize]}
|
||||
field={columnId}
|
||||
component={Component}
|
||||
testId={CELL_TIMELINE_BUTTON_TEST_ID}
|
||||
/>
|
||||
),
|
||||
])
|
||||
);
|
||||
}, [columns, indicators, pagination]);
|
||||
|
||||
const toolbarOptions = useToolbarOptions({
|
||||
browserFields,
|
||||
start,
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants';
|
||||
import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_settings_service';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { OpenIndicatorFlyoutButton } from './open_indicator_flyout_button';
|
||||
|
||||
|
@ -23,20 +22,7 @@ export default {
|
|||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
const coreMock = {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const settings = {
|
||||
[DEFAULT_DATE_FORMAT]: '',
|
||||
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
|
||||
};
|
||||
// @ts-expect-error
|
||||
return settings[key];
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
const KibanaReactContext = createKibanaReactContext({ uiSettings: mockUiSettingsService() });
|
||||
|
||||
const Template: ComponentStory<typeof OpenIndicatorFlyoutButton> = (args) => {
|
||||
return (
|
||||
|
|
|
@ -13,11 +13,23 @@ import { Indicator } from '../../../../../common/types/indicator';
|
|||
export const BUTTON_TEST_ID = 'tiToggleIndicatorFlyoutButton';
|
||||
|
||||
export interface OpenIndicatorFlyoutButtonProps {
|
||||
/**
|
||||
* {@link Indicator} passed to the flyout component.
|
||||
*/
|
||||
indicator: Indicator;
|
||||
/**
|
||||
* Method called by the onClick event to open/close the flyout.
|
||||
*/
|
||||
onOpen: (indicator: Indicator) => void;
|
||||
/**
|
||||
* Boolean used to change the color and type of icon depending on the state of the flyout.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button added to the actions column of the indicators table to open/close the IndicatorFlyout component.
|
||||
*/
|
||||
export const OpenIndicatorFlyoutButton: VFC<OpenIndicatorFlyoutButtonProps> = ({
|
||||
indicator,
|
||||
onOpen,
|
||||
|
@ -31,18 +43,16 @@ export const OpenIndicatorFlyoutButton: VFC<OpenIndicatorFlyoutButtonProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip content={buttonLabel} delay="long">
|
||||
<EuiButtonIcon
|
||||
data-test-subj={BUTTON_TEST_ID}
|
||||
color={isOpen ? 'primary' : 'text'}
|
||||
iconType={isOpen ? 'minimize' : 'expand'}
|
||||
isSelected={isOpen}
|
||||
iconSize="s"
|
||||
aria-label={buttonLabel}
|
||||
onClick={() => onOpen(indicator)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
<EuiToolTip content={buttonLabel} delay="long">
|
||||
<EuiButtonIcon
|
||||
data-test-subj={BUTTON_TEST_ID}
|
||||
color={isOpen ? 'primary' : 'text'}
|
||||
iconType={isOpen ? 'minimize' : 'expand'}
|
||||
isSelected={isOpen}
|
||||
iconSize="s"
|
||||
aria-label={buttonLabel}
|
||||
onClick={() => onOpen(indicator)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface UseAggregatedIndicatorsValue {
|
|||
indicators: ChartSeries[];
|
||||
onFieldChange: (field: string) => void;
|
||||
dateRange: TimeRangeBounds;
|
||||
selectedField: string;
|
||||
}
|
||||
|
||||
export interface Aggregation {
|
||||
|
@ -209,5 +210,6 @@ export const useAggregatedIndicators = ({
|
|||
dateRange,
|
||||
indicators,
|
||||
onFieldChange,
|
||||
selectedField: field,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('<IndicatorsPage />', () => {
|
|||
).mockReturnValue({
|
||||
dateRange: { min: moment(), max: moment() },
|
||||
indicators: [],
|
||||
selectedField: '',
|
||||
onFieldChange: () => {},
|
||||
});
|
||||
|
||||
|
|
|
@ -10,8 +10,9 @@ import {
|
|||
generateMockIndicator,
|
||||
generateMockUrlIndicator,
|
||||
Indicator,
|
||||
RawIndicatorFieldId,
|
||||
} from '../../../../common/types/indicator';
|
||||
import { displayValue } from './display_value';
|
||||
import { displayField, displayValue } from './display_value';
|
||||
|
||||
type ExpectedIndicatorValue = string | null;
|
||||
|
||||
|
@ -40,3 +41,18 @@ describe('displayValue()', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('displayValueField()', () => {
|
||||
it('should return correct RawIndicatorFieldId for valid field', () => {
|
||||
const mockIndicator = generateMockIndicator();
|
||||
const result = displayField(mockIndicator);
|
||||
expect(result).toEqual(RawIndicatorFieldId.Ip);
|
||||
});
|
||||
|
||||
it('should return null for invalid field', () => {
|
||||
const mockIndicator = generateMockIndicator();
|
||||
mockIndicator.fields['threat.indicator.type'] = ['abc'];
|
||||
const result = displayField(mockIndicator);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ export type IndicatorTypePredicate = (indicatorType: string | null) => boolean;
|
|||
type MapperRule = [predicate: IndicatorTypePredicate, extract: IndicatorValueExtractor];
|
||||
|
||||
/**
|
||||
* Predicates to help identify identicator by type
|
||||
* Predicates to help identify indicator by type
|
||||
*/
|
||||
const isIpIndicator: IndicatorTypePredicate = (indicatorType) =>
|
||||
!!indicatorType && ['ipv4-addr', 'ipv6-addr'].includes(indicatorType);
|
||||
|
@ -56,6 +56,25 @@ const findMappingRule = (indicatorType: string | null): IndicatorValueExtractor
|
|||
*/
|
||||
const rules: Record<string, IndicatorValueExtractor> = {};
|
||||
|
||||
/**
|
||||
* Mapping between the indicator type and the {@link RawIndicatorFieldId}.
|
||||
*/
|
||||
const indicatorTypeToField: { [id: string]: RawIndicatorFieldId } = {
|
||||
file: RawIndicatorFieldId.FileSha256,
|
||||
'ipv4-addr': RawIndicatorFieldId.Ip,
|
||||
'ipv6-addr': RawIndicatorFieldId.Ip,
|
||||
url: RawIndicatorFieldId.UrlFull,
|
||||
};
|
||||
|
||||
/**
|
||||
* Find and return indicator display value field
|
||||
*/
|
||||
export const displayField = (indicator: Indicator): string | null => {
|
||||
const indicatorType = (unwrapValue(indicator, RawIndicatorFieldId.Type) || '').toLowerCase();
|
||||
|
||||
return indicatorTypeToField[indicatorType];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find and return indicator display value, if possible
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,407 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<AddToTimeline /> should render empty component when calculated value is - 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<AddToTimeline /> should render empty component when data is - 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<AddToTimeline /> should render empty component when field doesn't exist in data 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<AddToTimeline /> should render empty component when field exist in data but isn't supported 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<AddToTimeline /> should render timeline button when Indicator data 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<AddToTimeline /> should render timeline button when string data 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
data-test-subj="test-add-to-timeline"
|
||||
>
|
||||
Add To Timeline
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { AddToTimeline } from './add_to_timeline';
|
||||
|
||||
export default {
|
||||
component: AddToTimeline,
|
||||
title: 'AddToTimeline',
|
||||
};
|
||||
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
timelines: mockKibanaTimelinesService,
|
||||
} as unknown as CoreStart);
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<AddToTimeline data={mockData} field={mockField} />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithIndicator: Story<void> = () => {
|
||||
const mockData: string = 'ip';
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<AddToTimeline data={mockData} field={mockField} />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyValue: Story<void> = () => (
|
||||
<KibanaReactContext.Provider>
|
||||
<AddToTimeline data={EMPTY_VALUE} field={mockField} />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { AddToTimeline } from './add_to_timeline';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
|
||||
describe('<AddToTimeline />', () => {
|
||||
it('should render timeline button when Indicator data', () => {
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<AddToTimeline field={mockField} data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render timeline button when string data', () => {
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
const mockString: string = 'ip';
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<AddToTimeline field={mockField} data={mockString} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`should render empty component when field doesn't exist in data`, () => {
|
||||
const mockField: string = 'abc';
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<AddToTimeline field={mockField} data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`should render empty component when field exist in data but isn't supported`, () => {
|
||||
const mockField: string = 'abc';
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
mockData.fields['threat.indicator.type'] = ['abc'];
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<AddToTimeline field={mockField} data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`should render empty component when calculated value is ${EMPTY_VALUE}`, () => {
|
||||
const mockField: string = 'threat.indicator.first_seen';
|
||||
const mockData: Indicator = generateMockIndicator();
|
||||
mockData.fields['threat.indicator.first_seen'] = [''];
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<AddToTimeline field={mockField} data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`should render empty component when data is ${EMPTY_VALUE}`, () => {
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
const mockData = EMPTY_VALUE;
|
||||
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<AddToTimeline field={mockField} data={mockData} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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, { VFC } from 'react';
|
||||
import { DataProvider, QueryOperator } from '@kbn/timelines-plugin/common';
|
||||
import { AddToTimelineButtonProps } from '@kbn/timelines-plugin/public';
|
||||
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui/src/components/button';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { displayField, displayValue } from '../../../indicators/lib/display_value';
|
||||
import { ComputedIndicatorFieldId } from '../../../indicators/components/indicators_table/cell_renderer';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import { unwrapValue } from '../../../indicators/lib/unwrap_value';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
export interface AddToTimelineProps {
|
||||
/**
|
||||
* Value passed to the timeline. Used in combination with field if is type of {@link Indicator}.
|
||||
*/
|
||||
data: Indicator | string;
|
||||
/**
|
||||
* Value passed to the timeline.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Only used with `EuiDataGrid` (see {@link AddToTimelineButtonProps}).
|
||||
*/
|
||||
component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
/**
|
||||
* Used as `data-test-subj` value for e2e tests.
|
||||
*/
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to timeline button, used in many places throughout the TI plugin.
|
||||
* Support being passed a {@link Indicator} or a string, can be used in a `EuiDataGrid` or as a normal button.
|
||||
* Leverages the built-in functionality retrieves from the timeLineService (see ThreatIntelligenceSecuritySolutionContext in x-pack/plugins/threat_intelligence/public/types.ts)
|
||||
* Clicking on the button will add a key-value pair to an Untitled timeline.
|
||||
*
|
||||
* @returns add to timeline button or an empty component.
|
||||
*/
|
||||
export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, component, testId }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const addToTimelineButton =
|
||||
useKibana().services.timelines.getHoverActions().getAddToTimelineButton;
|
||||
|
||||
let value: string | null;
|
||||
if (typeof data === 'string') {
|
||||
value = data;
|
||||
} else if (field === ComputedIndicatorFieldId.DisplayValue) {
|
||||
field = displayField(data) || '';
|
||||
value = displayValue(data);
|
||||
} else {
|
||||
value = unwrapValue(data, field as RawIndicatorFieldId);
|
||||
}
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !field) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const operator = ':' as QueryOperator;
|
||||
|
||||
const dataProvider: DataProvider[] = [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: `timeline-indicator-${field}-${value}`,
|
||||
name: value,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const addToTimelineProps: AddToTimelineButtonProps = {
|
||||
dataProvider,
|
||||
field,
|
||||
ownFocus: false,
|
||||
};
|
||||
if (component) addToTimelineProps.Component = component;
|
||||
|
||||
return (
|
||||
<div data-test-subj={testId} css={styles.button}>
|
||||
{addToTimelineButton(addToTimelineProps)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './add_to_timeline';
|
|
@ -5,11 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export function mockUiSetting(key: string): string | undefined {
|
||||
if (key === 'dateFormat') {
|
||||
return 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
}
|
||||
if (key === 'dateFormat:tz') {
|
||||
return 'America/New_York';
|
||||
}
|
||||
}
|
||||
import { CSSObject } from '@emotion/react';
|
||||
|
||||
export const useStyles = () => {
|
||||
const button: CSSObject = {
|
||||
display: 'inline-flex',
|
||||
};
|
||||
|
||||
return {
|
||||
button,
|
||||
};
|
||||
};
|
|
@ -14,6 +14,7 @@ import {
|
|||
FieldSpec,
|
||||
} from '@kbn/data-views-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
|
||||
import { DataViewBase } from '@kbn/es-query';
|
||||
|
@ -40,6 +41,7 @@ export type Services = {
|
|||
storage: Storage;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
triggersActionsUi: TriggersActionsStart;
|
||||
timelines: TimelinesUIStart;
|
||||
} & CoreStart;
|
||||
|
||||
export interface LicenseAware {
|
||||
|
@ -66,5 +68,8 @@ export interface SecuritySolutionPluginContext {
|
|||
* Get the user's license to drive the Threat Intelligence plugin's visibility.
|
||||
*/
|
||||
licenseService: LicenseAware;
|
||||
/**
|
||||
* Gets Security Solution shared information like browerFields, indexPattern and selectedPatterns in DataView
|
||||
*/
|
||||
sourcererDataView: SourcererDataView;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../timelines/tsconfig.json" },
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/unified_search/tsconfig.json" },
|
||||
|
|
|
@ -93,3 +93,5 @@ export function plugin() {
|
|||
|
||||
export { StatefulEventContext } from './components/stateful_event_context';
|
||||
export { TimelineContext } from './components/t_grid/shared';
|
||||
|
||||
export type { AddToTimelineButtonProps } from './components/hover_actions/actions/add_to_timeline';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue