mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[TIP] Add filter in and out to barchart legend (#140245)
- split FilterInOut component into FilterIn and FilterOut - each FilterIn, FilterOut and AddToTimeline components now support icon, EuiDataGrid and EuiContextMenu displays - replace testId props with data-test-subj to follow better convention - fix broken storybooks - add e2e tests for addToTimeline and Filter In/Out for the Flyout new Overview tab - fix icon alignment in flyout table actions
This commit is contained in:
parent
8647bd8c64
commit
0541eb6112
29 changed files with 1265 additions and 260 deletions
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used in multiple component to drive the render of the component depending on where they're used.
|
||||
*/
|
||||
export enum ComponentType {
|
||||
EuiDataGrid = 'EuiDataGrid',
|
||||
ContextMenu = 'ContextMenu',
|
||||
}
|
|
@ -6,14 +6,23 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
FILTER_IN_BUTTON,
|
||||
FILTER_OUT_BUTTON,
|
||||
FILTER_IN_COMPONENT,
|
||||
FILTER_OUT_COMPONENT,
|
||||
INDICATOR_TYPE_CELL,
|
||||
TOGGLE_FLYOUT_BUTTON,
|
||||
FLYOUT_CLOSE_BUTTON,
|
||||
KQL_FILTER,
|
||||
INDICATORS_TABLE_CELL_FILTER_IN_BUTTON,
|
||||
INDICATORS_TABLE_CELL_FILTER_OUT_BUTTON,
|
||||
FLYOUT_TABLE_TAB_ROW_FILTER_IN_BUTTON,
|
||||
FLYOUT_TABLE_TAB_ROW_FILTER_OUT_BUTTON,
|
||||
BARCHART_POPOVER_BUTTON,
|
||||
BARCHART_FILTER_IN_BUTTON,
|
||||
BARCHART_FILTER_OUT_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_IN_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_OUT_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_IN_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_OUT_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM,
|
||||
FLYOUT_TABS,
|
||||
} from '../screens/indicators';
|
||||
import { selectRange } from '../tasks/select_range';
|
||||
import { login } from '../tasks/login';
|
||||
|
@ -40,24 +49,100 @@ describe('Indicators', () => {
|
|||
selectRange();
|
||||
});
|
||||
|
||||
it('should filter in and out values when clicking in an indicators table cell', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover');
|
||||
cy.get(FILTER_IN_COMPONENT).should('exist');
|
||||
cy.get(FILTER_OUT_COMPONENT).should('exist');
|
||||
it('should add filter to kql and filter in values when clicking in the barchart legend', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(BARCHART_POPOVER_BUTTON).should('exist').first().click();
|
||||
cy.get(BARCHART_FILTER_IN_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should filter in and out values when clicking in an indicators flyout table action column', () => {
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FILTER_OUT_BUTTON).should('exist');
|
||||
cy.get(FILTER_IN_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
it('should add negated filter to kql and filter out values when clicking in the barchart legend', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(BARCHART_POPOVER_BUTTON).should('exist').first().click();
|
||||
cy.get(BARCHART_FILTER_OUT_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add filter to kql and filter in and out values when clicking in an indicators table cell', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover');
|
||||
cy.get(INDICATORS_TABLE_CELL_FILTER_IN_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add negated filter and filter out and out values when clicking in an indicators table cell', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(INDICATOR_TYPE_CELL).first().trigger('mouseover');
|
||||
cy.get(INDICATORS_TABLE_CELL_FILTER_OUT_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add filter to kql and filter in values when clicking in an indicators flyout overview tab block', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FILTER_IN_BUTTON).should('exist');
|
||||
cy.get(FILTER_OUT_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM).first().trigger('mouseover');
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_IN_BUTTON)
|
||||
.should('exist')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add negated filter to kql filter out values when clicking in an indicators flyout overview block', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM).first().trigger('mouseover');
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_OUT_BUTTON)
|
||||
.should('exist')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add filter to kql and filter in values when clicking in an indicators flyout overview tab table row', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_IN_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add negated filter to kql filter out values when clicking in an indicators flyout overview tab row', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_OUT_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add filter to kql and filter in values when clicking in an indicators flyout table tab action column', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click();
|
||||
cy.get(FLYOUT_TABLE_TAB_ROW_FILTER_IN_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
|
||||
it('should add negated filter to kql filter out values when clicking in an indicators flyout table tab action column', () => {
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click();
|
||||
cy.get(FLYOUT_TABLE_TAB_ROW_FILTER_OUT_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').click();
|
||||
cy.get(KQL_FILTER).should('exist');
|
||||
cy.get(INDICATOR_TYPE_CELL).its('length').should('be.gte', 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,19 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
BARCHART_POPOVER_BUTTON,
|
||||
BARCHART_TIMELINE_BUTTON,
|
||||
FLYOUT_CLOSE_BUTTON,
|
||||
FLYOUT_TABLE_ROW_TIMELINE_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_TABLE_ROW_TIMELINE_BUTTON,
|
||||
FLYOUT_TABLE_TAB_ROW_TIMELINE_BUTTON,
|
||||
FLYOUT_TABS,
|
||||
INDICATOR_TYPE_CELL,
|
||||
INDICATORS_TABLE_CELL_TIMELINE_BUTTON,
|
||||
TIMELINE_DRAGGABLE_ITEM,
|
||||
TOGGLE_FLYOUT_BUTTON,
|
||||
UNTITLED_TIMELINE_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON,
|
||||
FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM,
|
||||
} from '../screens/indicators';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||
import { login } from '../tasks/login';
|
||||
|
@ -41,6 +46,7 @@ describe('Indicators', () => {
|
|||
});
|
||||
|
||||
it('should add entry in timeline when clicking in the barchart legend', () => {
|
||||
cy.get(BARCHART_POPOVER_BUTTON).should('exist').first().click();
|
||||
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');
|
||||
|
@ -53,9 +59,31 @@ describe('Indicators', () => {
|
|||
cy.get(TIMELINE_DRAGGABLE_ITEM).should('exist');
|
||||
});
|
||||
|
||||
it('should add entry in timeline when clicking in an indicators flyout row', () => {
|
||||
it('should add entry in timeline when clicking in an indicator flyout overview tab table row', () => {
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_TABLE_ROW_TIMELINE_BUTTON).should('exist').first().click();
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_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');
|
||||
});
|
||||
|
||||
it('should add entry in timeline when clicking in an indicator flyout overview block', () => {
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM).first().trigger('mouseover');
|
||||
cy.get(FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON)
|
||||
.should('exist')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get(FLYOUT_CLOSE_BUTTON).should('exist').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 flyout table tab', () => {
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).first().click({ force: true });
|
||||
cy.get(FLYOUT_TABS).should('exist');
|
||||
cy.get(`${FLYOUT_TABS} button:nth-child(2)`).click();
|
||||
cy.get(FLYOUT_TABLE_TAB_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');
|
||||
|
|
|
@ -49,23 +49,55 @@ export const FIELD_BROWSER_MODAL_SOURCE_CHECKBOX = `[data-test-subj="field-_sour
|
|||
|
||||
export const FIELD_BROWSER_CLOSE = `[data-test-subj="close"]`;
|
||||
|
||||
export const BARCHART_TIMELINE_BUTTON = '[data-test-subj="tiTimelineButton"]';
|
||||
export const BARCHART_POPOVER_BUTTON = '[data-test-subj="tiBarchartPopoverButton"]';
|
||||
|
||||
export const BARCHART_TIMELINE_BUTTON = '[data-test-subj="tiBarchartTimelineButton"]';
|
||||
|
||||
export const BARCHART_FILTER_IN_BUTTON = '[data-test-subj="tiBarchartFilterInButton"]';
|
||||
|
||||
export const BARCHART_FILTER_OUT_BUTTON = '[data-test-subj="tiBarchartFilterOutButton"]';
|
||||
|
||||
export const INDICATORS_TABLE_CELL_TIMELINE_BUTTON =
|
||||
'[data-test-subj="tiIndicatorsTableCellTimelineButton"]';
|
||||
|
||||
export const FLYOUT_TABLE_ROW_TIMELINE_BUTTON = '[data-test-subj="tiFlyoutTableRowTimelineButton"]';
|
||||
export const INDICATORS_TABLE_CELL_FILTER_IN_BUTTON =
|
||||
'[data-test-subj="tiIndicatorsTableCellFilterInButton"]';
|
||||
|
||||
export const INDICATORS_TABLE_CELL_FILTER_OUT_BUTTON =
|
||||
'[data-test-subj="tiIndicatorsTableCellFilterOutButton"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_TIMELINE_BUTTON =
|
||||
'[data-test-subj="tiFlyoutOverviewTableRowTimelineButton"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_IN_BUTTON =
|
||||
'[data-test-subj="tiFlyoutOverviewTableRowFilterInButton"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_FILTER_OUT_BUTTON =
|
||||
'[data-test-subj="tiFlyoutOverviewTableRowFilterOutButton"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_BLOCKS_ITEM =
|
||||
'[data-test-subj="tiFlyoutOverviewHighLevelBlocksItem"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON =
|
||||
'[data-test-subj="tiFlyoutOverviewHighLevelBlocksTimelineButton"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_IN_BUTTON =
|
||||
'[data-test-subj="tiFlyoutOverviewHighLevelBlocksFilterInButton"]';
|
||||
|
||||
export const FLYOUT_OVERVIEW_TAB_BLOCKS_FILTER_OUT_BUTTON =
|
||||
'[data-test-subj="tiFlyoutOverviewHighLevelBlocksFilterOutButton"]';
|
||||
|
||||
export const FLYOUT_TABLE_TAB_ROW_TIMELINE_BUTTON =
|
||||
'[data-test-subj="tiFlyoutTableTabRowTimelineButton"]';
|
||||
|
||||
export const FLYOUT_TABLE_TAB_ROW_FILTER_IN_BUTTON =
|
||||
'[data-test-subj="tiFlyoutTableTabRowFilterInButton"]';
|
||||
|
||||
export const FLYOUT_TABLE_TAB_ROW_FILTER_OUT_BUTTON =
|
||||
'[data-test-subj="tiFlyoutTableTabRowFilterOutButton"]';
|
||||
|
||||
export const UNTITLED_TIMELINE_BUTTON = '[data-test-subj="flyoutOverlay"]';
|
||||
|
||||
export const TIMELINE_DRAGGABLE_ITEM = '[data-test-subj="providerContainer"]';
|
||||
|
||||
export const FILTER_IN_BUTTON = '[data-test-subj="tiFilterInIcon"]';
|
||||
|
||||
export const FILTER_OUT_BUTTON = '[data-test-subj="tiFilterOutIcon"]';
|
||||
|
||||
export const FILTER_IN_COMPONENT = '[data-test-subj="tiFilterInComponent"]';
|
||||
|
||||
export const FILTER_OUT_COMPONENT = '[data-test-subj="tiFilterOutComponent"]';
|
||||
|
||||
export const KQL_FILTER = '[id="popoverFor_filter0"]';
|
||||
|
|
|
@ -11,8 +11,10 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import { CoreStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { mockIndicatorsFiltersContext } from './mock_indicators_filters_context';
|
||||
import { SecuritySolutionContext } from '../../containers/security_solution_context';
|
||||
import { getSecuritySolutionContextMock } from './mock_security_context';
|
||||
import { IndicatorsFiltersContext } from '../../modules/indicators/context';
|
||||
import { FieldTypesContext } from '../../containers/field_types_provider';
|
||||
import { generateFieldTypeMap } from './mock_field_type_map';
|
||||
import { mockUiSettingsService } from './mock_kibana_ui_settings_service';
|
||||
|
@ -81,7 +83,9 @@ export const StoryProvidersComponent: VFC<StoryProvidersComponentProps> = ({
|
|||
<EuiThemeProvider>
|
||||
<FieldTypesContext.Provider value={generateFieldTypeMap()}>
|
||||
<SecuritySolutionContext.Provider value={securitySolutionContextMock}>
|
||||
<KibanaReactContext.Provider>{children}</KibanaReactContext.Provider>
|
||||
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
|
||||
<KibanaReactContext.Provider>{children}</KibanaReactContext.Provider>
|
||||
</IndicatorsFiltersContext.Provider>
|
||||
</SecuritySolutionContext.Provider>
|
||||
</FieldTypesContext.Provider>
|
||||
</EuiThemeProvider>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, { useState, VFC } from 'react';
|
||||
import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { FilterIn } from '../../../query_bar/components/filter_in';
|
||||
import { FilterOut } from '../../../query_bar/components/filter_out';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
|
||||
export const POPOVER_BUTTON_TEST_ID = 'tiBarchartPopoverButton';
|
||||
export const TIMELINE_BUTTON_TEST_ID = 'tiBarchartTimelineButton';
|
||||
export const FILTER_IN_BUTTON_TEST_ID = 'tiBarchartFilterInButton';
|
||||
export const FILTER_OUT_BUTTON_TEST_ID = 'tiBarchartFilterOutButton';
|
||||
|
||||
export interface IndicatorBarchartLegendActionProps {
|
||||
/**
|
||||
* Indicator
|
||||
*/
|
||||
data: string;
|
||||
/**
|
||||
* Indicator field selected in the IndicatorFieldSelector component, passed to the {@link AddToTimeline} to populate the timeline.
|
||||
*/
|
||||
field: string;
|
||||
}
|
||||
|
||||
export const IndicatorBarchartLegendAction: VFC<IndicatorBarchartLegendActionProps> = ({
|
||||
data,
|
||||
field,
|
||||
}) => {
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const popoverItems = [
|
||||
<FilterIn
|
||||
data={data}
|
||||
field={field}
|
||||
type={ComponentType.ContextMenu}
|
||||
data-test-subj={FILTER_IN_BUTTON_TEST_ID}
|
||||
/>,
|
||||
<FilterOut
|
||||
data={data}
|
||||
field={field}
|
||||
type={ComponentType.ContextMenu}
|
||||
data-test-subj={FILTER_OUT_BUTTON_TEST_ID}
|
||||
/>,
|
||||
<AddToTimeline
|
||||
data={data}
|
||||
field={field}
|
||||
type={ComponentType.ContextMenu}
|
||||
data-test-subj={TIMELINE_BUTTON_TEST_ID}
|
||||
/>,
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
data-test-subj={POPOVER_BUTTON_TEST_ID}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="boxesHorizontal"
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
onClick={() => setPopover(!isPopoverOpen)}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setPopover(false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={popoverItems} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -9,22 +9,39 @@ import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
|||
import React, { VFC } from 'react';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { FilterInOut } from '../../../query_bar/components/filter_in_out';
|
||||
import { FilterIn } from '../../../query_bar/components/filter_in';
|
||||
import { FilterOut } from '../../../query_bar/components/filter_out';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { getIndicatorFieldAndValue } from '../../lib/field_value';
|
||||
|
||||
export const TIMELINE_BUTTON_TEST_ID = 'TimelineButton';
|
||||
export const FILTER_IN_BUTTON_TEST_ID = 'FilterInButton';
|
||||
export const FILTER_OUT_BUTTON_TEST_ID = 'FilterOutButton';
|
||||
|
||||
interface IndicatorValueActions {
|
||||
/**
|
||||
* Indicator complete object.
|
||||
*/
|
||||
indicator: Indicator;
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
/**
|
||||
* Indicator field used for the filter in/out and add to timeline feature.
|
||||
*/
|
||||
field: string;
|
||||
testId?: string;
|
||||
/**
|
||||
* Only used with `EuiDataGrid` (see {@link AddToTimelineButtonProps}).
|
||||
*/
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
/**
|
||||
* Used for unit and e2e tests.
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
|
||||
indicator,
|
||||
field,
|
||||
testId,
|
||||
Component,
|
||||
...props
|
||||
}) => {
|
||||
const { key, value } = getIndicatorFieldAndValue(indicator, field);
|
||||
|
||||
|
@ -32,10 +49,20 @@ export const IndicatorValueActions: VFC<IndicatorValueActions> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const filterInTestId = `${props['data-test-subj']}${FILTER_IN_BUTTON_TEST_ID}`;
|
||||
const filterOutTestId = `${props['data-test-subj']}${FILTER_OUT_BUTTON_TEST_ID}`;
|
||||
const timelineTestId = `${props['data-test-subj']}${TIMELINE_BUTTON_TEST_ID}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterInOut as={Component} data={indicator} field={field} />
|
||||
<AddToTimeline as={Component} data={indicator} field={field} testId={testId} />
|
||||
<FilterIn as={Component} data={indicator} field={field} data-test-subj={filterInTestId} />
|
||||
<FilterOut as={Component} data={indicator} field={field} data-test-subj={filterOutTestId} />
|
||||
<AddToTimeline
|
||||
as={Component}
|
||||
data={indicator}
|
||||
field={field}
|
||||
data-test-subj={timelineTestId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,8 +9,7 @@ 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 { StoryProvidersComponent } from '../../../../common/mocks/story_providers';
|
||||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { ChartSeries } from '../../hooks/use_aggregated_indicators';
|
||||
import { IndicatorsBarChart } from './indicators_barchart';
|
||||
|
@ -58,38 +57,34 @@ const mockDateRange: TimeRangeBounds = {
|
|||
|
||||
const mockField: string = 'threat.indicator.ip';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
timelines: mockKibanaTimelinesService,
|
||||
} as unknown as CoreStart);
|
||||
|
||||
export default {
|
||||
component: IndicatorsBarChart,
|
||||
title: 'IndicatorsBarChart',
|
||||
};
|
||||
|
||||
export const Default: Story<void> = () => (
|
||||
<KibanaReactContext.Provider>
|
||||
<StoryProvidersComponent kibana={{ timelines: mockKibanaTimelinesService }}>
|
||||
<IndicatorsBarChart indicators={mockIndicators} field={mockField} dateRange={mockDateRange} />
|
||||
</KibanaReactContext.Provider>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
|
||||
export const NoData: Story<void> = () => (
|
||||
<KibanaReactContext.Provider>
|
||||
<StoryProvidersComponent kibana={{ timelines: mockKibanaTimelinesService }}>
|
||||
<IndicatorsBarChart indicators={[]} field={''} dateRange={mockDateRange} />
|
||||
</KibanaReactContext.Provider>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
|
||||
export const CustomHeight: Story<void> = () => {
|
||||
const mockHeight = '500px';
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<StoryProvidersComponent kibana={{ timelines: mockKibanaTimelinesService }}>
|
||||
<IndicatorsBarChart
|
||||
indicators={mockIndicators}
|
||||
field={mockField}
|
||||
dateRange={mockDateRange}
|
||||
height={mockHeight}
|
||||
/>
|
||||
</KibanaReactContext.Provider>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,12 +9,10 @@ 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 { IndicatorBarchartLegendAction } from '../indicator_barchart_legend_action/indicator_barchart_legend_action';
|
||||
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%';
|
||||
|
@ -54,9 +52,7 @@ export const IndicatorsBarChart: VFC<IndicatorsBarChartProps> = ({
|
|||
showLegend
|
||||
showLegendExtra
|
||||
legendPosition={Position.Right}
|
||||
legendAction={({ label }) => (
|
||||
<AddToTimeline data={label} field={field} testId={TIMELINE_BUTTON_TEST_ID} />
|
||||
)}
|
||||
legendAction={({ label }) => <IndicatorBarchartLegendAction field={field} data={label} />}
|
||||
/>
|
||||
<Axis
|
||||
id={`${ID}TimeAxis`}
|
||||
|
|
|
@ -49,14 +49,14 @@ export const IndicatorFieldsTable: VFC<IndicatorFieldsTableProps> = ({
|
|||
actions: [
|
||||
{
|
||||
render: (field: string) => (
|
||||
<IndicatorValueActions field={field} indicator={indicator} />
|
||||
<IndicatorValueActions field={field} indicator={indicator} {...rest} />
|
||||
),
|
||||
width: '72px',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as Array<EuiBasicTableColumn<string>>,
|
||||
[indicator]
|
||||
[indicator, rest]
|
||||
);
|
||||
|
||||
return <EuiInMemoryTable items={fields} columns={columns} sorting={true} {...rest} />;
|
||||
|
|
|
@ -44,15 +44,16 @@ const panelProps = {
|
|||
export interface IndicatorBlockProps {
|
||||
indicator: Indicator;
|
||||
field: string;
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders indicator field value in a rectangle, to highlight it even more
|
||||
*/
|
||||
export const IndicatorBlock: VFC<IndicatorBlockProps> = ({ field, indicator }) => {
|
||||
export const IndicatorBlock: VFC<IndicatorBlockProps> = ({ field, indicator, ...props }) => {
|
||||
return (
|
||||
<EuiPanel {...panelProps}>
|
||||
<VisibleOnHover>
|
||||
<VisibleOnHover data-test-subj={`${props['data-test-subj']}Item`}>
|
||||
<EuiText>
|
||||
<IndicatorFieldLabel field={field} />
|
||||
</EuiText>
|
||||
|
@ -60,7 +61,7 @@ export const IndicatorBlock: VFC<IndicatorBlockProps> = ({ field, indicator }) =
|
|||
<EuiText size="s">
|
||||
<IndicatorFieldValue indicator={indicator} field={field} />
|
||||
<span className="actionsWrapper">
|
||||
<IndicatorValueActions indicator={indicator} field={field} />
|
||||
<IndicatorValueActions indicator={indicator} field={field} {...props} />
|
||||
</span>
|
||||
</EuiText>
|
||||
</VisibleOnHover>
|
||||
|
|
|
@ -31,7 +31,7 @@ const highLevelFields = [
|
|||
RawIndicatorFieldId.Confidence,
|
||||
];
|
||||
|
||||
export const TI_FLYOUT_OVERVIEW_TABLE = 'tiFlyoutOverviewTable';
|
||||
export const TI_FLYOUT_OVERVIEW_TABLE = 'tiFlyoutOverviewTableRow';
|
||||
export const TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS = 'tiFlyoutOverviewHighLevelBlocks';
|
||||
|
||||
export interface IndicatorsFlyoutOverviewProps {
|
||||
|
@ -50,7 +50,11 @@ export const IndicatorsFlyoutOverview: VFC<IndicatorsFlyoutOverviewProps> = ({
|
|||
<EuiFlexGroup data-test-subj={TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS}>
|
||||
{highLevelFields.map((field) => (
|
||||
<EuiFlexItem key={field}>
|
||||
<IndicatorBlock indicator={indicator} field={field} />
|
||||
<IndicatorBlock
|
||||
indicator={indicator}
|
||||
field={field}
|
||||
data-test-subj={TI_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -10,8 +10,7 @@ import { Indicator } from '../../../../../../../common/types/indicator';
|
|||
import { IndicatorEmptyPrompt } from '../../components/indicator_empty_prompt';
|
||||
import { IndicatorFieldsTable } from '../../components/indicator_fields_table';
|
||||
|
||||
export const TABLE_TEST_ID = 'tiFlyoutTableMemoryTable';
|
||||
export const TIMELINE_BUTTON_TEST_ID = 'tiFlyoutTableRowTimelineButton';
|
||||
export const TABLE_TEST_ID = 'tiFlyoutTableTabRow';
|
||||
|
||||
const search = {
|
||||
box: {
|
||||
|
|
|
@ -7,14 +7,18 @@
|
|||
|
||||
import React, { VFC } from 'react';
|
||||
import { EuiDataGridColumnCellActionProps } from '@elastic/eui/src/components/datagrid/data_grid_types';
|
||||
import { FilterInOut } from '../../../query_bar/components/filter_in_out';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { Pagination } from '../../hooks/use_indicators';
|
||||
import { AddToTimeline } from '../../../timeline/components/add_to_timeline';
|
||||
import { getIndicatorFieldAndValue } from '../../lib/field_value';
|
||||
import { FilterIn } from '../../../query_bar/components/filter_in';
|
||||
import { FilterOut } from '../../../query_bar/components/filter_out';
|
||||
|
||||
export const CELL_TIMELINE_BUTTON_TEST_ID = 'tiIndicatorsTableCellTimelineButton';
|
||||
export const CELL_FILTER_IN_BUTTON_TEST_ID = 'tiIndicatorsTableCellFilterInButton';
|
||||
export const CELL_FILTER_OUT_BUTTON_TEST_ID = 'tiIndicatorsTableCellFilterOutButton';
|
||||
|
||||
export interface CellActionsProps
|
||||
extends Omit<EuiDataGridColumnCellActionProps, 'colIndex' | 'isExpanded'> {
|
||||
|
@ -50,12 +54,25 @@ export const CellActions: VFC<CellActionsProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<FilterInOut as={Component} data={indicator} field={key} />
|
||||
<FilterIn
|
||||
as={Component}
|
||||
data={indicator}
|
||||
field={key}
|
||||
type={ComponentType.EuiDataGrid}
|
||||
data-test-subj={CELL_FILTER_IN_BUTTON_TEST_ID}
|
||||
/>
|
||||
<FilterOut
|
||||
as={Component}
|
||||
data={indicator}
|
||||
field={key}
|
||||
type={ComponentType.EuiDataGrid}
|
||||
data-test-subj={CELL_FILTER_OUT_BUTTON_TEST_ID}
|
||||
/>
|
||||
<AddToTimeline
|
||||
data={indicator}
|
||||
field={key}
|
||||
as={Component}
|
||||
testId={CELL_TIMELINE_BUTTON_TEST_ID}
|
||||
data-test-subj={CELL_TIMELINE_BUTTON_TEST_ID}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilterInOut /> should render an empty component (wrong data input) 1`] = `
|
||||
exports[`<FilterIn /> should render an empty component (wrong data input) 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
|
@ -61,7 +61,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`<FilterInOut /> should render an empty component (wrong field input) 1`] = `
|
||||
exports[`<FilterIn /> should render an empty component (wrong field input) 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
|
@ -122,30 +122,13 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`<FilterInOut /> should render two Component (for DataGrid use) 1`] = `
|
||||
exports[`<FilterIn /> should render one Component (for EuiDataGrid use) 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
data-test-subj="tiFilterInComponent"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
data-test-subj="tiFilterOutComponent"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
|
@ -164,23 +147,6 @@ Object {
|
|||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
data-test-subj="tiFilterInComponent"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
data-test-subj="tiFilterOutComponent"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
|
@ -249,7 +215,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`<FilterInOut /> should render two EuiButtonIcon 1`] = `
|
||||
exports[`<FilterIn /> should render one EuiButtonIcon 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
|
@ -257,7 +223,7 @@ Object {
|
|||
<button
|
||||
aria-label="Filter In"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="tiFilterInIcon"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
@ -267,26 +233,13 @@ Object {
|
|||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="tiFilterOutIcon"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-label="Filter In"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="tiFilterInIcon"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
@ -296,18 +249,106 @@ Object {
|
|||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</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[`<FilterIn /> should render one EuiContextMenuItem (for EuiContextMenu use) 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
class="euiContextMenuItem euiContextMenuItem--small"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Filter In
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="tiFilterOutIcon"
|
||||
class="euiContextMenuItem euiContextMenuItem--small"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Filter In
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>,
|
||||
"debug": [Function],
|
|
@ -8,13 +8,13 @@
|
|||
import React from 'react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context';
|
||||
import { FilterInOut } from './filter_in_out';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFiltersContext } from '../../../indicators/context';
|
||||
import { FilterIn } from '.';
|
||||
|
||||
export default {
|
||||
component: FilterInOut,
|
||||
title: 'FilterInOut',
|
||||
component: FilterIn,
|
||||
title: 'FilterIn',
|
||||
};
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
|
@ -23,7 +23,7 @@ export const Default: Story<void> = () => {
|
|||
|
||||
return (
|
||||
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
|
||||
<FilterInOut data={mockIndicator} field={mockField} />
|
||||
<FilterIn data={mockIndicator} field={mockField} />
|
||||
</IndicatorsFiltersContext.Provider>
|
||||
);
|
||||
};
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { FilterInOut, IN_ICON_TEST_ID, OUT_ICON_TEST_ID } from './filter_in_out';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context';
|
||||
import { FilterIn } from '.';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
|
||||
jest.mock('../../../indicators/hooks/use_indicators_filters_context');
|
||||
|
||||
|
@ -19,39 +20,50 @@ const mockIndicator: Indicator = generateMockIndicator();
|
|||
|
||||
const mockField: string = 'threat.feed.name';
|
||||
|
||||
describe('<FilterInOut />', () => {
|
||||
const mockTestId: string = 'abc';
|
||||
|
||||
describe('<FilterIn />', () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
useIndicatorsFiltersContext as jest.MockedFunction<typeof useIndicatorsFiltersContext>
|
||||
).mockReturnValue(mockIndicatorsFiltersContext);
|
||||
});
|
||||
|
||||
it('should render two EuiButtonIcon', () => {
|
||||
const component = render(<FilterInOut data={mockIndicator} field={mockField} />);
|
||||
it('should render one EuiButtonIcon', () => {
|
||||
const component = render(
|
||||
<FilterIn data={mockIndicator} field={mockField} data-test-subj={mockTestId} />
|
||||
);
|
||||
|
||||
expect(component.getByTestId(IN_ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(component.getByTestId(OUT_ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(component.getByTestId(mockTestId)).toBeInTheDocument();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render two Component (for DataGrid use)', () => {
|
||||
it('should render one Component (for EuiDataGrid use)', () => {
|
||||
const mockType: ComponentType = ComponentType.EuiDataGrid;
|
||||
const mockComponent: FunctionComponent = () => <EuiButtonIcon iconType="plusInCircle" />;
|
||||
|
||||
const component = render(
|
||||
<FilterInOut data={mockIndicator} field={mockField} as={mockComponent} />
|
||||
<FilterIn data={mockIndicator} field={mockField} type={mockType} as={mockComponent} />
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render one EuiContextMenuItem (for EuiContextMenu use)', () => {
|
||||
const mockType: ComponentType = ComponentType.ContextMenu;
|
||||
const component = render(<FilterIn data={mockIndicator} field={mockField} type={mockType} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an empty component (wrong data input)', () => {
|
||||
const component = render(<FilterInOut data={''} field={mockField} />);
|
||||
const component = render(<FilterIn data={''} field={mockField} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an empty component (wrong field input)', () => {
|
||||
const component = render(<FilterInOut data={mockIndicator} field={''} />);
|
||||
const component = render(<FilterIn data={mockIndicator} field={''} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, VFC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { FilterIn as FilterInConst, updateFiltersArray } from '../../lib/filter';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
const ICON_TYPE = 'plusInCircle';
|
||||
const ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterInButton', {
|
||||
defaultMessage: 'Filter In',
|
||||
});
|
||||
|
||||
export interface FilterInProps {
|
||||
/**
|
||||
* Value used to filter in/out in the KQL bar. Used in combination with field if is type of {@link Indicator}.
|
||||
*/
|
||||
data: Indicator | string;
|
||||
/**
|
||||
* Value used to filter in /out in the KQL bar.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Dictates the way the FilterIn component is rendered depending on the situation in which it's used
|
||||
*/
|
||||
type?: ComponentType;
|
||||
/**
|
||||
* Display component for when the FilterIn component is used within a DataGrid
|
||||
*/
|
||||
as?: typeof EuiButtonEmpty | typeof EuiButtonIcon | typeof EuiContextMenuItem;
|
||||
/**
|
||||
* Used for unit and e2e tests.
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the indicator's field and value, then creates a new {@link Filter} and adds it to the {@link FilterManager}.
|
||||
*
|
||||
* The component has 3 renders depending on where it's used: within a EuiContextMenu, a EuiDataGrid or not.
|
||||
*
|
||||
* @returns filter in button
|
||||
*/
|
||||
export const FilterIn: VFC<FilterInProps> = ({ data, field, type, as: Component, ...props }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const { filterManager } = useIndicatorsFiltersContext();
|
||||
|
||||
const { key, value } =
|
||||
typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field);
|
||||
|
||||
const filterIn = useCallback((): void => {
|
||||
const existingFilters = filterManager.getFilters();
|
||||
const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterInConst);
|
||||
filterManager.setFilters(newFilters);
|
||||
}, [filterManager, key, value]);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (type === ComponentType.EuiDataGrid) {
|
||||
return (
|
||||
<div {...props} css={styles.button}>
|
||||
{/* @ts-ignore*/}
|
||||
<Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterIn} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === ComponentType.ContextMenu) {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="addToTimeline"
|
||||
icon="plusInCircle"
|
||||
size="s"
|
||||
onClick={filterIn}
|
||||
{...props}
|
||||
>
|
||||
Filter In
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={ICON_TITLE}
|
||||
iconType={ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterIn}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './filter_in_out';
|
||||
export * from './filter_in';
|
|
@ -1,111 +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, { useCallback, VFC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { FilterIn, FilterOut, updateFiltersArray } from '../../lib/filter';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
export const IN_ICON_TEST_ID = 'tiFilterInIcon';
|
||||
export const OUT_ICON_TEST_ID = 'tiFilterOutIcon';
|
||||
export const IN_COMPONENT_TEST_ID = 'tiFilterInComponent';
|
||||
export const OUT_COMPONENT_TEST_ID = 'tiFilterOutComponent';
|
||||
|
||||
const IN_ICON_TYPE = 'plusInCircle';
|
||||
const IN_ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterInButton', {
|
||||
defaultMessage: 'Filter In',
|
||||
});
|
||||
const OUT_ICON_TYPE = 'minusInCircle';
|
||||
const OUT_ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterOutButton', {
|
||||
defaultMessage: 'Filter Out',
|
||||
});
|
||||
|
||||
export interface FilterInOutProps {
|
||||
/**
|
||||
* Value used to filter in/out in the KQL bar. Used in combination with field if is type of {@link Indicator}.
|
||||
*/
|
||||
data: Indicator | string;
|
||||
/**
|
||||
* Value used to filter in /out in the KQL bar.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Display component for when the FilterIn component is used within a DataGrid
|
||||
*/
|
||||
as?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the indicator's field and value, then creates a new {@link Filter} and adds it to the {@link FilterManager}.
|
||||
*/
|
||||
export const FilterInOut: VFC<FilterInOutProps> = ({ data, field, as: Component }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const { filterManager } = useIndicatorsFiltersContext();
|
||||
|
||||
const { key, value } =
|
||||
typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field);
|
||||
|
||||
const filterIn = useCallback((): void => {
|
||||
const existingFilters = filterManager.getFilters();
|
||||
const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterIn);
|
||||
filterManager.setFilters(newFilters);
|
||||
}, [filterManager, key, value]);
|
||||
|
||||
const filterOut = useCallback(() => {
|
||||
const existingFilters: Filter[] = filterManager.getFilters();
|
||||
const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterOut);
|
||||
filterManager.setFilters(newFilters);
|
||||
}, [filterManager, key, value]);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return Component ? (
|
||||
<>
|
||||
<div data-test-subj={IN_COMPONENT_TEST_ID} css={styles.button}>
|
||||
<Component aria-label={IN_ICON_TITLE} iconType={IN_ICON_TYPE} onClick={filterIn} />
|
||||
</div>
|
||||
<div data-test-subj={OUT_COMPONENT_TEST_ID} css={styles.button}>
|
||||
<Component
|
||||
data-test-subj={IN_ICON_TEST_ID}
|
||||
aria-label={OUT_ICON_TITLE}
|
||||
iconType={OUT_ICON_TYPE}
|
||||
onClick={filterOut}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={IN_ICON_TEST_ID}
|
||||
aria-label={IN_ICON_TITLE}
|
||||
iconType={IN_ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterIn}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={OUT_ICON_TEST_ID}
|
||||
aria-label={OUT_ICON_TITLE}
|
||||
iconType={OUT_ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterOut}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,406 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilterOut /> should render an empty component (wrong data input) 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[`<FilterOut /> should render an empty component (wrong field input) 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[`<FilterOut /> should render one Component (for EuiDataGrid use) 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</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[`<FilterOut /> should render one EuiButtonIcon 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-label="Filter Out"
|
||||
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
|
||||
data-test-subj="abc"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</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[`<FilterOut /> should render one EuiContextMenuItem (for EuiContextMenu use) 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
class="euiContextMenuItem euiContextMenuItem--small"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Filter Out
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
class="euiContextMenuItem euiContextMenuItem--small"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="minusInCircle"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Filter Out
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</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,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 { Story } from '@storybook/react';
|
||||
import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFiltersContext } from '../../../indicators/context';
|
||||
import { FilterOut } from '.';
|
||||
|
||||
export default {
|
||||
component: FilterOut,
|
||||
title: 'FilterOut',
|
||||
};
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
const mockField: string = 'threat.feed.name';
|
||||
|
||||
return (
|
||||
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
|
||||
<FilterOut data={mockIndicator} field={mockField} />
|
||||
</IndicatorsFiltersContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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, { FunctionComponent } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context';
|
||||
import { FilterOut } from '.';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
|
||||
jest.mock('../../../indicators/hooks/use_indicators_filters_context');
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
const mockField: string = 'threat.feed.name';
|
||||
|
||||
const mockTestId: string = 'abc';
|
||||
|
||||
describe('<FilterOut />', () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
useIndicatorsFiltersContext as jest.MockedFunction<typeof useIndicatorsFiltersContext>
|
||||
).mockReturnValue(mockIndicatorsFiltersContext);
|
||||
});
|
||||
|
||||
it('should render one EuiButtonIcon', () => {
|
||||
const component = render(
|
||||
<FilterOut data={mockIndicator} field={mockField} data-test-subj={mockTestId} />
|
||||
);
|
||||
|
||||
expect(component.getByTestId(mockTestId)).toBeInTheDocument();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render one Component (for EuiDataGrid use)', () => {
|
||||
const mockType: ComponentType = ComponentType.EuiDataGrid;
|
||||
const mockComponent: FunctionComponent = () => <EuiButtonIcon iconType="plusInCircle" />;
|
||||
|
||||
const component = render(
|
||||
<FilterOut data={mockIndicator} field={mockField} type={mockType} as={mockComponent} />
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render one EuiContextMenuItem (for EuiContextMenu use)', () => {
|
||||
const mockType: ComponentType = ComponentType.ContextMenu;
|
||||
|
||||
const component = render(<FilterOut data={mockIndicator} field={mockField} type={mockType} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an empty component (wrong data input)', () => {
|
||||
const component = render(<FilterOut data={''} field={mockField} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an empty component (wrong field input)', () => {
|
||||
const component = render(<FilterOut data={mockIndicator} field={''} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, VFC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { useIndicatorsFiltersContext } from '../../../indicators/hooks/use_indicators_filters_context';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { FilterOut as FilterOutConst, updateFiltersArray } from '../../lib/filter';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
const ICON_TYPE = 'minusInCircle';
|
||||
const ICON_TITLE = i18n.translate('xpack.threatIntelligence.queryBar.filterOutButton', {
|
||||
defaultMessage: 'Filter Out',
|
||||
});
|
||||
|
||||
export interface FilterOutProps {
|
||||
/**
|
||||
* Value used to filter in/out in the KQL bar. Used in combination with field if is type of {@link Indicator}.
|
||||
*/
|
||||
data: Indicator | string;
|
||||
/**
|
||||
* Value used to filter in /out in the KQL bar.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Dictates the way the FilterOut component is rendered depending on the situation in which it's used
|
||||
*/
|
||||
type?: ComponentType;
|
||||
/**
|
||||
* Display component for when the FilterIn component is used within a DataGrid
|
||||
*/
|
||||
as?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
/**
|
||||
* Used for unit and e2e tests.
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the indicator's field and value, then creates a new {@link Filter} and adds it to the {@link FilterManager}.
|
||||
*
|
||||
* The component has 3 renders depending on where it's used: within a EuiContextMenu, a EuiDataGrid or not.
|
||||
*
|
||||
* @returns filter out button
|
||||
*/
|
||||
export const FilterOut: VFC<FilterOutProps> = ({ data, field, type, as: Component, ...props }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const { filterManager } = useIndicatorsFiltersContext();
|
||||
|
||||
const { key, value } =
|
||||
typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field);
|
||||
|
||||
const filterOut = useCallback(() => {
|
||||
const existingFilters: Filter[] = filterManager.getFilters();
|
||||
const newFilters: Filter[] = updateFiltersArray(existingFilters, key, value, FilterOutConst);
|
||||
filterManager.setFilters(newFilters);
|
||||
}, [filterManager, key, value]);
|
||||
|
||||
if (!value || value === EMPTY_VALUE || !key) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (type === ComponentType.EuiDataGrid) {
|
||||
return (
|
||||
<div {...props} css={styles.button}>
|
||||
{/* @ts-ignore*/}
|
||||
<Component aria-label={ICON_TITLE} iconType={ICON_TYPE} onClick={filterOut} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === ComponentType.ContextMenu) {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="filterOut"
|
||||
icon="minusInCircle"
|
||||
size="s"
|
||||
onClick={filterOut}
|
||||
{...props}
|
||||
>
|
||||
Filter Out
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={ICON_TITLE}
|
||||
iconType={ICON_TYPE}
|
||||
iconSize="s"
|
||||
size="xs"
|
||||
color="primary"
|
||||
onClick={filterOut}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 './filter_out';
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { CSSObject } from '@emotion/react';
|
||||
|
||||
export const useStyles = () => {
|
||||
const button: CSSObject = {
|
||||
display: 'inline-flex',
|
||||
};
|
||||
|
||||
return {
|
||||
button,
|
||||
};
|
||||
};
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { VFC } from 'react';
|
||||
import React, { useRef, 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 { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { ComponentType } from '../../../../../common/types/component_type';
|
||||
import { getIndicatorFieldAndValue } from '../../../indicators/lib/field_value';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
|
@ -24,14 +26,18 @@ export interface AddToTimelineProps {
|
|||
* Value passed to the timeline.
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Dictates the way the FilterIn component is rendered depending on the situation in which it's used
|
||||
*/
|
||||
type?: ComponentType;
|
||||
/**
|
||||
* Only used with `EuiDataGrid` (see {@link AddToTimelineButtonProps}).
|
||||
*/
|
||||
as?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
/**
|
||||
* Used as `data-test-subj` value for e2e tests.
|
||||
* Used for unit and e2e tests.
|
||||
*/
|
||||
testId?: string;
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,11 +46,15 @@ export interface AddToTimelineProps {
|
|||
* 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.
|
||||
*
|
||||
* The component has 2 renders depending on where it's used: within a EuiContextMenu or not.
|
||||
*
|
||||
* @returns add to timeline button or an empty component.
|
||||
*/
|
||||
export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, as, testId }) => {
|
||||
export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, type, as, ...props }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const contextMenuRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const addToTimelineButton =
|
||||
useKibana().services.timelines.getHoverActions().getAddToTimelineButton;
|
||||
|
||||
|
@ -78,10 +88,33 @@ export const AddToTimeline: VFC<AddToTimelineProps> = ({ data, field, as, testId
|
|||
field: key,
|
||||
ownFocus: false,
|
||||
};
|
||||
|
||||
// Use case is for the barchart legend (for example).
|
||||
// We can't use the addToTimelineButton directly because the UI doesn't work in a EuiContextMenu.
|
||||
// We hide it and use the defaultFocusedButtonRef props to programmatically click it.
|
||||
if (type === ComponentType.ContextMenu) {
|
||||
addToTimelineProps.defaultFocusedButtonRef = contextMenuRef;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div css={styles.displayNone}>{addToTimelineButton(addToTimelineProps)}</div>
|
||||
<EuiContextMenuItem
|
||||
key="addToTimeline"
|
||||
icon="timeline"
|
||||
size="s"
|
||||
onClick={() => contextMenuRef.current?.click()}
|
||||
{...props}
|
||||
>
|
||||
Add to Timeline
|
||||
</EuiContextMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (as) addToTimelineProps.Component = as;
|
||||
|
||||
return (
|
||||
<div data-test-subj={testId} css={styles.button}>
|
||||
<div {...props} css={styles.inlineFlex}>
|
||||
{addToTimelineButton(addToTimelineProps)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -8,11 +8,16 @@
|
|||
import { CSSObject } from '@emotion/react';
|
||||
|
||||
export const useStyles = () => {
|
||||
const button: CSSObject = {
|
||||
const inlineFlex: CSSObject = {
|
||||
display: 'inline-flex',
|
||||
};
|
||||
|
||||
const displayNone: CSSObject = {
|
||||
display: 'none',
|
||||
};
|
||||
|
||||
return {
|
||||
button,
|
||||
inlineFlex,
|
||||
displayNone,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue