[SIEM] Apply highlighting to the Timeline data providers drop area and flyout button (#45173)

## Summary

To indicate they accept droppable data providers, this PR applies highlighting to the Timeline flyout badge and data providers drop area.

Tested in dark and light mode in:
- Chrome `76.0.3809.132`
- Firefox `69.0`
- Safari `12.1.2`

## Timeline data providers (drop area) highlighting behavior

- While a data provider is being dragged, in a page or from within the timeline itself, apply `euiColorSuccess` to the timeline data providers drop area's dashed borders and "plain" text, but NOT to the other primitives, i.e. data provider badges, `AND` / `OR` circular badges, lines, etc  
- While a data provider is being dragged, in a page or from within the timeline itself, but NOT hovering over the data providers drop area, apply the `euiColorSuccess` color with `10%` alpha channel to the background of the data providers drop area
- While a data provider is being dragged over the drop target area inside the data providers drop zone, apply the `euiColorSuccess` color with `20%` alpha channel to the background of the data providers drop area
- While a data provider is being dragged over the drop target area inside the data providers drop zone that will add the data provider as an `AND` clause, apply `euiColorSuccess` color to the dashed border, and fill the background with the `euiColorSuccess` color with `30%` alpha channel to the background of the data providers drop area
- Do NOT apply highlighting styles when a column or field (non-data provider) is dragged from the timeline or the `Events` widget

### Dark mode

![timeline-highlighting-dark](https://user-images.githubusercontent.com/4459398/64755303-ebcb1680-d4e7-11e9-8452-0f91ddae85d1.gif)

### Light mode

![timeline-highlighting-light](https://user-images.githubusercontent.com/4459398/64755318-fc7b8c80-d4e7-11e9-8621-a7eb918468aa.gif)

## Flyout button highlighting behavior

- The (default) timeline flyout button is styled as non-filled EUI button
- The button text reads `Timeline ^` (with a trailing caret), instead of `T I M E L I N E`
- Hovering over the flyout button while NOT dragging underlines the flyout button text, but does not fill the background
- When a user starts dragging a data provider, apply `euiColorSuccess` to all visible flyout button properties (e.g. border, text)
- While a data provider is being dragged, in a page or from within the timeline itself, but NOT hovering over the flyout, apply the `euiColorSuccess` color with `10%` alpha channel to the background of the flyout button
- While a data provider is being dragged, in a page or from within the timeline itself, AND hovering over the flyout such that it's in a droppable state, apply the `euiColorSuccess` color with `20%` alpha channel to the background of the flyout button
- While a data provider is being dragged, in a page or from within the timeline itself, AND hovering over the flyout such that it's in a droppable state, replace the `Timeline ^` with a circle containing a `+` plus sign
- Apply `EuiNotificationBadge` styling to the data providers badge count, and position it in the upper left-hand corner
- Do NOT apply highlighting styles when a column or field (non-data provider) is dragged from the timeline or the `Events` widget

### Dark mode

![timeline-badge-dark](https://user-images.githubusercontent.com/4459398/64755512-5ed48d00-d4e8-11e9-9be2-02da70ac4e16.gif)

### Light mode

![timeline-badge-light](https://user-images.githubusercontent.com/4459398/64755531-7449b700-d4e8-11e9-9239-62ee78a6e652.gif)

Resolves https://github.com/elastic/siem-team/issues/457
This commit is contained in:
Andrew Goldstein 2019-09-12 01:04:20 -06:00 committed by GitHub
parent 9a109f2170
commit 40147fa83f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 427 additions and 127 deletions

View file

@ -37,3 +37,10 @@ export const drop = (dropTarget: JQuery<HTMLElement>) => {
.trigger('mousemove', { button: primaryButton })
.trigger('mouseup');
};
/** Drags the subject being dragged on the specified drop target, but does not drop it */
export const dragWithoutDrop = (dropTarget: JQuery<HTMLElement>) => {
cy.wrap(dropTarget).trigger('mousemove', 'center', {
button: primaryButton,
});
};

View file

@ -10,12 +10,20 @@ export const DATA_PROVIDER = '[data-test-subj="providerContainer"]';
/** Data providers are dropped and rendered in this area of the timeline */
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
/** The empty data providers area when no data providers have been added to the timeline */
export const TIMELINE_DATA_PROVIDERS_EMPTY =
'[data-test-subj="dataProviders"] [data-test-subj="empty"]';
/** Data providers that were dropped on a timeline */
export const TIMELINE_DROPPED_DATA_PROVIDERS = `${TIMELINE_DATA_PROVIDERS} ${DATA_PROVIDER}`;
/** The `T I M E L I N E` button that toggles visibility of the Timeline */
/** The `Timeline ^` button that toggles visibility of the Timeline */
export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]';
/** The flyout button shown when a data provider is not ready to be dropped on the timeline */
export const TIMELINE_NOT_READY_TO_DROP_BUTTON =
'[data-test-subj="flyout-button-not-ready-to-drop"]';
/** Contains the KQL bar for searching or filtering in the timeline */
export const SEARCH_OR_FILTER_CONTAINER =
'[data-test-subj="timeline-search-or-filter-search-container"]';
@ -26,3 +34,6 @@ export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count
/** Expands or collapses an event in the timeline */
export const TOGGLE_TIMELINE_EXPAND_EVENT =
'[data-test-subj="timeline"] [data-test-subj="expand-event"]';
/** The body of the timeline flyout */
export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]';

View file

@ -5,12 +5,17 @@
*/
import { logout } from '../../lib/logout';
import { TIMELINE_DROPPED_DATA_PROVIDERS } from '../../lib/timeline/selectors';
import {
TIMELINE_DATA_PROVIDERS,
TIMELINE_DROPPED_DATA_PROVIDERS,
TIMELINE_DATA_PROVIDERS_EMPTY,
} from '../../lib/timeline/selectors';
import { dragFromAllHostsToTimeline, toggleTimelineVisibility } from '../../lib/timeline/helpers';
import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../../lib/hosts/selectors';
import { HOSTS_PAGE } from '../../lib/urls';
import { waitForAllHostsWidget } from '../../lib/hosts/helpers';
import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers';
import { drag, dragWithoutDrop } from '../../lib/drag_n_drop/helpers';
describe('timeline data providers', () => {
beforeEach(() => {
@ -43,4 +48,60 @@ describe('timeline data providers', () => {
});
});
});
it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => {
waitForAllHostsWidget();
toggleTimelineVisibility();
cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS)
.first()
.then(host => drag(host));
cy.get(TIMELINE_DATA_PROVIDERS).should(
'have.css',
'background',
'rgba(125, 226, 209, 0.1) none repeat scroll 0% 0% / auto padding-box border-box'
);
});
it('sets the background to euiColorSuccess with a 20% alpha channel when the user starts dragging a host AND is hovering over the data providers', () => {
waitForAllHostsWidget();
toggleTimelineVisibility();
cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS)
.first()
.then(host => drag(host));
cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea =>
dragWithoutDrop(dataProvidersDropArea)
);
cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).should(
'have.css',
'background',
'rgba(125, 226, 209, 0.2) none repeat scroll 0% 0% / auto padding-box border-box'
);
});
it('renders the dashed border color as euiColorSuccess when hovering over the data providers', () => {
waitForAllHostsWidget();
toggleTimelineVisibility();
cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS)
.first()
.then(host => drag(host));
cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea =>
dragWithoutDrop(dataProvidersDropArea)
);
cy.get(TIMELINE_DATA_PROVIDERS).should(
'have.css',
'border',
'3.1875px dashed rgb(125, 226, 209)'
);
});
});

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 { logout } from '../../lib/logout';
import {
TIMELINE_FLYOUT_BODY,
TIMELINE_NOT_READY_TO_DROP_BUTTON,
} from '../../lib/timeline/selectors';
import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../../lib/hosts/selectors';
import { HOSTS_PAGE } from '../../lib/urls';
import { waitForAllHostsWidget } from '../../lib/hosts/helpers';
import { loginAndWaitForPage } from '../../lib/util/helpers';
import { drag } from '../../lib/drag_n_drop/helpers';
import { toggleTimelineVisibility } from '../../lib/timeline/helpers';
describe('timeline flyout button', () => {
beforeEach(() => {
loginAndWaitForPage(HOSTS_PAGE);
});
afterEach(() => {
return logout();
});
it('toggles open the timeline', () => {
toggleTimelineVisibility();
cy.get(TIMELINE_FLYOUT_BODY).should('have.css', 'visibility', 'visible');
});
it('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => {
waitForAllHostsWidget();
cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS)
.first()
.then(host => drag(host));
cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should(
'have.css',
'background',
'rgba(125, 226, 209, 0.1) none repeat scroll 0% 0% / auto padding-box border-box'
);
});
});

