Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM] Unify Histograms loading states #55961

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import React from 'react';
import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts';
import { getOr, get, isNumber } from 'lodash/fp';
import deepmerge from 'deepmerge';

import { useTimeZone } from '../../hooks';
import { AutoSizer } from '../auto_sizer';
import { ChartPlaceHolder } from './chart_place_holder';
Expand Down Expand Up @@ -52,8 +54,7 @@ export const BarChartBaseComponent = ({
const yAxisId = `stat-items-barchart-${data[0].key}-y`;
const settings = {
...chartDefaultSettings,
theme,
...get('configs.settings', chartConfigs),
...deepmerge(get('configs.settings', chartConfigs), { theme }),
};

return chartConfigs.width && chartConfigs.height ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ export const InspectButtonContainer = styled.div<{ show?: boolean }>`
display: flex;
flex-grow: 1;

> * {
max-width: 100%;
}

.${BUTTON_CLASS} {
pointer-events: none;
opacity: 0;
transition: opacity ${props => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease;
}
Expand All @@ -33,6 +38,7 @@ export const InspectButtonContainer = styled.div<{ show?: boolean }>`
show &&
css`
&:hover .${BUTTON_CLASS} {
pointer-events: auto;
opacity: 1;
}
`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@
import React, { useState, useEffect, useCallback } from 'react';
import { ScaleType } from '@elastic/charts';

import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiSelect } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect } from '@elastic/eui';
import { noop } from 'lodash/fp';
import * as i18n from './translations';
import { BarChart } from '../charts/barchart';
import { HeaderSection } from '../header_section';
import { DEFAULT_DARK_MODE } from '../../../common/constants';
import { useUiSetting$ } from '../../lib/kibana';
import { Loader } from '../loader';
import { MatrixLoader } from './matrix_loader';
import { Panel } from '../panel';
import { getBarchartConfigs, getCustomChartData } from '../../components/matrix_histogram/utils';
import { getBarchartConfigs, getCustomChartData } from './utils';
import { useQuery } from '../../containers/matrix_histogram/utils';
import {
MatrixHistogramProps,
Expand All @@ -31,7 +27,6 @@ import { InspectButtonContainer } from '../inspect';

export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
MatrixHistogramQueryProps> = ({
activePage,
dataKey,
defaultStackByOption,
endDate,
Expand Down Expand Up @@ -59,7 +54,6 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
title,
updateDateRange,
yTickFormatter,
sort,
}) => {
const barchartConfigs = getBarchartConfigs({
from: startDate,
Expand All @@ -70,20 +64,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
yTickFormatter,
showLegend,
});
const [showInspect, setShowInspect] = useState(false);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);

const handleOnMouseEnter = useCallback(() => {
if (!showInspect) {
setShowInspect(true);
}
}, [showInspect, setShowInspect]);
const handleOnMouseLeave = useCallback(() => {
if (showInspect) {
setShowInspect(false);
}
}, [showInspect, setShowInspect]);

const [isInitialLoading, setIsInitialLoading] = useState(true);
const [selectedStackByOption, setSelectedStackByOption] = useState<MatrixHistogramOption>(
defaultStackByOption
);
Expand Down Expand Up @@ -127,19 +108,19 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
if (subtitle != null)
setSubtitle(typeof subtitle === 'function' ? subtitle(totalCount) : subtitle);

if (totalCount <= 0) {
if (hideHistogramIfEmpty) {
setHideHistogram(true);
} else {
setHideHistogram(false);
}
if (totalCount <= 0 && hideHistogramIfEmpty) {
setHideHistogram(true);
} else {
setHideHistogram(false);
}

setBarChartData(getCustomChartData(data, mapping));

setQuery({ id, inspect, loading, refetch });

if (isInitialLoading && !!barChartData && data) {
setIsInitialLoading(false);
}
}, [
subtitle,
setSubtitle,
Expand All @@ -152,55 +133,57 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
loading,
data,
refetch,
isInitialLoading,
]);

