Skip to content

Commit

Permalink
[DataUsage][Serverless] Data usage charts enhancements (#196559)
Browse files Browse the repository at this point in the history
## Summary

follow up of:
- /pull/195556

Adds a lot of enhancements to the datastream dropdown including:

- [x] shows storage sizes on the data stream dropdown
- [x] preselects all data streams on the first page load
- [x] updates selected data streams to URL params
- [x] selects data streams based on URL load
- [x] doesn't allow deselecting all data streams
- [x] cancels older API requests

### screen
![Screenshot 2024-10-16 at 16 57
43](https:/user-attachments/assets/38db2d93-f531-4269-88ea-51b4926b6a72)

### clip

![metrics-ux-16-10](https:/user-attachments/assets/7913d1b6-31df-48e6-a3a9-f4dad0dc1b1e)

related PRs
- /pull/193966 
 
### Checklist
- [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:/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
ashokaditya and kibanamachine authored Oct 18, 2024
1 parent 8bf08bf commit 13e19cb
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 195 deletions.
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

0 comments on commit 13e19cb

Please sign in to comment.