[SIEM] Drag between ANDs in timeline queries / add to timeline test coverage / CSS Tweaks (#67150)

## Summary

- Increases test coverage for the [[SIEM] Drag between ANDs in timeline queries / add to timeline](https://github.com/elastic/kibana/pull/65228) `7.8` feature
- Removes the `show` prop from `DataProviders`, which added delay when closing the timeline
- CSS tweaks (detailed below)
- Removed files no longer in use

### CSS Tweaks 

- Providers in the timeline are now vertically centered, as shown in the _Before_ / _After_ screenshots below (containing two groups):

#### Before

<img width="1186" alt="providers-two-rows-before" src="https://user-images.githubusercontent.com/4459398/82508699-1e8bcc80-9ac3-11ea-8f44-b170d1528a12.png">

#### After

<img width="1186" alt="providers-two-rows-after" src="https://user-images.githubusercontent.com/4459398/82508708-264b7100-9ac3-11ea-8532-a24612573991.png">


- Very slightly increased the padding between the bottom of the last group of data providers and the dashed border, as shown in the _Before_ / _After_ screenshots below (containing three groups):

#### Before
<img width="1200" alt="providers-three-rows-padding-bottom-before" src="https://user-images.githubusercontent.com/4459398/82508573-b63ceb00-9ac2-11ea-9f8b-c3c024a3c91e.png">

#### After
<img width="1201" alt="providers-three-rows-padding-bottom-after" src="https://user-images.githubusercontent.com/4459398/82508578-b89f4500-9ac2-11ea-8560-a40eb10b22f1.png">

- Moved the timeline flyout button slightly to the right by a few pixels to avoid showing the flyout button's right border, as shown in the _Before_ / _After_ screenshots below:

#### Before

<img width="1179" alt="timeline-button-before" src="https://user-images.githubusercontent.com/4459398/82509015-f94b8e00-9ac3-11ea-9d2f-644721c0ec21.png">

#### After

<img width="1178" alt="timeline-button-after" src="https://user-images.githubusercontent.com/4459398/82509026-00729c00-9ac4-11ea-9029-6a4f698daa36.png">

### Desk testing

Desk tested in:
- Chrome `83.0.4103.61`
- Firefox `76.0.1`
- Safari `13.1`
This commit is contained in:
Andrew Goldstein 2020-05-21 15:32:52 -06:00 committed by GitHub
parent 90cabaef2d
commit 75d6682d86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1683 additions and 256 deletions

View file

@ -15,6 +15,7 @@ import { TestProviders } from '../../mock';
import { createKibanaCoreStartMock } from '../../mock/kibana_core';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { TimelineContext } from '../../../timelines/components/timeline/timeline_context';
import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
@ -27,11 +28,18 @@ jest.mock('uuid', () => {
};
});
jest.mock('../../hooks/use_add_to_timeline');
const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings;
const field = 'process.name';
const value = 'nice';
describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
// our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function:
(useAddToTimeline as jest.Mock).mockReturnValue(jest.fn());
});
// Suppress warnings about "react-beautiful-dnd"
/* eslint-disable no-console */
const originalError = console.error;
@ -329,6 +337,75 @@ describe('DraggableWrapperHoverContent', () => {
});
});
describe('Add to timeline', () => {
const aggregatableStringField = 'cloud.account.id';
const draggableId = 'draggable.id';
[false, true].forEach(showTopN => {
[value, null].forEach(maybeValue => {
[draggableId, undefined].forEach(maybeDraggableId => {
const shouldRender = !showTopN && maybeValue != null && maybeDraggableId != null;
const assertion = shouldRender ? 'should render' : 'should NOT render';
test(`it ${assertion} the 'Add to timeline investigation' button when showTopN is ${showTopN}, value is ${maybeValue}, and a draggableId is ${maybeDraggableId}`, () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mocksSource} addTypename={false}>
<DraggableWrapperHoverContent
draggableId={maybeDraggableId}
field={aggregatableStringField}
showTopN={showTopN}
toggleTopN={jest.fn()}
value={maybeValue}
/>
</MockedProvider>
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="add-to-timeline"]')
.first()
.exists()
).toBe(shouldRender);
});
});
});
});
test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mocksSource} addTypename={false}>
<DraggableWrapperHoverContent
draggableId={draggableId}
field={aggregatableStringField}
showTopN={false}
toggleTopN={jest.fn()}
value={value}
/>
</MockedProvider>
</TestProviders>
);
// The following "startDragToTimeline" function returned by our mock
// useAddToTimeline hook is called when the user clicks the
// Add to timeline investigation action:
const startDragToTimeline = useAddToTimeline({
draggableId,
fieldName: aggregatableStringField,
});
wrapper
.find('[data-test-subj="add-to-timeline"]')
.first()
.simulate('click');
wrapper.update();
expect(startDragToTimeline).toHaveBeenCalled();
});
});
describe('Top N', () => {
test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, async () => {
const aggregatableStringField = 'cloud.account.id';

View file

@ -5,8 +5,12 @@
*/
import { omit } from 'lodash/fp';
import { DropResult } from 'react-beautiful-dnd';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
import {
addProviderToTimeline,
allowTopN,
destinationIsTimelineButton,
destinationIsTimelineColumns,
@ -28,10 +32,14 @@ import {
getDroppableId,
getFieldIdFromDraggable,
getProviderIdFromDraggable,
getTimelineProviderDraggableId,
getTimelineProviderDroppableId,
providerWasDroppedOnTimeline,
reasonIsDrop,
sourceAndDestinationAreSameTimelineProviders,
sourceIsContent,
unEscapeFieldId,
userIsReArrangingProviders,
} from './helpers';
const DROPPABLE_ID_TIMELINE_PROVIDERS = `${droppableTimelineProvidersPrefix}timeline`;
@ -717,4 +725,263 @@ describe('helpers', () => {
).toBe(false);
});
});
describe('getTimelineProviderDroppableId', () => {
test('it returns the expected id', () => {
expect(
getTimelineProviderDroppableId({
groupIndex: 1234,
timelineId: 'i-hope-you-had-the-time-of-your-life',
})
).toEqual('droppableId.timelineProviders.i-hope-you-had-the-time-of-your-life.group.1234');
});
});
describe('getTimelineProviderDraggableId', () => {
test('it returns the expected id', () => {
const dataProviderId =
'port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828';
expect(
getTimelineProviderDraggableId({
dataProviderId,
groupIndex: 1234,
timelineId: 'time-to-make-the-doughnuts',
})
).toEqual(
`draggableId.timelineProviders.time-to-make-the-doughnuts.group.1234.${dataProviderId}`
);
});
});
describe('sourceAndDestinationAreSameTimelineProviders', () => {
test('it returns true when the source and destination droppable IDs exactly match', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(sourceAndDestinationAreSameTimelineProviders(result)).toBe(true);
});
test('it returns true when the source and destination droppable IDs are the same timeline, but different groups', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.1' },
type: 'DEFAULT',
};
expect(sourceAndDestinationAreSameTimelineProviders(result)).toBe(true);
});
test('it returns false when destination is undefined', () => {
const result: DropResult = {
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.1' },
type: 'DEFAULT',
};
expect(sourceAndDestinationAreSameTimelineProviders(result)).toBe(false);
});
test('it returns false when the source and destination droppable IDs are for different timelines', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-2.group.0' },
type: 'DEFAULT',
};
expect(sourceAndDestinationAreSameTimelineProviders(result)).toBe(false);
});
test('it returns false when the destination is NOT timeline providers', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.otherProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(sourceAndDestinationAreSameTimelineProviders(result)).toBe(false);
});
test('it returns false when the source is NOT timeline providers', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.otherProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(sourceAndDestinationAreSameTimelineProviders(result)).toBe(false);
});
});
describe('userIsReArrangingProviders', () => {
test('it returns true when reason IS DROP and source + destination are the SAME timeline providers', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(userIsReArrangingProviders(result)).toBe(true);
});
test('it returns false when reason IS DROP, but source + destination are NOT the same timeline providers', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.otherProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(userIsReArrangingProviders(result)).toBe(false);
});
test('it returns false when the reason is NOT DROP and source + destination are the SAME timeline providers', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'CANCEL',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(userIsReArrangingProviders(result)).toBe(false);
});
test('it returns false when the reason is NOT DROP and source + destination are NOT the same timeline providers', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.otherProviders.timeline-1.group.0', index: 1 },
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'CANCEL',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.0' },
type: 'DEFAULT',
};
expect(userIsReArrangingProviders(result)).toBe(false);
});
test('it returns false when reason IS DROP, but destination is undefined', () => {
const result: DropResult = {
draggableId:
'draggableId.timelineProviders.timeline-1.group.0.port-default-draggable-netflow-renderer-timeline-1-Ib4zD3IBbNV0npT21btr-Ib4zD3IBbNV0npT21btr-source_port-57828',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.timelineProviders.timeline-1.group.1' },
type: 'DEFAULT',
};
expect(userIsReArrangingProviders(result)).toBe(false);
});
});
describe('addProviderToTimeline', () => {
const result: DropResult = {
destination: { droppableId: 'droppableId.timelineProviders.timeline-1', index: 0 },
draggableId: 'draggableId.content.hosts-table-hostName-ENDPOINT-W-0-01',
mode: 'FLUID',
reason: 'DROP',
source: { index: 0, droppableId: 'droppableId.content.hosts-table-hostName-ENDPOINT-W-0-01' },
type: 'DEFAULT',
};
test('it dispatches the expected UPDATE_PROVIDERS action when the provider to add exists in the `dataProviders` collection of `id -> `DataProvider`', () => {
const dispatch = jest.fn();
const onAddedToTimeline = jest.fn();
const dataProviders: IdToDataProvider = {
'hosts-table-hostName-ENDPOINT-W-0-01': {
and: [],
enabled: true,
excluded: false,
id: 'hosts-table-hostName-ENDPOINT-W-0-01',
kqlQuery: '',
name: 'ENDPOINT-W-0-01',
queryMatch: { field: 'host.name', value: 'ENDPOINT-W-0-01', operator: ':' },
},
};
addProviderToTimeline({
activeTimelineDataProviders: [],
dataProviders,
dispatch,
result,
timelineId: 'timeline-1',
onAddedToTimeline,
});
expect(dispatch).toBeCalledWith({
payload: {
id: 'timeline-1',
providers: [
{
and: [],
enabled: true,
excluded: false,
id: 'hosts-table-hostName-ENDPOINT-W-0-01',
kqlQuery: '',
name: 'ENDPOINT-W-0-01',
queryMatch: { field: 'host.name', operator: ':', value: 'ENDPOINT-W-0-01' },
},
],
},
type: 'x-pack/siem/local/timeline/UPDATE_PROVIDERS',
});
});
test('it dispatches the expected NO_PROVIDER_FOUND action when the provider to add does NOT exist in the `dataProviders` collection of `id -> `DataProvider`', () => {
const dispatch = jest.fn();
const onAddedToTimeline = jest.fn();
addProviderToTimeline({
activeTimelineDataProviders: [],
dataProviders: {}, // <-- the specified data provider ID does not exist in this empty collection
dispatch,
result,
timelineId: 'timeline-1',
onAddedToTimeline,
});
expect(dispatch).toBeCalledWith({
payload: {
id: 'hosts-table-hostName-ENDPOINT-W-0-01',
},
type: 'x-pack/siem/local/drag_and_drop/NO_PROVIDER_FOUND',
});
});
});
});

