[TIP] use indicators flyout in cases view (#145459)

- modify indicators flyout to hide filter in/out button
- delete duplicate flyout component in cases module and use indicators flyout instead
- add fields to query to fetch indicator by id
- sort fields in flyout table views (overview and table tabs)
This commit is contained in:
Philippe Oberti 2022-11-28 21:51:08 -06:00 committed by GitHub
parent 9e0da9802e
commit f2532f3424
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 827 additions and 828 deletions

View file

@ -9,6 +9,7 @@ import React from 'react';
import { Story } from '@storybook/react';
import { of } from 'rxjs';
import { IKibanaSearchResponse } from '@kbn/data-plugin/common';
import { generateMockFileIndicator } from '../../../../../../common/types/indicator';
import { CommentChildren } from './comment_children';
import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers';
import { AttachmentMetadata } from '../../../utils';
@ -23,7 +24,6 @@ export const Default: Story<void> = () => {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
const response: IKibanaSearchResponse = {
@ -31,12 +31,7 @@ export const Default: Story<void> = () => {
isPartial: false,
rawResponse: {
hits: {
hits: [
{
prop1: 'prop1',
prop2: 'prop2',
},
],
hits: [generateMockFileIndicator()],
},
},
};
@ -61,7 +56,6 @@ export const Loading: Story<void> = () => {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
const response: IKibanaSearchResponse = {

View file

@ -16,6 +16,7 @@ import {
import { AttachmentMetadata } from '../../../utils';
import { TestProvidersComponent } from '../../../../../common/mocks/test_providers';
import { useIndicatorById } from '../../../hooks';
import { generateMockFileIndicator, Indicator } from '../../../../../../common/types/indicator';
jest.mock('../../../hooks/use_indicator_by_id');
@ -26,14 +27,10 @@ describe('attachment_children initComponent', () => {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
(useIndicatorById as jest.MockedFunction<typeof useIndicatorById>).mockReturnValue({
indicator: {
prop1: 'prop1',
prop2: 'prop2',
},
indicator: generateMockFileIndicator(),
isLoading: false,
});
@ -53,11 +50,10 @@ describe('attachment_children initComponent', () => {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
(useIndicatorById as jest.MockedFunction<typeof useIndicatorById>).mockReturnValue({
indicator: {},
indicator: {} as Indicator,
isLoading: true,
});

View file

@ -8,10 +8,11 @@
import React, { useMemo, useState, VFC } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingLogo, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { Indicator } from '../../../../../../common/types/indicator';
import { IndicatorsFlyout } from '../../../../indicators/components/flyout';
import { useStyles } from '../styles';
import { useIndicatorById } from '../../../hooks';
import { AttachmentMetadata } from '../../../utils';
import { CasesFlyout } from '../flyout';
export const INDICATOR_NAME_TEST_ID = 'tiCasesIndicatorName';
export const INDICATOR_FEED_NAME_TEST_ID = 'tiCasesIndicatorFeedName';
@ -19,7 +20,7 @@ export const INDICATOR_TYPE_TEST_ID = 'tiCasesIndicatorTYPE';
export interface CommentChildrenProps {
/**
* Id of the document (indicator) to be fetched
* Indicator's id of the indicator to fetch
*/
id: string;
/**
@ -43,13 +44,13 @@ export const CommentChildren: VFC<CommentChildrenProps> = ({ id, metadata }) =>
const flyoutFragment = useMemo(
() =>
expanded ? (
<CasesFlyout
metadata={metadata}
rawDocument={indicator as Record<string, unknown>}
<IndicatorsFlyout
indicator={indicator as Indicator}
closeFlyout={() => setExpanded(false)}
kqlBarIntegration={true}
/>
) : null,
[expanded, indicator, metadata]
[expanded, indicator]
);
if (isLoading) {

View file

@ -1,206 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CasesFlyout should render flyout with json details 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
data-eui="EuiFlyout"
role="dialog"
>
<button
aria-label="Close this dialog"
data-test-subj="euiFlyoutCloseButton"
type="button"
/>
<div
class="euiFlyoutHeader emotion-euiFlyoutHeader-hasBorder"
>
<h2
class="euiTitle emotion-euiTitle-m"
data-test-subj="tiCasesFlyoutTitle"
id="simpleFlyoutTitle_generated-id"
>
Indicator details
</h2>
<div
class="euiSpacer euiSpacer--s emotion-euiSpacer-s"
/>
<div
class="euiText emotion-euiText-xs"
>
<p
data-test-subj="tiCasesFlyoutSubtitle"
>
First seen:
Dec 31, 2021 @ 20:01:01.000
</p>
</div>
<div
class="euiSpacer euiSpacer--m emotion-euiSpacer-m"
/>
</div>
<div
class="euiFlyoutBody emotion-euiFlyoutBody"
>
<div
class="euiFlyoutBody__overflow css-18yrfj9-noBanner"
tabindex="0"
>
<div
class="euiFlyoutBody__overflowContent"
>
<h2
class="euiTitle emotion-euiTitle-m"
>
abc
</h2>
<div>
<pre>
<code
data-code-language="json"
data-test-subj="tiCasesFlyoutJsonCodeBlock"
>
{
"prop1": "prop1",
"prop2": "prop2"
}
</code>
</pre>
</div>
</div>
</div>
</div>
<div
class="euiFlyoutFooter emotion-euiFlyoutFooter"
/>
</div>
</div>
</body>,
"container": <div>
<div
data-eui="EuiFlyout"
role="dialog"
>
<button
aria-label="Close this dialog"
data-test-subj="euiFlyoutCloseButton"
type="button"
/>
<div
class="euiFlyoutHeader emotion-euiFlyoutHeader-hasBorder"
>
<h2
class="euiTitle emotion-euiTitle-m"
data-test-subj="tiCasesFlyoutTitle"
id="simpleFlyoutTitle_generated-id"
>
Indicator details
</h2>
<div
class="euiSpacer euiSpacer--s emotion-euiSpacer-s"
/>
<div
class="euiText emotion-euiText-xs"
>
<p
data-test-subj="tiCasesFlyoutSubtitle"
>
First seen:
Dec 31, 2021 @ 20:01:01.000
</p>
</div>
<div
class="euiSpacer euiSpacer--m emotion-euiSpacer-m"
/>
</div>
<div
class="euiFlyoutBody emotion-euiFlyoutBody"
>
<div
class="euiFlyoutBody__overflow css-18yrfj9-noBanner"
tabindex="0"
>
<div
class="euiFlyoutBody__overflowContent"
>
<h2
class="euiTitle emotion-euiTitle-m"
>
abc
</h2>
<div>
<pre>
<code
data-code-language="json"
data-test-subj="tiCasesFlyoutJsonCodeBlock"
>
{
"prop1": "prop1",
"prop2": "prop2"
}
</code>
</pre>
</div>
</div>
</div>
</div>
<div
class="euiFlyoutFooter emotion-euiFlyoutFooter"
/>
</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],
}
`;

View file

@ -1,53 +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 { Story } from '@storybook/react';
import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers';
import { AttachmentMetadata } from '../../../utils';
import { CasesFlyout } from './flyout';
export default {
title: 'CasesFlyout',
};
export const Default: Story<void> = () => {
const metadata: AttachmentMetadata = {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
const rawDocument: Record<string, unknown> = {
prop1: 'prop1',
prop2: 'prop2',
};
const closeFlyout = () => window.alert('Closing flyout');
return (
<StoryProvidersComponent>
<CasesFlyout metadata={metadata} rawDocument={rawDocument} closeFlyout={closeFlyout} />
</StoryProvidersComponent>
);
};
export const EmptyRawDocument: Story<void> = () => {
const metadata: AttachmentMetadata = {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
const rawDocument = null as unknown as Record<string, unknown>;
const closeFlyout = () => window.alert('Closing flyout');
return (
<StoryProvidersComponent>
<CasesFlyout metadata={metadata} rawDocument={rawDocument} closeFlyout={closeFlyout} />
</StoryProvidersComponent>
);
};

View file

@ -1,34 +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 { render } from '@testing-library/react';
import { AttachmentMetadata } from '../../../utils';
import { TestProvidersComponent } from '../../../../../common/mocks/test_providers';
import { CasesFlyout } from './flyout';
describe('CasesFlyout', () => {
it('should render flyout with json details', () => {
const metadata: AttachmentMetadata = {
indicatorName: 'abc',
indicatorType: 'file',
indicatorFeedName: 'feed',
indicatorFirstSeen: '2022-01-01T01:01:01.000Z',
};
const rawDocument: Record<string, unknown> = {
prop1: 'prop1',
prop2: 'prop2',
};
const closeFlyout = () => window.alert('closing');
const component = render(
<TestProvidersComponent>
<CasesFlyout metadata={metadata} rawDocument={rawDocument} closeFlyout={closeFlyout} />
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -1,79 +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, { VFC } from 'react';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiSpacer,
EuiText,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { AttachmentMetadata } from '../../../utils';
import { DateFormatter } from '../../../../../components/date_formatter/date_formatter';
import { CasesFlyoutJson } from '../json';
export const TITLE_TEST_ID = 'tiCasesFlyoutTitle';
export const SUBTITLE_TEST_ID = 'tiCasesFlyoutSubtitle';
export interface CasesFlyoutProps {
/**
* Metadata saved in the case attachment (indicator)
*/
metadata: AttachmentMetadata;
/**
* Document (indicator) retrieve by id
*/
rawDocument: Record<string, unknown>;
/**
* 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 CasesFlyout: VFC<CasesFlyoutProps> = ({ metadata, rawDocument, closeFlyout }) => {
const flyoutTitleId = useGeneratedHtmlId({
prefix: 'simpleFlyoutTitle',
});
return (
<EuiFlyout onClose={closeFlyout} aria-labelledby={flyoutTitleId}>
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2 data-test-subj={TITLE_TEST_ID} id={flyoutTitleId}>
<FormattedMessage
id="xpack.threatIntelligence.cases.flyout.title"
defaultMessage="Indicator details"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size={'xs'}>
<p data-test-subj={SUBTITLE_TEST_ID}>
<FormattedMessage
id="xpack.threatIntelligence.cases.flyout.subTitle"
defaultMessage="First seen: "
/>
<DateFormatter date={metadata.indicatorFirstSeen} />
</p>
</EuiText>
<EuiSpacer size="m" />
</EuiFlyoutHeader>
<EuiFlyoutBody>
<CasesFlyoutJson rawDocument={rawDocument} metadata={metadata} />
</EuiFlyoutBody>
<EuiFlyoutFooter />
</EuiFlyout>
);
};

View file

@ -1,243 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CasesFlyoutJsonProps should render error if input is null 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiPanel euiPanel--danger euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge emotion-euiPanel-m-danger"
data-test-subj="indicatorEmptyPrompt"
>
<div
class="euiEmptyPrompt__main"
>
<div
class="euiEmptyPrompt__icon"
>
<span
color="danger"
data-euiicon-type="alert"
/>
</div>
<div
class="euiEmptyPrompt__content"
>
<div
class="euiEmptyPrompt__contentInner"
>
<h2
class="euiTitle emotion-euiTitle-m"
>
Unable to display indicator information
</h2>
<div
class="euiSpacer euiSpacer--m emotion-euiSpacer-m"
/>
<div
class="euiText emotion-euiText-m-euiTextColor-subdued"
>
<p>
There was an error displaying the indicator fields and values.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiPanel euiPanel--danger euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge emotion-euiPanel-m-danger"
data-test-subj="indicatorEmptyPrompt"
>
<div
class="euiEmptyPrompt__main"
>
<div
class="euiEmptyPrompt__icon"
>
<span
color="danger"
data-euiicon-type="alert"
/>
</div>
<div
class="euiEmptyPrompt__content"
>
<div
class="euiEmptyPrompt__contentInner"
>
<h2
class="euiTitle emotion-euiTitle-m"
>
Unable to display indicator information
</h2>
<div
class="euiSpacer euiSpacer--m emotion-euiSpacer-m"
/>
<div
class="euiText emotion-euiText-m-euiTextColor-subdued"
>
<p>
There was an error displaying the indicator fields and values.
</p>
</div>
</div>
</div>
</div>
</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[`CasesFlyoutJsonProps should render json 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<h2
class="euiTitle emotion-euiTitle-m"
>
abc
</h2>
<div>
<pre>
<code
data-code-language="json"
data-test-subj="tiCasesFlyoutJsonCodeBlock"
>
{
"prop1": "prop1",
"prop2": "prop2"
}
</code>
</pre>
</div>
</div>
</body>,
"container": <div>
<h2
class="euiTitle emotion-euiTitle-m"
>
abc
</h2>
<div>
<pre>
<code
data-code-language="json"
data-test-subj="tiCasesFlyoutJsonCodeBlock"
>
{
"prop1": "prop1",
"prop2": "prop2"
}
</code>
</pre>
</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],
}
`;

View file

@ -1,8 +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.
*/
export * from './json';

View file

@ -1,51 +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 { Story } from '@storybook/react';
import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers';
import { CasesFlyoutJson } from './json';
import { AttachmentMetadata } from '../../../utils';
export default {
title: 'CasesFlyoutJson',
};
export const Default: Story<void> = () => {
const metadata: AttachmentMetadata = {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
const rawDocument: Record<string, unknown> = {
prop1: 'prop1',
prop2: 'prop2',
};
return (
<StoryProvidersComponent>
<CasesFlyoutJson metadata={metadata} rawDocument={rawDocument} />
</StoryProvidersComponent>
);
};
export const EmptyRawDocument: Story<void> = () => {
const metadata: AttachmentMetadata = {
indicatorName: 'indicatorName',
indicatorFeedName: 'indicatorFeedName',
indicatorType: 'indicatorType',
indicatorFirstSeen: 'indicatorFirstSeen',
};
const rawDocument = null as unknown as Record<string, unknown>;
return (
<StoryProvidersComponent>
<CasesFlyoutJson metadata={metadata} rawDocument={rawDocument} />
</StoryProvidersComponent>
);
};

View file

@ -1,49 +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 { render } from '@testing-library/react';
import { AttachmentMetadata } from '../../../utils';
import { TestProvidersComponent } from '../../../../../common/mocks/test_providers';
import { CasesFlyoutJson } from './json';
describe('CasesFlyoutJsonProps', () => {
it('should render json', () => {
const metadata: AttachmentMetadata = {
indicatorName: 'abc',
indicatorType: 'file',
indicatorFeedName: 'feed',
indicatorFirstSeen: '2022-01-01T01:01:01.000Z',
};
const rawDocument: Record<string, unknown> = {
prop1: 'prop1',
prop2: 'prop2',
};
const component = render(
<TestProvidersComponent>
<CasesFlyoutJson metadata={metadata} rawDocument={rawDocument} />
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
it('should render error if input is null', () => {
const metadata: AttachmentMetadata = {
indicatorName: 'abc',
indicatorType: 'file',
indicatorFeedName: 'feed',
indicatorFirstSeen: '2022-01-01T01:01:01.000Z',
};
const rawDocument: Record<string, unknown> = null as unknown as Record<string, unknown>;
const component = render(
<TestProvidersComponent>
<CasesFlyoutJson metadata={metadata} rawDocument={rawDocument} />
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -1,50 +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, { VFC } from 'react';
import { EuiCodeBlock, EuiTitle } from '@elastic/eui';
import { AttachmentMetadata } from '../../../utils';
import { IndicatorEmptyPrompt } from '../../../../indicators/components/flyout/empty_prompt';
export const CODE_BLOCK_TEST_ID = 'tiCasesFlyoutJsonCodeBlock';
interface CasesFlyoutJsonProps {
/**
* Metadata saved in the case attachment (indicator)
*/
metadata: AttachmentMetadata;
/**
* Document (indicator) retrieve by id
*/
rawDocument: Record<string, unknown>;
/**
* Used for unit and e2e tests.
*/
['data-test-subj']?: string;
}
/**
* Displays highlighted indicator values based on indicator type
*/
export const CasesFlyoutJson: VFC<CasesFlyoutJsonProps> = ({
metadata,
rawDocument,
'data-test-subj': dataTestSubj,
}) => {
return !rawDocument || Object.keys(rawDocument).length === 0 ? (
<IndicatorEmptyPrompt />
) : (
<>
<EuiTitle>
<h2>{metadata.indicatorName}</h2>
</EuiTitle>
<EuiCodeBlock language="json" lineNumbers data-test-subj={CODE_BLOCK_TEST_ID}>
{JSON.stringify(rawDocument, null, 2)}
</EuiCodeBlock>
</>
);
};

View file

@ -11,6 +11,7 @@ import {
IKibanaSearchResponse,
isCompleteResponse,
} from '@kbn/data-plugin/common';
import { Indicator } from '../../../../common/types/indicator';
import { useKibana } from '../../../hooks';
import type { RawIndicatorsResponse } from '../../indicators/services/fetch_indicators';
@ -25,7 +26,7 @@ export const useIndicatorById = (indicatorId: string) => {
data: { search: searchService },
},
} = useKibana();
const [indicator, setIndicator] = useState<Record<string, unknown>>();
const [indicator, setIndicator] = useState<Indicator>();
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
@ -42,11 +43,18 @@ export const useIndicatorById = (indicatorId: string) => {
],
},
};
const fields = [
{
field: '*',
include_unmapped: true,
},
];
const req = {
params: {
index: ['filebeat-*'],
body: {
query,
fields,
},
},
};

View file

@ -20,7 +20,6 @@ describe('generateAttachmentsWithoutOwner', () => {
indicatorName: 'indicatorName',
indicatorType: 'file',
indicatorFeedName: 'Filebeat] AbuseCH Malwar',
indicatorFirstSeen: '2022-01-01T01:01:01.000Z',
};
const result = generateAttachmentsWithoutOwner(externalReferenceId, metadata);
@ -33,7 +32,6 @@ describe('generateAttachmentsWithoutOwner', () => {
indicatorName: 'indicatorName',
indicatorType: 'file',
indicatorFeedName: 'Filebeat] AbuseCH Malwar',
indicatorFirstSeen: '2022-01-01T01:01:01.000Z',
};
const result: CaseAttachmentsWithoutOwner = generateAttachmentsWithoutOwner(

View file

@ -26,7 +26,6 @@ export interface AttachmentMetadata {
indicatorName: string;
indicatorType: string;
indicatorFeedName: string;
indicatorFirstSeen: string;
}
/**
@ -111,15 +110,10 @@ export const generateAttachmentsMetadata = (indicator: Indicator): AttachmentMet
indicator,
RawIndicatorFieldId.Feed
).value;
const indicatorFirstSeen: string | null = getIndicatorFieldAndValue(
indicator,
RawIndicatorFieldId.FirstSeen
).value;
return {
indicatorName: indicatorName || EMPTY_VALUE,
indicatorType: indicatorType || EMPTY_VALUE,
indicatorFeedName: indicatorFeedName || EMPTY_VALUE,
indicatorFirstSeen: indicatorFirstSeen || EMPTY_VALUE,
};
};

View file

@ -223,3 +223,100 @@ Object {
"unmount": [Function],
}
`;
exports[`<CopyToClipboardButtonEmpty /> <CopyToClipboardContextMenu /> should render one EuibuttonIcon 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Copy to clipboard"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="abc"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="copyClipboard"
/>
</button>
</span>
</div>
</body>,
"container": <div>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Copy to clipboard"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="abc"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="copyClipboard"
/>
</button>
</span>
</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],
}
`;

View file

@ -8,7 +8,11 @@
import React from 'react';
import { Story } from '@storybook/react';
import { EuiContextMenuPanel } from '@elastic/eui';
import { CopyToClipboardButtonEmpty, CopyToClipboardContextMenu } from '.';
import {
CopyToClipboardButtonEmpty,
CopyToClipboardButtonIcon,
CopyToClipboardContextMenu,
} from '.';
export default {
title: 'CopyToClipboard',
@ -25,3 +29,7 @@ export const ContextMenu: Story<void> = () => {
return <EuiContextMenuPanel items={items} />;
};
export const ButtonIcon: Story<void> = () => {
return <CopyToClipboardButtonIcon value={mockValue} />;
};

View file

@ -7,7 +7,11 @@
import React from 'react';
import { render } from '@testing-library/react';
import { CopyToClipboardButtonEmpty, CopyToClipboardContextMenu } from '.';
import {
CopyToClipboardButtonEmpty,
CopyToClipboardButtonIcon,
CopyToClipboardContextMenu,
} from '.';
const mockValue: string = 'Text copied';
@ -30,4 +34,12 @@ describe('<CopyToClipboardButtonEmpty /> <CopyToClipboardContextMenu />', () =>
expect(component).toMatchSnapshot();
});
it('should render one EuibuttonIcon', () => {
const component = render(
<CopyToClipboardButtonIcon value={mockValue} data-test-subj={mockTestId} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -6,7 +6,7 @@
*/
import React, { VFC } from 'react';
import { EuiButtonEmpty, EuiContextMenuItem, EuiCopy } from '@elastic/eui';
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem, EuiCopy } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const COPY_ICON = 'copyClipboard';
@ -80,3 +80,30 @@ export const CopyToClipboardContextMenu: VFC<CopyToClipboardProps> = ({
)}
</EuiCopy>
);
/**
* Takes a string and copies it to the clipboard.
*
* This component renders an {@link EuiButtonIcon}.
*
* @returns An EuiCopy element
*/
export const CopyToClipboardButtonIcon: VFC<CopyToClipboardProps> = ({
value,
'data-test-subj': dataTestSub,
}) => (
<EuiCopy textToCopy={value}>
{(copy) => (
<EuiButtonIcon
aria-label={COPY_TITLE}
iconType={COPY_ICON}
iconSize="s"
color="primary"
onClick={copy}
data-test-subj={dataTestSub}
>
{COPY_TITLE}
</EuiButtonIcon>
)}
</EuiCopy>
);

View file

@ -5,4 +5,12 @@
* 2.0.
*/
export * from './flyout';
import { createContext } from 'react';
export interface IndicatorsFlyoutContextValue {
kqlBarIntegration: boolean;
}
export const IndicatorsFlyoutContext = createContext<IndicatorsFlyoutContextValue | undefined>(
undefined
);

View file

@ -11,6 +11,7 @@ import { IndicatorFieldsTable } from '.';
import { generateMockIndicator } from '../../../../../../common/types/indicator';
import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers';
import { IndicatorsFiltersContext } from '../../../containers/filters';
import { IndicatorsFlyoutContext } from '../context';
export default {
component: IndicatorFieldsTable,
@ -19,15 +20,41 @@ export default {
export function WithIndicators() {
const indicator = generateMockIndicator();
const kqlBarIntegration = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
<IndicatorFieldsTable
fields={['threat.indicator.type']}
indicator={indicator}
search={false}
/>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorFieldsTable
fields={['threat.indicator.type']}
indicator={indicator}
search={false}
/>
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
</StoryProvidersComponent>
);
}
export function NoFilterButtons() {
const indicator = generateMockIndicator();
const kqlBarIntegration = {
kqlBarIntegration: true,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorFieldsTable
fields={['threat.indicator.type']}
indicator={indicator}
search={false}
/>
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
</StoryProvidersComponent>
);

View file

@ -65,7 +65,7 @@ export const IndicatorFieldsTable: VFC<IndicatorFieldsTableProps> = ({
return (
<EuiInMemoryTable
items={fields}
items={fields.sort()}
columns={columns}
sorting={true}
data-test-subj={dataTestSubj}

View file

@ -21,6 +21,7 @@ import {
useGeneratedHtmlId,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { IndicatorsFlyoutContext } from './context';
import { TakeAction } from './take_action/take_action';
import { DateFormatter } from '../../../../components/date_formatter/date_formatter';
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
@ -49,12 +50,21 @@ export interface IndicatorsFlyoutProps {
* Event to close flyout (used by {@link EuiFlyout}).
*/
closeFlyout: () => void;
/**
* Boolean deciding if we show or hide the filter in/out feature in the flyout.
* We should be showing the filter in and out buttons when the flyout is used in the cases view.
*/
kqlBarIntegration?: boolean;
}
/**
* Leverages the {@link EuiFlyout} from the @elastic/eui library to dhow the details of a specific {@link Indicator}.
*/
export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ indicator, closeFlyout }) => {
export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({
indicator,
closeFlyout,
kqlBarIntegration = false,
}) => {
const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.overview);
const tabs = useMemo(
@ -146,7 +156,11 @@ export const IndicatorsFlyout: VFC<IndicatorsFlyoutProps> = ({ indicator, closeF
{renderTabs}
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>
<EuiFlyoutBody>
<IndicatorsFlyoutContext.Provider value={{ kqlBarIntegration }}>
{selectedTabContent}
</IndicatorsFlyoutContext.Provider>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>

View file

@ -0,0 +1,386 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IndicatorValueActions should only render add to timeline and copy to clipboard 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-none-center-center-row"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
data-test-subj="undefinedTimelineButton"
>
<span
data-test-subj="test-add-to-timeline"
>
Add To Timeline
</span>
</div>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Copy to clipboard"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="undefinedCopyToClipboardButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="copyClipboard"
/>
</button>
</span>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-none-center-center-row"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
data-test-subj="undefinedTimelineButton"
>
<span
data-test-subj="test-add-to-timeline"
>
Add To Timeline
</span>
</div>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Copy to clipboard"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="undefinedCopyToClipboardButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="copyClipboard"
/>
</button>
</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[`IndicatorValueActions should render filter in/out and dropdown for add to timeline and copy to clipboard 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-none-center-center-row"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Filter In"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="undefinedFilterInButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="plusInCircle"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Filter Out"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="undefinedFilterOutButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="minusInCircle"
/>
</button>
</span>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="undefinedPopoverButton"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="More actions"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
style="height: 100%;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</span>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup emotion-euiFlexGroup-responsive-none-center-center-row"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Filter In"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="undefinedFilterInButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="plusInCircle"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Filter Out"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
data-test-subj="undefinedFilterOutButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="minusInCircle"
/>
</button>
</span>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="undefinedPopoverButton"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="More actions"
class="euiButtonIcon euiButtonIcon--xSmall emotion-euiButtonIcon-empty-primary-hoverStyles"
style="height: 100%;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</span>
</div>
</div>
</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[`IndicatorValueActions should return null if field and value are invalid 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],
}
`;

View file

@ -0,0 +1,46 @@
/*
* 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 { StoryProvidersComponent } from '../../../../../common/mocks/story_providers';
import { generateMockFileIndicator, Indicator } from '../../../../../../common/types/indicator';
import { IndicatorValueActions } from '.';
import { IndicatorsFlyoutContext } from '../context';
export default {
title: 'IndicatorValueActions',
};
const indicator: Indicator = generateMockFileIndicator();
const field: string = 'threat.indicator.name';
export const Default: Story<void> = () => {
const kqlBarIntegration = {
kqlBarIntegration: true,
};
return (
<StoryProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</StoryProvidersComponent>
);
};
export const WithoutFilterInOut: Story<void> = () => {
const kqlBarIntegration = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</StoryProvidersComponent>
);
};

View file

@ -0,0 +1,60 @@
/*
* 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 { generateMockFileIndicator, Indicator } from '../../../../../../common/types/indicator';
import { render } from '@testing-library/react';
import { IndicatorValueActions } from './indicator_value_actions';
import { IndicatorsFlyoutContext } from '../context';
import { TestProvidersComponent } from '../../../../../common/mocks/test_providers';
describe('IndicatorValueActions', () => {
const indicator: Indicator = generateMockFileIndicator();
it('should return null if field and value are invalid', () => {
const field: string = 'invalid';
const kqlBarIntegration = {
kqlBarIntegration: true,
};
const component = render(
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
);
expect(component).toMatchSnapshot();
});
it('should only render add to timeline and copy to clipboard', () => {
const field: string = 'threat.indicator.name';
const kqlBarIntegration = {
kqlBarIntegration: true,
};
const component = render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
it('should render filter in/out and dropdown for add to timeline and copy to clipboard', () => {
const field: string = 'threat.indicator.name';
const kqlBarIntegration = {
kqlBarIntegration: false,
};
const component = render(
<TestProvidersComponent>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorValueActions indicator={indicator} field={field} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -14,11 +14,12 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useIndicatorsFlyoutContext } from '../use_context';
import { Indicator } from '../../../../../../common/types/indicator';
import { FilterInButtonIcon, FilterOutButtonIcon } from '../../../../query_bar';
import { AddToTimelineContextMenu } from '../../../../timeline';
import { AddToTimelineButtonIcon, AddToTimelineContextMenu } from '../../../../timeline';
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../../utils';
import { CopyToClipboardContextMenu } from '../../copy_to_clipboard';
import { CopyToClipboardButtonIcon, CopyToClipboardContextMenu } from '../../copy_to_clipboard';
export const TIMELINE_BUTTON_TEST_ID = 'TimelineButton';
export const FILTER_IN_BUTTON_TEST_ID = 'FilterInButton';
@ -45,11 +46,21 @@ interface IndicatorValueActions {
['data-test-subj']?: string;
}
/**
* This component render a set of actions for the user.
* Currently used in the indicators flyout (overview and table tabs).
*
* It gets a readOnly boolean from context, that drives what is displayed.
* - in the cases view usage, we only display add to timeline and copy to clipboard.
* - in the indicators table usave, we display all options
*/
export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
indicator,
field,
'data-test-subj': dataTestSubj,
}) => {
const { kqlBarIntegration } = useIndicatorsFlyoutContext();
const [isPopoverOpen, setPopover] = useState(false);
const { key, value } = getIndicatorFieldAndValue(indicator, field);
@ -63,13 +74,22 @@ export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
const copyToClipboardTestId = `${dataTestSubj}${COPY_TO_CLIPBOARD_BUTTON_TEST_ID}`;
const popoverTestId = `${dataTestSubj}${POPOVER_BUTTON_TEST_ID}`;
if (kqlBarIntegration) {
return (
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="none">
<AddToTimelineButtonIcon data={indicator} field={field} data-test-subj={timelineTestId} />
<CopyToClipboardButtonIcon value={value as string} data-test-subj={copyToClipboardTestId} />
</EuiFlexGroup>
);
}
const popoverItems = [
<AddToTimelineContextMenu data={indicator} field={field} data-test-subj={timelineTestId} />,
<CopyToClipboardContextMenu value={value as string} data-test-subj={copyToClipboardTestId} />,
];
return (
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="none">
<FilterInButtonIcon data={indicator} field={field} data-test-subj={filterInTestId} />
<FilterOutButtonIcon data={indicator} field={field} data-test-subj={filterOutTestId} />
<EuiPopover

View file

@ -10,6 +10,7 @@ import { IndicatorsFiltersContext } from '../../../../containers/filters';
import { StoryProvidersComponent } from '../../../../../../common/mocks/story_providers';
import { generateMockIndicator } from '../../../../../../../common/types/indicator';
import { IndicatorBlock } from '.';
import { IndicatorsFlyoutContext } from '../../context';
export default {
component: IndicatorBlock,
@ -20,11 +21,33 @@ const mockIndicator = generateMockIndicator();
export function Default() {
const mockField = 'threat.indicator.ip';
const kqlBarIntegration = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={{} as any}>
<IndicatorBlock indicator={mockIndicator} field={mockField} />
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorBlock indicator={mockIndicator} field={mockField} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
</StoryProvidersComponent>
);
}
export function NoFilterButtons() {
const mockField = 'threat.indicator.ip';
const kqlBarIntegration = {
kqlBarIntegration: true,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={{} as any}>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorBlock indicator={mockIndicator} field={mockField} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
</StoryProvidersComponent>
);

View file

@ -11,6 +11,7 @@ import { StoryProvidersComponent } from '../../../../../common/mocks/story_provi
import { generateMockIndicator, Indicator } from '../../../../../../common/types/indicator';
import { IndicatorsFlyoutOverview } from '.';
import { IndicatorsFiltersContext } from '../../../containers/filters';
import { IndicatorsFlyoutContext } from '../context';
export default {
component: IndicatorsFlyoutOverview,
@ -25,11 +26,16 @@ export default {
export const Default: Story<void> = () => {
const mockIndicator: Indicator = generateMockIndicator();
const kqlBarIntegration = {
kqlBarIntegration: false,
};
return (
<StoryProvidersComponent>
<IndicatorsFiltersContext.Provider value={{} as any}>
<IndicatorsFlyoutOverview onViewAllFieldsInTable={() => {}} indicator={mockIndicator} />
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutOverview onViewAllFieldsInTable={() => {}} indicator={mockIndicator} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
</StoryProvidersComponent>
);

View file

@ -15,6 +15,7 @@ import {
TI_FLYOUT_OVERVIEW_TABLE,
} from '.';
import { EMPTY_PROMPT_TEST_ID } from '../empty_prompt';
import { IndicatorsFlyoutContext } from '../context';
describe('<IndicatorsFlyoutOverview />', () => {
describe('invalid indicator', () => {
@ -33,12 +34,18 @@ describe('<IndicatorsFlyoutOverview />', () => {
});
it('should render the highlighted blocks and table when valid indicator is passed', () => {
const kqlBarIntegration = {
kqlBarIntegration: false,
};
render(
<TestProvidersComponent>
<IndicatorsFlyoutOverview
onViewAllFieldsInTable={() => {}}
indicator={generateMockIndicator()}
/>
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutOverview
onViewAllFieldsInTable={() => {}}
indicator={generateMockIndicator()}
/>
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);

View file

@ -15,15 +15,18 @@ import { mockKibanaTimelinesService } from '../../../../../common/mocks/mock_kib
import { generateMockIndicator, Indicator } from '../../../../../../common/types/indicator';
import { IndicatorsFlyoutTable } from '.';
import { IndicatorsFiltersContext } from '../../../containers/filters';
import { IndicatorsFlyoutContext } from '../context';
export default {
component: IndicatorsFlyoutTable,
title: 'IndicatorsFlyoutTable',
};
const kqlBarIntegration = {
kqlBarIntegration: false,
};
export const Default: Story<void> = () => {
const mockIndicator: Indicator = generateMockIndicator();
const KibanaReactContext = createKibanaReactContext({
uiSettings: mockUiSettingsService(),
timelines: mockKibanaTimelinesService,
@ -32,12 +35,18 @@ export const Default: Story<void> = () => {
return (
<KibanaReactContext.Provider>
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
<IndicatorsFlyoutTable indicator={mockIndicator} />
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutTable indicator={mockIndicator} />
</IndicatorsFlyoutContext.Provider>
</IndicatorsFiltersContext.Provider>
</KibanaReactContext.Provider>
);
};
export const EmptyIndicator: Story<void> = () => {
return <IndicatorsFlyoutTable indicator={{ fields: {} } as unknown as Indicator} />;
return (
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutTable indicator={{ fields: {} } as unknown as Indicator} />
</IndicatorsFlyoutContext.Provider>
);
};

View file

@ -16,14 +16,21 @@ import {
import { IndicatorsFlyoutTable, TABLE_TEST_ID } from '.';
import { unwrapValue } from '../../../utils';
import { EMPTY_PROMPT_TEST_ID } from '../empty_prompt';
import { IndicatorsFlyoutContext } from '../context';
const mockIndicator: Indicator = generateMockIndicator();
describe('<IndicatorsFlyoutTable />', () => {
it('should render fields and values in table', () => {
const kqlBarIntegration = {
kqlBarIntegration: false,
};
const { getByTestId, getByText, getAllByText } = render(
<TestProvidersComponent>
<IndicatorsFlyoutTable indicator={mockIndicator} />
<IndicatorsFlyoutContext.Provider value={kqlBarIntegration}>
<IndicatorsFlyoutTable indicator={mockIndicator} />
</IndicatorsFlyoutContext.Provider>
</TestProvidersComponent>
);

View file

@ -0,0 +1,24 @@
/*
* 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 { useContext } from 'react';
import { IndicatorsFlyoutContext, IndicatorsFlyoutContextValue } from './context';
/**
* Hook to retrieve {@link IndicatorsFiltersContext} (contains FilterManager)
*/
export const useIndicatorsFlyoutContext = (): IndicatorsFlyoutContextValue => {
const contextValue = useContext(IndicatorsFlyoutContext);
if (!contextValue) {
throw new Error(
'IndicatorsFlyoutContext can only be used within IndicatorsFlyoutContext provider'
);
}
return contextValue;
};