Timeline filter with and & or (#29101)

* option1

* wip filter and -or

* wip provider item with or/and design as wanted

* just add the and and or to the kql query

* fix types

* fix merge with feature-secops branch

* do not show date tooltip when it is an And Provider

* fix existing unit testing

* add unit testing

* put back original theme

* review

* review-part 1

* review part-2

* add translation for i18n
This commit is contained in:
Xavier Mouligneau 2019-01-23 11:01:00 -05:00 committed by GitHub
parent 873763f320
commit d784ca5c69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2886 additions and 576 deletions

View file

@ -9,9 +9,10 @@
"@types/boom": "^7.2.0",
"@types/color": "^3.0.0",
"@types/lodash": "^4.14.110",
"@types/react-beautiful-dnd": "^7.1.2"
"@types/react-beautiful-dnd": "^10.0.1"
},
"dependencies": {
"react-beautiful-dnd": "^10.0.1",
"lodash": "^4.17.10"
}
}

View file

@ -34,9 +34,7 @@ interface OnDragEndHandlerParams {
const onDragEndHandler = ({ result, dataProviders, dispatch }: OnDragEndHandlerParams) => {
if (providerWasDroppedOnTimeline(result)) {
addProviderToTimeline({ dataProviders, result, dispatch });
}
if (providerWasDroppedOnTimelineButton(result)) {
} else if (providerWasDroppedOnTimelineButton(result)) {
addProviderToTimeline({ dataProviders, result, dispatch });
}
};

View file

@ -21,10 +21,51 @@ const getBackgroundColor = (theme: Theme): string =>
const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; themeName: Theme }>`
transition: background-color 0.7s ease;
min-height: 100px;
.euiPanel {
background-color: ${({ isDraggingOver, themeName }) =>
isDraggingOver ? '#f0f8ff' : getBackgroundColor(themeName)};
width: 100%;
height: 100%;
.flyout-overlay {
.euiPanel {
background-color: ${({ themeName }) => getBackgroundColor(themeName)};
}
}
${({ isDraggingOver }) =>
isDraggingOver
? `
.drop-and-provider-timeline {
&:hover {
background: repeating-linear-gradient(
-55deg,
rgb(52, 55, 65),
rgb(52, 55, 65) 10px,
rgb(245, 247, 250) 10px,
rgb(245, 247, 250) 20px
);
}
}
> div.timeline-drop-area {
background-color: rgb(245, 247, 250);
.provider-item-filter-container div:first-child{
/// Ooverwride dragNdrop beautifull so we do not have our droppable moving around for no good reason
transform: none !important;
}
}
.flyout-overlay {
.euiPanel {
background-color: rgb(245, 247, 250);
}
+ div {
// Ooverwride dragNdrop beautifull so we do not have our droppable moving around for no good reason
display: none !important;
}
}
`
: ''}
> div.timeline-drop-area {
& + div {
// overwride dragNdrop beautifull so we do not have our droppable moving around for no good reason
display: none !important;
}
}
`;

View file

@ -52,6 +52,7 @@ describe('helpers', () => {
reason: 'DROP',
source: { index: 0, droppableId: getDroppableId('2119990039033485') },
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(true);
});
@ -64,6 +65,7 @@ describe('helpers', () => {
reason: 'DROP',
source: { index: 0, droppableId: `${droppableIdPrefix}.somethingElse.2119990039033485` },
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -84,6 +86,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(true);
});
@ -99,6 +102,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -119,6 +123,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(true);
});
@ -137,6 +142,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -157,6 +163,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(true);
});
@ -172,6 +179,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -190,6 +198,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -210,6 +219,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(true);
});
@ -225,6 +235,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -243,6 +254,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -263,6 +275,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual('timeline');
});
@ -281,6 +294,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual('timeline');
});
@ -296,6 +310,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual('');
});
@ -314,6 +329,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual('');
});
@ -333,6 +349,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
});
const expected = '2119990039033485';
@ -355,6 +372,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(true);
});
@ -373,6 +391,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -388,6 +407,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -400,6 +420,7 @@ describe('helpers', () => {
reason: 'DROP',
source: { index: 0, droppableId: `${droppableIdPrefix}.somethingElse.2119990039033485` },
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});
@ -418,6 +439,7 @@ describe('helpers', () => {
index: 0,
},
type: 'DEFAULT',
mode: 'FLUID',
})
).toEqual(false);
});

View file

@ -100,17 +100,17 @@ export const getItems = ({ data, populatedFields }: GetItemsParams): Item[] => {
data,
fieldName: field.name,
})}`,
queryMatch: `${getOr(
field.name,
field.name,
mappedEcsSchemaFieldNames
)}: "${escapeQueryValue(
getMappedEcsValue({
data,
fieldName: field.name,
})
)}"`,
negated: false,
queryMatch: {
field: getOr(field.name, field.name, mappedEcsSchemaFieldNames),
value: escapeQueryValue(
getMappedEcsValue({
data,
fieldName: field.name,
})
),
},
excluded: false,
kqlQuery: '',
and: [],
}}
render={() => `${getMappedEcsValue({ data, fieldName: field.name })}`}

View file