return !hideHistogram ? (
<InspectButtonContainer show={showInspect}>
<Panel
data-test-subj={`${id}Panel`}
loading={loading}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
>
<HeaderSection
id={id}
title={titleWithStackByField}
subtitle={!loading && (totalCount >= 0 ? subtitleWithCounts : null)}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{stackByOptions?.length > 1 && (
<EuiSelect
onChange={setSelectedChartOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY}
value={selectedStackByOption?.value}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{headerChildren}</EuiFlexItem>
</EuiFlexGroup>
</HeaderSection>
{loading ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelMatrixOverTime" lines={10} />
if (hideHistogram) {
return null;
}

return (
<InspectButtonContainer show={!isInitialLoading}>
<Panel data-test-subj={`${id}Panel`}>
{loading && !isInitialLoading && (
<EuiProgress
data-test-subj="initialLoadingPanelMatrixOverTime"
size="xs"
position="absolute"
color="accent"
/>
)}

{isInitialLoading ? (
<>
<HeaderSection id={id} title={titleWithStackByField} />
<MatrixLoader />
</>
) : (
<>
<HeaderSection
id={id}
title={titleWithStackByField}
subtitle={!loading && (totalCount >= 0 ? subtitleWithCounts : null)}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{stackByOptions?.length > 1 && (
<EuiSelect
onChange={setSelectedChartOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY}
value={selectedStackByOption?.value}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{headerChildren}</EuiFlexItem>
</EuiFlexGroup>
</HeaderSection>
<BarChart barChart={barChartData} configs={barchartConfigs} />

{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</>
)}
</Panel>
</InspectButtonContainer>
) : null;
);
};

export const MatrixHistogram = React.memo(MatrixHistogramComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import styled from 'styled-components';

const StyledEuiFlexGroup = styled(EuiFlexGroup)`
height: 350px; /* to avoid jump when histogram loads */
`;

const MatrixLoaderComponent = () => (
<StyledEuiFlexGroup alignItems="center" justifyContent="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</StyledEuiFlexGroup>
);

export const MatrixLoader = React.memo(MatrixLoaderComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts';
import { ScaleType, Position } from '@elastic/charts';
import { get, groupBy, map, toPairs } from 'lodash/fp';

import { UpdateDateRange, ChartSeriesData } from '../charts/common';
import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types';
import { histogramDateTimeFormatter } from '../utils';

interface GetBarchartConfigsProps {
from: number;
legendPosition?: Position;
to: number;
scaleType: ScaleType;
onBrushEnd: UpdateDateRange;
yTickFormatter?: (value: number) => string;
showLegend?: boolean;
}

export const getBarchartConfigs = ({
from,
Expand All @@ -17,22 +28,15 @@ export const getBarchartConfigs = ({
onBrushEnd,
yTickFormatter,
showLegend,
}: {
from: number;
legendPosition?: Position;
to: number;
scaleType: ScaleType;
onBrushEnd: UpdateDateRange;
yTickFormatter?: (value: number) => string;
showLegend?: boolean;
}) => ({
}: GetBarchartConfigsProps) => ({
series: {
xScaleType: scaleType || ScaleType.Time,
yScaleType: ScaleType.Linear,
stackAccessors: ['g'],
},
axis: {
xTickFormatter: scaleType === ScaleType.Time ? niceTimeFormatter([from, to]) : undefined,
xTickFormatter:
scaleType === ScaleType.Time ? histogramDateTimeFormatter([from, to]) : undefined,
yTickFormatter:
yTickFormatter != null
? yTickFormatter
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
ScaleType,
niceTimeFormatter,
Rotation,
BrushEndListener,
ElementClickListener,
} from '@elastic/charts';
import { ScaleType, Rotation, BrushEndListener, ElementClickListener } from '@elastic/charts';
import {
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -28,6 +22,7 @@ import { KpiHostsData, KpiNetworkData } from '../../graphql/types';
import { AreaChart } from '../charts/areachart';
import { BarChart } from '../charts/barchart';
import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common';
import { histogramDateTimeFormatter } from '../utils';
import { getEmptyTagValue } from '../empty_value';

import { InspectButton, InspectButtonContainer } from '../inspect';
Expand Down Expand Up @@ -274,7 +269,7 @@ export const StatItemsComponent = React.memo<StatItemsProps>(
<AreaChart
areaChart={areaChart}
configs={areachartConfigs({
xTickFormatter: niceTimeFormatter([from, to]),
xTickFormatter: histogramDateTimeFormatter([from, to]),
onBrushEnd: narrowDateRange,
})}
/>
Expand Down
24 changes: 24 additions & 0 deletions x-pack/legacy/plugins/siem/public/components/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { niceTimeFormatByDay, timeFormatter } from '@elastic/charts';
import moment from 'moment-timezone';

export const getDaysDiff = (minDate: moment.Moment, maxDate: moment.Moment) => {
const diff = maxDate.diff(minDate, 'days');

if (diff <= 1 && !minDate.isSame(maxDate)) {
return 2; // to return proper pattern from niceTimeFormatByDay
}

return diff;
};

export const histogramDateTimeFormatter = (domain: [number, number] | null, fixedDiff?: number) => {
const diff = fixedDiff ?? getDaysDiff(moment(domain![0]), moment(domain![1]));
const format = niceTimeFormatByDay(diff);
return timeFormatter(format);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from '../types';
import { SignalSearchResponse } from '../../../../../containers/detection_engine/signals/types';
import * as i18n from '../translations';
import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types';
import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types';
import * as i18n from './translations';

export const formatSignalsData = (
signalsData: SignalSearchResponse<{}, SignalsAggregation> | null
Expand Down
Loading