View file

@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mount } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../../common/mock/test_providers';
import { twoGroups } from '../../timeline/data_providers/mock/mock_and_providers';
import { FlyoutButton, getBadgeCount } from '.';
describe('FlyoutButton', () => {
describe('getBadgeCount', () => {
test('it returns 0 when dataProviders is empty', () => {
expect(getBadgeCount([])).toEqual(0);
});
test('it returns a count that includes every provider in every group of ANDs', () => {
expect(getBadgeCount(twoGroups)).toEqual(6);
});
});
test('it renders the button when show is true', () => {
const onOpen = jest.fn();
const wrapper = mount(
<TestProviders>
<FlyoutButton dataProviders={[]} onOpen={onOpen} show={true} timelineId="test" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toBe(true);
});
test('it renders the expected button text', () => {
const onOpen = jest.fn();
const wrapper = mount(
<TestProviders>
<FlyoutButton dataProviders={[]} onOpen={onOpen} show={true} timelineId="test" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="flyout-button-not-ready-to-drop"]')
.first()
.text()
).toEqual('Timeline');
});
test('it renders the data providers drop target area', () => {
const onOpen = jest.fn();
const wrapper = mount(
<TestProviders>
<FlyoutButton dataProviders={[]} onOpen={onOpen} show={true} timelineId="test" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toBe(true);
});
test('it does NOT render the button when show is false', () => {
const onOpen = jest.fn();
const wrapper = mount(
<TestProviders>
<FlyoutButton dataProviders={[]} onOpen={onOpen} show={false} timelineId="test" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toBe(false);
});
test('it invokes `onOpen` when clicked', () => {
const onOpen = jest.fn();
const wrapper = mount(
<TestProviders>
<FlyoutButton dataProviders={[]} onOpen={onOpen} show={true} timelineId="test" />
</TestProviders>
);
wrapper
.find('[data-test-subj="flyout-button-not-ready-to-drop"]')
.first()
.simulate('click');
wrapper.update();
expect(onOpen).toBeCalled();
});
});

View file

@ -22,7 +22,7 @@ export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
export const getBadgeCount = (dataProviders: DataProvider[]): number =>
flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0);
const SHOW_HIDE_TRANSLATE_X = 497; // px
const SHOW_HIDE_TRANSLATE_X = 501; // px
const Container = styled.div`
padding-top: 8px;
@ -131,7 +131,6 @@ export const FlyoutButton = React.memo<FlyoutButtonProps>(
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
show={show}
/>
)}
</WithSource>

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Providers rendering renders correctly against snapshot 1`] = `
<Fragment>
<div>
<EuiFlexGroup
alignItems="center"
gutterSize="none"
@ -530,5 +530,5 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
</styled.span>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
</div>
`;

