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

[8.x] [DataUsage][Serverless] Data usage charts enhancements (#196559) #196996

Merged
merged 1 commit into from
Oct 23, 2024
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 @@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import numeral from '@elastic/numeral';

import { EuiFlexItem, EuiPanel, EuiTitle, useEuiTheme } from '@elastic/eui';
import {
Chart,
Expand All @@ -20,6 +20,7 @@ import {
import { i18n } from '@kbn/i18n';
import { LegendAction } from './legend_action';
import { MetricTypes, MetricSeries } from '../../../common/rest_types';
import { formatBytes } from '../../utils/format_bytes';

// TODO: Remove this when we have a title for each metric type
type ChartKey = Extract<MetricTypes, 'ingest_rate' | 'storage_retained'>;
Expand Down Expand Up @@ -118,7 +119,3 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
</EuiFlexItem>
);
};

const formatBytes = (bytes: number) => {
return numeral(bytes).format('0.0 b');
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* 2.0.
*/

import React, { useCallback, useEffect, memo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiCallOut } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
import { Charts } from './charts';
import { useBreadcrumbs } from '../../utils/use_breadcrumbs';
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
Expand All @@ -16,31 +16,40 @@ import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics';
import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params';
import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from '../hooks/use_date_picker';
import { DEFAULT_METRIC_TYPES, UsageMetricsRequestBody } from '../../../common/rest_types';
import { ChartFilters } from './filters/charts_filters';
import { UX_LABELS } from '../translations';
import { ChartFilters, ChartFiltersProps } from './filters/charts_filters';
import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams';

const EuiItemCss = css`
width: 100%;
`;

const FlexItemWithCss = memo(({ children }: { children: React.ReactNode }) => (
const FlexItemWithCss = ({ children }: { children: React.ReactNode }) => (
<EuiFlexItem css={EuiItemCss}>{children}</EuiFlexItem>
));
);

export const DataUsageMetrics = () => {
const {
services: { chrome, appParams },
} = useKibanaContextForPlugin();
useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);

const {
metricTypes: metricTypesFromUrl,
dataStreams: dataStreamsFromUrl,
startDate: startDateFromUrl,
endDate: endDateFromUrl,
setUrlMetricTypesFilter,
setUrlDataStreamsFilter,
setUrlDateRangeFilter,
} = useDataUsageMetricsUrlParams();

const { data: dataStreams, isFetching: isFetchingDataStreams } = useGetDataUsageDataStreams({
selectedDataStreams: dataStreamsFromUrl,
options: {
enabled: true,
},
});

const [metricsFilters, setMetricsFilters] = useState<UsageMetricsRequestBody>({
metricTypes: [...DEFAULT_METRIC_TYPES],
dataStreams: [],
Expand All @@ -52,15 +61,22 @@ export const DataUsageMetrics = () => {
if (!metricTypesFromUrl) {
setUrlMetricTypesFilter(metricsFilters.metricTypes.join(','));
}
if (!dataStreamsFromUrl && dataStreams) {
setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(','));
}
if (!startDateFromUrl || !endDateFromUrl) {
setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to });
}
}, [
dataStreams,
dataStreamsFromUrl,
endDateFromUrl,
metricTypesFromUrl,
metricsFilters.dataStreams,
metricsFilters.from,
metricsFilters.metricTypes,
metricsFilters.to,
setUrlDataStreamsFilter,
setUrlDateRangeFilter,
setUrlMetricTypesFilter,
startDateFromUrl,
Expand All @@ -77,7 +93,6 @@ export const DataUsageMetrics = () => {
const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker();

const {
error,
data,
isFetching,
isFetched,
Expand All @@ -90,6 +105,7 @@ export const DataUsageMetrics = () => {
},
{
retry: false,
enabled: !!metricsFilters.dataStreams.length,
}
);

Expand All @@ -111,33 +127,51 @@ export const DataUsageMetrics = () => {
[setMetricsFilters]
);

useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);
const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => {
const dataStreamsOptions = dataStreams?.reduce<Record<string, number>>((acc, ds) => {
acc[ds.name] = ds.storageSizeBytes;
return acc;
}, {});

return {
dataStreams: {
filterName: 'dataStreams',
options: dataStreamsOptions ? Object.keys(dataStreamsOptions) : metricsFilters.dataStreams,
appendOptions: dataStreamsOptions,
selectedOptions: metricsFilters.dataStreams,
onChangeFilterOptions: onChangeDataStreamsFilter,
isFilterLoading: isFetchingDataStreams,
},
metricTypes: {
filterName: 'metricTypes',
options: metricsFilters.metricTypes,
onChangeFilterOptions: onChangeMetricTypesFilter,
},
};
}, [
dataStreams,
isFetchingDataStreams,
metricsFilters.dataStreams,
metricsFilters.metricTypes,
onChangeDataStreamsFilter,
onChangeMetricTypesFilter,
]);

return (
<EuiFlexGroup alignItems="flexStart" direction="column">
<FlexItemWithCss>
<ChartFilters
dateRangePickerState={dateRangePickerState}
isDataLoading={isFetching}
isDataLoading={isFetchingDataStreams}
onClick={refetchDataUsageMetrics}
onRefresh={onRefresh}
onRefreshChange={onRefreshChange}
onTimeChange={onTimeChange}
onChangeDataStreamsFilter={onChangeDataStreamsFilter}
onChangeMetricTypesFilter={onChangeMetricTypesFilter}
filterOptions={filterOptions}
showMetricsTypesFilter={false}
/>
</FlexItemWithCss>
{!isFetching && error?.message && (
<FlexItemWithCss>
<EuiCallOut
size="s"
title={UX_LABELS.noDataStreamsSelected}
iconType="iInCircle"
color="warning"
/>
</FlexItemWithCss>
)}

<FlexItemWithCss>
{isFetched && data?.metrics ? (
<Charts data={data} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

import { orderBy } from 'lodash/fp';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiSelectable } from '@elastic/eui';
import { EuiPopoverTitle, EuiSelectable } from '@elastic/eui';

import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
import {
METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP,
type MetricTypes,
} from '../../../../common/rest_types';

import { ClearAllButton } from './clear_all_button';
import { UX_LABELS } from '../../translations';
import { ChartsFilterPopover } from './charts_filter_popover';
import { FilterItems, FilterName, useChartsFilter } from '../../hooks';
Expand All @@ -27,20 +26,34 @@ const getSearchPlaceholder = (filterName: FilterName) => {
return UX_LABELS.filterSearchPlaceholder('metric types');
};

export const ChartsFilter = memo(
export interface ChartsFilterProps {
filterOptions: {
filterName: FilterName;
options: string[];
appendOptions?: Record<string, number>;
selectedOptions?: string[];
onChangeFilterOptions: (selectedOptions: string[]) => void;
isFilterLoading?: boolean;
};
'data-test-subj'?: string;
}

export const ChartsFilter = memo<ChartsFilterProps>(
({
filterName,
onChangeFilterOptions,
filterOptions: {
filterName,
options,
appendOptions,
selectedOptions,
onChangeFilterOptions,
isFilterLoading = false,
},
'data-test-subj': dataTestSubj,
}: {
filterName: FilterName;
onChangeFilterOptions?: (selectedOptions: string[]) => void;
'data-test-subj'?: string;
}) => {
const getTestId = useTestIdGenerator(dataTestSubj);

const isMetricsFilter = filterName === 'metricTypes';
const isDataStreamsFilter = filterName === 'dataStreams';

// popover states and handlers
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onPopoverButtonClick = useCallback(() => {
Expand All @@ -50,11 +63,8 @@ export const ChartsFilter = memo(
setIsPopoverOpen(false);
}, [setIsPopoverOpen]);

// search string state
const [searchString, setSearchString] = useState('');
const {
areDataStreamsSelectedOnMount,
isLoading,
items,
setItems,
hasActiveFilters,
Expand All @@ -64,17 +74,18 @@ export const ChartsFilter = memo(
setUrlDataStreamsFilter,
setUrlMetricTypesFilter,
} = useChartsFilter({
filterName,
searchString,
filterOptions: {
filterName,
options,
appendOptions,
selectedOptions,
onChangeFilterOptions,
isFilterLoading,
},
});

// track popover state to pin selected options
const wasPopoverOpen = useRef(isPopoverOpen);
useEffect(() => {
return () => {
wasPopoverOpen.current = isPopoverOpen;
};
}, [isPopoverOpen, wasPopoverOpen]);

// compute if selected dataStreams should be pinned
const shouldPinSelectedDataStreams = useCallback(
Expand Down Expand Up @@ -104,8 +115,16 @@ export const ChartsFilter = memo(

const onOptionsChange = useCallback(
(newOptions: FilterItems) => {
const optionItemsToSet = newOptions.map((option) => option);
const currChecks = optionItemsToSet.filter((option) => option.checked === 'on');

// don't update filter state if trying to uncheck all options
if (currChecks.length < 1) {
return;
}

// update filter UI options state
setItems(newOptions.map((option) => option));
setItems(optionItemsToSet);

// compute a selected list of options
const selectedItems = newOptions.reduce<string[]>((acc, curr) => {
Expand All @@ -129,10 +148,7 @@ export const ChartsFilter = memo(
shouldPinSelectedDataStreams(false);
setAreDataStreamsSelectedOnMount(false);

// update overall query state
if (typeof onChangeFilterOptions !== 'undefined') {
onChangeFilterOptions(selectedItems);
}
onChangeFilterOptions(selectedItems);
},
[
setItems,
Expand All @@ -146,35 +162,11 @@ export const ChartsFilter = memo(
]
);

// clear all selected options
const onClearAll = useCallback(() => {
// update filter UI options state
setItems(
items.map((option) => {
option.checked = undefined;
return option;
})
);

// update URL params based on filter on page
if (isMetricsFilter) {
setUrlMetricTypesFilter('');
} else if (isDataStreamsFilter) {
setUrlDataStreamsFilter('');
}

if (typeof onChangeFilterOptions !== 'undefined') {
onChangeFilterOptions([]);
}
}, [
setItems,
items,
isMetricsFilter,
isDataStreamsFilter,
onChangeFilterOptions,
setUrlMetricTypesFilter,
setUrlDataStreamsFilter,
]);
useEffect(() => {
return () => {
wasPopoverOpen.current = isPopoverOpen;
};
}, [isPopoverOpen, wasPopoverOpen]);

return (
<ChartsFilterPopover
Expand All @@ -190,14 +182,13 @@ export const ChartsFilter = memo(
<EuiSelectable
aria-label={`${filterName}`}
emptyMessage={UX_LABELS.filterEmptyMessage(filterName)}
isLoading={isLoading}
isLoading={isFilterLoading}
onChange={onOptionsChange}
options={sortedHostsFilterOptions}
searchable={isSearchable ? true : undefined}
searchProps={{
placeholder: getSearchPlaceholder(filterName),
compressed: true,
onChange: (searchValue) => setSearchString(searchValue.trim()),
}}
>
{(list, search) => {
Expand All @@ -215,17 +206,6 @@ export const ChartsFilter = memo(
</EuiPopoverTitle>
)}
{list}
{!isMetricsFilter && (
<EuiFlexGroup>
<EuiFlexItem>
<ClearAllButton
data-test-subj={getTestId(`${filterName}-filter-clearAllButton`)}
isDisabled={!hasActiveFilters}
onClick={onClearAll}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
);
}}
Expand Down
Loading