View file

@ -6,7 +6,7 @@
import { defaultTo, noop } from 'lodash/fp';
import * as React from 'react';
import { DragDropContext, DropResult, ResponderProvided } from 'react-beautiful-dnd';
import { DragDropContext, DropResult, ResponderProvided, DragStart } from 'react-beautiful-dnd';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
@ -19,8 +19,10 @@ import {
addFieldToTimelineColumns,
addProviderToTimeline,
fieldWasDroppedOnTimelineColumns,
IS_DRAGGING_CLASS_NAME,
providerWasDroppedOnTimeline,
providerWasDroppedOnTimelineButton,
draggableIsField,
} from './helpers';
interface Props {
@ -65,7 +67,7 @@ export class DragDropContextWrapperComponent extends React.Component<Props> {
const { children } = this.props;
return (
<DragDropContext onDragEnd={this.onDragEnd} onDragStart={disableScrolling}>
<DragDropContext onDragEnd={this.onDragEnd} onDragStart={onDragStart}>
{children}
</DragDropContext>
);
@ -86,6 +88,10 @@ export class DragDropContextWrapperComponent extends React.Component<Props> {
dispatch,
});
}
if (!draggableIsField(result)) {
document.body.classList.remove(IS_DRAGGING_CLASS_NAME);
}
};
}
@ -102,7 +108,7 @@ const mapStateToProps = (state: State) => {
export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent);
const disableScrolling = () => {
const onDragStart = (initial: DragStart) => {
const x =
window.pageXOffset !== undefined
? window.pageXOffset
@ -114,6 +120,10 @@ const disableScrolling = () => {
: (document.documentElement || document.body.parentNode || document.body).scrollTop;
window.onscroll = () => window.scrollTo(x, y);
if (!draggableIsField(initial)) {
document.body.classList.add(IS_DRAGGING_CLASS_NAME);
}
};
const enableScrolling = () => (window.onscroll = () => noop);

View file

@ -33,7 +33,7 @@ describe('DroppableWrapper', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders the children', () => {
test('it renders the children when a render prop is not provided', () => {
const message = 'draggable wrapper content';
const wrapper = mount(
@ -48,5 +48,40 @@ describe('DroppableWrapper', () => {
expect(wrapper.text()).toEqual(message);
});
test('it does NOT render the children if a render method is provided', () => {
const message = 'draggable wrapper content';
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mocksSource} addTypename={false}>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper render={() => null} droppableId="testing">
<div data-test-subj="this-should-not-render">{message}</div>
</DroppableWrapper>
</DragDropContextWrapper>
</MockedProvider>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="this-should-not-render"]').exists()).toBe(false);
});
test('it renders the render prop contents when a render prop is provided', () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mocksSource} addTypename={false}>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper
render={({ isDraggingOver }) => <div>{`isDraggingOver is: ${isDraggingOver}`}</div>}
droppableId="testing"
/>
</DragDropContextWrapper>
</MockedProvider>
</TestProviders>
);
expect(wrapper.text()).toEqual('isDraggingOver is: false');
});
});
});

