Skip to content

Commit

Permalink
[SecuritySolution] Replace donuts with Lens (elastic#148519)
Browse files Browse the repository at this point in the history
## Summary

This pr is a part of elastic#147261:
Replace donut charts with Lens:
This is behind feature flag `chartEmbeddablesEnabled `

Items to verify:
1. If the queries and filters from the global search box are applied.
2. If the correct alert index is applied by different spaced.
3. Visualization actions working correctly.

Detection and Response dashboard:
<img width="1662" alt="Screenshot 2023-01-09 at 09 21 56"
src="https://user-images.githubusercontent.com/6295984/211275765-8177e9bd-3623-4bb2-b1d9-8a3044d523a0.png">

Host details:
<img width="1666" alt="Screenshot 2023-01-09 at 09 21 38"
src="https://user-images.githubusercontent.com/6295984/211275770-be95353f-4d1b-410a-b7bf-b232692af1ab.png">

User details:
<img width="1662" alt="Screenshot 2023-01-09 at 09 21 19"
src="https://user-images.githubusercontent.com/6295984/211275773-dd0bcaaf-58e6-404b-b010-d1c464cbd101.png">

Network details:
<img width="1663" alt="Screenshot 2023-01-09 at 09 20 47"
src="https://user-images.githubusercontent.com/6295984/211275775-0fd39ac3-e977-44bd-bd40-304463dce613.png">

Known issues:
1. Not showing legend for alerts donut charts at the moment: There is a
logic in Lens that it doesn't show the legend item if its value is zero.
4. JS warnings triggered by incorrect state changed. Fixed by
elastic#148552
5. No label in the donut by default when open in Lens - Lens doesn't
support displaying a label in the middle of the donut chart by default,
so it is currently available in Security Solution.
6. Applying filters or extra action while clicking the donut is not
available atm



### Checklist

Delete any items that are not applicable to this PR.


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
angorayc and kibanamachine authored Jan 10, 2023
1 parent ef2faad commit 09e10b8
Show file tree
Hide file tree
Showing 77 changed files with 2,159 additions and 222 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import {
Wrapper,
ChartWrapper,
} from './common';
import { VisualizationActions, HISTOGRAM_ACTIONS_BUTTON_CLASS } from '../visualization_actions';
import { VisualizationActions } from '../visualization_actions';
import type { VisualizationActionsProps } from '../visualization_actions/types';

import { HoverVisibilityContainer } from '../hover_visibility_container';
import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils';

// custom series styles: https://ela.st/areachart-styling
const getSeriesLineStyle = (): RecursivePartial<AreaSeriesStyle> => {
Expand Down Expand Up @@ -155,7 +156,7 @@ export const AreaChartComponent: React.FC<AreaChartComponentProps> = ({

return (
<Wrapper>
<HoverVisibilityContainer targetClassNames={[HISTOGRAM_ACTIONS_BUTTON_CLASS]}>
<HoverVisibilityContainer targetClassNames={[VISUALIZATION_ACTIONS_BUTTON_CLASS]}>
{isValidSeriesExist && areaChart && (
<ChartWrapper gutterSize="none">
<EuiFlexItem grow={true}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ import {
import { DraggableLegend } from './draggable_legend';
import type { LegendItem } from './draggable_legend_item';
import type { ChartData, ChartSeriesConfigs, ChartSeriesData } from './common';
import { VisualizationActions, HISTOGRAM_ACTIONS_BUTTON_CLASS } from '../visualization_actions';
import { VisualizationActions } from '../visualization_actions';
import type { VisualizationActionsProps } from '../visualization_actions/types';
import { HoverVisibilityContainer } from '../hover_visibility_container';
import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils';

const LegendFlexItem = styled(EuiFlexItem)`
overview: hidden;
Expand Down Expand Up @@ -207,7 +208,7 @@ export const BarChartComponent: React.FC<BarChartComponentProps> = ({

return (
<Wrapper>
<HoverVisibilityContainer targetClassNames={[HISTOGRAM_ACTIONS_BUTTON_CLASS]}>
<HoverVisibilityContainer targetClassNames={[VISUALIZATION_ACTIONS_BUTTON_CLASS]}>
{isValidSeriesExist && barChart && (
<BarChartWrapper gutterSize="none">
<EuiFlexItem grow={true}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import type { EuiFlexGroupProps } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui';
import React, { useMemo } from 'react';

Expand Down Expand Up @@ -44,46 +45,54 @@ export interface DonutChartProps {
data: DonutChartData[] | null | undefined;
fillColor: FillColor;
height?: number;
isChartEmbeddablesEnabled?: boolean;
label: React.ReactElement | string;
legendItems?: LegendItem[] | null | undefined;
onElementClick?: ElementClickListener;
title: React.ReactElement | string | number | null;
totalCount: number | null | undefined;
onElementClick?: ElementClickListener;
}

export interface DonutChartWrapperProps {
children?: React.ReactElement;
dataExists: boolean;
label: React.ReactElement | string;
title: React.ReactElement | string | number | null;
isChartEmbeddablesEnabled?: boolean;
}

/* Make this position absolute in order to overlap the text onto the donut */
const DonutTextWrapper = styled(EuiFlexGroup)`
top: 34%;
export const DonutTextWrapper = styled(EuiFlexGroup)<
EuiFlexGroupProps & { $isChartEmbeddablesEnabled?: boolean; $dataExists?: boolean }
>`
top: ${({ $isChartEmbeddablesEnabled, $dataExists }) =>
$isChartEmbeddablesEnabled && !$dataExists ? `66%` : `34%;`};
width: 100%;
max-width: 77px;
position: absolute;
z-index: 1;
`;

const StyledEuiFlexItem = styled(EuiFlexItem)`
export const StyledEuiFlexItem = styled(EuiFlexItem)`
position: relative;
align-items: center;
`;

export const DonutChart = ({
data,
fillColor,
height = 90,
const DonutChartWrapperComponent: React.FC<DonutChartWrapperProps> = ({
children,
dataExists,
isChartEmbeddablesEnabled,
label,
legendItems,
title,
totalCount,
onElementClick,
}: DonutChartProps) => {
const theme = useTheme();
}) => {
const { euiTheme } = useEuiTheme();
const emptyLabelStyle = useMemo(
() => ({
color: euiTheme.colors.disabled,
}),
[euiTheme.colors.disabled]
);

const className = isChartEmbeddablesEnabled ? undefined : 'eui-textTruncate';
return (
<EuiFlexGroup
alignItems="center"
Expand All @@ -92,26 +101,56 @@ export const DonutChart = ({
gutterSize="l"
data-test-subj="donut-chart"
>
<StyledEuiFlexItem grow={false}>
<StyledEuiFlexItem grow={isChartEmbeddablesEnabled}>
<DonutTextWrapper
$dataExists={dataExists}
$isChartEmbeddablesEnabled={isChartEmbeddablesEnabled}
alignItems="center"
direction="column"
gutterSize="none"
alignItems="center"
justifyContent="center"
>
<EuiFlexItem>{title}</EuiFlexItem>
<EuiFlexItem className="eui-textTruncate">
<EuiFlexItem className={className}>
<EuiToolTip content={label}>
<EuiText
className="eui-textTruncate"
className={className}
size="s"
style={data ? undefined : emptyLabelStyle}
style={dataExists ? undefined : emptyLabelStyle}
>
{label}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</DonutTextWrapper>
{children}
</StyledEuiFlexItem>
</EuiFlexGroup>
);
};
export const DonutChartWrapper = React.memo(DonutChartWrapperComponent);

export const DonutChart = ({
data,
fillColor,
height = 90,
isChartEmbeddablesEnabled,
label,
legendItems,
onElementClick,
title,
totalCount,
}: DonutChartProps) => {
const theme = useTheme();

return (
<DonutChartWrapper
dataExists={data != null && data.length > 0}
label={label}
title={title}
isChartEmbeddablesEnabled={isChartEmbeddablesEnabled}
>
<>
{data == null || totalCount == null || totalCount === 0 ? (
<DonutChartEmpty size={height} />
) : (
Expand All @@ -135,12 +174,13 @@ export const DonutChart = ({
/>
</Chart>
)}
</StyledEuiFlexItem>
{legendItems && legendItems?.length > 0 && (
<EuiFlexItem>
<DraggableLegend legendItems={legendItems} height={height} />
</EuiFlexItem>
)}
</EuiFlexGroup>

{legendItems && legendItems?.length > 0 && (
<EuiFlexItem>
<DraggableLegend legendItems={legendItems} height={height} />
</EuiFlexItem>
)}
</>
</DonutChartWrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import numeral from '@elastic/numeral';

import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { getExternalAlertLensAttributes } from '../visualization_actions/lens_attributes/common/external_alert';
import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_attributes/hosts/events';
import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_attributes/common/events';
import type { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types';
import * as i18n from './translations';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const DescriptionListStyled = styled(EuiDescriptionList)`
DescriptionListStyled.displayName = 'DescriptionListStyled';

export interface ModalInspectProps {
adHocDataViews?: string[] | null;
additionalRequests?: string[] | null;
additionalResponses?: string[] | null;
closeModal: () => void;
Expand Down Expand Up @@ -108,6 +109,7 @@ export const formatIndexPatternRequested = (indices: string[] = []) => {
};

export const ModalInspectQuery = ({
adHocDataViews,
additionalRequests,
additionalResponses,
closeModal,
Expand All @@ -120,6 +122,7 @@ export const ModalInspectQuery = ({
const { selectedPatterns } = useSourcererDataView(
inputId === 'timeline' ? SourcererScopeName.timeline : getScopeFromPath(pathname)
);

const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])];
const responses: string[] = [
response,
Expand Down Expand Up @@ -150,7 +153,13 @@ export const ModalInspectQuery = ({
),
description: (
<span data-test-subj="index-pattern-description">
<p>{formatIndexPatternRequested(inspectRequests[0]?.index ?? [])}</p>
<p>
{formatIndexPatternRequested(
adHocDataViews != null && adHocDataViews.length > 0
? adHocDataViews
: inspectRequests[0]?.index ?? []
)}
</p>

{!isSourcererPattern && (
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ import type { GlobalTimeArgs } from '../../containers/use_global_time';
import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { InputsModelId } from '../../store/inputs/constants';
import { HoverVisibilityContainer } from '../hover_visibility_container';
import { HISTOGRAM_ACTIONS_BUTTON_CLASS, VisualizationActions } from '../visualization_actions';
import { VisualizationActions } from '../visualization_actions';
import type { GetLensAttributes, LensAttributes } from '../visualization_actions/types';
import { SecurityPageName } from '../../../../common/constants';
import { useRouteSpy } from '../../utils/route/use_route_spy';
import { useQueryToggle } from '../../containers/query_toggle';
import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils';

export type MatrixHistogramComponentProps = MatrixHistogramProps &
Omit<MatrixHistogramQueryProps, 'stackByField'> & {
Expand Down Expand Up @@ -236,7 +237,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
<>
<HoverVisibilityContainer
show={!isInitialLoading}
targetClassNames={[HISTOGRAM_ACTIONS_BUTTON_CLASS]}
targetClassNames={[VISUALIZATION_ACTIONS_BUTTON_CLASS]}
>
<HistogramPanel
data-test-subj={`${id}Panel`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe(`useRefetchByRestartingSession`, () => {
children: React.ReactNode;
},
{
searchSessionId: string;
searchSessionId: string | undefined;
refetchByRestartingSession: Refetch;
}
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { useCallback, useRef, useEffect, useMemo } from 'react';
import { useCallback, useRef, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useKibana } from '../../lib/kibana';
Expand All @@ -23,21 +23,28 @@ interface UseRefetchByRestartingSessionProps {
export const useRefetchByRestartingSession = ({
inputId,
queryId,
skip,
}: UseRefetchByRestartingSessionProps): {
searchSessionId: string;
searchSessionId: string | undefined;
refetchByRestartingSession: Refetch;
} => {
const dispatch = useDispatch();
const { data } = useKibana().services;

const session = useRef(data.search.session);
const searchSessionId = useMemo(() => session.current.start(), [session]);

const getGlobalQuery = inputsSelectors.globalQueryByIdSelector();
const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector();
const { selectedInspectIndex } = useDeepEqualSelector((state) =>
inputId === InputsModelId.global
? getGlobalQuery(state, queryId)
: getTimelineQuery(state, queryId)
const { selectedInspectIndex, searchSessionId: existingSearchSessionId } = useDeepEqualSelector(
(state) =>
inputId === InputsModelId.global
? getGlobalQuery(state, queryId)
: getTimelineQuery(state, queryId)
);

const searchSessionId = useMemo(
() => (skip ? undefined : existingSearchSessionId ?? session.current.start()),
[existingSearchSessionId, skip]
);

const refetchByRestartingSession = useCallback(() => {
Expand All @@ -51,16 +58,10 @@ export const useRefetchByRestartingSession = ({
* like most of our components, it refetches when receiving a new search
* session ID.
**/
searchSessionId: session.current.start(),
searchSessionId: skip ? undefined : session.current.start(),
})
);
}, [dispatch, queryId, selectedInspectIndex]);

useEffect(() => {
return () => {
data.search.session.clear();
};
});
}, [dispatch, queryId, selectedInspectIndex, skip]);

return { searchSessionId, refetchByRestartingSession };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';

export const VisualizationActions = () => <div data-test-subj="visualizationActions" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';

export const LensEmbeddable = () => <div data-test-subj="lens-embeddable" />;
Loading

0 comments on commit 09e10b8

Please sign in to comment.