View file

@ -30,7 +30,6 @@ describe('DataProviders', () => {
onDataProviderRemoved={jest.fn()}
onToggleDataProviderEnabled={jest.fn()}
onToggleDataProviderExcluded={jest.fn()}
show={true}
/>
);
expect(wrapper).toMatchSnapshot();
@ -49,7 +48,6 @@ describe('DataProviders', () => {
onDataProviderRemoved={jest.fn()}
onToggleDataProviderEnabled={jest.fn()}
onToggleDataProviderExcluded={jest.fn()}
show={true}
/>
</TestProviders>
);
@ -68,7 +66,6 @@ describe('DataProviders', () => {
onDataProviderRemoved={jest.fn()}
onToggleDataProviderEnabled={jest.fn()}
onToggleDataProviderExcluded={jest.fn()}
show={true}
/>
</TestProviders>
);

View file

@ -14,11 +14,15 @@ import { DataProvider, DataProvidersAnd } from './data_provider';
export const omitAnd = (provider: DataProvider): DataProvidersAnd => omit('and', provider);
export const reorder = (
group: DataProvidersAnd[],
startIndex: number,
endIndex: number
): DataProvidersAnd[] => {
export const reorder = ({
endIndex,
group,
startIndex,
}: {
endIndex: number;
group: DataProvidersAnd[];
startIndex: number;
}): DataProvidersAnd[] => {
const groupClone = [...group];
const [removed] = groupClone.splice(startIndex, 1); // ⚠️ mutation
groupClone.splice(endIndex, 0, removed); // ⚠️ mutation
@ -95,7 +99,11 @@ export const reArrangeProvidersInSameGroup = ({
dataProviderGroups,
})
) {
const reorderedGroup = reorder(dataProviderGroups[groupIndex], source.index, destination.index);
const reorderedGroup = reorder({
group: dataProviderGroups[groupIndex],
startIndex: source.index,
endIndex: destination.index,
});
const updatedGroups = dataProviderGroups.reduce<DataProvidersAnd[][]>(
(acc, group, i) => [...acc, i === groupIndex ? [...reorderedGroup] : [...group]],

View file

@ -34,7 +34,6 @@ interface Props {
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
show: boolean;
}
const DropTargetDataProvidersContainer = styled.div`
@ -55,10 +54,14 @@ const DropTargetDataProvidersContainer = styled.div`
`;
const DropTargetDataProviders = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
padding-bottom: 2px;
position: relative;
border: 0.2rem dashed ${props => props.theme.eui.euiColorMediumShade};
border-radius: 5px;
margin: 5px 0 5px 0;
margin: 2px 0 2px 0;
min-height: 100px;
overflow-y: auto;
background-color: ${props => props.theme.eui.euiFormBackgroundColor};
@ -94,7 +97,6 @@ export const DataProviders = React.memo<Props>(
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
show,
}) => {
return (
<DropTargetDataProvidersContainer className="drop-target-data-providers-container">
@ -116,10 +118,7 @@ export const DataProviders = React.memo<Props>(
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
) : (
<DroppableWrapper
isDropDisabled={!show || isLoading}
droppableId={getDroppableId(id)}
>
<DroppableWrapper isDropDisabled={isLoading} droppableId={getDroppableId(id)}>
<Empty />
</DroppableWrapper>
)}

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DataProvider, DataProvidersAnd, IS_OPERATOR } from '../data_provider';
export const providerA: DataProvidersAnd = {
enabled: true,
excluded: false,
id: 'context-field.name-a',
kqlQuery: '',
name: 'a',
queryMatch: {
field: 'field.name',
value: 'a',
operator: IS_OPERATOR,
},
};
export const providerB: DataProvidersAnd = {
enabled: true,
excluded: false,
id: 'context-field.name-b',
kqlQuery: '',
name: 'b',
queryMatch: {
field: 'field.name',
value: 'b',
operator: IS_OPERATOR,
},
};
export const providerC: DataProvidersAnd = {
enabled: true,
excluded: false,
id: 'context-field.name-c',
kqlQuery: '',
name: 'c',
queryMatch: {
field: 'field.name',
value: 'c',
operator: IS_OPERATOR,
},
};
export const providerD: DataProvidersAnd = {
enabled: true,
excluded: false,
id: 'context-field.name-d',
kqlQuery: '',
name: 'd',
queryMatch: {
field: 'field.name',
value: 'd',
operator: IS_OPERATOR,
},
};
export const providerE: DataProvidersAnd = {
enabled: true,
excluded: false,
id: 'context-field.name-e',
kqlQuery: '',
name: 'e',
queryMatch: {
field: 'field.name',
value: 'e',
operator: IS_OPERATOR,
},
};
export const providerF: DataProvidersAnd = {
enabled: true,
excluded: false,
id: 'context-field.name-f',
kqlQuery: '',
name: 'f',
queryMatch: {
field: 'field.name',
value: 'f',
operator: IS_OPERATOR,
},
};
export const twoGroups: DataProvider[] = [
{ ...providerA, and: [providerB, providerC] },
{ ...providerD, and: [providerE, providerF] },
];

View file

@ -1,95 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { AndOrBadge } from '../and_or_badge';
import { BrowserFields } from '../../../../common/containers/source';
import {
OnChangeDataProviderKqlQuery,
OnDataProviderEdited,
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { DataProvidersAnd, IS_OPERATOR } from './data_provider';
import { ProviderItemBadge } from './provider_item_badge';
interface ProviderItemAndPopoverProps {
browserFields: BrowserFields;
dataProvidersAnd: DataProvidersAnd[];
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onDataProviderEdited: OnDataProviderEdited;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
providerId: string;
timelineId: string;
}
export class ProviderItemAnd extends React.PureComponent<ProviderItemAndPopoverProps> {
public render() {
const {
browserFields,
dataProvidersAnd,
onDataProviderEdited,
providerId,
timelineId,
} = this.props;
return dataProvidersAnd.map((providerAnd: DataProvidersAnd, index: number) => (
<React.Fragment key={`provider-item-and-${timelineId}-${providerId}-${providerAnd.id}`}>
<EuiFlexItem>
<AndOrBadge type="and" />
</EuiFlexItem>
<EuiFlexItem>
<ProviderItemBadge
andProviderId={providerAnd.id}
browserFields={browserFields}
deleteProvider={() => this.deleteAndProvider(providerId, providerAnd.id)}
field={providerAnd.queryMatch.displayField || providerAnd.queryMatch.field}
kqlQuery={providerAnd.kqlQuery}
isEnabled={providerAnd.enabled}
isExcluded={providerAnd.excluded}
onDataProviderEdited={onDataProviderEdited}
operator={providerAnd.queryMatch.operator || IS_OPERATOR}
providerId={providerId}
timelineId={timelineId}
toggleEnabledProvider={() =>
this.toggleEnabledAndProvider(providerId, !providerAnd.enabled, providerAnd.id)
}
toggleExcludedProvider={() =>
this.toggleExcludedAndProvider(providerId, !providerAnd.excluded, providerAnd.id)
}
val={providerAnd.queryMatch.displayValue || providerAnd.queryMatch.value}
/>
</EuiFlexItem>
</React.Fragment>
));
}
private deleteAndProvider = (providerId: string, andProviderId: string) => {
this.props.onDataProviderRemoved(providerId, andProviderId);
};
private toggleEnabledAndProvider = (
providerId: string,
enabled: boolean,
andProviderId: string
) => {
this.props.onToggleDataProviderEnabled({ providerId, enabled, andProviderId });
};
private toggleExcludedAndProvider = (
providerId: string,
excluded: boolean,
andProviderId: string
) => {
this.props.onToggleDataProviderExcluded({ providerId, excluded, andProviderId });
};
}

View file

@ -1,136 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { rgba } from 'polished';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { AndOrBadge } from '../and_or_badge';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnDataProviderEdited,
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { BrowserFields } from '../../../../common/containers/source';
import { DataProvider } from './data_provider';
import { ProviderItemAnd } from './provider_item_and';
import * as i18n from './translations';
const DropAndTargetDataProvidersContainer = styled(EuiFlexItem)`
margin: 0px 8px;
`;
DropAndTargetDataProvidersContainer.displayName = 'DropAndTargetDataProvidersContainer';
const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>`
min-width: 230px;
width: auto;
border: 0.1rem dashed ${props => props.theme.eui.euiColorSuccess};
border-radius: 5px;
text-align: center;
padding: 3px 10px;
display: flex;
justify-content: center;
align-items: center;
${props =>
props.hasAndItem
? `&:hover {
transition: background-color 0.7s ease;
background-color: ${() => rgba(props.theme.eui.euiColorSuccess, 0.2)};
}`
: ''};
cursor: ${({ hasAndItem }) => (!hasAndItem ? `default` : 'inherit')};
`;
DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders';
const NumberProviderAndBadge = (styled(EuiBadge)`
margin: 0px 5px;
` as unknown) as typeof EuiBadge;
NumberProviderAndBadge.displayName = 'NumberProviderAndBadge';
interface ProviderItemDropProps {
browserFields: BrowserFields;
dataProvider: DataProvider;
mousePosition?: { x: number; y: number; boundLeft: number; boundTop: number };
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onChangeDroppableAndProvider: OnChangeDroppableAndProvider;
onDataProviderEdited: OnDataProviderEdited;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
timelineId: string;
}
export const ProviderItemAndDragDrop = React.memo<ProviderItemDropProps>(
({
browserFields,
dataProvider,
onChangeDataProviderKqlQuery,
onChangeDroppableAndProvider,
onDataProviderEdited,
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
timelineId,
}) => {
const onMouseEnter = useCallback(() => onChangeDroppableAndProvider(dataProvider.id), [
onChangeDroppableAndProvider,
dataProvider.id,
]);
const onMouseLeave = useCallback(() => onChangeDroppableAndProvider(''), [
onChangeDroppableAndProvider,
]);
const hasAndItem = dataProvider.and.length > 0;
return (
<EuiFlexGroup
direction="row"
gutterSize="none"
justifyContent="flexStart"
alignItems="center"
>
<DropAndTargetDataProvidersContainer className="drop-and-provider-timeline">
<DropAndTargetDataProviders
hasAndItem={hasAndItem}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{hasAndItem && (
<NumberProviderAndBadge color="primary">
{dataProvider.and.length}
</NumberProviderAndBadge>
)}
<EuiText color="subdued" size="xs">
{i18n.DROP_HERE_TO_ADD_AN}
</EuiText>
<AndOrBadge type="and" />
</DropAndTargetDataProviders>
</DropAndTargetDataProvidersContainer>
<ProviderItemAnd
browserFields={browserFields}
dataProvidersAnd={dataProvider.and}
providerId={dataProvider.id}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onDataProviderEdited={onDataProviderEdited}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
timelineId={timelineId}
/>
</EuiFlexGroup>
);
}
);
ProviderItemAndDragDrop.displayName = 'ProviderItemAndDragDrop';

View file

@ -129,7 +129,7 @@ export const Providers = React.memo<Props>(
);
return (
<>
<div>
{dataProviderGroups.map((group, groupIndex) => (
<EuiFlexGroup alignItems="center" gutterSize="none" key={`droppable-${groupIndex}`}>
<OrFlexItem grow={false}>
@ -259,7 +259,7 @@ export const Providers = React.memo<Props>(
</EuiFlexItem>
</EuiFlexGroup>
))}
</>
</div>
);
}
);

View file

@ -143,7 +143,6 @@ exports[`Header rendering renders correctly against snapshot 1`] = `
onDataProviderRemoved={[MockFunction]}
onToggleDataProviderEnabled={[MockFunction]}
onToggleDataProviderExcluded={[MockFunction]}
show={true}
/>
<Connect(StatefulSearchOrFilterComponent)
browserFields={Object {}}

View file

@ -44,7 +44,7 @@ describe('Header', () => {
expect(wrapper).toMatchSnapshot();
});
test('it renders the data providers', () => {
test('it renders the data providers when show is true', () => {
const wrapper = mount(
<TestProviders>
<TimelineHeader
@ -66,6 +66,28 @@ describe('Header', () => {
expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true);
});
test('it does NOT render the data providers when show is false', () => {
const wrapper = mount(
<TestProviders>
<TimelineHeader
browserFields={{}}
dataProviders={mockDataProviders}
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
id="foo"
indexPattern={indexPattern}
onDataProviderEdited={jest.fn()}
onDataProviderRemoved={jest.fn()}
onToggleDataProviderEnabled={jest.fn()}
onToggleDataProviderExcluded={jest.fn()}
show={false}
showCallOutUnauthorizedMsg={false}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(false);
});
test('it renders the unauthorized call out providers', () => {
const wrapper = mount(
<TestProviders>

View file

@ -68,7 +68,6 @@ const TimelineHeaderComponent: React.FC<Props> = ({
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
show={show}
/>
)}