[SIEM] Adds custom tooltip to map for dragging fields to timeline (#46879)
## Summary Resolves https://github.com/elastic/kibana/issues/46301, by adding a custom tooltip for the map that enables dragging to the timeline. ##### Features: * Adds new portal pattern to enable DnD from outside the main react component tree * Adds `<DraggablePortalContext>` component to enable DnD from within an `EuiPopover` * Just wrap `EuiPopover` contents in `<DraggablePortalContext.Provider value={true}></...>` and all child `DefaultDraggable`'s will now function correctly * Displays netflow renderer within tooltip for line features, w/ draggable src/dest.bytes * Displays detailed description list within tooltip for point features. Fields include: * host.name * source/destination.ip * source/destination.domain * source/destination.geo.country_iso_code * source/destination.as.organization.name * Retains ability to add filter to KQL bar ![map_custom_tooltips](https://user-images.githubusercontent.com/2946766/66288691-64c74f00-e897-11e9-9845-54e8c9b9c5ab.gif) ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
1947608378
commit
0461a1fefe
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import {
|
||||
Draggable,
|
||||
DraggableProvided,
|
||||
|
@ -16,6 +16,7 @@ import { connect } from 'react-redux';
|
|||
import styled, { css } from 'styled-components';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
|
||||
import { EuiPortal } from '@elastic/eui';
|
||||
import { dragAndDropActions } from '../../store/drag_and_drop';
|
||||
import { DataProvider } from '../timeline/data_providers/data_provider';
|
||||
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../timeline/helpers';
|
||||
|
@ -27,6 +28,9 @@ export const DragEffects = styled.div``;
|
|||
|
||||
DragEffects.displayName = 'DragEffects';
|
||||
|
||||
export const DraggablePortalContext = createContext<boolean>(false);
|
||||
export const useDraggablePortalContext = () => useContext(DraggablePortalContext);
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
|
@ -127,7 +131,7 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>`
|
|||
${isDragging &&
|
||||
`
|
||||
& {
|
||||
z-index: ${theme.eui.euiZLevel9} !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
`}
|
||||
`}
|
||||
|
@ -164,6 +168,8 @@ type Props = OwnProps & DispatchProps;
|
|||
|
||||
const DraggableWrapperComponent = React.memo<Props>(
|
||||
({ dataProvider, registerProvider, render, truncate, unRegisterProvider }) => {
|
||||
const usePortal = useDraggablePortalContext();
|
||||
|
||||
useEffect(() => {
|
||||
registerProvider!({ provider: dataProvider });
|
||||
return () => {
|
||||
|
@ -182,26 +188,28 @@ const DraggableWrapperComponent = React.memo<Props>(
|
|||
key={getDraggableId(dataProvider.id)}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<ProviderContainer
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
innerRef={provided.innerRef}
|
||||
data-test-subj="providerContainer"
|
||||
isDragging={snapshot.isDragging}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
}}
|
||||
>
|
||||
{truncate && !snapshot.isDragging ? (
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">
|
||||
{render(dataProvider, provided, snapshot)}
|
||||
</TruncatableText>
|
||||
) : (
|
||||
<span data-test-subj={`draggable-content-${dataProvider.queryMatch.field}`}>
|
||||
{render(dataProvider, provided, snapshot)}
|
||||
</span>
|
||||
)}
|
||||
</ProviderContainer>
|
||||
<ConditionalPortal usePortal={snapshot.isDragging && usePortal}>
|
||||
<ProviderContainer
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
innerRef={provided.innerRef}
|
||||
data-test-subj="providerContainer"
|
||||
isDragging={snapshot.isDragging}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
}}
|
||||
>
|
||||
{truncate && !snapshot.isDragging ? (
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">
|
||||
{render(dataProvider, provided, snapshot)}
|
||||
</TruncatableText>
|
||||
) : (
|
||||
<span data-test-subj={`draggable-content-${dataProvider.queryMatch.field}`}>
|
||||
{render(dataProvider, provided, snapshot)}
|
||||
</span>
|
||||
)}
|
||||
</ProviderContainer>
|
||||
</ConditionalPortal>
|
||||
)}
|
||||
</Draggable>
|
||||
{droppableProvided.placeholder}
|
||||
|
@ -229,3 +237,15 @@ export const DraggableWrapper = connect(
|
|||
unRegisterProvider: dragAndDropActions.unRegisterProvider,
|
||||
}
|
||||
)(DraggableWrapperComponent);
|
||||
|
||||
/**
|
||||
* Conditionally wraps children in an EuiPortal to ensure drag offsets are correct when dragging
|
||||
* from containers that have css transforms
|
||||
*
|
||||
* See: https://github.com/atlassian/react-beautiful-dnd/issues/499
|
||||
*/
|
||||
const ConditionalPortal = React.memo<{ children: React.ReactNode; usePortal: boolean }>(
|
||||
({ children, usePortal }) => (usePortal ? <EuiPortal>{children}</EuiPortal> : <>{children}</>)
|
||||
);
|
||||
|
||||
ConditionalPortal.displayName = 'ConditionalPortal';
|
||||
|
|
|
@ -16,7 +16,13 @@ export const mockSourceLayer = {
|
|||
type: 'ES_SEARCH',
|
||||
geoField: 'source.geo.location',
|
||||
filterByMapBounds: false,
|
||||
tooltipProperties: ['host.name', 'source.ip', 'source.domain', 'source.as.organization.name'],
|
||||
tooltipProperties: [
|
||||
'host.name',
|
||||
'source.ip',
|
||||
'source.domain',
|
||||
'source.geo.country_iso_code',
|
||||
'source.as.organization.name',
|
||||
],
|
||||
useTopHits: false,
|
||||
topHitsTimeField: '@timestamp',
|
||||
topHitsSize: 1,
|
||||
|
@ -55,6 +61,7 @@ export const mockDestinationLayer = {
|
|||
'host.name',
|
||||
'destination.ip',
|
||||
'destination.domain',
|
||||
'destination.geo.country_iso_code',
|
||||
'destination.as.organization.name',
|
||||
],
|
||||
useTopHits: false,
|
||||
|
@ -92,9 +99,8 @@ export const mockLineLayer = {
|
|||
sourceGeoField: 'source.geo.location',
|
||||
destGeoField: 'destination.geo.location',
|
||||
metrics: [
|
||||
{ type: 'sum', field: 'source.bytes', label: 'Total Src Bytes' },
|
||||
{ type: 'sum', field: 'destination.bytes', label: 'Total Dest Bytes' },
|
||||
{ type: 'count', label: 'Total Documents' },
|
||||
{ type: 'sum', field: 'source.bytes', label: 'source.bytes' },
|
||||
{ type: 'sum', field: 'destination.bytes', label: 'destination.bytes' },
|
||||
],
|
||||
},
|
||||
style: {
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
exports[`EmbeddedMap renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<InPortal
|
||||
node={<div />}
|
||||
>
|
||||
<MapToolTip />
|
||||
</InPortal>
|
||||
<Styled(EuiFlexGroup)>
|
||||
<Loader
|
||||
data-test-subj="loading-panel"
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
|
||||
import { createPortalNode, InPortal } from 'react-reverse-portal';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
|
||||
|
@ -23,6 +24,7 @@ import { MapEmbeddable, SetQuery } from './types';
|
|||
import * as i18n from './translations';
|
||||
import { useStateToaster } from '../toasters';
|
||||
import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers';
|
||||
import { MapToolTip } from './map_tool_tip/map_tool_tip';
|
||||
|
||||
const EmbeddableWrapper = styled(EuiFlexGroup)`
|
||||
position: relative;
|
||||
|
@ -53,6 +55,12 @@ export const EmbeddedMap = React.memo<EmbeddedMapProps>(
|
|||
const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns();
|
||||
const [siemDefaultIndices] = useKibanaUiSetting(DEFAULT_INDEX_KEY);
|
||||
|
||||
// This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our
|
||||
// own component tree instead of the embeddables (default). This is necessary to have access to
|
||||
// the Redux store, theme provider, etc, which is required to register and un-register the draggable
|
||||
// Search InPortal/OutPortal for implementation touch points
|
||||
const portalNode = React.useMemo(() => createPortalNode(), []);
|
||||
|
||||
// Initial Load useEffect
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
@ -84,7 +92,8 @@ export const EmbeddedMap = React.memo<EmbeddedMapProps>(
|
|||
queryExpression,
|
||||
startDate,
|
||||
endDate,
|
||||
setQuery
|
||||
setQuery,
|
||||
portalNode
|
||||
);
|
||||
if (isSubscribed) {
|
||||
setEmbeddable(embeddableObject);
|
||||
|
@ -129,6 +138,9 @@ export const EmbeddedMap = React.memo<EmbeddedMapProps>(
|
|||
|
||||
return isError ? null : (
|
||||
<>
|
||||
<InPortal node={portalNode}>
|
||||
<MapToolTip />
|
||||
</InPortal>
|
||||
<EmbeddableWrapper>
|
||||
{embeddable != null ? (
|
||||
<EmbeddablePanel
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { createPortalNode } from 'react-reverse-portal';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../lib/settings/use_kibana_ui_setting');
|
||||
|
@ -60,13 +61,13 @@ describe('embedded_map_helpers', () => {
|
|||
describe('createEmbeddable', () => {
|
||||
test('attaches refresh action', async () => {
|
||||
const setQueryMock = jest.fn();
|
||||
await createEmbeddable([], '', 0, 0, setQueryMock);
|
||||
await createEmbeddable([], '', 0, 0, setQueryMock, createPortalNode());
|
||||
expect(setQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('attaches refresh action with correct reference', async () => {
|
||||
const setQueryMock = jest.fn(({ id, inspect, loading, refetch }) => refetch);
|
||||
const embeddable = await createEmbeddable([], '', 0, 0, setQueryMock);
|
||||
const embeddable = await createEmbeddable([], '', 0, 0, setQueryMock, createPortalNode());
|
||||
expect(setQueryMock.mock.calls[0][0].refetch).not.toBe(embeddable.reload);
|
||||
setQueryMock.mock.results[0].value();
|
||||
expect(embeddable.reload).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import React from 'react';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { OutPortal, PortalNode } from 'react-reverse-portal';
|
||||
import { ActionToaster, AppToast } from '../toasters';
|
||||
import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
|
||||
import {
|
||||
|
@ -19,7 +21,7 @@ import {
|
|||
APPLY_SIEM_FILTER_ACTION_ID,
|
||||
ApplySiemFilterAction,
|
||||
} from './actions/apply_siem_filter_action';
|
||||
import { IndexPatternMapping, MapEmbeddable, SetQuery } from './types';
|
||||
import { IndexPatternMapping, MapEmbeddable, RenderTooltipContentParams, SetQuery } from './types';
|
||||
import { getLayerList } from './map_config';
|
||||
// @ts-ignore Missing type defs as maps moves to Typescript
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants';
|
||||
|
@ -88,6 +90,7 @@ export const setupEmbeddablesAPI = (
|
|||
* @param startDate
|
||||
* @param endDate
|
||||
* @param setQuery function as provided by the GlobalTime component for reacting to refresh
|
||||
* @param portalNode wrapper for MapToolTip so it is not rendered in the embeddables component tree
|
||||
*
|
||||
* @throws Error if EmbeddableFactory does not exist
|
||||
*/
|
||||
|
@ -96,7 +99,8 @@ export const createEmbeddable = async (
|
|||
queryExpression: string,
|
||||
startDate: number,
|
||||
endDate: number,
|
||||
setQuery: SetQuery
|
||||
setQuery: SetQuery,
|
||||
portalNode: PortalNode
|
||||
): Promise<MapEmbeddable> => {
|
||||
const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
|
||||
|
||||
|
@ -121,8 +125,34 @@ export const createEmbeddable = async (
|
|||
mapCenter: { lon: -1.05469, lat: 15.96133, zoom: 1 },
|
||||
};
|
||||
|
||||
const renderTooltipContent = ({
|
||||
addFilters,
|
||||
closeTooltip,
|
||||
features,
|
||||
isLocked,
|
||||
getLayerName,
|
||||
loadFeatureProperties,
|
||||
loadFeatureGeometry,
|
||||
}: RenderTooltipContentParams) => {
|
||||
const props = {
|
||||
addFilters,
|
||||
closeTooltip,
|
||||
features,
|
||||
isLocked,
|
||||
getLayerName,
|
||||
loadFeatureProperties,
|
||||
loadFeatureGeometry,
|
||||
};
|
||||
return <OutPortal node={portalNode} {...props} />;
|
||||
};
|
||||
|
||||
// @ts-ignore method added in https://github.com/elastic/kibana/pull/43878
|
||||
const embeddableObject = await factory.createFromState(state, input);
|
||||
const embeddableObject = await factory.createFromState(
|
||||
state,
|
||||
input,
|
||||
undefined,
|
||||
renderTooltipContent
|
||||
);
|
||||
|
||||
// Wire up to app refresh action
|
||||
setQuery({
|
||||
|
|
|
@ -6,6 +6,33 @@
|
|||
|
||||
import uuid from 'uuid';
|
||||
import { IndexPatternMapping } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
// Update source/destination field mappings to modify what fields will be returned to map tooltip
|
||||
const sourceFieldMappings: Record<string, string> = {
|
||||
'host.name': i18n.HOST,
|
||||
'source.ip': i18n.SOURCE_IP,
|
||||
'source.domain': i18n.SOURCE_DOMAIN,
|
||||
'source.geo.country_iso_code': i18n.LOCATION,
|
||||
'source.as.organization.name': i18n.ASN,
|
||||
};
|
||||
const destinationFieldMappings: Record<string, string> = {
|
||||
'host.name': i18n.HOST,
|
||||
'destination.ip': i18n.DESTINATION_IP,
|
||||
'destination.domain': i18n.DESTINATION_DOMAIN,
|
||||
'destination.geo.country_iso_code': i18n.LOCATION,
|
||||
'destination.as.organization.name': i18n.ASN,
|
||||
};
|
||||
|
||||
// Mapping of field -> display name for use within map tooltip
|
||||
export const sourceDestinationFieldMappings: Record<string, string> = {
|
||||
...sourceFieldMappings,
|
||||
...destinationFieldMappings,
|
||||
};
|
||||
|
||||
// Field names of LineLayer props returned from Maps API
|
||||
export const SUM_OF_SOURCE_BYTES = 'sum_of_source.bytes';
|
||||
export const SUM_OF_DESTINATION_BYTES = 'sum_of_destination.bytes';
|
||||
|
||||
/**
|
||||
* Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source,
|
||||
|
@ -51,7 +78,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string
|
|||
type: 'ES_SEARCH',
|
||||
geoField: 'source.geo.location',
|
||||
filterByMapBounds: false,
|
||||
tooltipProperties: ['host.name', 'source.ip', 'source.domain', 'source.as.organization.name'],
|
||||
tooltipProperties: Object.keys(sourceFieldMappings),
|
||||
useTopHits: false,
|
||||
topHitsTimeField: '@timestamp',
|
||||
topHitsSize: 1,
|
||||
|
@ -69,7 +96,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string
|
|||
},
|
||||
},
|
||||
id: uuid.v4(),
|
||||
label: `${indexPatternTitle} | Source Point`,
|
||||
label: `${indexPatternTitle} | ${i18n.SOURCE_LAYER}`,
|
||||
minZoom: 0,
|
||||
maxZoom: 24,
|
||||
alpha: 0.75,
|
||||
|
@ -93,12 +120,7 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s
|
|||
type: 'ES_SEARCH',
|
||||
geoField: 'destination.geo.location',
|
||||
filterByMapBounds: true,
|
||||
tooltipProperties: [
|
||||
'host.name',
|
||||
'destination.ip',
|
||||
'destination.domain',
|
||||
'destination.as.organization.name',
|
||||
],
|
||||
tooltipProperties: Object.keys(destinationFieldMappings),
|
||||
useTopHits: false,
|
||||
topHitsTimeField: '@timestamp',
|
||||
topHitsSize: 1,
|
||||
|
@ -116,7 +138,7 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s
|
|||
},
|
||||
},
|
||||
id: uuid.v4(),
|
||||
label: `${indexPatternTitle} | Destination Point`,
|
||||
label: `${indexPatternTitle} | ${i18n.DESTINATION_LAYER}`,
|
||||
minZoom: 0,
|
||||
maxZoom: 24,
|
||||
alpha: 0.75,
|
||||
|
@ -141,9 +163,8 @@ export const getLineLayer = (indexPatternTitle: string, indexPatternId: string)
|
|||
sourceGeoField: 'source.geo.location',
|
||||
destGeoField: 'destination.geo.location',
|
||||
metrics: [
|
||||
{ type: 'sum', field: 'source.bytes', label: 'Total Src Bytes' },
|
||||
{ type: 'sum', field: 'destination.bytes', label: 'Total Dest Bytes' },
|
||||
{ type: 'count', label: 'Total Documents' },
|
||||
{ type: 'sum', field: 'source.bytes', label: 'source.bytes' },
|
||||
{ type: 'sum', field: 'destination.bytes', label: 'destination.bytes' },
|
||||
],
|
||||
},
|
||||
style: {
|
||||
|
@ -172,7 +193,7 @@ export const getLineLayer = (indexPatternTitle: string, indexPatternId: string)
|
|||
},
|
||||
},
|
||||
id: uuid.v4(),
|
||||
label: `${indexPatternTitle} | Line`,
|
||||
label: `${indexPatternTitle} | ${i18n.LINE_LAYER}`,
|
||||
minZoom: 0,
|
||||
maxZoom: 24,
|
||||
alpha: 1,
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LineToolTipContent renders correctly against snapshot 1`] = `
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<Styled(EuiBadge)
|
||||
color="hollow"
|
||||
>
|
||||
<Styled(EuiFlexGroup)
|
||||
direction="column"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
Source
|
||||
</EuiFlexItem>
|
||||
</Styled(EuiFlexGroup)>
|
||||
</Styled(EuiBadge)>
|
||||
</EuiFlexItem>
|
||||
<SourceDestinationArrows
|
||||
contextId="contextId"
|
||||
destinationBytes={
|
||||
Array [
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
eventId="map-line-tooltip-contextId"
|
||||
sourceBytes={
|
||||
Array [
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<EuiFlexItem>
|
||||
<Styled(EuiBadge)
|
||||
color="hollow"
|
||||
>
|
||||
<Styled(EuiFlexGroup)>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
Destination
|
||||
</EuiFlexItem>
|
||||
</Styled(EuiFlexGroup)>
|
||||
</Styled(EuiBadge)>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MapToolTip full component renders correctly against snapshot 1`] = `
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLoadingSpinner
|
||||
size="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`MapToolTip placeholder component renders correctly against snapshot 1`] = `
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLoadingSpinner
|
||||
size="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -0,0 +1,25 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PointToolTipContent renders correctly against snapshot 1`] = `
|
||||
<Component>
|
||||
<PointToolTipContent
|
||||
addFilters={[MockFunction]}
|
||||
closeTooltip={[MockFunction]}
|
||||
contextId="contextId"
|
||||
featureProps={
|
||||
Array [
|
||||
Object {
|
||||
"_propertyKey": "host.name",
|
||||
"_rawValue": "testPropValue",
|
||||
"getESFilters": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
featurePropsFilters={
|
||||
Object {
|
||||
"host.name": Object {},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ToolTipFilter renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<EuiHorizontalRule
|
||||
margin="s"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiText
|
||||
size="xs"
|
||||
>
|
||||
1 of 100 features
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<span>
|
||||
<EuiButtonIcon
|
||||
aria-label="Next"
|
||||
color="text"
|
||||
data-test-subj="previous-feature-button"
|
||||
disabled={true}
|
||||
iconType="arrowLeft"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Next"
|
||||
color="text"
|
||||
data-test-subj="next-feature-button"
|
||||
disabled={false}
|
||||
iconType="arrowRight"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { LineToolTipContent } from './line_tool_tip_content';
|
||||
import { FeatureProperty } from '../types';
|
||||
|
||||
describe('LineToolTipContent', () => {
|
||||
const mockFeatureProps: FeatureProperty[] = [
|
||||
{
|
||||
_propertyKey: 'host.name',
|
||||
_rawValue: 'testPropValue',
|
||||
getESFilters: () => new Promise(resolve => setTimeout(resolve)),
|
||||
},
|
||||
];
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<LineToolTipContent contextId={'contextId'} featureProps={mockFeatureProps} />
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { SourceDestinationArrows } from '../../source_destination/source_destination_arrows';
|
||||
import { SUM_OF_DESTINATION_BYTES, SUM_OF_SOURCE_BYTES } from '../map_config';
|
||||
import { FeatureProperty } from '../types';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const FlowBadge = styled(EuiBadge)`
|
||||
height: 45px;
|
||||
min-width: 85px;
|
||||
`;
|
||||
|
||||
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
interface LineToolTipContentProps {
|
||||
contextId: string;
|
||||
featureProps: FeatureProperty[];
|
||||
}
|
||||
|
||||
export const LineToolTipContent = React.memo<LineToolTipContentProps>(
|
||||
({ contextId, featureProps }) => {
|
||||
const lineProps = featureProps.reduce<Record<string, string>>(
|
||||
(acc, f) => ({ ...acc, ...{ [f._propertyKey]: f._rawValue } }),
|
||||
{}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="center" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<FlowBadge color="hollow">
|
||||
<EuiFlexGroupStyled direction="column">
|
||||
<EuiFlexItem grow={false}>{i18n.SOURCE}</EuiFlexItem>
|
||||
</EuiFlexGroupStyled>
|
||||
</FlowBadge>
|
||||
</EuiFlexItem>
|
||||
<SourceDestinationArrows
|
||||
contextId={contextId}
|
||||
destinationBytes={[lineProps[SUM_OF_DESTINATION_BYTES]]}
|
||||
eventId={`map-line-tooltip-${contextId}`}
|
||||
sourceBytes={[lineProps[SUM_OF_SOURCE_BYTES]]}
|
||||
/>
|
||||
<EuiFlexItem>
|
||||
<FlowBadge color="hollow">
|
||||
<EuiFlexGroupStyled>
|
||||
<EuiFlexItem grow={false}>{i18n.DESTINATION}</EuiFlexItem>
|
||||
</EuiFlexGroupStyled>
|
||||
</FlowBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LineToolTipContent.displayName = 'LineToolTipContent';
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { MapToolTip } from './map_tool_tip';
|
||||
import { MapFeature } from '../types';
|
||||
|
||||
describe('MapToolTip', () => {
|
||||
test('placeholder component renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(<MapToolTip />);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('full component renders correctly against snapshot', () => {
|
||||
const addFilters = jest.fn();
|
||||
const closeTooltip = jest.fn();
|
||||
const features: MapFeature[] = [
|
||||
{
|
||||
id: 1,
|
||||
layerId: 'layerId',
|
||||
},
|
||||
];
|
||||
const getLayerName = jest.fn();
|
||||
const loadFeatureProperties = jest.fn();
|
||||
const loadFeatureGeometry = jest.fn();
|
||||
|
||||
const wrapper = shallow(
|
||||
<MapToolTip
|
||||
addFilters={addFilters}
|
||||
closeTooltip={closeTooltip}
|
||||
features={features}
|
||||
isLocked={false}
|
||||
getLayerName={getLayerName}
|
||||
loadFeatureProperties={loadFeatureProperties}
|
||||
loadFeatureGeometry={loadFeatureGeometry}
|
||||
/>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiOutsideClickDetector,
|
||||
} from '@elastic/eui';
|
||||
import { FeatureGeometry, FeatureProperty, MapToolTipProps } from '../types';
|
||||
import { DraggablePortalContext } from '../../drag_and_drop/draggable_wrapper';
|
||||
import { ToolTipFooter } from './tooltip_footer';
|
||||
import { LineToolTipContent } from './line_tool_tip_content';
|
||||
import { PointToolTipContent } from './point_tool_tip_content';
|
||||
import { Loader } from '../../loader';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export const MapToolTip = React.memo<MapToolTipProps>(
|
||||
({
|
||||
addFilters,
|
||||
closeTooltip,
|
||||
features = [],
|
||||
isLocked,
|
||||
getLayerName,
|
||||
loadFeatureProperties,
|
||||
loadFeatureGeometry,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isLoadingNextFeature, setIsLoadingNextFeature] = useState<boolean>(false);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const [featureIndex, setFeatureIndex] = useState<number>(0);
|
||||
const [featureProps, setFeatureProps] = useState<FeatureProperty[]>([]);
|
||||
const [featurePropsFilters, setFeaturePropsFilters] = useState<Record<string, object>>({});
|
||||
const [featureGeometry, setFeatureGeometry] = useState<FeatureGeometry | null>(null);
|
||||
const [, setLayerName] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
// Early return if component doesn't yet have props -- result of mounting in portal before actual rendering
|
||||
if (
|
||||
features.length === 0 ||
|
||||
getLayerName == null ||
|
||||
loadFeatureProperties == null ||
|
||||
loadFeatureGeometry == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate loaders for initial load vs loading next feature to keep tooltip from drastically resizing
|
||||
if (!isLoadingNextFeature) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
setIsError(false);
|
||||
|
||||
const fetchFeatureProps = async () => {
|
||||
if (features[featureIndex] != null) {
|
||||
const layerId = features[featureIndex].layerId;
|
||||
const featureId = features[featureIndex].id;
|
||||
|
||||
try {
|
||||
const featureGeo = loadFeatureGeometry({ layerId, featureId });
|
||||
const [featureProperties, layerNameString] = await Promise.all([
|
||||
loadFeatureProperties({ layerId, featureId }),
|
||||
getLayerName(layerId),
|
||||
]);
|
||||
|
||||
// Fetch ES filters in advance while loader is present to prevent lag when user clicks to add filter
|
||||
const featurePropsPromises = await Promise.all(
|
||||
featureProperties.map(property => property.getESFilters())
|
||||
);
|
||||
const featurePropsESFilters = featureProperties.reduce(
|
||||
(acc, property, index) => ({
|
||||
...acc,
|
||||
[property._propertyKey]: featurePropsPromises[index],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
setFeatureProps(featureProperties);
|
||||
setFeaturePropsFilters(featurePropsESFilters);
|
||||
setFeatureGeometry(featureGeo);
|
||||
setLayerName(layerNameString);
|
||||
} catch (e) {
|
||||
setIsError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadingNextFeature(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchFeatureProps();
|
||||
}, [
|
||||
featureIndex,
|
||||
features
|
||||
.map(f => `${f.id}-${f.layerId}`)
|
||||
.sort()
|
||||
.join(),
|
||||
]);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>{i18n.MAP_TOOL_TIP_ERROR}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return isLoading && !isLoadingNextFeature ? (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<DraggablePortalContext.Provider value={true}>
|
||||
<EuiOutsideClickDetector
|
||||
onOutsideClick={() => {
|
||||
if (closeTooltip != null) {
|
||||
closeTooltip();
|
||||
setFeatureIndex(0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{featureGeometry != null && featureGeometry.type === 'LineString' ? (
|
||||
<LineToolTipContent
|
||||
contextId={`${features[featureIndex].layerId}-${features[featureIndex].id}-${featureIndex}`}
|
||||
featureProps={featureProps}
|
||||
/>
|
||||
) : (
|
||||
<PointToolTipContent
|
||||
contextId={`${features[featureIndex].layerId}-${features[featureIndex].id}-${featureIndex}`}
|
||||
featureProps={featureProps}
|
||||
featurePropsFilters={featurePropsFilters}
|
||||
addFilters={addFilters}
|
||||
closeTooltip={closeTooltip}
|
||||
/>
|
||||
)}
|
||||
{features.length > 1 && (
|
||||
<ToolTipFooter
|
||||
featureIndex={featureIndex}
|
||||
totalFeatures={features.length}
|
||||
previousFeature={() => {
|
||||
setFeatureIndex(featureIndex - 1);
|
||||
setIsLoadingNextFeature(true);
|
||||
}}
|
||||
nextFeature={() => {
|
||||
setFeatureIndex(featureIndex + 1);
|
||||
setIsLoadingNextFeature(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isLoadingNextFeature && <Loader data-test-subj="loading-panel" overlay size="m" />}
|
||||
</div>
|
||||
</EuiOutsideClickDetector>
|
||||
</DraggablePortalContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
MapToolTip.displayName = 'MapToolTip';
|
|
@ -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 { mount, shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { FeatureProperty } from '../types';
|
||||
import { getRenderedFieldValue, PointToolTipContent } from './point_tool_tip_content';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { getEmptyStringTag } from '../../empty_value';
|
||||
import { HostDetailsLink, IPDetailsLink } from '../../links';
|
||||
|
||||
describe('PointToolTipContent', () => {
|
||||
const mockFeatureProps: FeatureProperty[] = [
|
||||
{
|
||||
_propertyKey: 'host.name',
|
||||
_rawValue: 'testPropValue',
|
||||
getESFilters: () => new Promise(resolve => setTimeout(resolve)),
|
||||
},
|
||||
];
|
||||
const mockFeaturePropsFilters: Record<string, object> = { 'host.name': {} };
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const addFilters = jest.fn();
|
||||
const closeTooltip = jest.fn();
|
||||
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<PointToolTipContent
|
||||
contextId={'contextId'}
|
||||
featureProps={mockFeatureProps}
|
||||
featurePropsFilters={mockFeaturePropsFilters}
|
||||
addFilters={addFilters}
|
||||
closeTooltip={closeTooltip}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('tooltip closes when filter for value hover action is clicked', () => {
|
||||
const addFilters = jest.fn();
|
||||
const closeTooltip = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<PointToolTipContent
|
||||
contextId={'contextId'}
|
||||
featureProps={mockFeatureProps}
|
||||
featurePropsFilters={mockFeaturePropsFilters}
|
||||
addFilters={addFilters}
|
||||
closeTooltip={closeTooltip}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="hover-actions-${mockFeatureProps[0]._propertyKey}"]`)
|
||||
.first()
|
||||
.simulate('mouseenter');
|
||||
wrapper
|
||||
.find(`[data-test-subj="add-to-filter-${mockFeatureProps[0]._propertyKey}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(closeTooltip).toHaveBeenCalledTimes(1);
|
||||
expect(addFilters).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('#getRenderedFieldValue', () => {
|
||||
test('it returns empty tag if value is empty', () => {
|
||||
expect(getRenderedFieldValue('host.name', '')).toStrictEqual(getEmptyStringTag());
|
||||
});
|
||||
|
||||
test('it returns HostDetailsLink if field is host.name', () => {
|
||||
const value = 'suricata-ross';
|
||||
expect(getRenderedFieldValue('host.name', value)).toStrictEqual(
|
||||
<HostDetailsLink hostName={value} />
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns IPDetailsLink if field is source.ip', () => {
|
||||
const value = '127.0.0.1';
|
||||
expect(getRenderedFieldValue('source.ip', value)).toStrictEqual(<IPDetailsLink ip={value} />);
|
||||
});
|
||||
|
||||
test('it returns IPDetailsLink if field is destination.ip', () => {
|
||||
const value = '127.0.0.1';
|
||||
expect(getRenderedFieldValue('destination.ip', value)).toStrictEqual(
|
||||
<IPDetailsLink ip={value} />
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns nothing if field is not host.name or source/destination.ip', () => {
|
||||
const value = 'Kramerica.co';
|
||||
expect(getRenderedFieldValue('destination.domain', value)).toStrictEqual(<>{value}</>);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import * as i18n from '../translations';
|
||||
import { sourceDestinationFieldMappings } from '../map_config';
|
||||
import { WithHoverActions } from '../../with_hover_actions';
|
||||
import { HoverActionsContainer } from '../../page/add_to_kql';
|
||||
import { getEmptyTagValue, getOrEmptyTagFromValue } from '../../empty_value';
|
||||
import { DescriptionListStyled } from '../../page';
|
||||
import { FeatureProperty } from '../types';
|
||||
import { HostDetailsLink, IPDetailsLink } from '../../links';
|
||||
import { DefaultFieldRenderer } from '../../field_renderers/field_renderers';
|
||||
|
||||
interface PointToolTipContentProps {
|
||||
contextId: string;
|
||||
featureProps: FeatureProperty[];
|
||||
featurePropsFilters: Record<string, object>;
|
||||
addFilters?(filter: object): void;
|
||||
closeTooltip?(): void;
|
||||
}
|
||||
|
||||
export const PointToolTipContent = React.memo<PointToolTipContentProps>(
|
||||
({ contextId, featureProps, featurePropsFilters, addFilters, closeTooltip }) => {
|
||||
const featureDescriptionListItems = featureProps.map(property => ({
|
||||
title: sourceDestinationFieldMappings[property._propertyKey],
|
||||
description: (
|
||||
<WithHoverActions
|
||||
data-test-subj={`hover-actions-${property._propertyKey}`}
|
||||
hoverContent={
|
||||
<HoverActionsContainer>
|
||||
<EuiToolTip content={i18n.FILTER_FOR_VALUE}>
|
||||
<EuiIcon
|
||||
data-test-subj={`add-to-filter-${property._propertyKey}`}
|
||||
type="filter"
|
||||
onClick={() => {
|
||||
if (closeTooltip != null && addFilters != null) {
|
||||
closeTooltip();
|
||||
addFilters(featurePropsFilters[property._propertyKey]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</HoverActionsContainer>
|
||||
}
|
||||
render={() =>
|
||||
property._rawValue != null ? (
|
||||
<DefaultFieldRenderer
|
||||
rowItems={
|
||||
Array.isArray(property._rawValue) ? property._rawValue : [property._rawValue]
|
||||
}
|
||||
attrName={property._propertyKey}
|
||||
idPrefix={`map-point-tooltip-${contextId}-${property._propertyKey}-${property._rawValue}`}
|
||||
render={item => getRenderedFieldValue(property._propertyKey, item)}
|
||||
/>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
return <DescriptionListStyled listItems={featureDescriptionListItems} />;
|
||||
}
|
||||
);
|
||||
|
||||
PointToolTipContent.displayName = 'PointToolTipContent';
|
||||
|
||||
export const getRenderedFieldValue = (field: string, value: string) => {
|
||||
if (value === '') {
|
||||
return getOrEmptyTagFromValue(value);
|
||||
} else if (['host.name'].includes(field)) {
|
||||
return <HostDetailsLink hostName={value} />;
|
||||
} else if (['source.ip', 'destination.ip'].includes(field)) {
|
||||
return <IPDetailsLink ip={value} />;
|
||||
}
|
||||
return <>{value}</>;
|
||||
};
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { ToolTipFooter } from './tooltip_footer';
|
||||
|
||||
describe('ToolTipFilter', () => {
|
||||
let nextFeature = jest.fn();
|
||||
let previousFeature = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
nextFeature = jest.fn();
|
||||
previousFeature = jest.fn();
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={100}
|
||||
/>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Lower bounds', () => {
|
||||
test('previousButton is disabled when featureIndex is 0', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('previousFeature is not called when featureIndex is 0', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(previousFeature).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('nextButton is enabled when featureIndex is < totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('nextFeature is called when featureIndex is < totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(nextFeature).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upper bounds', () => {
|
||||
test('previousButton is enabled when featureIndex >== totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={4}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('previousFunction is called when featureIndex >== totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={4}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(previousFeature).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('nextButton is disabled when featureIndex >== totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={4}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('nextFunction is not called when featureIndex >== totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={4}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(nextFeature).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Within bounds, single feature', () => {
|
||||
test('previousButton is not enabled when only a single feature is provided', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('previousFunction is not called when only a single feature is provided', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={1}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(previousFeature).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('nextButton is not enabled when only a single feature is provided', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('nextFunction is not called when only a single feature is provided', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={0}
|
||||
totalFeatures={1}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(nextFeature).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Within bounds, multiple features', () => {
|
||||
test('previousButton is enabled when featureIndex > 0 && featureIndex < totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={1}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('previousFunction is called when featureIndex > 0 && featureIndex < totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={1}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="previous-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(previousFeature).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('nextButton is enabled when featureIndex > 0 && featureIndex < totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={1}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('nextFunction is called when featureIndex > 0 && featureIndex < totalFeatures', () => {
|
||||
const wrapper = mount(
|
||||
<ToolTipFooter
|
||||
nextFeature={nextFeature}
|
||||
previousFeature={previousFeature}
|
||||
featureIndex={1}
|
||||
totalFeatures={5}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="next-feature-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(nextFeature).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export const Icon = styled(EuiIcon)`
|
||||
margin-right: ${theme.euiSizeS};
|
||||
`;
|
||||
|
||||
Icon.displayName = 'Icon';
|
||||
|
||||
interface MapToolTipFooterProps {
|
||||
featureIndex: number;
|
||||
totalFeatures: number;
|
||||
previousFeature: () => void;
|
||||
nextFeature: () => void;
|
||||
}
|
||||
|
||||
export const ToolTipFooter = React.memo<MapToolTipFooterProps>(
|
||||
({ featureIndex, totalFeatures, previousFeature, nextFeature }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
{i18n.MAP_TOOL_TIP_FEATURES_FOOTER(featureIndex + 1, totalFeatures)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={'previous-feature-button'}
|
||||
color={'text'}
|
||||
onClick={previousFeature}
|
||||
iconType="arrowLeft"
|
||||
aria-label="Next"
|
||||
disabled={featureIndex <= 0}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={'next-feature-button'}
|
||||
color={'text'}
|
||||
onClick={nextFeature}
|
||||
iconType="arrowRight"
|
||||
aria-label="Next"
|
||||
disabled={featureIndex >= totalFeatures - 1}
|
||||
/>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ToolTipFooter.displayName = 'ToolTipFooter';
|
|
@ -13,6 +13,27 @@ export const MAP_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SOURCE_LAYER = i18n.translate(
|
||||
'xpack.siem.components.embeddables.embeddedMap.sourceLayerLabel',
|
||||
{
|
||||
defaultMessage: 'Source Point',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESTINATION_LAYER = i18n.translate(
|
||||
'xpack.siem.components.embeddables.embeddedMap.destinationLayerLabel',
|
||||
{
|
||||
defaultMessage: 'Destination Point',
|
||||
}
|
||||
);
|
||||
|
||||
export const LINE_LAYER = i18n.translate(
|
||||
'xpack.siem.components.embeddables.embeddedMap.lineLayerLabel',
|
||||
{
|
||||
defaultMessage: 'Line',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_CONFIGURING_EMBEDDABLES_API = i18n.translate(
|
||||
'xpack.siem.components.embeddables.embeddedMap.errorConfiguringEmbeddableApiTitle',
|
||||
{
|
||||
|
@ -48,3 +69,87 @@ export const ERROR_BUTTON = i18n.translate(
|
|||
defaultMessage: 'Configure index patterns',
|
||||
}
|
||||
);
|
||||
|
||||
export const FILTER_FOR_VALUE = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.filterForValueHoverAction',
|
||||
{
|
||||
defaultMessage: 'Filter for value',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAP_TOOL_TIP_ERROR = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.errorTitle',
|
||||
{
|
||||
defaultMessage: 'Error loading map features',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAP_TOOL_TIP_FEATURES_FOOTER = (currentFeature: number, totalFeatures: number) =>
|
||||
i18n.translate('xpack.siem.components.embeddables.mapToolTip.footerLabel', {
|
||||
values: { currentFeature, totalFeatures },
|
||||
defaultMessage:
|
||||
'{currentFeature} of {totalFeatures} {totalFeatures, plural, =1 {feature} other {features}}',
|
||||
});
|
||||
|
||||
export const HOST = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.hostTitle',
|
||||
{
|
||||
defaultMessage: 'Host',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_IP = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.sourceIPTitle',
|
||||
{
|
||||
defaultMessage: 'Source IP',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESTINATION_IP = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.destinationIPTitle',
|
||||
{
|
||||
defaultMessage: 'Destination IP',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_DOMAIN = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.sourceDomainTitle',
|
||||
{
|
||||
defaultMessage: 'Source domain',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESTINATION_DOMAIN = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.destinationDomainTitle',
|
||||
{
|
||||
defaultMessage: 'Destination domain',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOCATION = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.locationTitle',
|
||||
{
|
||||
defaultMessage: 'Location',
|
||||
}
|
||||
);
|
||||
|
||||
export const ASN = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.pointContent.asnTitle',
|
||||
{
|
||||
defaultMessage: 'ASN',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.lineContent.sourceLabel',
|
||||
{
|
||||
defaultMessage: 'Source',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESTINATION = i18n.translate(
|
||||
'xpack.siem.components.embeddables.mapToolTip.lineContent.destinationLabel',
|
||||
{
|
||||
defaultMessage: 'Destination',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -39,3 +39,36 @@ export type SetQuery = (params: {
|
|||
loading: boolean;
|
||||
refetch: inputsModel.Refetch;
|
||||
}) => void;
|
||||
|
||||
export interface MapFeature {
|
||||
id: number;
|
||||
layerId: string;
|
||||
}
|
||||
|
||||
export interface LoadFeatureProps {
|
||||
layerId: string;
|
||||
featureId: number;
|
||||
}
|
||||
|
||||
export interface FeatureProperty {
|
||||
_propertyKey: string;
|
||||
_rawValue: string;
|
||||
getESFilters(): Promise<object>;
|
||||
}
|
||||
|
||||
export interface FeatureGeometry {
|
||||
coordinates: [number];
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface RenderTooltipContentParams {
|
||||
addFilters(filter: object): void;
|
||||
closeTooltip(): void;
|
||||
features: MapFeature[];
|
||||
isLocked: boolean;
|
||||
getLayerName(layerId: string): Promise<string>;
|
||||
loadFeatureProperties({ layerId, featureId }: LoadFeatureProps): Promise<FeatureProperty[]>;
|
||||
loadFeatureGeometry({ layerId, featureId }: LoadFeatureProps): FeatureGeometry;
|
||||
}
|
||||
|
||||
export type MapToolTipProps = Partial<RenderTooltipContentParams>;
|
||||
|
|
|
@ -50,7 +50,7 @@ const AddToKqlComponent = React.memo<Props>(
|
|||
|
||||
AddToKqlComponent.displayName = 'AddToKqlComponent';
|
||||
|
||||
const HoverActionsContainer = styled(EuiPanel)`
|
||||
export const HoverActionsContainer = styled(EuiPanel)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDescriptionList, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DEFAULT_DARK_MODE } from '../../../../../common/constants';
|
||||
import { DescriptionList } from '../../../../../common/utility_types';
|
||||
|
@ -24,7 +23,7 @@ import { MlCapabilitiesContext } from '../../../ml/permissions/ml_capabilities_p
|
|||
import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions';
|
||||
import { AnomalyScores } from '../../../ml/score/anomaly_scores';
|
||||
import { Anomalies, NarrowDateRange } from '../../../ml/types';
|
||||
import { OverviewWrapper } from '../../index';
|
||||
import { DescriptionListStyled, OverviewWrapper } from '../../index';
|
||||
import { FirstLastSeenHost, FirstLastSeenHostType } from '../first_last_seen_host';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
@ -40,16 +39,6 @@ interface HostSummaryProps {
|
|||
narrowDateRange: NarrowDateRange;
|
||||
}
|
||||
|
||||
const DescriptionListStyled = styled(EuiDescriptionList)`
|
||||
${({ theme }) => `
|
||||
dt {
|
||||
font-size: ${theme.eui.euiFontSizeXS} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
DescriptionListStyled.displayName = 'DescriptionListStyled';
|
||||
|
||||
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
|
||||
<EuiFlexItem key={key}>
|
||||
<DescriptionListStyled listItems={descriptionList} />
|
||||
|
|
|
@ -5,7 +5,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiBadgeProps,
|
||||
EuiDescriptionList,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
EuiPage,
|
||||
} from '@elastic/eui';
|
||||
import styled, { injectGlobal } from 'styled-components';
|
||||
|
||||
// SIDE EFFECT: the following `injectGlobal` overrides default styling in angular code that was not theme-friendly
|
||||
|
@ -20,6 +27,16 @@ injectGlobal`
|
|||
}
|
||||
`;
|
||||
|
||||
export const DescriptionListStyled = styled(EuiDescriptionList)`
|
||||
${({ theme }) => `
|
||||
dt {
|
||||
font-size: ${theme.eui.euiFontSizeXS} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
DescriptionListStyled.displayName = 'DescriptionListStyled';
|
||||
|
||||
export const PageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDescriptionList, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DEFAULT_DARK_MODE } from '../../../../../common/constants';
|
||||
import { DescriptionList } from '../../../../../common/utility_types';
|
||||
|
@ -28,7 +27,7 @@ import {
|
|||
whoisRenderer,
|
||||
} from '../../../field_renderers/field_renderers';
|
||||
import * as i18n from './translations';
|
||||
import { OverviewWrapper } from '../../index';
|
||||
import { DescriptionListStyled, OverviewWrapper } from '../../index';
|
||||
import { Loader } from '../../../loader';
|
||||
import { Anomalies, NarrowDateRange } from '../../../ml/types';
|
||||
import { AnomalyScores } from '../../../ml/score/anomaly_scores';
|
||||
|
@ -52,16 +51,6 @@ interface OwnProps {
|
|||
|
||||
export type IpOverviewProps = OwnProps;
|
||||
|
||||
const DescriptionListStyled = styled(EuiDescriptionList)`
|
||||
${({ theme }) => `
|
||||
dt {
|
||||
font-size: ${theme.eui.euiFontSizeXS} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
DescriptionListStyled.displayName = 'DescriptionListStyled';
|
||||
|
||||
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => {
|
||||
return (
|
||||
<EuiFlexItem key={key}>
|
||||
|
|
|
@ -327,6 +327,7 @@
|
|||
"react-redux": "^5.1.1",
|
||||
"react-redux-request": "^1.5.6",
|
||||
"react-resize-detector": "^4.2.0",
|
||||
"react-reverse-portal": "^1.0.2",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-select": "^1.2.1",
|
||||
"react-shortcuts": "^2.0.0",
|
||||
|
|
|
@ -23311,6 +23311,11 @@ react-resize-detector@^4.0.5, react-resize-detector@^4.2.0:
|
|||
raf-schd "^4.0.0"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
react-reverse-portal@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-reverse-portal/-/react-reverse-portal-1.0.3.tgz#38cd2d40f40862352dd63905f9b923f9c41f474b"
|
||||
integrity sha512-mCtpp3BzPedmGTAMqT2v5U1hwnAvRfSqMusriON/GxnedT9gvNNTvai24NnrfKfQ78zqPo4e3N5nWPLpY7bORQ==
|
||||
|
||||
react-router-dom@4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
|
||||
|
|
Loading…
Reference in a new issue