@ -78,7 +78,11 @@ export const FlyoutButton = pure(
droppableId={`${droppableTimelineFlyoutButtonPrefix}${timelineId}`}
theme={theme}
>
<BadgeButtonContainer data-test-subj="flyoutOverlay" onClick={onOpen}>
<BadgeButtonContainer
className="flyout-overlay"
data-test-subj="flyoutOverlay"
onClick={onOpen}
>
{dataProviders.length !== 0 && (
<Badge data-test-subj="badge" color="primary">
{dataProviders.length}

View file

@ -6,14 +6,12 @@
import { EuiBadge } from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n/react';
import { noop } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { pure } from 'recompose';
import moment from 'moment';
import { AuthenticationsEdges } from '../../../../graphql/types';
import { escapeQueryValue } from '../../../../lib/keury';
import { authenticationsSelector, hostsActions, State } from '../../../../store';
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
import { defaultToEmpty, getEmptyValue } from '../../../empty_value';
@ -117,18 +115,21 @@ const getAuthenticationColumns = (startDate: number) => [
enabled: true,
id: node._id,
name: userName!,
negated: false,
queryMatch: `auditd.data.acct: "${escapeQueryValue(userName!)}"`,
queryDate: `@timestamp >= ${startDate} and @timestamp <= ${moment().valueOf()}`,
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'auditd.data.acct',
value: userName!,
},
queryDate: {
from: startDate,
to: moment().valueOf(),
},
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider
dataProvider={dataProvider}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
/>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
userName

View file

@ -5,14 +5,13 @@
*/
import { EuiBadge } from '@elastic/eui';
import { has, noop } from 'lodash/fp';
import { has } from 'lodash/fp';
import moment from 'moment';
import React from 'react';
import { connect } from 'react-redux';
import { pure } from 'recompose';
import { Ecs, EcsEdges } from '../../../../graphql/types';
import { escapeQueryValue } from '../../../../lib/keury';
import { eventsSelector, hostsActions, State } from '../../../../store';
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
import { getEmptyValue, getOrEmpty } from '../../../empty_value';
@ -119,18 +118,23 @@ const getEventsColumns = (startDate: number) => [
enabled: true,
id: node._id!,
name: hostName,
negated: false,
queryMatch: `host.id: "${escapeQueryValue(node.host!.id!)}"`,
queryDate: `@timestamp >= ${startDate} and @timestamp <= ${moment().valueOf()}`,
excluded: false,
kqlQuery: '',
queryMatch: {
displayField: 'host.name',
displayValue: hostName,
field: 'host.id',
value: node.host!.id!,
},
queryDate: {
from: startDate,
to: moment().valueOf(),
},
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider
dataProvider={dataProvider}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
/>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
hostName

View file

@ -5,14 +5,13 @@
*/
import { EuiBadge, EuiLink } from '@elastic/eui';
import { get, isNil, noop } from 'lodash/fp';
import { get, isNil } from 'lodash/fp';
import moment from 'moment';
import React from 'react';
import { connect } from 'react-redux';
import { pure } from 'recompose';
import { HostsEdges } from '../../../../graphql/types';
import { escapeQueryValue } from '../../../../lib/keury';
import { hostsActions, hostsSelector, State } from '../../../../store';
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
import { defaultToEmpty, getOrEmpty } from '../../../empty_value';
@ -115,22 +114,25 @@ const getHostsColumns = () => [
dataProvider={{
and: [],
enabled: true,
excluded: false,
id: node._id!,
name: hostName,
negated: false,
queryMatch: `host.id: "${escapeQueryValue(node.host!.id!)}"`,
queryDate: `@timestamp >= ${moment(
node.firstSeen!
).valueOf()} and @timestamp <= ${moment().valueOf()}`,
kqlQuery: '',
queryMatch: {
displayField: 'host.name',
displayValue: hostName,
field: 'host.id',
value: node.host!.id!,
},
queryDate: {
from: moment(node.firstSeen!).valueOf(),
to: moment().valueOf(),
},
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider
dataProvider={dataProvider}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
/>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : isNil(get('host.id', node)) ? (
<>{hostName}</>

View file

@ -5,14 +5,12 @@
*/
import { EuiBadge } from '@elastic/eui';
import { noop } from 'lodash/fp';
import moment from 'moment';
import React from 'react';
import { connect } from 'react-redux';
import { pure } from 'recompose';
import { HostEcsFields, UncommonProcessesEdges } from '../../../../graphql/types';
import { escapeQueryValue } from '../../../../lib/keury';
import { hostsActions, State, uncommonProcessesSelector } from '../../../../store';
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
import { defaultToEmpty, getEmptyValue, getOrEmpty } from '../../../empty_value';
@ -118,18 +116,21 @@ const getUncommonColumns = (startDate: number) => [
enabled: true,
id: node._id,
name: processName!,
negated: false,
queryMatch: `process.name: "${escapeQueryValue(processName!)}"`,
queryDate: `@timestamp >= ${startDate} and @timestamp <= ${moment().valueOf()}`,
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'process.name',
value: processName!,
},
queryDate: {
from: startDate,
to: moment().valueOf(),
},
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider
dataProvider={dataProvider}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
/>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
processName

View file

@ -1,57 +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 { EuiSpacer } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { OnDataProviderRemoved, OnToggleDataProviderEnabled } from '../events';
import { CloseButton } from './close_button';
import { DataProvider } from './data_provider';
import { SwitchButton } from './switch_button';
interface Props {
dataProvider: DataProvider;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
}
const Spacer = styled(EuiSpacer)`
border-left: 1px solid #ccc;
margin: 0 5px 0 5px;
`;
const ActionsContainer = styled.div`
display: flex;
flex-direction: row;
margin-top: 10px;
`;
/**
* Renders an interactive card representation of the data providers. It also
* affords uniform UI controls for the following actions:
* 1) removing a data provider
* 2) temporarily disabling a data provider
* 3) TODO: applying boolean negation to the data provider
*/
export const Actions = pure<Props>(
({ dataProvider, onDataProviderRemoved, onToggleDataProviderEnabled }: Props) => (
<ActionsContainer data-test-subj="data-provider-actions">
<SwitchButton
data-test-subj="data-provider-action-toggle-enabled"
dataProvider={dataProvider}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
/>
<Spacer />
<CloseButton
data-test-subj="data-provider-action-close"
dataProvider={dataProvider}
onDataProviderRemoved={onDataProviderRemoved}
/>
</ActionsContainer>
)
);

View file

@ -1,34 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonIcon } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import { OnDataProviderRemoved } from '../events';
import { DataProvider } from './data_provider';
import * as i18n from './translations';
interface Props {
onDataProviderRemoved: OnDataProviderRemoved;
dataProvider: DataProvider;
}
/** An affordance for removing a data provider. It invokes `onDataProviderRemoved` when clicked */
export const CloseButton = pure<Props>(({ onDataProviderRemoved, dataProvider }) => {
const onClick = () => {
onDataProviderRemoved(dataProvider);
};
return (
<EuiButtonIcon
data-test-subj="closeButton"
onClick={onClick}
iconType="cross"
aria-label={i18n.REMOVE_DATA_PROVIDER}
/>
);
});

View file

@ -4,6 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
export interface QueryDate {
from: number;
to: number;
}
/** Represents the Timeline data providers */
export interface DataProvider {
/** Uniquely identifies a data provider */
@ -15,17 +20,25 @@ export interface DataProvider {
* the timeline. default: `true`
*/
enabled: boolean;
/**
* When `true`, a data provider is excluding the match, but not removed from
* the timeline. default: `false`
*/
excluded: boolean;
/**
* Return the KQL query who have been added by user
*/
kqlQuery: string;
/**
* Returns a query properties that, when executed, returns the data for this provider
*/
queryMatch: string;
queryDate?: string;
/**
* When `true`, boolean logic is applied to the data provider to negate it.
* default: `false`
*/
negated: boolean;
queryMatch: {
field: string;
displayField?: string;
value: string | number;
displayValue?: string | number;
};
queryDate?: QueryDate;
/**
* Additional query clauses that are ANDed with this query to narrow results
*/

View file

@ -10,7 +10,7 @@ import * as React from 'react';
import { DragDropContext } from 'react-beautiful-dnd';
import { DataProviders } from '.';
import { DataProvider } from './data_provider';
import { mockDataProviderNames, mockDataProviders } from './mock/mock_data_providers';
import { mockDataProviders } from './mock/mock_data_providers';
describe('DataProviders', () => {
describe('rendering', () => {
@ -24,8 +24,11 @@ describe('DataProviders', () => {
<DataProviders
id="foo"
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
show={true}
theme="dark"
/>
@ -35,21 +38,24 @@ describe('DataProviders', () => {
dropMessage.forEach(word => expect(wrapper.text()).toContain(word));
});
test('it should NOT render a placeholder given a non-empty collection of data providers', () => {
test('it should STILL render a placeholder given a non-empty collection of data providers', () => {
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DataProviders
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
show={true}
theme="dark"
/>
</DragDropContext>
);
dropMessage.forEach(word => expect(wrapper.text()).not.toContain(word));
dropMessage.forEach(word => expect(wrapper.text()).toContain(word));
});
test('it renders the data providers', () => {
@ -58,15 +64,22 @@ describe('DataProviders', () => {
<DataProviders
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
show={true}
theme="dark"
/>
</DragDropContext>
);
mockDataProviderNames().forEach(name => expect(wrapper.text()).toContain(name));
mockDataProviders.forEach(dataProvider =>
expect(wrapper.text()).toContain(
dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value
)
);
});
});
});

View file

@ -40,6 +40,11 @@ const EmptyContainer = styled.div`
justify-content: center;
min-height: 100px;
user-select: none;
flex: 1;
align-content: center;
+ div {
display: none !important;
}
`;
const NoWrap = styled.div`
@ -53,7 +58,7 @@ const NoWrap = styled.div`
* Prompts the user to drop anything with a facet count into the data providers section.
*/
export const Empty = pure(() => (
<EmptyContainer data-test-subj="empty">
<EmptyContainer className="timeline-drop-area" data-test-subj="empty">
<NoWrap>
<Text>{i18n.DROP_ANYTHING}</Text>
<BadgeHighlighted color="#d9d9d9">{i18n.HIGHLIGHTED}</BadgeHighlighted>

View file

@ -1,52 +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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiIcon, EuiToolTip } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import moment from 'moment';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { DataProvider } from './data_provider';
const HorizontalBar = styled(EuiHorizontalRule)`
margin: 4px 0px;
`;
const GroupIcons = styled(EuiFlexGroup)`
width: 100%;
`;
interface OwnProps {
dataProvider: DataProvider;
}
export const IconsFooter = pure<OwnProps>(({ dataProvider }: OwnProps) => {
if (!dataProvider.queryDate || isEmpty(dataProvider.queryDate)) {
return null;
}
const dates = dataProvider.queryDate.trim().match(/\d+/g);
const tooltipStr = `${moment(parseInt(dates![0], 10)).format('L LTS')} - ${moment(
parseInt(dates![1], 10)
).format('L LTS')}`;
return (
<>
<HorizontalBar margin="xs" />
<GroupIcons
data-test-subj="data-provider-icons-footer"
gutterSize="none"
alignItems="center"
justifyContent="flexStart"
direction="row"
>
<EuiFlexItem>
<EuiToolTip data-test-subj="add-tool-tip" content={tooltipStr} position="bottom">
<EuiIcon type="calendar" />
</EuiToolTip>
</EuiFlexItem>
</GroupIcons>
</>
);
});

View file

@ -11,7 +11,13 @@ import styled from 'styled-components';
import { Theme } from '../../../store/local/app/model';
import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper';
import { droppableTimelineProvidersPrefix } from '../../drag_and_drop/helpers';
import { OnDataProviderRemoved, OnToggleDataProviderEnabled } from '../events';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { DataProvider } from './data_provider';
import { Empty } from './empty';
import { Providers } from './providers';
@ -19,21 +25,26 @@ import { Providers } from './providers';
interface Props {
id: string;
dataProviders: DataProvider[];
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onChangeDroppableAndProvider: OnChangeDroppableAndProvider;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
show: boolean;
theme: Theme;
}
const DropTargetDataProviders = styled.div`
border: 0.3rem dashed #999999;
position: relative;
border: 0.2rem dashed #999999;
border-radius: 5px;
display: flex;
flex-direction: column;
justify-content: center;
margin: 5px;
padding: 0px;
min-height: 100px;
padding: 5px;
overflow-y: auto;
`;
const getDroppableId = (id: string): string => `${droppableTimelineProvidersPrefix}${id}`;
@ -59,19 +70,25 @@ export const DataProviders = pure<Props>(
({
id,
dataProviders,
onChangeDataProviderKqlQuery,
onChangeDroppableAndProvider,
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
show,
theme,
}: Props) => (
}) => (
<DropTargetDataProviders data-test-subj="dataProviders">
<DroppableWrapper isDropDisabled={!show} droppableId={getDroppableId(id)} theme={theme}>
{dataProviders.length ? (
<Providers
id={id}
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
) : (
<Empty />

View file

@ -4,11 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiText } from '@elastic/eui';
import * as React from 'react';
import styled from 'styled-components';
import { EventsQuery } from '../../../../containers/events';
import { DataProvider } from '../data_provider';
interface NameToEventCount<TValue> {
@ -39,31 +34,25 @@ export const mockDataProviderNames = (): string[] => Object.keys(mockSourceNameT
export const getEventCount = (dataProviderName: string): number =>
mockSourceNameToEventCount[dataProviderName] || 0;
const Text = styled(EuiText)`
display: inline;
padding-left: 5px;
`;
/**
* A collection of mock data providers, that can both be rendered
* in the browser, and also used as mocks in unit and functional tests.
*/
export const mockDataProviders: DataProvider[] = Object.keys(mockSourceNameToEventCount).map(
name => ({
enabled: true,
id: `id-${name}`,
name,
componentResultParam: 'events',
componentQuery: EventsQuery,
queryMatch: 'host.name: "testHostName"',
queryDate: '@timestamp >= 1521830963132 and @timestamp <= 1521862432253',
negated: false,
render: () => (
<div data-test-subj="mockDataProvider">
<EuiBadge color="primary">{getEventCount(name)}</EuiBadge>
<Text> {name} </Text>
</div>
),
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'name',
value: name,
},
queryDate: {
from: 1521830963132,
to: 1521862432253,
},
and: [],
})
);

View file

@ -5,11 +5,11 @@
*/
import { mount } from 'enzyme';
import { noop, pick } from 'lodash/fp';
import { noop } from 'lodash/fp';
import * as React from 'react';
import { DragDropContext } from 'react-beautiful-dnd';
import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper';
import { mockDataProviderNames, mockDataProviders } from './mock/mock_data_providers';
import { mockDataProviders } from './mock/mock_data_providers';
import { Provider } from './provider';
describe('Provider', () => {
@ -18,88 +18,12 @@ describe('Provider', () => {
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Provider
dataProvider={mockDataProviders[0]}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
/>
<Provider dataProvider={mockDataProviders[0]} />
</DroppableWrapper>
</DragDropContext>
);
expect(wrapper.text()).toContain(mockDataProviderNames()[0]);
});
});
describe('#onDataProviderRemoved', () => {
test('it invokes the onDataProviderRemoved callback when the close button is clicked', () => {
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Provider
dataProvider={mockDataProviders[0]}
onDataProviderRemoved={mockOnDataProviderRemoved}
onToggleDataProviderEnabled={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="closeButton"]')
.first()
.simulate('click');
const callbackParams = pick(
['enabled', 'id', 'name', 'negated'],
mockOnDataProviderRemoved.mock.calls[0][0]
);
expect(callbackParams).toEqual({
enabled: true,
id: 'id-Provider 1',
name: 'Provider 1',
negated: false,
});
});
});
describe('#onToggleDataProviderEnabled', () => {
test('it invokes the onToggleDataProviderEnabled callback when the switch button is clicked', () => {
const mockOnToggleDataProviderEnabled = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Provider
dataProvider={mockDataProviders[0]}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="switchButton"]')
.at(1)
.simulate('click');
const callbackParams = pick(
['enabled', 'dataProvider.id', 'dataProvider.name', 'dataProvider.negated'],
mockOnToggleDataProviderEnabled.mock.calls[0][0]
);
expect(callbackParams).toEqual({
dataProvider: {
name: 'Provider 1',
negated: false,
id: 'id-Provider 1',
},
enabled: false,
});
expect(wrapper.text()).toContain('name: "Provider 1"');
});
});
});

View file

@ -4,45 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
interface Props {
import { noop } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import { DataProvider } from './data_provider';
import { ProviderItemBadge } from './provider_item_badge';
interface OwnProps {
dataProvider: DataProvider;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
}
import { EuiPanel } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { OnDataProviderRemoved, OnToggleDataProviderEnabled } from '../events';
import { Actions } from './actions';
import { DataProvider } from './data_provider';
import { IconsFooter } from './icons_footer';
const PanelProvider = styled(EuiPanel)`
&& {
align-items: center;
display: flex;
flex-direction: column;
justify-content: space-between;
margin: 5px;
min-height: 60px;
padding: 5px 5px 5px 10px;
min-width: 150px;
}
`;
export const Provider = pure<Props>(
({ dataProvider, onDataProviderRemoved, onToggleDataProviderEnabled }: Props) => (
<PanelProvider data-test-subj="provider" key={dataProvider.id}>
{dataProvider.name}
<Actions
dataProvider={dataProvider}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
/>
<IconsFooter dataProvider={dataProvider} />
</PanelProvider>
)
);
export const Provider = pure<OwnProps>(({ dataProvider }) => (
<ProviderItemBadge
deleteProvider={noop}
field={dataProvider.queryMatch.displayField || dataProvider.queryMatch.field}
kqlQuery={dataProvider.kqlQuery}
isEnabled={dataProvider.enabled}
isExcluded={dataProvider.excluded}
providerId={dataProvider.id}
queryDate={dataProvider.queryDate}
toggleEnabledProvider={noop}
toggleExcludedProvider={noop}
val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value}
/>
));

View file

@ -0,0 +1,105 @@
/*
* 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, EuiIcon, EuiToolTip } from '@elastic/eui';
import classNames from 'classnames';
import { isEmpty } from 'lodash/fp';
import moment from 'moment';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { QueryDate } from './data_provider';
import * as i18n from './translations';
const ProviderBadgeStyled = styled(EuiBadge)`
border: none;
.euiToolTipAnchor {
&::after {
font-style: normal;
content: '|';
padding: 0px 3px;
}
}
.field-value {
font-weight: 200;
}
&.globalFilterItem {
line-height: 28px;
border: none;
&.globalFilterItem-isDisabled {
text-decoration: line-through;
font-weight: 400;
font-style: italic;
}
}
`;
interface ProviderBadgeProps {
deleteProvider: () => void;
field: string;
kqlQuery: string;
isEnabled: boolean;
isExcluded: boolean;
providerId: string;
queryDate?: QueryDate;
togglePopover?: () => void;
val: string | number;
}
export const ProviderBadge = pure<ProviderBadgeProps>(
({ deleteProvider, field, isEnabled, isExcluded, queryDate, providerId, togglePopover, val }) => {
const deleteFilter: React.MouseEventHandler<HTMLButtonElement> = (
event: React.MouseEvent<HTMLButtonElement>
) => {
// Make sure it doesn't also trigger the onclick for the whole badge
if (event.stopPropagation) {
event.stopPropagation();
}
deleteProvider();
};
const classes = classNames('globalFilterItem', {
'globalFilterItem-isDisabled': !isEnabled,
'globalFilterItem-isExcluded': isExcluded,
});
const prefix = isExcluded ? <span>{i18n.NOT} </span> : null;
const title = `${field}: "${val}"`;
const tooltipStr = isEmpty(queryDate)
? null
: `${moment(queryDate!.from).format('L LTS')} - ${moment(queryDate!.to).format('L LTS')}`;
return (
<ProviderBadgeStyled
id={`${providerId}-${field}-${val}`}
className={classes}
title={title}
iconOnClick={deleteFilter}
iconOnClickAriaLabel={i18n.REMOVE_DATA_PROVIDER}
iconType="cross"
iconSide="right"
onClick={togglePopover}
onClickAriaLabel={`${i18n.SHOW_OPTIONS_DATA_PROVIDER} ${val}`}
closeButtonProps={{
// Removing tab focus on close button because the same option can be obtained through the context menu
// TODO: add a `DEL` keyboard press functionality
tabIndex: '-1',
}}
data-test-subj="providerBadge"
>
{tooltipStr !== null && (
<EuiToolTip data-test-subj="add-tool-tip" content={tooltipStr} position="bottom">
<EuiIcon type="calendar" />
</EuiToolTip>
)}
{prefix}
<span className="field-value">{field}: </span>
<span className="field-value">&quot;{val}&quot;</span>
</ProviderBadgeStyled>
);
}
);

View file

@ -0,0 +1,106 @@
/*
* 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 { EuiContextMenu, EuiPopover } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import * as i18n from './translations';
interface OwnProps {
button: JSX.Element;
closePopover: () => void;
deleteProvider: () => void;
field: string;
kqlQuery: string;
isEnabled: boolean;
isExcluded: boolean;
isOpen: boolean;
providerId: string;
toggleEnabledProvider: () => void;
toggleExcludedProvider: () => void;
value: string | number;
}
const MyEuiPopover = styled(EuiPopover)`
height: 100%;
`;
export const getProviderActions = (
deleteItem: () => void,
isEnabled: boolean,
isExcluded: boolean,
toggleEnabled: () => void,
toggleExcluded: () => void
) => [
{
id: 0,
items: [
// {
// name: 'Edit filter query',
// icon: 'pencil',
// panel: 1,
// },
{
name: isExcluded ? i18n.INCLUDE_DATA_PROVIDER : i18n.EXCLUDE_DATA_PROVIDER,
icon: `${isExcluded ? 'plusInCircle' : 'minusInCircle'}`,
onClick: toggleExcluded,
},
{
name: isEnabled ? i18n.TEMPORARILY_DISABLE_DATA_PROVIDER : i18n.RE_ENABLE_DATA_PROVIDER,
icon: `${isEnabled ? 'eyeClosed' : 'eye'}`,
onClick: toggleEnabled,
},
{
name: 'Delete',
icon: 'trash',
onClick: deleteItem,
},
],
},
// {
// id: 1,
// width: 400,
// content: <div style={{ padding: 16 }}>ADD KQL BAR</div>,
// },
];
export const ProviderItemActions = pure<OwnProps>(
({
button,
closePopover,
deleteProvider,
kqlQuery,
field,
isEnabled,
isExcluded,
isOpen,
providerId,
toggleEnabledProvider,
toggleExcludedProvider,
value,
}) => {
const panelTree = getProviderActions(
deleteProvider,
isEnabled,
isExcluded,
toggleEnabledProvider,
toggleExcludedProvider
);
return (
<MyEuiPopover
id={`popoverFor_${providerId}-${field}-${value}`}
isOpen={isOpen}
closePopover={closePopover}
button={button}
anchorPosition="downCenter"
panelPaddingSize="none"
>
<EuiContextMenu initialPanelId={0} panels={panelTree} data-test-subj="providerActions" />
</MyEuiPopover>
);
}
);

View file

@ -0,0 +1,87 @@
/*
* 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 * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { DataProvider } from './data_provider';
import { ProviderItemAndPopover } from './provider_item_and_popover';
const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>`
border: 0.1rem dashed #999999;
border-radius: 5px;
text-align: center;
padding: 2px 3px;
${({ hasAndItem }) =>
hasAndItem
? `&:hover {
transition: background-color 0.7s ease;
background-color: rgb(52, 55, 65);
}`
: ''};
cursor: ${({ hasAndItem }) => (!hasAndItem ? `default` : 'inherit')};
.euiPopover {
display: inherit;
.euiPopover__anchor {
display: inherit;
.euiButtonEmpty {
width: 100%;
.euiButtonEmpty__content {
padding: 0px;
}
}
}
}
`;
interface ProviderItemDropProps {
dataProvider: DataProvider;
mousePosition?: { x: number; y: number; boundLeft: number; boundTop: number };
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onChangeDroppableAndProvider: OnChangeDroppableAndProvider;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
}
export const ProviderItemAndDragDrop = pure<ProviderItemDropProps>(
({
dataProvider,
onChangeDataProviderKqlQuery,
onChangeDroppableAndProvider,
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
}) => {
const onMouseEnter = () => onChangeDroppableAndProvider(dataProvider.id);
const onMouseLeave = () => onChangeDroppableAndProvider('');
return (
<DropAndTargetDataProviders
className="drop-and-provider-timeline"
hasAndItem={dataProvider.and.length > 0}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<ProviderItemAndPopover
dataProvidersAnd={dataProvider.and}
providerId={dataProvider.id}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
</DropAndTargetDataProviders>
);
}
);

View file

@ -0,0 +1,199 @@
/*
* 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 {
EuiAccordion,
EuiBadge,
EuiButtonEmpty,
EuiButtonEmptyProps,
EuiContextMenu,
EuiHorizontalRule,
EuiPopover,
} from '@elastic/eui';
import * as React from 'react';
import styled from 'styled-components';
import {
OnChangeDataProviderKqlQuery,
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { DataProvider } from './data_provider';
import { ProviderBadge } from './provider_badge';
import { getProviderActions } from './provider_item_actions';
import * as i18n from './translations';
const NumberProviderAndBadge = styled(EuiBadge)`
margin: 0px 5px;
`;
const EuiBadgeAndStyled = styled(EuiBadge)`
position: absolute;
left: calc(50% - 15px);
top: -18px;
z-index: 1;
width: 27px;
height: 27px;
padding: 8px 3px 0px 3px;
border-radius: 100%;
`;
const AndStyled = styled.div`
position: relative;
.euiHorizontalRule {
margin: 28px 0px;
}
`;
const EuiButtonContent = styled.div`
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
.euiBadge {
position: inherit;
}
`;
interface ProviderItemAndPopoverProps {
dataProvidersAnd: DataProvider[];
providerId: string;
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
}
interface ProviderItemAndPopoverState {
isPopoverOpen: boolean;
}
export class ProviderItemAndPopover extends React.PureComponent<
ProviderItemAndPopoverProps,
ProviderItemAndPopoverState
> {
public readonly state = {
isPopoverOpen: false,
};
public render() {
const { dataProvidersAnd, providerId } = this.props;
const hasAndItem = dataProvidersAnd.length > 0;
const euiButtonProps: EuiButtonEmptyProps = hasAndItem
? { iconType: 'arrowDown', iconSide: 'right' }
: {};
const button = (
<EuiButtonEmpty
{...euiButtonProps}
onClick={this.togglePopover}
style={hasAndItem ? {} : { cursor: 'default' }}
>
<EuiButtonContent>
{hasAndItem && (
<NumberProviderAndBadge color="primary">
{dataProvidersAnd.length}
</NumberProviderAndBadge>
)}
<EuiBadgeAndStyled>{i18n.AND}</EuiBadgeAndStyled>
</EuiButtonContent>
</EuiButtonEmpty>
);
return (
<EuiPopover
id={`${providerId}-popover`}
ownFocus
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
data-test-subj="andProviderButton"
>
<div style={{ width: 'auto' }}>
{dataProvidersAnd.map((providerAnd: DataProvider, index: number) => {
const badge = (
<ProviderBadge
deleteProvider={() => this.deleteAndProvider(providerId, providerAnd.id)}
field={providerAnd.queryMatch.displayField || providerAnd.queryMatch.field}
kqlQuery={providerAnd.kqlQuery}
isEnabled={providerAnd.enabled}
isExcluded={providerAnd.excluded}
providerId={`${providerId}.${providerAnd.id}`}
val={providerAnd.queryMatch.displayValue || providerAnd.queryMatch.value}
/>
);
const panelTree = getProviderActions(
() => this.deleteAndProvider(providerId, providerAnd.id),
providerAnd.enabled,
providerAnd.excluded,
() => this.toggleEnabledAndProvider(providerId, !providerAnd.enabled, providerAnd.id),
() =>
this.toggleExcludedAndProvider(providerId, !providerAnd.excluded, providerAnd.id)
);
return (
<div key={`${providerId}-${providerAnd.id}-accordion`}>
<EuiAccordion
id={`${providerId}-${providerAnd.id}-accordion`}
buttonContent={badge}
paddingSize="l"
data-test-subj="andProviderAccordion"
>
<EuiContextMenu initialPanelId={0} panels={panelTree} />
</EuiAccordion>
{index < dataProvidersAnd.length - 1 && (
<AndStyled>
<EuiBadgeAndStyled color="default">{i18n.AND}</EuiBadgeAndStyled>
<EuiHorizontalRule />
</AndStyled>
)}
</div>
);
})}
</div>
</EuiPopover>
);
}
private closePopover = () => {
this.setState({
...this.state,
isPopoverOpen: false,
});
};
private togglePopover = () => {
if (this.props.dataProvidersAnd.length > 0) {
this.setState({
...this.state,
isPopoverOpen: !this.state.isPopoverOpen,
});
}
};
private deleteAndProvider = (providerId: string, andProviderId: string) => {
this.props.onDataProviderRemoved(providerId, andProviderId);
this.closePopover();
};
private toggleEnabledAndProvider = (
providerId: string,
enabled: boolean,
andProviderId: string
) => {
this.props.onToggleDataProviderEnabled({ providerId, enabled, andProviderId });
this.closePopover();
};
private toggleExcludedAndProvider = (
providerId: string,
excluded: boolean,
andProviderId: string
) => {
this.props.onToggleDataProviderExcluded({ providerId, excluded, andProviderId });
this.closePopover();
};
}

View file

@ -0,0 +1,100 @@
/*
* 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 React, { PureComponent } from 'react';
import { QueryDate } from './data_provider';
import { ProviderBadge } from './provider_badge';
import { ProviderItemActions } from './provider_item_actions';
interface ProviderItemBadgeProps {
deleteProvider: () => void;
field: string;
kqlQuery: string;
isEnabled: boolean;
isExcluded: boolean;
providerId: string;
queryDate?: QueryDate;
toggleEnabledProvider: () => void;
toggleExcludedProvider: () => void;
val: string | number;
}
interface OwnState {
isPopoverOpen: boolean;
}
export class ProviderItemBadge extends PureComponent<ProviderItemBadgeProps, OwnState> {
public readonly state = {
isPopoverOpen: false,
};
public render() {
const {
deleteProvider,
field,
kqlQuery,
isEnabled,
isExcluded,
queryDate,
providerId,
val,
} = this.props;
const badge = (
<ProviderBadge
deleteProvider={deleteProvider}
field={field}
kqlQuery={kqlQuery}
isEnabled={isEnabled}
isExcluded={isExcluded}
providerId={providerId}
queryDate={queryDate}
togglePopover={this.togglePopover}
val={val}
/>
);
return (
<ProviderItemActions
button={badge}
closePopover={this.closePopover}
deleteProvider={deleteProvider}
field={field}
kqlQuery={kqlQuery}
isEnabled={isEnabled}
isExcluded={isExcluded}
isOpen={this.state.isPopoverOpen}
providerId={providerId}
toggleEnabledProvider={this.toggleEnabledProvider}
toggleExcludedProvider={this.toggleExcludedProvider}
value={val}
/>
);
}
private togglePopover = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};
private closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};
private toggleEnabledProvider = () => {
this.props.toggleEnabledProvider();
this.closePopover();
};
private toggleExcludedProvider = () => {
this.props.toggleExcludedProvider();
this.closePopover();
};
}

View file

@ -5,11 +5,11 @@
*/
import { mount } from 'enzyme';
import { noop, pick } from 'lodash/fp';
import { noop } from 'lodash/fp';
import * as React from 'react';
import { DragDropContext } from 'react-beautiful-dnd';
import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper';
import { mockDataProviderNames, mockDataProviders } from './mock/mock_data_providers';
import { mockDataProviders } from './mock/mock_data_providers';
import { getDraggableId, Providers } from './providers';
describe('Providers', () => {
@ -21,14 +21,21 @@ describe('Providers', () => {
<Providers
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
mockDataProviderNames().forEach(name => expect(wrapper.text()).toContain(name));
mockDataProviders.forEach(dataProvider =>
expect(wrapper.text()).toContain(
dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value
)
);
});
});
@ -42,29 +49,54 @@ describe('Providers', () => {
<Providers
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={mockOnDataProviderRemoved}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="closeButton"]')
.find('[data-test-subj="providerBadge"] svg')
.first()
.simulate('click');
const callbackParams = pick(
['enabled', 'id', 'name', 'negated'],
mockOnDataProviderRemoved.mock.calls[0][0]
);
expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1');
});
expect(callbackParams).toEqual({
enabled: true,
id: 'id-Provider 1',
name: 'Provider 1',
negated: false,
});
test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => {
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Providers
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={mockOnDataProviderRemoved}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="providerBadge"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="providerActions"] button.euiContextMenuItem')
.at(2)
.simulate('click');
expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1');
});
});
@ -77,7 +109,7 @@ describe('Providers', () => {
});
describe('#onToggleDataProviderEnabled', () => {
test('it invokes the onToggleDataProviderEnabled callback when the switch button is clicked', () => {
test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
const mockOnToggleDataProviderEnabled = jest.fn();
const wrapper = mount(
@ -86,30 +118,215 @@ describe('Providers', () => {
<Providers
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="switchButton"]')
.find('[data-test-subj="providerBadge"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="providerActions"] button.euiContextMenuItem')
.at(1)
.simulate('click');
const callbackParams = pick(
['enabled', 'dataProvider.id', 'dataProvider.name', 'dataProvider.negated'],
mockOnToggleDataProviderEnabled.mock.calls[0][0]
expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({
enabled: false,
providerId: 'id-Provider 1',
});
});
});
describe('#onToggleDataProviderExcluded', () => {
test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
const onToggleDataProviderExcluded = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Providers
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
</DroppableWrapper>
</DragDropContext>
);
expect(callbackParams).toEqual({
dataProvider: {
name: 'Provider 1',
negated: false,
id: 'id-Provider 1',
},
wrapper
.find('[data-test-subj="providerBadge"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="providerActions"] button.euiContextMenuItem')
.first()
.simulate('click');
expect(onToggleDataProviderExcluded.mock.calls[0][0]).toEqual({
excluded: true,
providerId: 'id-Provider 1',
});
});
});
describe('#ProviderWithAndProvider', () => {
test('Rendering And Provider', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Providers
id="foo"
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
const andProviderBadge = wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first();
expect(andProviderBadge.text()).toEqual('2');
});
test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the accordeon menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Providers
id="foo"
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={mockOnDataProviderRemoved}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="andProviderAccordion"] button.euiContextMenuItem')
.at(2)
.simulate('click');
expect(mockOnDataProviderRemoved.mock.calls[0]).toEqual(['id-Provider 1', 'id-Provider 2']);
});
test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the accordeon menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderEnabled = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Providers
id="foo"
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled}
onToggleDataProviderExcluded={noop}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="andProviderAccordion"] button.euiContextMenuItem')
.at(1)
.simulate('click');
expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({
andProviderId: 'id-Provider 2',
enabled: false,
providerId: 'id-Provider 1',
});
});
test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the accordeon menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderExcluded = jest.fn();
const wrapper = mount(
<DragDropContext onDragEnd={noop}>
<DroppableWrapper droppableId="unitTest" theme="dark">
<Providers
id="foo"
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={mockOnToggleDataProviderExcluded}
/>
</DroppableWrapper>
</DragDropContext>
);
wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="andProviderAccordion"] button.euiContextMenuItem')
.first()
.simulate('click');
expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({
andProviderId: 'id-Provider 2',
excluded: true,
providerId: 'id-Provider 1',
});
});
});