View file

@ -9,11 +9,15 @@ import { Droppable } from 'react-beautiful-dnd';
import { pure } from 'recompose';
import styled from 'styled-components';
import { THIRTY_PERCENT_ALPHA_HEX_SUFFIX, TWENTY_PERCENT_ALPHA_HEX_SUFFIX } from './helpers';
interface Props {
children?: React.ReactNode;
droppableId: string;
height?: string;
isDropDisabled?: boolean;
type?: string;
render?: ({ isDraggingOver }: { isDraggingOver: boolean }) => React.ReactNode;
}
const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string }>`
@ -30,14 +34,22 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string
? `
.drop-and-provider-timeline {
&:hover {
background-color: ${props.theme.eui.euiColorEmptyShade};
background-color: ${props.theme.eui.euiColorSuccess}${THIRTY_PERCENT_ALPHA_HEX_SUFFIX};
}
}
.drop-and-provider-timeline:hover {
background-color: ${props.theme.eui.euiColorSuccess}${THIRTY_PERCENT_ALPHA_HEX_SUFFIX};
}
> div.timeline-drop-area-empty {
background-color: ${props.theme.eui.euiColorLightShade};
color: ${props.theme.eui.euiColorSuccess}
background-color: ${props.theme.eui.euiColorSuccess}${TWENTY_PERCENT_ALPHA_HEX_SUFFIX};
& .euiTextColor--subdued {
color: ${props.theme.eui.euiColorSuccess};
}
}
> div.timeline-drop-area {
background-color: ${props.theme.eui.euiColorLightShade};
background-color: ${props.theme.eui.euiColorSuccess}${TWENTY_PERCENT_ALPHA_HEX_SUFFIX};
.provider-item-filter-container div:first-child{
// Override dragNdrop beautiful so we do not have our droppable moving around for no good reason
transform: none !important;
@ -48,6 +60,10 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string
display: none;
}
}
& .euiFormHelpText {
color: ${props.theme.eui.euiColorSuccess};
}
}
.flyout-overlay {
.euiPanel {
@ -74,7 +90,14 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string
ReactDndDropTarget.displayName = 'ReactDndDropTarget';
export const DroppableWrapper = pure<Props>(
({ children, droppableId, height = '100%', isDropDisabled = false, type }) => (
({
children = null,
droppableId,
height = '100%',
isDropDisabled = false,
type,
render = null,
}) => (
<Droppable
isDropDisabled={isDropDisabled}
droppableId={droppableId}
@ -88,7 +111,7 @@ export const DroppableWrapper = pure<Props>(
{...provided.droppableProps}
isDraggingOver={snapshot.isDraggingOver}
>
{children}
{render == null ? children : render({ isDraggingOver: snapshot.isDraggingOver })}
{provided.placeholder}
</ReactDndDropTarget>
)}

View file

@ -52,10 +52,10 @@ export const getDroppableId = (visualizationPlaceholderId: string): string =>
export const sourceIsContent = (result: DropResult): boolean =>
result.source.droppableId.startsWith(droppableContentPrefix);
export const draggableIsContent = (result: DropResult): boolean =>
export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
result.draggableId.startsWith(draggableContentPrefix);
export const draggableIsField = (result: DropResult): boolean =>
export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
result.draggableId.startsWith(draggableFieldPrefix);
export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
@ -221,3 +221,15 @@ export const updateShowTimeline = ({
* header drop zone in the timeline
*/
export const DRAG_TYPE_FIELD = 'drag-type-field';
/** This class is added to the document body while dragging */
export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
/** A hex alpha channel suffix representing 10% for the `AA` in `#RRGGBBAA` */
export const TEN_PERCENT_ALPHA_HEX_SUFFIX = '1A';
/** A hex alpha channel suffix representing 20% for the `AA` in `#RRGGBBAA` */
export const TWENTY_PERCENT_ALPHA_HEX_SUFFIX = '33';
/** A hex alpha channel suffix representing 30% for the `AA` in `#RRGGBBAA` */
export const THIRTY_PERCENT_ALPHA_HEX_SUFFIX = '4d';

View file

@ -4,88 +4,88 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiBadgeProps, EuiPanel, EuiText } from '@elastic/eui';
import { EuiNotificationBadge, EuiIcon, EuiButton } from '@elastic/eui';
import * as React from 'react';
import styled from 'styled-components';
import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper';
import { droppableTimelineFlyoutButtonPrefix } from '../../drag_and_drop/helpers';
import {
droppableTimelineFlyoutButtonPrefix,
IS_DRAGGING_CLASS_NAME,
TEN_PERCENT_ALPHA_HEX_SUFFIX,
TWENTY_PERCENT_ALPHA_HEX_SUFFIX,
} from '../../drag_and_drop/helpers';
import { DataProvider } from '../../timeline/data_providers/data_provider';
import * as i18n from './translations';
export const NOT_READY_TO_DROP_CLASS_NAME = 'not-ready-to-drop';
export const READY_TO_DROP_CLASS_NAME = 'ready-to-drop';
const Container = styled.div`
overflow-x: auto;
overflow-y: hidden;
padding-top: 8px;
position: fixed;
top: 40%;
right: -3px;
min-width: 50px;
max-width: 80px;
right: -49px;
z-index: 9;
height: 240px;
max-height: 240px;
transform: rotate(-90deg);
user-select: none;
button {
border-bottom: none;
border-radius: 4px 4px 0 0;
box-shadow: none;
height: 46px;
margin: 1px 0 1px 1px;
width: 136px;
}
.euiButton:hover:not(:disabled) {
transform: none;
}
.euiButton--primary:enabled {
box-shadow: none;
}
.euiButton--primary:enabled:hover,
.euiButton--primary:enabled:focus {
animation: none;
background-color: inherit;
box-shadow: none;
}
.${IS_DRAGGING_CLASS_NAME} & .${NOT_READY_TO_DROP_CLASS_NAME} {
color: ${({ theme }) => theme.eui.euiColorSuccess};
background: ${({ theme }) =>
`${theme.eui.euiColorSuccess}${TEN_PERCENT_ALPHA_HEX_SUFFIX} !important`};
border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess};
border-bottom: none;
text-decoration: none;
}
.${READY_TO_DROP_CLASS_NAME} {
color: ${({ theme }) => theme.eui.euiColorSuccess};
background: ${({ theme }) =>
`${theme.eui.euiColorSuccess}${TWENTY_PERCENT_ALPHA_HEX_SUFFIX} !important`};
border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess};
border-bottom: none;
text-decoration: none;
}
`;
Container.displayName = 'Container';
const BadgeButtonContainer = styled.div`
align-items: center;
align-items: flex-start;
display: flex;
flex-direction: column;
flex-direction: row;
`;
BadgeButtonContainer.displayName = 'BadgeButtonContainer';
export const Button = styled(EuiPanel)`
display: flex;
z-index: 9;
justify-content: center;
text-align: center;
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
border-radius: 6px 0 0 6px;
box-shadow: ${({ theme }) =>
`0 3px 3px -1px ${theme.eui.euiColorLightestShade}, 0 5px 7px -2px ${theme.eui.euiColorLightestShade}`};
background-color: inherit;
cursor: pointer;
`;
Button.displayName = 'Button';
export const Text = styled(EuiText)`
width: 12px;
z-index: 10;
user-select: none;
`;
Text.displayName = 'Text';
// Ref: https://github.com/elastic/eui/issues/1655
// export const Badge = styled(EuiBadge)`
// border-radius: 5px;
// min-width: 25px;
// padding: 0px;
// transform: translateY(10px);
// z-index: 10;
// `;
export const Badge = (props: EuiBadgeProps) => (
<EuiBadge
{...props}
style={{
borderRadius: '5px',
minWidth: '25px',
padding: '0px',
textAlign: 'center',
transform: 'translateY(10px)',
zIndex: 10,
}}
/>
);
Badge.displayName = 'Badge';
interface FlyoutButtonProps {
dataProviders: DataProvider[];
onOpen: () => void;
@ -96,27 +96,53 @@ interface FlyoutButtonProps {
export const FlyoutButton = React.memo<FlyoutButtonProps>(
({ onOpen, show, dataProviders, timelineId }) =>
show ? (
<Container>
<DroppableWrapper droppableId={`${droppableTimelineFlyoutButtonPrefix}${timelineId}`}>
<BadgeButtonContainer
className="flyout-overlay"
data-test-subj="flyoutOverlay"
onClick={onOpen}
>
{dataProviders.length !== 0 && (
<Badge data-test-subj="badge" color="primary">
<Container onClick={onOpen}>
<DroppableWrapper
data-test-subj="flyout-droppable-wrapper"
droppableId={`${droppableTimelineFlyoutButtonPrefix}${timelineId}`}
render={({ isDraggingOver }) => (
<BadgeButtonContainer
className="flyout-overlay"
data-test-subj="flyoutOverlay"
onClick={onOpen}
>
{!isDraggingOver ? (
<EuiButton
className={NOT_READY_TO_DROP_CLASS_NAME}
data-test-subj="flyout-button-not-ready-to-drop"
fill={false}
iconSide="right"
iconType="arrowUp"
>
{i18n.FLYOUT_BUTTON}
</EuiButton>
) : (
<EuiButton
className={READY_TO_DROP_CLASS_NAME}
data-test-subj="flyout-button-ready-to-drop"
fill={false}
>
<EuiIcon data-test-subj="flyout-button-plus-icon" type="plusInCircleFilled" />
</EuiButton>
)}
<EuiNotificationBadge
color="accent"
data-test-subj="badge"
style={{
left: '-9px',
position: 'relative',
top: '-6px',
transform: 'rotate(90deg)',
visibility: dataProviders.length !== 0 ? 'inherit' : 'hidden',
zIndex: 10,
}}
>
{dataProviders.length}
</Badge>
)}
<Button>
<Text data-test-subj="flyoutButton" size="s">
{i18n.TIMELINE.toLocaleUpperCase()
.split('')
.join(' ')}
</Text>
</Button>
</BadgeButtonContainer>
</DroppableWrapper>
</EuiNotificationBadge>
</BadgeButtonContainer>
)}
/>
</Container>
) : null,
(prevProps, nextProps) =>

View file

@ -9,3 +9,7 @@ import { i18n } from '@kbn/i18n';
export const TIMELINE = i18n.translate('xpack.siem.flyout.button.timeline', {
defaultMessage: 'timeline',
});
export const FLYOUT_BUTTON = i18n.translate('xpack.siem.flyout.button.text', {
defaultMessage: 'Timeline',
});

View file

@ -52,10 +52,10 @@ describe('Flyout', () => {
expect(
wrapper
.find('[data-test-subj="flyoutButton"]')
.find('[data-test-subj="flyout-button-not-ready-to-drop"]')
.first()
.text()
).toContain('T I M E L I N E');
).toContain('Timeline');
});
test('it renders the title field when its state is set to flyout is true', () => {
@ -96,7 +96,9 @@ describe('Flyout', () => {
</TestProviders>
);
expect(wrapper.find('[data-test-subj="flyoutButton"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual(
false
);
});
test('it renders the flyout body', () => {
@ -173,7 +175,7 @@ describe('Flyout', () => {
).toContain('10');
});
test('it does NOT render the data providers badge when the number is equal to 0', () => {
test('it hides the data providers badge when the timeline does NOT have data providers', () => {
const wrapper = mount(
<TestProviders>
<Flyout
@ -185,7 +187,39 @@ describe('Flyout', () => {
</TestProviders>
);
expect(wrapper.find('[data-test-subj="badge"]').exists()).toEqual(false);
expect(
wrapper
.find('[data-test-subj="badge"]')
.first()
.props().style!.visibility
).toEqual('hidden');
});
test('it does NOT hide the data providers badge when the timeline has data providers', () => {
const stateWithDataProviders = set(
'timeline.timelineById.test.dataProviders',
mockDataProviders,
state
);
const storeWithDataProviders = createStore(stateWithDataProviders, apolloClientObservable);
const wrapper = mount(
<TestProviders store={storeWithDataProviders}>
<Flyout
flyoutHeight={testFlyoutHeight}
headerHeight={flyoutHeaderHeight}
timelineId="test"
usersViewing={usersViewing}
/>
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="badge"]')
.first()
.props().style!.visibility
).toEqual('inherit');
});
test('should call the onOpen when the mouse is clicked for rendering', () => {
@ -255,7 +289,9 @@ describe('Flyout', () => {
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="flyoutButton"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual(
true
);
});
test('should NOT show the flyout button when show is false', () => {
@ -270,7 +306,9 @@ describe('Flyout', () => {
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="flyoutButton"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual(
false
);
});
test('should return the flyout button with text', () => {
@ -287,10 +325,10 @@ describe('Flyout', () => {
);
expect(
wrapper
.find('[data-test-subj="flyoutButton"]')
.find('[data-test-subj="flyout-button-not-ready-to-drop"]')
.first()
.text()
).toContain('T I M E L I N E');
).toContain('Timeline');
});
test('should call the onOpen when it is clicked', () => {

View file

@ -10,7 +10,11 @@ import styled from 'styled-components';
import { BrowserFields } from '../../../containers/source';
import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper';
import { droppableTimelineProvidersPrefix } from '../../drag_and_drop/helpers';
import {
droppableTimelineProvidersPrefix,
IS_DRAGGING_CLASS_NAME,
TEN_PERCENT_ALPHA_HEX_SUFFIX,
} from '../../drag_and_drop/helpers';
import {
OnChangeDataProviderKqlQuery,
OnChangeDroppableAndProvider,
@ -38,6 +42,21 @@ interface Props {
show: boolean;
}
const DropTargetDataProvidersContainer = styled.div`
.${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers {
background: ${({ theme }) => `${theme.eui.euiColorSuccess}${TEN_PERCENT_ALPHA_HEX_SUFFIX}`};
border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorSuccess};
& .euiTextColor--subdued {
color: ${({ theme }) => theme.eui.euiColorSuccess};
}
& .euiFormHelpText {
color: ${({ theme }) => theme.eui.euiColorSuccess};
}
}
`;
const DropTargetDataProviders = styled.div`
position: relative;
border: 0.2rem dashed ${props => props.theme.eui.euiColorMediumShade};
@ -85,29 +104,34 @@ export const DataProviders = pure<Props>(
onToggleDataProviderExcluded,
show,
}) => (
<DropTargetDataProviders data-test-subj="dataProviders">
<TimelineContext.Consumer>
{isLoading => (
<DroppableWrapper isDropDisabled={!show || isLoading} droppableId={getDroppableId(id)}>
{dataProviders != null && dataProviders.length ? (
<Providers
browserFields={browserFields}
id={id}
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onDataProviderEdited={onDataProviderEdited}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
) : (
<Empty />
)}
</DroppableWrapper>
)}
</TimelineContext.Consumer>
</DropTargetDataProviders>
<DropTargetDataProvidersContainer className="drop-target-data-providers-container">
<DropTargetDataProviders
className="drop-target-data-providers"
data-test-subj="dataProviders"
>
<TimelineContext.Consumer>
{isLoading => (
<DroppableWrapper isDropDisabled={!show || isLoading} droppableId={getDroppableId(id)}>
{dataProviders != null && dataProviders.length ? (
<Providers
browserFields={browserFields}
id={id}
dataProviders={dataProviders}
onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery}
onChangeDroppableAndProvider={onChangeDroppableAndProvider}
onDataProviderEdited={onDataProviderEdited}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
/>
) : (
<Empty />
)}
</DroppableWrapper>
)}
</TimelineContext.Consumer>
</DropTargetDataProviders>
</DropTargetDataProvidersContainer>
)
);

View file

@ -19,11 +19,13 @@ import {
OnToggleDataProviderExcluded,
} from '../events';
import { BrowserFields } from '../../../containers/source';
import { TWENTY_PERCENT_ALPHA_HEX_SUFFIX } from '../../drag_and_drop/helpers';
import { DataProvider } from './data_provider';
import { ProviderItemAnd } from './provider_item_and';
import * as i18n from './translations';
import { BrowserFields } from '../../../containers/source';
const DropAndTargetDataProvidersContainer = styled(EuiFlexItem)`
margin: 0px 8px;
@ -34,7 +36,7 @@ DropAndTargetDataProvidersContainer.displayName = 'DropAndTargetDataProvidersCon
const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>`
min-width: 230px;
width: auto;
border: 0.1rem dashed ${props => props.theme.eui.euiColorMediumShade};
border: 0.1rem dashed ${props => props.theme.eui.euiColorSuccess};
border-radius: 5px;
text-align: center;
padding: 3px 10px;
@ -45,7 +47,7 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>`
props.hasAndItem
? `&:hover {
transition: background-color 0.7s ease;
background-color: ${props.theme.eui.euiColorEmptyShade};
background-color: ${() => props.theme.eui.euiColorSuccess}${TWENTY_PERCENT_ALPHA_HEX_SUFFIX};
}`
: ''};
cursor: ${({ hasAndItem }) => (!hasAndItem ? `default` : 'inherit')};