View file

@ -4,26 +4,75 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiBadge,
// @ts-ignore
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
} from '@elastic/eui';
import * as React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { pure } from 'recompose';
import styled from 'styled-components';
import { OnDataProviderRemoved, OnToggleDataProviderEnabled } from '../events';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { DataProvider } from './data_provider';
import { Provider } from './provider';
import { Empty } from './empty';
import { ProviderItemAndDragDrop } from './provider_item_and_drag_drop';
import { ProviderItemBadge } from './provider_item_badge';
import * as i18n from './translations';
interface Props {
id: string;
dataProviders: DataProvider[];
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onChangeDroppableAndProvider: OnChangeDroppableAndProvider;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
}
const PanelProviders = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
min-height: 100px;
padding: 10px;
overflow-y: auto;
`;
const EuiBadgeOrStyled = styled(EuiBadge)`
position: absolute;
right: -37px;
top: 27px;
z-index: 1;
width: 20px;
height: 20px;
padding: 7px 6px 4px 6px;
border-radius: 100%;
`;
const PanelProvidersGroupContainer = styled(EuiFlexGroup)`
position: relative;
flex-grow: unset;
margin-right: 40px;
`;
const PanelProviderItemContainer = styled(EuiFlexItem)`
height: 100%;
.euiHorizontalRule {
transform: rotate(90deg);
position: absolute;
top: 23px;
width: 80px;
right: -60px;
}
`;
interface GetDraggableIdParams {
@ -42,33 +91,92 @@ export const getDraggableId = ({ id, dataProviderId }: GetDraggableIdParams): st
* 3) applying boolean negation to the data provider
*/
export const Providers = pure<Props>(
({ id, dataProviders, onDataProviderRemoved, onToggleDataProviderEnabled }: Props) => (
<PanelProviders data-test-subj="providers">
{dataProviders.map((dataProvider, i) => (
// Providers are a special drop target that can't be drag-and-dropped
// to another destination, so it doesn't use our DraggableWrapper
<Draggable
draggableId={getDraggableId({ id, dataProviderId: dataProvider.id })}
index={i}
key={dataProvider.id}
>
{provided => (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
data-test-subj="providerContainer"
>
<Provider
data-test-subj="provider"
dataProvider={dataProvider}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
/>
</div>
)}
</Draggable>
))}
({
id,
dataProviders,
onChangeDataProviderKqlQuery,
onChangeDroppableAndProvider,
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
}) => (
<PanelProviders className="timeline-drop-area" data-test-subj="providers">
{dataProviders.map((dataProvider, i) => {
const deleteProvider = () => onDataProviderRemoved(dataProvider.id);
const toggleEnabledProvider = () =>
onToggleDataProviderEnabled({
providerId: dataProvider.id,
enabled: !dataProvider.enabled,
});
const toggleExcludedProvider = () =>
onToggleDataProviderExcluded({
providerId: dataProvider.id,
excluded: !dataProvider.excluded,
});
return (
// Providers are a special drop target that can't be drag-and-dropped
// to another destination, so it doesn't use our DraggableWrapper
<PanelProvidersGroupContainer
key={dataProvider.id}
direction="row"
className="provider-item-container"
alignItems="center"
gutterSize="none"
>
<PanelProviderItemContainer grow={false}>
<EuiFlexGroup direction="column" gutterSize="none" justifyContent="spaceAround">
<EuiFlexItem className="provider-item-filter-container" grow={false}>
<Draggable
draggableId={getDraggableId({ id, dataProviderId: dataProvider.id })}
index={i}
>
{provided => (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
data-test-subj="providerContainer"
>
<ProviderItemBadge
field={
dataProvider.queryMatch.displayField || dataProvider.queryMatch.field
}
kqlQuery={dataProvider.kqlQuery}
isEnabled={dataProvider.enabled}
isExcluded={dataProvider.excluded}
deleteProvider={deleteProvider}
toggleEnabledProvider={toggleEnabledProvider}
toggleExcludedProvider={toggleExcludedProvider}
providerId={dataProvider.id}
queryDate={dataProvider.queryDate}
val={
dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value
}
/>
</div>
)}
</Draggable>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ProviderItemAndDragDrop
dataProvider={dataProvider}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
</EuiFlexItem>
</EuiFlexGroup>
</PanelProviderItemContainer>
<PanelProviderItemContainer grow={false}>
<EuiBadgeOrStyled color="default">{i18n.OR.toLocaleUpperCase()}</EuiBadgeOrStyled>
<EuiHorizontalRule />
</PanelProviderItemContainer>
</PanelProvidersGroupContainer>
);
})}
<Empty />
</PanelProviders>
)
);

View file

@ -1,34 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSwitch } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import { OnToggleDataProviderEnabled } from '../events';
import { DataProvider } from './data_provider';
import * as i18n from './translations';
interface Props {
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
dataProvider: DataProvider;
}
/** An affordance for enabling/disabling a data provider. It invokes `onToggleDataProviderEnabled` when clicked */
export const SwitchButton = pure<Props>(({ onToggleDataProviderEnabled, dataProvider }) => {
const onClick = () => {
onToggleDataProviderEnabled({ dataProvider, enabled: !dataProvider.enabled });
};
return (
<EuiSwitch
aria-label={i18n.TOGGLE}
data-test-subj="switchButton"
defaultChecked={dataProvider.enabled}
onClick={onClick}
/>
);
});

View file

@ -6,10 +6,28 @@
import { i18n } from '@kbn/i18n';
export const AND = i18n.translate('xpack.secops.dataProviders.and', {
defaultMessage: 'AND',
});
export const DELETE_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.deleteDataProvider',
{
defaultMessage: 'Delete',
}
);
export const DROP_ANYTHING = i18n.translate('xpack.secops.dataProviders.dropAnything', {
defaultMessage: 'Drop anything',
});
export const EXCLUDE_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.excludeDataProvider',
{
defaultMessage: 'Exclude results',
}
);
export const HIGHLIGHTED = i18n.translate('xpack.secops.dataProviders.highlighted', {
defaultMessage: 'highlighted',
});
@ -18,6 +36,17 @@ export const HERE_TO_BUILD_AN = i18n.translate('xpack.secops.dataProviders.hereT
defaultMessage: 'here to build an',
});
export const INCLUDE_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.includeDataProvider',
{
defaultMessage: 'Include results',
}
);
export const NOT = i18n.translate('xpack.secops.dataProviders.not', {
defaultMessage: 'not',
});
export const OR = i18n.translate('xpack.secops.dataProviders.or', {
defaultMessage: 'or',
});
@ -30,9 +59,30 @@ export const TOGGLE = i18n.translate('xpack.secops.dataProviders.toggle', {
defaultMessage: 'toggle',
});
export const RE_ENABLE_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.reEnableDataProvider',
{
defaultMessage: 'Re-enable',
}
);
export const REMOVE_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.removeDataProvider',
{
defaultMessage: 'Remove Data Provider',
}
);
export const SHOW_OPTIONS_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.showOptionsDataProvider',
{
defaultMessage: 'Show options for',
}
);
export const TEMPORARILY_DISABLE_DATA_PROVIDER = i18n.translate(
'xpack.secops.dataProviders.temporaryDisableDataProvider',
{
defaultMessage: 'Temporarily disable',
}
);

View file

@ -6,24 +6,33 @@
import { ColumnId } from './body/column_id';
import { SortDirection } from './body/sort';
import { DataProvider } from './data_providers/data_provider';
/** Invoked when a user clicks the close button to remove a data provider */
export type OnDataProviderRemoved = (removed: DataProvider) => void;
export type OnDataProviderRemoved = (providerId: string, andProviderId?: string) => void;
/** Invoked when a user temporarily disables or re-enables a data provider */
export type OnToggleDataProviderEnabled = (
toggled: {
dataProvider: DataProvider;
providerId: string;
enabled: boolean;
andProviderId?: string;
}
) => void;
/** Invoked when a user toggles negation ("boolean NOT") of a data provider */
export type OnToggleDataProviderNegated = (
negated: {
dataProvider: DataProvider;
negated: boolean;
export type OnToggleDataProviderExcluded = (
excluded: {
providerId: string;
excluded: boolean;
andProviderId?: string;
}
) => void;
/** Invoked when a user change the kql query of our data provider */
export type OnChangeDataProviderKqlQuery = (
edit: {
providerId: string;
kqlQuery: string;
}
) => void;
@ -51,3 +60,5 @@ export type OnChangeItemsPerPage = (itemsPerPage: number) => void;
/** Invoked when a user clicks to load more item */
export type OnLoadMore = (cursor: string, tieBreaker: string) => void;
export type OnChangeDroppableAndProvider = (providerId: string) => void;

View file

@ -34,11 +34,14 @@ describe('Header', () => {
id="foo"
columnHeaders={[]}
dataProviders={mockDataProviders}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range="1 Day"
show={true}
sort={{

View file

@ -16,11 +16,14 @@ import { Sort } from '../body/sort';
import { DataProviders } from '../data_providers';
import { DataProvider } from '../data_providers/data_provider';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnColumnSorted,
OnDataProviderRemoved,
OnFilterChange,
OnRangeSelected,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from '../events';
import { StatefulSearchOrFilter } from '../search_or_filter';
@ -28,11 +31,14 @@ interface Props {
columnHeaders: ColumnHeader[];
id: string;
dataProviders: DataProvider[];
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onChangeDroppableAndProvider: OnChangeDroppableAndProvider;
onColumnSorted: OnColumnSorted;
onDataProviderRemoved: OnDataProviderRemoved;
onFilterChange: OnFilterChange;
onRangeSelected: OnRangeSelected;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
range: string;
show: boolean;
sort: Sort;
@ -48,11 +54,14 @@ export const TimelineHeader = pure<Props>(
columnHeaders,
id,
dataProviders,
onChangeDataProviderKqlQuery,
onChangeDroppableAndProvider,
onColumnSorted,
onDataProviderRemoved,
onFilterChange,
onRangeSelected,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
range,
show,
sort,
@ -62,8 +71,11 @@ export const TimelineHeader = pure<Props>(
<DataProviders
id={id}
dataProviders={dataProviders}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
show={show}
theme={theme}
/>

View file

@ -0,0 +1,47 @@
/*
* 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 { mockDataProviders } from './data_providers/mock/mock_data_providers';
import { buildGlobalQuery } from './helpers';
const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' ');
describe('Build KQL Query', () => {
test('Buld KQL query with one data provider', () => {
const dataProviders = mockDataProviders.slice(0, 1);
const kqlQuery = buildGlobalQuery(dataProviders);
expect(cleanUpKqlQuery(kqlQuery)).toEqual(
'( name : Provider 1 and @timestamp >= 1521830963132 and @timestamp <= 1521862432253 )'
);
});
test('Buld KQL query with two data provider', () => {
const dataProviders = mockDataProviders.slice(0, 2);
const kqlQuery = buildGlobalQuery(dataProviders);
expect(cleanUpKqlQuery(kqlQuery)).toEqual(
'( name : Provider 1 and @timestamp >= 1521830963132 and @timestamp <= 1521862432253 ) or ( name : Provider 2 and @timestamp >= 1521830963132 and @timestamp <= 1521862432253 )'
);
});
test('Buld KQL query with one data provider and one and', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 2);
const kqlQuery = buildGlobalQuery(dataProviders);
expect(cleanUpKqlQuery(kqlQuery)).toEqual(
'( name : Provider 1 and @timestamp >= 1521830963132 and @timestamp <= 1521862432253 and name : Provider 2)'
);
});
test('Buld KQL query with two data provider and mutiple and', () => {
const dataProviders = mockDataProviders.slice(0, 2);
dataProviders[0].and = mockDataProviders.slice(2, 4);
dataProviders[1].and = mockDataProviders.slice(4, 5);
const kqlQuery = buildGlobalQuery(dataProviders);
expect(cleanUpKqlQuery(kqlQuery)).toEqual(
'( name : Provider 1 and @timestamp >= 1521830963132 and @timestamp <= 1521862432253 and name : Provider 3 and name : Provider 4) or ( name : Provider 2 and @timestamp >= 1521830963132 and @timestamp <= 1521862432253 and name : Provider 5)'
);
});
});

View file

@ -4,12 +4,53 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEmpty } from 'lodash/fp';
import { isEmpty, isNumber } from 'lodash/fp';
import { StaticIndexPattern } from 'ui/index_patterns';
import { convertKueryToElasticSearchQuery } from '../../lib/keury';
import { convertKueryToElasticSearchQuery, escapeQueryValue } from '../../lib/keury';
import { DataProvider } from './data_providers/data_provider';
const buildQueryMatch = (dataProvider: DataProvider) =>
`${dataProvider.excluded ? 'NOT ' : ''}${
dataProvider.queryMatch
? `${dataProvider.queryMatch.field} : ${
isNumber(dataProvider.queryMatch.value)
? dataProvider.queryMatch.value
: escapeQueryValue(dataProvider.queryMatch.value)
}`
: ''
}`.trim();
const buildQueryDate = (dataProvider: DataProvider) =>
dataProvider.queryDate
? `@timestamp >= ${dataProvider.queryDate.from} and @timestamp <= ${dataProvider.queryDate.to}`
: '';
const buildQueryForAndProvider = (dataAndProviders: DataProvider[]) =>
dataAndProviders
.reduce((andQuery, andDataProvider) => {
const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`;
return andDataProvider.enabled
? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider)}`
: andQuery;
}, '')
.trim();
export const buildGlobalQuery = (dataProviders: DataProvider[]) =>
dataProviders
.reduce((query, dataProvider) => {
const prepend = (q: string) => `${q !== '' ? `${q} or ` : ''}`;
return dataProvider.enabled
? `${prepend(query)}(
${buildQueryMatch(dataProvider)}
${dataProvider.queryDate ? ` and ${buildQueryDate(dataProvider)}` : ''}
${
dataProvider.and.length > 0 ? ` and ${buildQueryForAndProvider(dataProvider.and)}` : ''
})`.trim()
: query;
}, '')
.trim();
export const combineQueries = (
dataProviders: DataProvider[],
indexPattern: StaticIndexPattern
@ -18,19 +59,11 @@ export const combineQueries = (
return null;
}
const globalQuery = dataProviders.reduce((query, dataProvider) => {
const prepend = (q: string) => `${q !== '' ? `${q} or ` : ''}`;
return dataProvider.enabled
? `${prepend(query)} (${dataProvider.queryMatch}${
dataProvider.queryDate ? ` and ${dataProvider.queryDate})` : ')'
}`
: query;
}, '');
const globalQuery = buildGlobalQuery(dataProviders);
if (isEmpty(globalQuery)) {
return null;
}
return {
filterQuery: convertKueryToElasticSearchQuery(globalQuery, indexPattern),
};

View file

@ -21,11 +21,14 @@ import { columnRenderers, rowRenderers } from './body/renderers';
import { Sort } from './body/sort';
import { DataProvider } from './data_providers/data_provider';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnChangeItemsPerPage,
OnColumnSorted,
OnDataProviderRemoved,
OnRangeSelected,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from './events';
import { Timeline } from './timeline';
@ -69,11 +72,24 @@ interface DispatchProps {
removeProvider?: ActionCreator<{
id: string;
providerId: string;
andProviderId?: string;
}>;
updateDataProviderEnabled?: ActionCreator<{
id: string;
providerId: string;
enabled: boolean;
andProviderId?: string;
}>;
updateDataProviderExcluded?: ActionCreator<{
id: string;
excluded: boolean;
providerId: string;
andProviderId?: string;
}>;
updateDataProviderKqlQuery?: ActionCreator<{
id: string;
kqlQuery: string;
providerId: string;
}>;
updateItemsPerPage?: ActionCreator<{
id: string;
@ -87,6 +103,10 @@ interface DispatchProps {
id: string;
activePage: number;
}>;
updateHighlightedDropAndProviderId?: ActionCreator<{
id: string;
providerId: string;
}>;
}
type Props = OwnProps & StateReduxProps & DispatchProps;
@ -115,23 +135,43 @@ class StatefulTimelineComponent extends React.PureComponent<Props> {
updateRange,
updateSort,
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderKqlQuery,
updateHighlightedDropAndProviderId,
updateItemsPerPage,
} = this.props;
const onColumnSorted: OnColumnSorted = sorted => updateSort!({ id, sort: sorted });
const onDataProviderRemoved: OnDataProviderRemoved = dataProvider =>
removeProvider!({ id, providerId: dataProvider.id });
const onDataProviderRemoved: OnDataProviderRemoved = (
providerId: string,
andProviderId?: string
) => removeProvider!({ id, providerId, andProviderId });
const onRangeSelected: OnRangeSelected = selectedRange =>
updateRange!({ id, range: selectedRange });
const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = ({ dataProvider, enabled }) =>
updateDataProviderEnabled!({ id, enabled, providerId: dataProvider.id });
const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = ({
providerId,
enabled,
andProviderId,
}) => updateDataProviderEnabled!({ id, enabled, providerId, andProviderId });
const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = ({
providerId,
excluded,
andProviderId,
}) => updateDataProviderExcluded!({ id, excluded, providerId, andProviderId });
const onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery = ({ providerId, kqlQuery }) =>
updateDataProviderKqlQuery!({ id, kqlQuery, providerId });
const onChangeItemsPerPage: OnChangeItemsPerPage = itemsChangedPerPage =>
updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage });
const onChangeDroppableAndProvider: OnChangeDroppableAndProvider = providerId =>
updateHighlightedDropAndProviderId!({ id, providerId });
return (
<WithSource sourceId="default">
{({ indexPattern }) => (
@ -144,12 +184,15 @@ class StatefulTimelineComponent extends React.PureComponent<Props> {
flyoutHeight={flyoutHeight}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onChangeItemsPerPage={onChangeItemsPerPage}
onColumnSorted={onColumnSorted}
onDataProviderRemoved={onDataProviderRemoved}
onFilterChange={noop} // TODO: this is the callback for column filters, which is out scope for this phase of delivery
onRangeSelected={onRangeSelected}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
range={range!}
rowRenderers={rowRenderers}
show={show!}
@ -181,6 +224,9 @@ export const StatefulTimeline = connect(
updateRange: timelineActions.updateRange,
updateSort: timelineActions.updateSort,
updateDataProviderEnabled: timelineActions.updateDataProviderEnabled,
updateDataProviderExcluded: timelineActions.updateDataProviderExcluded,
updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery,
updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId,
updateItemsPerPage: timelineActions.updateItemsPerPage,
updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions,
removeProvider: timelineActions.removeProvider,

View file

@ -6,7 +6,7 @@
import { I18nProvider } from '@kbn/i18n/react';
import { mount } from 'enzyme';
import { noop, pick } from 'lodash/fp';
import { noop } from 'lodash/fp';
import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { DragDropContext } from 'react-beautiful-dnd';
@ -104,12 +104,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -141,12 +144,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -178,12 +184,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -220,12 +229,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onColumnSorted={mockOnColumnSorted}
onChangeItemsPerPage={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -252,7 +264,7 @@ describe('Timeline', () => {
});
describe('onDataProviderRemoved', () => {
test('it invokes the onDataProviderRemoved callback when the close button on a provider is clicked', () => {
test('it invokes the onDataProviderRemoved callback when the delete button on a provider is clicked', () => {
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
@ -269,12 +281,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={mockOnDataProviderRemoved}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -289,21 +304,64 @@ describe('Timeline', () => {
);
wrapper
.find('[data-test-subj="closeButton"]')
.find('[data-test-subj="providerBadge"] svg')
.first()
.simulate('click');
const callbackParams = pick(
['enabled', 'id', 'name', 'negated'],
mockOnDataProviderRemoved.mock.calls[0][0]
);
expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1');
});
expect(callbackParams).toEqual({
enabled: true,
id: 'id-Provider 1',
name: 'Provider 1',
negated: false,
});
test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => {
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
<I18nProvider>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<MockedProvider mocks={mocks}>
<Timeline
id="foo"
columnHeaders={headers}
columnRenderers={columnRenderers}
dataProviders={mockDataProviders}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={mockOnDataProviderRemoved}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
sort={sort}
theme="dark"
indexPattern={indexPattern}
/>
</MockedProvider>
</DragDropContext>
</ReduxStoreProvider>
</I18nProvider>
);
wrapper
.find('[data-test-subj="providerBadge"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="providerActions"] button.euiContextMenuItem')
.at(2)
.simulate('click');
expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1');
});
});
@ -332,12 +390,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={mockOnFilterChange}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -364,7 +425,7 @@ describe('Timeline', () => {
});
describe('onToggleDataProviderEnabled', () => {
test('it invokes the onToggleDataProviderEnabled callback when the input is updated', () => {
test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
const mockOnToggleDataProviderEnabled = jest.fn();
// for this test, all columns have text filters
@ -387,12 +448,15 @@ describe('Timeline', () => {
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
@ -407,22 +471,334 @@ describe('Timeline', () => {
);
wrapper
.find('[data-test-subj="switchButton"]')
.find('[data-test-subj="providerBadge"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="providerActions"] button.euiContextMenuItem')
.at(1)
.simulate('click');
const callbackParams = pick(
['enabled', 'dataProvider.id', 'dataProvider.name', 'dataProvider.negated'],
mockOnToggleDataProviderEnabled.mock.calls[0][0]
expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({
providerId: 'id-Provider 1',
enabled: false,
});
});
});
describe('onToggleDataProviderExcluded', () => {
test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
const mockOnToggleDataProviderExcluded = jest.fn();
// for this test, all columns have text filters
const allColumnsHaveTextFilters = headers.map(header => ({
...header,
columnHeaderType: 'text-filter' as ColumnHeaderType,
}));
const wrapper = mount(
<I18nProvider>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<MockedProvider mocks={mocks}>
<Timeline
id="foo"
columnHeaders={allColumnsHaveTextFilters}
columnRenderers={columnRenderers}
dataProviders={mockDataProviders}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={mockOnToggleDataProviderExcluded}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
sort={sort}
theme="dark"
indexPattern={indexPattern}
/>
</MockedProvider>
</DragDropContext>
</ReduxStoreProvider>
</I18nProvider>
);
expect(callbackParams).toEqual({
dataProvider: {
name: 'Provider 1',
negated: false,
id: 'id-Provider 1',
},
wrapper
.find('[data-test-subj="providerBadge"]')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="providerActions"] button.euiContextMenuItem')
.first()
.simulate('click');
expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({
providerId: 'id-Provider 1',
excluded: true,
});
});
});
describe('#ProviderWithAndProvider', () => {
test('Rendering And Provider', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
// for this test, all columns have text filters
const allColumnsHaveTextFilters = headers.map(header => ({
...header,
columnHeaderType: 'text-filter' as ColumnHeaderType,
}));
const wrapper = mount(
<I18nProvider>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<MockedProvider mocks={mocks}>
<Timeline
id="foo"
columnHeaders={allColumnsHaveTextFilters}
columnRenderers={columnRenderers}
dataProviders={dataProviders}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
sort={sort}
theme="dark"
indexPattern={indexPattern}
/>
</MockedProvider>
</DragDropContext>
</ReduxStoreProvider>
</I18nProvider>
);
const andProviderBadge = wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first();
expect(andProviderBadge.text()).toEqual('2');
});
test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the accordeon menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnDataProviderRemoved = jest.fn();
// for this test, all columns have text filters
const allColumnsHaveTextFilters = headers.map(header => ({
...header,
columnHeaderType: 'text-filter' as ColumnHeaderType,
}));
const wrapper = mount(
<I18nProvider>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<MockedProvider mocks={mocks}>
<Timeline
id="foo"
columnHeaders={allColumnsHaveTextFilters}
columnRenderers={columnRenderers}
dataProviders={dataProviders}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={mockOnDataProviderRemoved}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
sort={sort}
theme="dark"
indexPattern={indexPattern}
/>
</MockedProvider>
</DragDropContext>
</ReduxStoreProvider>
</I18nProvider>
);
wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="andProviderAccordion"] button.euiContextMenuItem')
.at(2)
.simulate('click');
expect(mockOnDataProviderRemoved.mock.calls[0]).toEqual(['id-Provider 1', 'id-Provider 2']);
});
test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the accordeon menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderEnabled = jest.fn();
// for this test, all columns have text filters
const allColumnsHaveTextFilters = headers.map(header => ({
...header,
columnHeaderType: 'text-filter' as ColumnHeaderType,
}));
const wrapper = mount(
<I18nProvider>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<MockedProvider mocks={mocks}>
<Timeline
id="foo"
columnHeaders={allColumnsHaveTextFilters}
columnRenderers={columnRenderers}
dataProviders={dataProviders}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled}
onToggleDataProviderExcluded={noop}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
sort={sort}
theme="dark"
indexPattern={indexPattern}
/>
</MockedProvider>
</DragDropContext>
</ReduxStoreProvider>
</I18nProvider>
);
wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="andProviderAccordion"] button.euiContextMenuItem')
.at(1)
.simulate('click');
expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({
andProviderId: 'id-Provider 2',
enabled: false,
providerId: 'id-Provider 1',
});
});
test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the accordeon menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderExcluded = jest.fn();
// for this test, all columns have text filters
const allColumnsHaveTextFilters = headers.map(header => ({
...header,
columnHeaderType: 'text-filter' as ColumnHeaderType,
}));
const wrapper = mount(
<I18nProvider>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<MockedProvider mocks={mocks}>
<Timeline
id="foo"
columnHeaders={allColumnsHaveTextFilters}
columnRenderers={columnRenderers}
dataProviders={dataProviders}
flyoutHeight={testFlyoutHeight}
flyoutHeaderHeight={flyoutHeaderHeight}
itemsPerPage={5}
itemsPerPageOptions={[5, 10, 20]}
onChangeDataProviderKqlQuery={noop}
onChangeDroppableAndProvider={noop}
onChangeItemsPerPage={noop}
onColumnSorted={noop}
onDataProviderRemoved={noop}
onFilterChange={noop}
onRangeSelected={noop}
onToggleDataProviderEnabled={noop}
onToggleDataProviderExcluded={mockOnToggleDataProviderExcluded}
range={'1 Day'}
rowRenderers={rowRenderers}
show={true}
sort={sort}
theme="dark"
indexPattern={indexPattern}
/>
</MockedProvider>
</DragDropContext>
</ReduxStoreProvider>
</I18nProvider>
);
wrapper
.find('[data-test-subj="andProviderButton"] span.euiBadge')
.first()
.simulate('click');
wrapper.update();
wrapper
.find('[data-test-subj="andProviderAccordion"] button.euiContextMenuItem')
.first()
.simulate('click');
expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({
andProviderId: 'id-Provider 2',
excluded: true,
providerId: 'id-Provider 1',
});
});
});

View file

@ -20,12 +20,15 @@ import { ColumnRenderer } from './body/renderers';
import { Sort } from './body/sort';
import { DataProvider } from './data_providers/data_provider';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
OnChangeItemsPerPage,
OnColumnSorted,
OnDataProviderRemoved,
OnFilterChange,
OnRangeSelected,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
} from './events';
import { Footer, footerHeight } from './footer';
import { TimelineHeader } from './header/timeline_header';
@ -41,12 +44,15 @@ interface Props {
indexPattern: StaticIndexPattern;
itemsPerPage: number;
itemsPerPageOptions: number[];
onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery;
onChangeDroppableAndProvider: OnChangeDroppableAndProvider;
onChangeItemsPerPage: OnChangeItemsPerPage;
onColumnSorted: OnColumnSorted;
onDataProviderRemoved: OnDataProviderRemoved;
onFilterChange: OnFilterChange;
onRangeSelected: OnRangeSelected;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
range: string;
rowRenderers: RowRenderer[];
show: boolean;
@ -70,12 +76,15 @@ export const Timeline = pure<Props>(
indexPattern,
itemsPerPage,
itemsPerPageOptions,
onChangeDataProviderKqlQuery,
onChangeDroppableAndProvider,
onChangeItemsPerPage,
onColumnSorted,
onDataProviderRemoved,
onFilterChange,
onRangeSelected,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
range,
rowRenderers,
show,
@ -93,11 +102,14 @@ export const Timeline = pure<Props>(
columnHeaders={columnHeaders}
id={id}
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onColumnSorted={onColumnSorted}
onDataProviderRemoved={onDataProviderRemoved}
onFilterChange={onFilterChange}
onRangeSelected={onRangeSelected}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
range={range}
show={show}
sort={sort}

View file

@ -5,7 +5,7 @@
*/
import { EuiPanel } from '@elastic/eui';
import { noop, range } from 'lodash/fp';
import { range } from 'lodash/fp';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
@ -61,11 +61,7 @@ class PlaceholdersComponent extends React.PureComponent<Props> {
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider
dataProvider={dataProvider}
onDataProviderRemoved={noop}
onToggleDataProviderEnabled={noop}
/>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
mockDataProviders[i].name

View file

@ -56,6 +56,7 @@ export const mockGlobalState: State = {
dataProviders: [],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,

View file

@ -34,7 +34,11 @@ export const createTimeline = actionCreator<{ id: string; show?: boolean }>('CRE
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
export const removeProvider = actionCreator<{ id: string; providerId: string }>('REMOVE_PROVIDER');
export const removeProvider = actionCreator<{
id: string;
providerId: string;
andProviderId?: string;
}>('REMOVE_PROVIDER');
export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
@ -44,8 +48,27 @@ export const updateDataProviderEnabled = actionCreator<{
id: string;
enabled: boolean;
providerId: string;
andProviderId?: string;
}>('TOGGLE_PROVIDER_ENABLED');
export const updateDataProviderExcluded = actionCreator<{
id: string;
excluded: boolean;
providerId: string;
andProviderId?: string;
}>('TOGGLE_PROVIDER_EXCLUDED');
export const updateDataProviderKqlQuery = actionCreator<{
id: string;
kqlQuery: string;
providerId: string;
}>('PROVIDER_EDIT_KQL_QUERY');
export const updateHighlightedDropAndProviderId = actionCreator<{
id: string;
providerId: string;
}>('UPDATE_DROP_AND_PROVIDER');
export const updateDescription = actionCreator<{ id: string; description: string }>(
'UPDATE_DESCRIPTION'
);

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { filter, getOr, omit, uniq } from 'lodash/fp';
import { getOr, omit, uniq } from 'lodash/fp';
import { TimelineById, TimelineState } from '.';
import { Sort } from '../../../components/timeline/body/sort';
import { DataProvider } from '../../../components/timeline/data_providers/data_provider';
import { KqlMode, timelineDefaults } from './model';
import { KqlMode, timelineDefaults, TimelineModel } from './model';
const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference
@ -187,20 +187,50 @@ export const applyDeltaToCurrentWidth = ({
};
};
interface AddTimelineProviderParams {
id: string;
provider: DataProvider;
timelineById: TimelineById;
}
const addAndToProviderInTimeline = (
id: string,
provider: DataProvider,
timeline: TimelineModel,
timelineById: TimelineById
): TimelineById => {
const alreadyExistsProviderIndex = timeline.dataProviders.findIndex(
p => p.id === timeline.highlightedDropAndProviderId
);
const newProvider = timeline.dataProviders[alreadyExistsProviderIndex];
const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id);
export const addTimelineProvider = ({
id,
provider,
timelineById,
}: AddTimelineProviderParams): TimelineById => {
const timeline = timelineById[id];
const dataProviders = [
...timeline.dataProviders.slice(0, alreadyExistsProviderIndex),
{
...timeline.dataProviders[alreadyExistsProviderIndex],
and:
alreadyExistsAndProviderIndex > -1
? [
...newProvider.and.slice(0, alreadyExistsAndProviderIndex),
provider,
...newProvider.and.slice(alreadyExistsAndProviderIndex + 1),
]
: [...newProvider.and, provider],
},
...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1),
];
return {
...timelineById,
[id]: {
...timeline,
dataProviders,
},
};
};
const addProviderToTimeline = (
id: string,
provider: DataProvider,
timeline: TimelineModel,
timelineById: TimelineById
): TimelineById => {
const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id);
const dataProviders =
alreadyExistsAtIndex > -1
? [
@ -218,6 +248,25 @@ export const addTimelineProvider = ({
},
};
};
interface AddTimelineProviderParams {
id: string;
provider: DataProvider;
timelineById: TimelineById;
}
export const addTimelineProvider = ({
id,
provider,
timelineById,
}: AddTimelineProviderParams): TimelineById => {
const timeline = timelineById[id];
if (timeline.highlightedDropAndProviderId !== '') {
return addAndToProviderInTimeline(id, provider, timeline, timelineById);
} else {
return addProviderToTimeline(id, provider, timeline, timelineById);
}
};
interface UpdateTimelineKqlModeParams {
id: string;
@ -415,11 +464,39 @@ export const updateTimelineSort = ({
};
};
const updateEnabledAndProvider = (
andProviderId: string,
enabled: boolean,
providerId: string,
timeline: TimelineModel
) =>
timeline.dataProviders.map(provider =>
provider.id === providerId
? {
...provider,
and: provider.and.map(andProvider =>
andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider
),
}
: provider
);
const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) =>
timeline.dataProviders.map(provider =>
provider.id === providerId
? {
...provider,
enabled,
}
: provider
);
interface UpdateTimelineProviderEnabledParams {
id: string;
providerId: string;
enabled: boolean;
timelineById: TimelineById;
andProviderId?: string;
}
export const updateTimelineProviderEnabled = ({
@ -427,14 +504,94 @@ export const updateTimelineProviderEnabled = ({
providerId,
enabled,
timelineById,
andProviderId,
}: UpdateTimelineProviderEnabledParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
dataProviders: andProviderId
? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline)
: updateEnabledProvider(enabled, providerId, timeline),
},
};
};
const updateExcludedAndProvider = (
andProviderId: string,
excluded: boolean,
providerId: string,
timeline: TimelineModel
) =>
timeline.dataProviders.map(provider =>
provider.id === providerId
? {
...provider,
and: provider.and.map(andProvider =>
andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider
),
}
: provider
);
const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) =>
timeline.dataProviders.map(provider =>
provider.id === providerId
? {
...provider,
excluded,
}
: provider
);
interface UpdateTimelineProviderExcludedParams {
id: string;
providerId: string;
excluded: boolean;
timelineById: TimelineById;
andProviderId?: string;
}
export const updateTimelineProviderExcluded = ({
id,
providerId,
excluded,
timelineById,
andProviderId,
}: UpdateTimelineProviderExcludedParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
dataProviders: andProviderId
? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline)
: updateExcludedProvider(excluded, providerId, timeline),
},
};
};
interface UpdateTimelineProviderKqlQueryParams {
id: string;
providerId: string;
kqlQuery: string;
timelineById: TimelineById;
}
export const updateTimelineProviderKqlQuery = ({
id,
providerId,
kqlQuery,
timelineById,
}: UpdateTimelineProviderKqlQueryParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
dataProviders: timeline.dataProviders.map(provider =>
provider.id === providerId ? { ...provider, ...{ enabled } } : provider
provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider
),
},
};
@ -503,23 +660,54 @@ export const updateTimelinePerPageOptions = ({
};
};
const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => {
const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId);
const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex(
p => p.id === andProviderId
);
return [
...timeline.dataProviders.slice(0, providerIndex),
{
...timeline.dataProviders[providerIndex],
and: [
...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex),
...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1),
],
},
...timeline.dataProviders.slice(providerIndex + 1),
];
};
const removeProvider = (providerId: string, timeline: TimelineModel) => {
const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId);
return [
...timeline.dataProviders.slice(0, providerIndex),
...timeline.dataProviders.slice(providerIndex + 1),
];
};
interface RemoveTimelineProviderParams {
id: string;
providerId: string;
timelineById: TimelineById;
andProviderId?: string;
}
export const removeTimelineProvider = ({
id,
providerId,
timelineById,
andProviderId,
}: RemoveTimelineProviderParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
dataProviders: filter(p => p.id !== providerId, timeline.dataProviders),
dataProviders: andProviderId
? removeAndProvider(andProviderId, providerId, timeline)
: removeProvider(providerId, timeline),
},
};
};
@ -544,3 +732,25 @@ export const unPinTimelineEvent = ({
},
};
};
interface UpdateHighlightedDropAndProviderIdParams {
id: string;
providerId: string;
timelineById: TimelineById;
}
export const updateHighlightedDropAndProvider = ({
id,
providerId,
timelineById,
}: UpdateHighlightedDropAndProviderIdParams): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
highlightedDropAndProviderId: providerId,
},
};
};

View file

@ -21,6 +21,8 @@ export interface TimelineModel {
eventIdToNoteIds: { [eventId: string]: string[] };
/** The chronological history of actions related to this timeline */
historyIds: string[];
/** The chronological history of actions related to this timeline */
highlightedDropAndProviderId: string;
/** Uniquely identifies the timeline */
id: string;
/** When true, this timeline was marked as "favorite" by the user */
@ -57,6 +59,7 @@ export const timelineDefaults: Readonly<
| 'dataProviders'
| 'description'
| 'eventIdToNoteIds'
| 'highlightedDropAndProviderId'
| 'historyIds'
| 'isFavorite'
| 'isLive'
@ -76,6 +79,7 @@ export const timelineDefaults: Readonly<
dataProviders: [],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,

View file

@ -15,6 +15,7 @@ import {
updateTimelineItemsPerPage,
updateTimelinePerPageOptions,
updateTimelineProviderEnabled,
updateTimelineProviderExcluded,
updateTimelineProviders,
updateTimelineRange,
updateTimelineShowTimeline,
@ -30,13 +31,21 @@ const timelineByIdMock: TimelineById = {
id: '123',
name: 'data provider 1',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',
isFavorite: false,
@ -109,9 +118,16 @@ describe('Timeline', () => {
id: '567',
name: 'data provider 2',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
timelineById: timelineByIdMock,
});
@ -124,9 +140,16 @@ describe('Timeline', () => {
id: '567',
name: 'data provider 2',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
@ -143,9 +166,16 @@ describe('Timeline', () => {
id: '123',
name: 'data provider 1',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
@ -161,9 +191,16 @@ describe('Timeline', () => {
id: '123',
name: 'my name changed',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
@ -174,6 +211,132 @@ describe('Timeline', () => {
});
});
describe('#addAndProviderToTimelineProvider', () => {
test('should add a new and provider to an existing timeline provider', () => {
const providerToAdd = {
and: [],
id: '567',
name: 'data provider 2',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const newTimeline = addTimelineProvider({
id: 'foo',
provider: providerToAdd,
timelineById: timelineByIdMock,
});
newTimeline.foo.highlightedDropAndProviderId = '567';
const andProviderToAdd = {
and: [],
id: '568',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
provider: andProviderToAdd,
timelineById: newTimeline,
});
const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567');
const addedAndDataProvider = update.foo.dataProviders[indexProvider].and[0];
expect(addedAndDataProvider).toEqual(andProviderToAdd);
newTimeline.foo.highlightedDropAndProviderId = '';
});
test('should NOT add a new timeline and provider if it already exists', () => {
const providerToAdd = {
and: [
{
and: [],
id: '568',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
id: '567',
name: 'data provider 1',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const newTimeline = addTimelineProvider({
id: 'foo',
provider: providerToAdd,
timelineById: timelineByIdMock,
});
newTimeline.foo.highlightedDropAndProviderId = '567';
const andProviderToAdd = {
and: [],
id: '568',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
provider: andProviderToAdd,
timelineById: newTimeline,
});
const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567');
expect(update.foo.dataProviders[indexProvider].and.length).toEqual(1);
newTimeline.foo.highlightedDropAndProviderId = '';
});
});
describe('#updateTimelineProviders', () => {
test('should return a new reference and not the same reference', () => {
const update = updateTimelineProviders({
@ -184,9 +347,16 @@ describe('Timeline', () => {
id: '567',
name: 'data provider 2',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
timelineById: timelineByIdMock,
@ -200,9 +370,16 @@ describe('Timeline', () => {
id: '567',
name: 'data provider 2',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
const update = updateTimelineProviders({
id: 'foo',
@ -302,13 +479,21 @@ describe('Timeline', () => {
id: '123',
name: 'data provider 1',
enabled: false, // This value changed from true to false
queryMatch: '',
queryDate: '',
negated: false,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
@ -337,9 +522,16 @@ describe('Timeline', () => {
id: '456',
name: 'data provider 1',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
});
const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock);
const update = updateTimelineProviderEnabled({
@ -357,22 +549,37 @@ describe('Timeline', () => {
id: '123',
name: 'data provider 1',
enabled: false, // value we are updating from true to false
queryMatch: '',
queryDate: '',
negated: false,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
},
{
and: [],
id: '456',
name: 'data provider 1',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
@ -396,6 +603,405 @@ describe('Timeline', () => {
});
});
describe('#updateTimelineAndProviderEnabled', () => {
let timelineByIdwithAndMock: TimelineById = timelineByIdMock;
beforeEach(() => {
const providerToAdd = {
and: [
{
and: [],
id: '568',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
id: '567',
name: 'data provider 1',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
timelineByIdwithAndMock = addTimelineProvider({
id: 'foo',
provider: providerToAdd,
timelineById: timelineByIdMock,
});
});
test('should return a new reference and not the same reference', () => {
const update = updateTimelineProviderEnabled({
id: 'foo',
providerId: '567',
enabled: false, // value we are updating from true to false
timelineById: timelineByIdwithAndMock,
andProviderId: '568',
});
expect(update).not.toBe(timelineByIdwithAndMock);
});
test('should return a new reference for and data provider and not the same reference of data and provider', () => {
const update = updateTimelineProviderEnabled({
id: 'foo',
providerId: '567',
enabled: false, // value we are updating from true to false
timelineById: timelineByIdwithAndMock,
andProviderId: '568',
});
expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders);
});
test('should update the timeline and provider enabled from true to false', () => {
const update = updateTimelineProviderEnabled({
id: 'foo',
providerId: '567',
enabled: false, // value we are updating from true to false
timelineById: timelineByIdwithAndMock,
andProviderId: '568',
});
const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567');
expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(false);
});
test('should update only one and data provider and not two and data providers', () => {
const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex(
i => i.id === '567'
);
const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[
indexProvider
].and.concat({
and: [],
id: '456',
name: 'new and data provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
});
const multiAndDataProviderMock = set(
`foo.dataProviders[${indexProvider}].and`,
multiAndDataProvider,
timelineByIdwithAndMock
);
const update = updateTimelineProviderEnabled({
id: 'foo',
providerId: '567',
enabled: false, // value we are updating from true to false
timelineById: multiAndDataProviderMock,
andProviderId: '568',
});
const oldAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '568');
const newAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '456');
expect(oldAndProvider!.enabled).toEqual(false);
expect(newAndProvider!.enabled).toEqual(true);
});
});
describe('#updateTimelineProviderExcluded', () => {
test('should return a new reference and not the same reference', () => {
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '123',
excluded: true, // value we are updating from false to true
timelineById: timelineByIdMock,
});
expect(update).not.toBe(timelineByIdMock);
});
test('should return a new reference for data provider and not the same reference of data provider', () => {
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '123',
excluded: true, // value we are updating from false to true
timelineById: timelineByIdMock,
});
expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders);
});
test('should update the timeline provider excluded from true to false', () => {
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '123',
excluded: true, // value we are updating from false to true
timelineById: timelineByIdMock,
});
const expected: TimelineById = {
foo: {
id: 'foo',
dataProviders: [
{
and: [],
id: '123',
name: 'data provider 1',
enabled: true,
excluded: true, // This value changed from true to false
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
kqlMode: 'filter',
kqlQuery: '',
title: '',
noteIds: [],
range: '1 Day',
show: true,
sort: {
columnId: 'timestamp',
sortDirection: Direction.descending,
},
pinnedEventIds: {},
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50],
width: defaultWidth,
},
};
expect(update).toEqual(expected);
});
test('should update only one data provider and not two data providers', () => {
const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({
and: [],
id: '456',
name: 'data provider 1',
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
});
const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock);
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '123',
excluded: true, // value we are updating from false to true
timelineById: multiDataProviderMock,
});
const expected: TimelineById = {
foo: {
id: 'foo',
dataProviders: [
{
and: [],
id: '123',
name: 'data provider 1',
enabled: true,
excluded: true, // value we are updating from false to true
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
},
{
and: [],
id: '456',
name: 'data provider 1',
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
kqlMode: 'filter',
kqlQuery: '',
title: '',
noteIds: [],
range: '1 Day',
show: true,
sort: {
columnId: 'timestamp',
sortDirection: Direction.descending,
},
pinnedEventIds: {},
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50],
width: defaultWidth,
},
};
expect(update).toEqual(expected);
});
});
describe('#updateTimelineAndProviderExcluded', () => {
let timelineByIdwithAndMock: TimelineById = timelineByIdMock;
beforeEach(() => {
const providerToAdd = {
and: [
{
and: [],
id: '568',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
id: '567',
name: 'data provider 1',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
};
timelineByIdwithAndMock = addTimelineProvider({
id: 'foo',
provider: providerToAdd,
timelineById: timelineByIdMock,
});
});
test('should return a new reference and not the same reference', () => {
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '567',
excluded: true, // value we are updating from true to false
timelineById: timelineByIdwithAndMock,
andProviderId: '568',
});
expect(update).not.toBe(timelineByIdwithAndMock);
});
test('should return a new reference for and data provider and not the same reference of data and provider', () => {
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '567',
excluded: true, // value we are updating from false to true
timelineById: timelineByIdwithAndMock,
andProviderId: '568',
});
expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders);
});
test('should update the timeline and provider excluded from true to false', () => {
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '567',
excluded: true, // value we are updating from true to false
timelineById: timelineByIdwithAndMock,
andProviderId: '568',
});
const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567');
expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(true);
});
test('should update only one and data provider and not two and data providers', () => {
const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex(
i => i.id === '567'
);
const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[
indexProvider
].and.concat({
and: [],
id: '456',
name: 'new and data provider',
enabled: true,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
});
const multiAndDataProviderMock = set(
`foo.dataProviders[${indexProvider}].and`,
multiAndDataProvider,
timelineByIdwithAndMock
);
const update = updateTimelineProviderExcluded({
id: 'foo',
providerId: '567',
excluded: true, // value we are updating from true to false
timelineById: multiAndDataProviderMock,
andProviderId: '568',
});
const oldAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '568');
const newAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '456');
expect(oldAndProvider!.excluded).toEqual(true);
expect(newAndProvider!.excluded).toEqual(false);
});
});
describe('#updateTimelineItemsPerPage', () => {
test('should return a new reference and not the same reference', () => {
const update = updateTimelineItemsPerPage({
@ -421,13 +1027,21 @@ describe('Timeline', () => {
id: '123',
name: 'data provider 1',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
@ -475,13 +1089,21 @@ describe('Timeline', () => {
id: '123',
name: 'data provider 1',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,
isLive: false,
@ -531,9 +1153,16 @@ describe('Timeline', () => {
id: '456',
name: 'data provider 2',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
});
const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock);
const update = removeTimelineProvider({
@ -549,13 +1178,21 @@ describe('Timeline', () => {
id: '456',
name: 'data provider 2',
enabled: true,
queryMatch: '',
queryDate: '',
negated: false,
queryMatch: {
field: '',
value: '',
},
queryDate: {
from: 0,
to: 1,
},
excluded: false,
kqlQuery: '',
},
],
description: '',
eventIdToNoteIds: {},
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',
isFavorite: false,

View file

@ -18,7 +18,10 @@ import {
showTimeline,
unPinEvent,
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderKqlQuery,
updateDescription,
updateHighlightedDropAndProviderId,
updateIsFavorite,
updateIsLive,
updateItemsPerPage,
@ -41,6 +44,7 @@ import {
pinTimelineEvent,
removeTimelineProvider,
unPinTimelineEvent,
updateHighlightedDropAndProvider,
updateTimelineDescription,
updateTimelineIsFavorite,
updateTimelineIsLive,
@ -50,6 +54,8 @@ import {
updateTimelinePageIndex,
updateTimelinePerPageOptions,
updateTimelineProviderEnabled,
updateTimelineProviderExcluded,
updateTimelineProviderKqlQuery,
updateTimelineProviders,
updateTimelineRange,
updateTimelineShowTimeline,
@ -118,9 +124,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }),
}))
.case(removeProvider, (state, { id, providerId }) => ({
.case(removeProvider, (state, { id, providerId, andProviderId }) => ({
...state,
timelineById: removeTimelineProvider({ id, providerId, timelineById: state.timelineById }),
timelineById: removeTimelineProvider({
id,
providerId,
timelineById: state.timelineById,
andProviderId,
}),
}))
.case(unPinEvent, (state, { id, eventId }) => ({
...state,
@ -162,13 +173,33 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineSort({ id, sort, timelineById: state.timelineById }),
}))
.case(updateDataProviderEnabled, (state, { id, enabled, providerId }) => ({
.case(updateDataProviderEnabled, (state, { id, enabled, providerId, andProviderId }) => ({
...state,
timelineById: updateTimelineProviderEnabled({
id,
enabled,
providerId,
timelineById: state.timelineById,
andProviderId,
}),
}))
.case(updateDataProviderExcluded, (state, { id, excluded, providerId, andProviderId }) => ({
...state,
timelineById: updateTimelineProviderExcluded({
id,
excluded,
providerId,
timelineById: state.timelineById,
andProviderId,
}),
}))
.case(updateDataProviderKqlQuery, (state, { id, kqlQuery, providerId }) => ({
...state,
timelineById: updateTimelineProviderKqlQuery({
id,
kqlQuery,
providerId,
timelineById: state.timelineById,
}),
}))
.case(updateItemsPerPage, (state, { id, itemsPerPage }) => ({
@ -195,4 +226,12 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
.case(updateHighlightedDropAndProviderId, (state, { id, providerId }) => ({
...state,
timelineById: updateHighlightedDropAndProvider({
id,
providerId,
timelineById: state.timelineById,
}),
}))
.build();

View file

@ -721,6 +721,14 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-typescript" "^7.1.0"
"@babel/runtime-corejs2@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.2.0.tgz#5ccd722b72d2c18c6a7224b5751f4b9816b60ada"
integrity sha512-kPfmKoRI8Hpo5ZJGACWyrc9Eq1j3ZIUpUAQT2yH045OuYpccFJ9kYA/eErwzOM2jeBG1sC8XX1nl1EArtuM8tg==
dependencies:
core-js "^2.5.7"
regenerator-runtime "^0.12.0"
"@babel/runtime@7.0.0-beta.54":
version "7.0.0-beta.54"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf"
@ -1709,10 +1717,10 @@
"@types/events" "*"
"@types/node" "*"
"@types/react-beautiful-dnd@^7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-7.1.2.tgz#98a5bc1dca5dadfe462ba73180e8c66b5a275dcf"
integrity sha512-SY+1maGYsCAQEurbnVEca8u97hwhN+/jGitsXG9aXNaySEFJInBgx1HknhtMxJDdcqKAkNMRRVlWyfBC1fwe3A==
"@types/react-beautiful-dnd@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.0.1.tgz#8be4449a2f7843433542c97fcf18333383cc3e24"
integrity sha512-RA3mPgJFJP+zpkhVQL9T2SIkFDbtPx+R09hApQt2BvBUHYpiLaO3PygmnPgcDwCAultn3l+8jABbnVgOFtvZTg==
dependencies:
"@types/react" "*"
@ -6395,6 +6403,13 @@ css-box-model@^1.0.0:
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.0.0.tgz#60142814f2b25be00c4aac65ea1a55a531b18922"
integrity sha512-MGipbCM6/HGmsOwN6Enq1OvNKy8H5Q1XKoyBszxwv2efly7ZVg+HcFILX8O6S0xfj27l1+6P7FyCjcQ90m5HBQ==
css-box-model@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.1.1.tgz#c9fd8e7a8b1d59d41d6812fd1765433f671b2ee0"
integrity sha512-ZxbuLFeAPEDb0wPbGfT7783Vb00MVAkvOlMKwr0kA2PD5EGxk6P3MAhedvVuyVJCWb54bb+6HQ7pdPYENf8AZw==
dependencies:
tiny-invariant "^1.0.3"
css-color-keywords@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
@ -14402,7 +14417,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0"
loose-envify@^1.3.0:
loose-envify@^1.3.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -14713,6 +14728,11 @@ memoize-one@^4.0.0:
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.2.tgz#3fb8db695aa14ab9c0f1644e1585a8806adc1aee"
integrity sha512-ucx2DmXTeZTsS4GPPUZCbULAN7kdPT1G+H49Y34JjbQ5ESc6OGhVxKvb1iKhr9v19ZB9OtnHwNnhUnNR/7Wteg==
memoize-one@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906"
integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA==
memoizee@0.4.X:
version "0.4.14"
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
@ -17826,6 +17846,20 @@ react-apollo@^2.1.4:
lodash "^4.17.10"
prop-types "^15.6.0"
react-beautiful-dnd@^10.0.1:
version "10.0.3"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.0.3.tgz#75e7989b639493cefcf71a678e93c171ac6a207b"
integrity sha512-A6N50lv2RnXH9kNRKy+HYcQYOgRmgQRdn8qaKTTb4sYzX0H/4UhEYuOnh5PQB8i66bvCpfkxxD9El/OjgbtpPw==
dependencies:
"@babel/runtime-corejs2" "^7.2.0"
css-box-model "^1.1.1"
memoize-one "^4.1.0"
prop-types "^15.6.1"
raf-schd "^4.0.0"
react-redux "^5.0.7"
redux "^4.0.1"
tiny-invariant "^1.0.3"
react-beautiful-dnd@^8.0.7:
version "8.0.7"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43"
@ -18600,6 +18634,14 @@ redux@4.0.0, redux@^4.0.0:
loose-envify "^1.1.0"
symbol-observable "^1.2.0"
redux@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"
integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c"
@ -21425,6 +21467,11 @@ tiny-invariant@^0.0.3:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-0.0.3.tgz#4c7283c950e290889e9e94f64d3586ec9156cf44"
integrity sha512-SA2YwvDrCITM9fTvHTHRpq9W6L2fBsClbqm3maT5PZux4Z73SPPDYwJMtnoWh6WMgmCkJij/LaOlWiqJqFMK8g==
tiny-invariant@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.3.tgz#91efaaa0269ccb6271f0296aeedb05fc3e067b7a"
integrity sha512-ytQx8T4DL8PjlX53yYzcIC0WhIZbpR0p1qcYjw2pHu3w6UtgWwFJQ/02cnhOnBBhlFx/edUIfcagCaQSe3KMWg==
tiny-lr@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab"