diff --git a/x-pack/plugins/data_usage/common/rest_types/data_streams.ts b/x-pack/plugins/data_usage/common/rest_types/data_streams.ts index b1c02bb40854d..87af7e29eccb6 100644 --- a/x-pack/plugins/data_usage/common/rest_types/data_streams.ts +++ b/x-pack/plugins/data_usage/common/rest_types/data_streams.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; export const DataStreamsResponseSchema = { body: () => @@ -16,3 +16,5 @@ export const DataStreamsResponseSchema = { }) ), }; + +export type DataStreamsResponseBodySchemaBody = TypeOf; diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts index 473e64c6b03d9..e4feb438cc801 100644 --- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts @@ -41,7 +41,7 @@ describe('usage_metrics schemas', () => { ).not.toThrow(); }); - it('should error if `dataStream` list is empty', () => { + it('should not error if `dataStream` list is empty', () => { expect(() => UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), @@ -49,7 +49,7 @@ describe('usage_metrics schemas', () => { metricTypes: ['storage_retained'], dataStreams: [], }) - ).toThrowError('[dataStreams]: array size is [0], but cannot be smaller than [1]'); + ).not.toThrow(); }); it('should error if `dataStream` is given type not array', () => { @@ -71,7 +71,7 @@ describe('usage_metrics schemas', () => { metricTypes: ['storage_retained'], dataStreams: ['ds_1', ' '], }) - ).toThrow('[dataStreams]: [dataStreams] list cannot contain empty values'); + ).toThrow('[dataStreams]: list cannot contain empty values'); }); it('should error if `metricTypes` is empty string', () => { @@ -82,7 +82,7 @@ describe('usage_metrics schemas', () => { dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ' ', }) - ).toThrow(); + ).toThrow('[metricTypes]: could not parse array value from json input'); }); it('should error if `metricTypes` contains an empty item', () => { @@ -93,7 +93,7 @@ describe('usage_metrics schemas', () => { dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: [' ', 'storage_retained'], // First item is invalid }) - ).toThrowError(/list cannot contain empty values/); + ).toThrow('list cannot contain empty values'); }); it('should error if `metricTypes` is not a valid type', () => { @@ -116,7 +116,7 @@ describe('usage_metrics schemas', () => { metricTypes: ['storage_retained', 'foo'], }) ).toThrow( - '[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate' + '[metricTypes]: must be one of ingest_rate, storage_retained, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate' ); }); diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts index 3dceeadc198b0..40194494854fc 100644 --- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts @@ -7,9 +7,11 @@ import { schema, type TypeOf } from '@kbn/config-schema'; -const METRIC_TYPE_VALUES = [ - 'storage_retained', - 'ingest_rate', +// note these should be sorted alphabetically as we sort the URL params on the browser side +// before making the request, else the cache key will be different and that would invoke a new request +export const DEFAULT_METRIC_TYPES = ['ingest_rate', 'storage_retained'] as const; +export const METRIC_TYPE_VALUES = [ + ...DEFAULT_METRIC_TYPES, 'search_vcu', 'ingest_vcu', 'ml_vcu', @@ -21,6 +23,22 @@ const METRIC_TYPE_VALUES = [ export type MetricTypes = (typeof METRIC_TYPE_VALUES)[number]; +export const isDefaultMetricType = (metricType: string) => + // @ts-ignore + DEFAULT_METRIC_TYPES.includes(metricType); + +export const METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP = Object.freeze>({ + storage_retained: 'Data Retained in Storage', + ingest_rate: 'Data Ingested', + search_vcu: 'Search VCU', + ingest_vcu: 'Ingest VCU', + ml_vcu: 'ML VCU', + index_latency: 'Index Latency', + index_rate: 'Index Rate', + search_latency: 'Search Latency', + search_rate: 'Search Rate', +}); + // type guard for MetricTypes export const isMetricType = (type: string): type is MetricTypes => METRIC_TYPE_VALUES.includes(type as MetricTypes); @@ -47,21 +65,20 @@ export const UsageMetricsRequestSchema = schema.object({ if (trimmedValues.some((v) => !v.length)) { return '[metricTypes] list cannot contain empty values'; } else if (trimmedValues.some((v) => !isValidMetricType(v))) { - return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; + return `must be one of ${METRIC_TYPE_VALUES.join(', ')}`; } }, }), dataStreams: schema.arrayOf(schema.string(), { - minSize: 1, validate: (values) => { if (values.map((v) => v.trim()).some((v) => !v.length)) { - return '[dataStreams] list cannot contain empty values'; + return 'list cannot contain empty values'; } }, }), }); -export type UsageMetricsRequestSchemaQueryParams = TypeOf; +export type UsageMetricsRequestBody = TypeOf; export const UsageMetricsResponseSchema = { body: () => diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx new file mode 100644 index 0000000000000..cc443c78562ee --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx @@ -0,0 +1,150 @@ +/* + * 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, { useCallback, useEffect, memo, useState } from 'react'; +import { css } from '@emotion/react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiCallOut } from '@elastic/eui'; +import { Charts } from './charts'; +import { useBreadcrumbs } from '../../utils/use_breadcrumbs'; +import { useKibanaContextForPlugin } from '../../utils/use_kibana'; +import { PLUGIN_NAME } from '../../../common'; +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'; + +const EuiItemCss = css` + width: 100%; +`; + +const FlexItemWithCss = memo(({ children }: { children: React.ReactNode }) => ( + {children} +)); + +export const DataUsageMetrics = () => { + const { + services: { chrome, appParams }, + } = useKibanaContextForPlugin(); + + const { + metricTypes: metricTypesFromUrl, + dataStreams: dataStreamsFromUrl, + startDate: startDateFromUrl, + endDate: endDateFromUrl, + setUrlMetricTypesFilter, + setUrlDateRangeFilter, + } = useDataUsageMetricsUrlParams(); + + const [metricsFilters, setMetricsFilters] = useState({ + metricTypes: [...DEFAULT_METRIC_TYPES], + dataStreams: [], + from: DEFAULT_DATE_RANGE_OPTIONS.startDate, + to: DEFAULT_DATE_RANGE_OPTIONS.endDate, + }); + + useEffect(() => { + if (!metricTypesFromUrl) { + setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); + } + if (!startDateFromUrl || !endDateFromUrl) { + setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); + } + }, [ + endDateFromUrl, + metricTypesFromUrl, + metricsFilters.from, + metricsFilters.metricTypes, + metricsFilters.to, + setUrlDateRangeFilter, + setUrlMetricTypesFilter, + startDateFromUrl, + ]); + + useEffect(() => { + setMetricsFilters((prevState) => ({ + ...prevState, + metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, + dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, + })); + }, [metricTypesFromUrl, dataStreamsFromUrl]); + + const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); + + const { + error, + data, + isFetching, + isFetched, + refetch: refetchDataUsageMetrics, + } = useGetDataUsageMetrics( + { + ...metricsFilters, + from: dateRangePickerState.startDate, + to: dateRangePickerState.endDate, + }, + { + retry: false, + } + ); + + const onRefresh = useCallback(() => { + refetchDataUsageMetrics(); + }, [refetchDataUsageMetrics]); + + const onChangeDataStreamsFilter = useCallback( + (selectedDataStreams: string[]) => { + setMetricsFilters((prevState) => ({ ...prevState, dataStreams: selectedDataStreams })); + }, + [setMetricsFilters] + ); + + const onChangeMetricTypesFilter = useCallback( + (selectedMetricTypes: string[]) => { + setMetricsFilters((prevState) => ({ ...prevState, metricTypes: selectedMetricTypes })); + }, + [setMetricsFilters] + ); + + useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); + + return ( + + + + + {!isFetching && error?.message && ( + + + + )} + + {isFetched && data?.metrics ? ( + + ) : isFetching ? ( + + ) : null} + + + ); +}; diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx new file mode 100644 index 0000000000000..466bc6debae77 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx @@ -0,0 +1,238 @@ +/* + * 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 { orderBy } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, 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'; + +const getSearchPlaceholder = (filterName: FilterName) => { + if (filterName === 'dataStreams') { + return UX_LABELS.filterSearchPlaceholder('data streams'); + } + return UX_LABELS.filterSearchPlaceholder('metric types'); +}; + +export const ChartsFilter = memo( + ({ + filterName, + onChangeFilterOptions, + '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(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [setIsPopoverOpen, isPopoverOpen]); + const onClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, [setIsPopoverOpen]); + + // search string state + const [searchString, setSearchString] = useState(''); + const { + areDataStreamsSelectedOnMount, + isLoading, + items, + setItems, + hasActiveFilters, + numActiveFilters, + numFilters, + setAreDataStreamsSelectedOnMount, + setUrlDataStreamsFilter, + setUrlMetricTypesFilter, + } = useChartsFilter({ + filterName, + searchString, + }); + + // 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( + (isNotChangingOptions: boolean = true) => { + // case 1: when no dataStreams are selected initially + return ( + isNotChangingOptions && + wasPopoverOpen.current && + isPopoverOpen && + filterName === 'dataStreams' + ); + }, + [filterName, isPopoverOpen] + ); + + // augmented options based on the dataStreams filter + const sortedHostsFilterOptions = useMemo(() => { + if (shouldPinSelectedDataStreams() || areDataStreamsSelectedOnMount) { + // pin checked items to the top + return orderBy('checked', 'asc', items); + } + // return options as are for other filters + return items; + }, [areDataStreamsSelectedOnMount, shouldPinSelectedDataStreams, items]); + + const isSearchable = useMemo(() => !isMetricsFilter, [isMetricsFilter]); + + const onOptionsChange = useCallback( + (newOptions: FilterItems) => { + // update filter UI options state + setItems(newOptions.map((option) => option)); + + // compute a selected list of options + const selectedItems = newOptions.reduce((acc, curr) => { + if (curr.checked === 'on' && curr.key) { + acc.push(curr.key); + } + return acc; + }, []); + + // update URL params + if (isMetricsFilter) { + setUrlMetricTypesFilter( + selectedItems + .map((item) => METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP[item as MetricTypes]) + .join() + ); + } else if (isDataStreamsFilter) { + setUrlDataStreamsFilter(selectedItems.join()); + } + // reset shouldPinSelectedDataStreams, setAreDataStreamsSelectedOnMount + shouldPinSelectedDataStreams(false); + setAreDataStreamsSelectedOnMount(false); + + // update overall query state + if (typeof onChangeFilterOptions !== 'undefined') { + onChangeFilterOptions(selectedItems); + } + }, + [ + setItems, + isMetricsFilter, + isDataStreamsFilter, + shouldPinSelectedDataStreams, + setAreDataStreamsSelectedOnMount, + onChangeFilterOptions, + setUrlMetricTypesFilter, + setUrlDataStreamsFilter, + ] + ); + + // 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, + ]); + + return ( + + setSearchString(searchValue.trim()), + }} + > + {(list, search) => { + return ( +
+ {isSearchable && ( + + {search} + + )} + {list} + {!isMetricsFilter && ( + + + + + + )} +
+ ); + }} +
+
+ ); + } +); + +ChartsFilter.displayName = 'ChartsFilter'; diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx new file mode 100644 index 0000000000000..2ed96f012c497 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx @@ -0,0 +1,81 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { EuiFilterButton, EuiPopover, useGeneratedHtmlId } from '@elastic/eui'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import { type FilterName } from '../../hooks/use_charts_filter'; +import { FILTER_NAMES } from '../../translations'; + +export const ChartsFilterPopover = memo( + ({ + children, + closePopover, + filterName, + hasActiveFilters, + isPopoverOpen, + numActiveFilters, + numFilters, + onButtonClick, + 'data-test-subj': dataTestSubj, + }: { + children: React.ReactNode; + closePopover: () => void; + filterName: FilterName; + hasActiveFilters: boolean; + isPopoverOpen: boolean; + numActiveFilters: number; + numFilters: number; + onButtonClick: () => void; + 'data-test-subj'?: string; + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + const filterGroupPopoverId = useGeneratedHtmlId({ + prefix: 'filterGroupPopover', + }); + + const button = useMemo( + () => ( + + {FILTER_NAMES[filterName]} + + ), + [ + filterName, + getTestId, + hasActiveFilters, + isPopoverOpen, + numActiveFilters, + numFilters, + onButtonClick, + ] + ); + + return ( + + {children} + + ); + } +); + +ChartsFilterPopover.displayName = 'ChartsFilterPopover'; diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx new file mode 100644 index 0000000000000..72608f4a62c75 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx @@ -0,0 +1,93 @@ +/* + * 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, { memo, useCallback, useMemo } from 'react'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiSuperUpdateButton } from '@elastic/eui'; +import type { + DurationRange, + OnRefreshChangeProps, +} from '@elastic/eui/src/components/date_picker/types'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import { useGetDataUsageMetrics } from '../../../hooks/use_get_usage_metrics'; +import { DateRangePickerValues, UsageMetricsDateRangePicker } from './date_picker'; +import { ChartsFilter } from './charts_filter'; + +interface ChartFiltersProps { + dateRangePickerState: DateRangePickerValues; + isDataLoading: boolean; + onChangeDataStreamsFilter: (selectedDataStreams: string[]) => void; + onChangeMetricTypesFilter?: (selectedMetricTypes: string[]) => void; + onRefresh: () => void; + onRefreshChange: (evt: OnRefreshChangeProps) => void; + onTimeChange: ({ start, end }: DurationRange) => void; + onClick: ReturnType['refetch']; + showMetricsTypesFilter?: boolean; + 'data-test-subj'?: string; +} + +export const ChartFilters = memo( + ({ + dateRangePickerState, + isDataLoading, + onClick, + onChangeMetricTypesFilter, + onChangeDataStreamsFilter, + onRefresh, + onRefreshChange, + onTimeChange, + showMetricsTypesFilter = false, + 'data-test-subj': dataTestSubj, + }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + const filters = useMemo(() => { + return ( + <> + {showMetricsTypesFilter && ( + + )} + + + ); + }, [onChangeDataStreamsFilter, onChangeMetricTypesFilter, showMetricsTypesFilter]); + + const onClickRefreshButton = useCallback(() => onClick(), [onClick]); + + return ( + + + {filters} + + + + + + + + + ); + } +); + +ChartFilters.displayName = 'ChartFilters'; diff --git a/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx b/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx new file mode 100644 index 0000000000000..afa4c2fe72917 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx @@ -0,0 +1,43 @@ +/* + * 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, { memo } from 'react'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { UX_LABELS } from '../../translations'; + +const buttonCss = css` + border-top: ${euiThemeVars.euiBorderThin}; + border-radius: 0; +`; +export const ClearAllButton = memo( + ({ + 'data-test-subj': dataTestSubj, + isDisabled, + onClick, + }: { + 'data-test-subj'?: string; + isDisabled: boolean; + onClick: () => void; + }) => { + return ( + + {UX_LABELS.filterClearAll} + + ); + } +); + +ClearAllButton.displayName = 'ClearAllButton'; diff --git a/x-pack/plugins/data_usage/public/app/components/date_picker.tsx b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx similarity index 61% rename from x-pack/plugins/data_usage/public/app/components/date_picker.tsx rename to x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx index ca29acf8c96a6..4d9b280d763ce 100644 --- a/x-pack/plugins/data_usage/public/app/components/date_picker.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx @@ -6,8 +6,7 @@ */ import React, { memo, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiSuperDatePicker } from '@elastic/eui'; import type { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public'; import type { EuiSuperDatePickerRecentRange } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -37,7 +36,6 @@ interface UsageMetricsDateRangePickerProps { export const UsageMetricsDateRangePicker = memo( ({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => { - const { euiTheme } = useEuiTheme(); const kibana = useKibana(); const { uiSettings } = kibana.services; const [commonlyUsedRanges] = useState(() => { @@ -55,32 +53,22 @@ export const UsageMetricsDateRangePicker = memo - - - - - - + ); } ); diff --git a/x-pack/plugins/data_usage/public/app/components/page.tsx b/x-pack/plugins/data_usage/public/app/components/page.tsx new file mode 100644 index 0000000000000..d7ff20f5e933f --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/page.tsx @@ -0,0 +1,69 @@ +/* + * 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 type { PropsWithChildren } from 'react'; +import React, { memo, useMemo } from 'react'; +import type { CommonProps } from '@elastic/eui'; +import { + EuiPageHeader, + EuiPageSection, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +interface DataUsagePageProps { + title: React.ReactNode; + subtitle?: React.ReactNode; + actions?: React.ReactNode; + restrictWidth?: boolean | number; + hasBottomBorder?: boolean; + hideHeader?: boolean; +} + +export const DataUsagePage = memo>( + ({ title, subtitle, children, restrictWidth = false, hasBottomBorder = true, ...otherProps }) => { + const header = useMemo(() => { + return ( + + + + {title} + + + + ); + }, [, title]); + + const description = useMemo(() => { + return subtitle ? ( + {subtitle} + ) : undefined; + }, [subtitle]); + + return ( +
+ <> + + + + + {children} + +
+ ); + } +); + +DataUsagePage.displayName = 'DataUsagePage'; diff --git a/x-pack/plugins/data_usage/public/app/data_usage.tsx b/x-pack/plugins/data_usage/public/app/data_usage.tsx deleted file mode 100644 index bea9f2b511a77..0000000000000 --- a/x-pack/plugins/data_usage/public/app/data_usage.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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, { useCallback, useEffect, useState } from 'react'; -import { - EuiTitle, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingElastic, - EuiPageSection, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { UsageMetricsRequestSchemaQueryParams } from '../../common/rest_types'; -import { Charts } from './components/charts'; -import { UsageMetricsDateRangePicker } from './components/date_picker'; -import { useBreadcrumbs } from '../utils/use_breadcrumbs'; -import { useKibanaContextForPlugin } from '../utils/use_kibana'; -import { PLUGIN_NAME } from '../../common'; -import { useGetDataUsageMetrics } from '../hooks/use_get_usage_metrics'; -import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from './hooks/use_date_picker'; -import { useDataUsageMetricsUrlParams } from './hooks/use_charts_url_params'; - -export const DataUsage = () => { - const { - services: { chrome, appParams }, - } = useKibanaContextForPlugin(); - - const { - metricTypes: metricTypesFromUrl, - dataStreams: dataStreamsFromUrl, - startDate: startDateFromUrl, - endDate: endDateFromUrl, - setUrlMetricTypesFilter, - setUrlDateRangeFilter, - } = useDataUsageMetricsUrlParams(); - - const [metricsFilters, setMetricsFilters] = useState({ - metricTypes: ['storage_retained', 'ingest_rate'], - // TODO: Replace with data streams from /data_streams api - dataStreams: [ - '.alerts-ml.anomaly-detection-health.alerts-default', - '.alerts-stack.alerts-default', - ], - from: DEFAULT_DATE_RANGE_OPTIONS.startDate, - to: DEFAULT_DATE_RANGE_OPTIONS.endDate, - }); - - useEffect(() => { - if (!metricTypesFromUrl) { - setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); - } - if (!startDateFromUrl || !endDateFromUrl) { - setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); - } - }, [ - endDateFromUrl, - metricTypesFromUrl, - metricsFilters.from, - metricsFilters.metricTypes, - metricsFilters.to, - setUrlDateRangeFilter, - setUrlMetricTypesFilter, - startDateFromUrl, - ]); - - useEffect(() => { - setMetricsFilters((prevState) => ({ - ...prevState, - metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, - dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, - })); - }, [metricTypesFromUrl, dataStreamsFromUrl]); - - const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); - - const { - error, - data, - isFetching, - isFetched, - refetch: refetchDataUsageMetrics, - } = useGetDataUsageMetrics( - { - ...metricsFilters, - from: dateRangePickerState.startDate, - to: dateRangePickerState.endDate, - }, - { - retry: false, - } - ); - - const onRefresh = useCallback(() => { - refetchDataUsageMetrics(); - }, [refetchDataUsageMetrics]); - - useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); - - // TODO: show a toast? - if (!isFetching && error?.body) { - return
{error.body.message}
; - } - - return ( - <> - -

- {i18n.translate('xpack.dataUsage.pageTitle', { - defaultMessage: 'Data Usage', - })} -

-
- - - - - - - - - - - - - - {isFetched && data ? : } - - - ); -}; diff --git a/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx b/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx new file mode 100644 index 0000000000000..69edb7a7f01ce --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/data_usage_metrics_page.tsx @@ -0,0 +1,25 @@ +/* + * 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'; +import { DataUsagePage } from './components/page'; +import { DATA_USAGE_PAGE } from './translations'; +import { DataUsageMetrics } from './components/data_usage_metrics'; + +export const DataUsageMetricsPage = () => { + return ( + + + + ); +}; + +DataUsageMetricsPage.displayName = 'DataUsageMetricsPage'; diff --git a/x-pack/plugins/data_usage/common/types.ts b/x-pack/plugins/data_usage/public/app/hooks/index.tsx similarity index 53% rename from x-pack/plugins/data_usage/common/types.ts rename to x-pack/plugins/data_usage/public/app/hooks/index.tsx index d80bae2458d09..984f9ebde46f0 100644 --- a/x-pack/plugins/data_usage/common/types.ts +++ b/x-pack/plugins/data_usage/public/app/hooks/index.tsx @@ -5,5 +5,6 @@ * 2.0. */ -// temporary type until agreed on -export type MetricKey = 'ingestedMax' | 'retainedMax'; +export { useChartsFilter, type FilterName, type FilterItems } from './use_charts_filter'; +export { useDataUsageMetricsUrlParams } from './use_charts_url_params'; +export { useDateRangePicker } from './use_date_picker'; diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx new file mode 100644 index 0000000000000..330c9a633396d --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx @@ -0,0 +1,123 @@ +/* + * 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 { useState, useEffect, useMemo } from 'react'; +import { + isDefaultMetricType, + METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP, + METRIC_TYPE_VALUES, +} from '../../../common/rest_types'; +import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; +import { FILTER_NAMES } from '../translations'; +import { useDataUsageMetricsUrlParams } from './use_charts_url_params'; + +export type FilterName = keyof typeof FILTER_NAMES; + +export type FilterItems = Array<{ + key?: string; + label: string; + isGroupLabel?: boolean; + checked?: 'on' | undefined; + 'data-test-subj'?: string; +}>; + +export const useChartsFilter = ({ + filterName, + searchString, +}: { + filterName: FilterName; + searchString: string; +}): { + areDataStreamsSelectedOnMount: boolean; + isLoading: boolean; + items: FilterItems; + setItems: React.Dispatch>; + hasActiveFilters: boolean; + numActiveFilters: number; + numFilters: number; + setAreDataStreamsSelectedOnMount: (value: React.SetStateAction) => void; + setUrlDataStreamsFilter: ReturnType< + typeof useDataUsageMetricsUrlParams + >['setUrlDataStreamsFilter']; + setUrlMetricTypesFilter: ReturnType< + typeof useDataUsageMetricsUrlParams + >['setUrlMetricTypesFilter']; +} => { + const { + dataStreams: selectedDataStreamsFromUrl, + setUrlMetricTypesFilter, + setUrlDataStreamsFilter, + } = useDataUsageMetricsUrlParams(); + const isMetricTypesFilter = filterName === 'metricTypes'; + const isDataStreamsFilter = filterName === 'dataStreams'; + const { data: dataStreams, isFetching } = useGetDataUsageDataStreams({ + searchString, + selectedDataStreams: selectedDataStreamsFromUrl, + }); + + // track the state of selected data streams via URL + // when the page is loaded via selected data streams on URL + const [areDataStreamsSelectedOnMount, setAreDataStreamsSelectedOnMount] = + useState(false); + + useEffect(() => { + if (selectedDataStreamsFromUrl && selectedDataStreamsFromUrl.length > 0) { + setAreDataStreamsSelectedOnMount(true); + } + // don't sync with changes to further selectedDataStreamsFromUrl + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // filter options + const [items, setItems] = useState( + isMetricTypesFilter + ? METRIC_TYPE_VALUES.map((metricType) => ({ + key: metricType, + label: METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP[metricType], + checked: isDefaultMetricType(metricType) ? 'on' : undefined, // default metrics are selected by default + disabled: isDefaultMetricType(metricType), + 'data-test-subj': `${filterName}-filter-option`, + })) + : [] + ); + + useEffect(() => { + if (isDataStreamsFilter && dataStreams) { + setItems( + dataStreams?.map((dataStream) => ({ + key: dataStream.name, + label: dataStream.name, + checked: dataStream.selected ? 'on' : undefined, + 'data-test-subj': `${filterName}-filter-option`, + })) + ); + } + }, [dataStreams, filterName, isDataStreamsFilter, setItems]); + + const hasActiveFilters = useMemo(() => !!items.find((item) => item.checked === 'on'), [items]); + const numActiveFilters = useMemo( + () => items.filter((item) => item.checked === 'on').length, + [items] + ); + const numFilters = useMemo( + () => items.filter((item) => item.key && item.checked !== 'on').length, + [items] + ); + + return { + areDataStreamsSelectedOnMount, + isLoading: isDataStreamsFilter && isFetching, + items, + setItems, + hasActiveFilters, + numActiveFilters, + numFilters, + setAreDataStreamsSelectedOnMount, + setUrlMetricTypesFilter, + setUrlDataStreamsFilter, + }; +}; diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx index b5407ae9e46d5..cc4bfd2376da1 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx @@ -11,7 +11,7 @@ import type { OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; import { useDataUsageMetricsUrlParams } from './use_charts_url_params'; -import { DateRangePickerValues } from '../components/date_picker'; +import { DateRangePickerValues } from '../components/filters/date_picker'; export const DEFAULT_DATE_RANGE_OPTIONS = Object.freeze({ autoRefreshOptions: { diff --git a/x-pack/plugins/data_usage/public/app/translations.tsx b/x-pack/plugins/data_usage/public/app/translations.tsx new file mode 100644 index 0000000000000..687cdcf499b0d --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/translations.tsx @@ -0,0 +1,54 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const FILTER_NAMES = Object.freeze({ + metricTypes: i18n.translate('xpack.dataUsage.metrics.filter.metricTypes', { + defaultMessage: 'Metric types', + }), + dataStreams: i18n.translate('xpack.dataUsage.metrics.filter.dataStreams', { + defaultMessage: 'Data streams', + }), +}); + +export const CHART_TITLES = Object.freeze({ + ingest_rate: i18n.translate('xpack.dataUsage.charts.ingestedMax', { + defaultMessage: 'Data Ingested', + }), + storage_retained: i18n.translate('xpack.dataUsage.charts.retainedMax', { + defaultMessage: 'Data Retained in Storage', + }), +}); + +export const DATA_USAGE_PAGE = Object.freeze({ + title: i18n.translate('xpack.dataUsage.name', { + defaultMessage: 'Data Usage', + }), + subTitle: i18n.translate('xpack.dataUsage.pageSubtitle', { + defaultMessage: 'Monitor data ingested and retained by data streams.', + }), +}); + +export const UX_LABELS = Object.freeze({ + filterClearAll: i18n.translate('xpack.dataUsage.metrics.filter.clearAll', { + defaultMessage: 'Clear all', + }), + filterSearchPlaceholder: (filterName: string) => + i18n.translate('xpack.dataUsage.metrics.filter.searchPlaceholder', { + defaultMessage: 'Search {filterName}', + values: { filterName }, + }), + filterEmptyMessage: (filterName: string) => + i18n.translate('xpack.dataUsage.metrics.filter.emptyMessage', { + defaultMessage: 'No {filterName} available', + values: { filterName }, + }), + noDataStreamsSelected: i18n.translate('xpack.dataUsage.metrics.noDataStreamsSelected', { + defaultMessage: 'Select one or more data streams to view data usage metrics.', + }), +}); diff --git a/x-pack/plugins/data_usage/public/application.tsx b/x-pack/plugins/data_usage/public/application.tsx index 054aae397e5e1..0e6cdc6192c7c 100644 --- a/x-pack/plugins/data_usage/public/application.tsx +++ b/x-pack/plugins/data_usage/public/application.tsx @@ -16,7 +16,7 @@ import { PerformanceContextProvider } from '@kbn/ebt-tools'; import { useKibanaContextForPluginProvider } from './utils/use_kibana'; import { DataUsageStartDependencies, DataUsagePublicStart } from './types'; import { PLUGIN_ID } from '../common'; -import { DataUsage } from './app/data_usage'; +import { DataUsageMetricsPage } from './app/data_usage_metrics_page'; import { DataUsageReactQueryClientProvider } from '../common/query_client'; export const renderApp = ( @@ -53,7 +53,7 @@ const AppWithExecutionContext = ({ - + diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts new file mode 100644 index 0000000000000..59b36e156a824 --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts @@ -0,0 +1,84 @@ +/* + * 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 type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../common'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; + +type GetDataUsageDataStreamsResponse = Array<{ + name: string; + selected: boolean; +}>; + +const PAGING_PARAMS = Object.freeze({ + default: 50, + all: 10000, +}); + +export const useGetDataUsageDataStreams = ({ + searchString, + selectedDataStreams, + options = {}, +}: { + searchString: string; + selectedDataStreams?: string[]; + options?: UseQueryOptions; +}): UseQueryResult => { + const http = useKibanaContextForPlugin().services.http; + + return useQuery({ + queryKey: ['get-data-usage-data-streams'], + ...options, + keepPreviousData: true, + queryFn: async () => { + const dataStreamsResponse = await http.get( + DATA_USAGE_DATA_STREAMS_API_ROUTE, + { + version: '1', + query: {}, + } + ); + + const augmentedDataStreamsBasedOnSelectedItems = dataStreamsResponse.reduce<{ + selected: GetDataUsageDataStreamsResponse; + rest: GetDataUsageDataStreamsResponse; + }>( + (acc, list) => { + const item = { + name: list.name, + }; + + if (selectedDataStreams?.includes(list.name)) { + acc.selected.push({ ...item, selected: true }); + } else { + acc.rest.push({ ...item, selected: false }); + } + + return acc; + }, + { selected: [], rest: [] } + ); + + let selectedDataStreamsCount = 0; + if (selectedDataStreams) { + selectedDataStreamsCount = selectedDataStreams.length; + } + + return [ + ...augmentedDataStreamsBasedOnSelectedItems.selected, + ...augmentedDataStreamsBasedOnSelectedItems.rest, + ].slice( + 0, + selectedDataStreamsCount >= PAGING_PARAMS.default + ? selectedDataStreamsCount + 10 + : PAGING_PARAMS.default + ); + }, + }); +}; diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 3d648eb183f07..3998c736c839e 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -8,10 +8,7 @@ import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import { - UsageMetricsRequestSchemaQueryParams, - UsageMetricsResponseSchemaBody, -} from '../../common/rest_types'; +import { UsageMetricsRequestBody, UsageMetricsResponseSchemaBody } from '../../common/rest_types'; import { DATA_USAGE_METRICS_API_ROUTE } from '../../common'; import { useKibanaContextForPlugin } from '../utils/use_kibana'; @@ -21,7 +18,7 @@ interface ErrorType { } export const useGetDataUsageMetrics = ( - body: UsageMetricsRequestSchemaQueryParams, + body: UsageMetricsRequestBody, options: UseQueryOptions> = {} ): UseQueryResult> => { const http = useKibanaContextForPlugin().services.http; diff --git a/x-pack/plugins/data_usage/public/hooks/use_test_id_generator.ts b/x-pack/plugins/data_usage/public/hooks/use_test_id_generator.ts new file mode 100644 index 0000000000000..662cb2abd9813 --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_test_id_generator.ts @@ -0,0 +1,19 @@ +/* + * 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 { useCallback } from 'react'; + +export const useTestIdGenerator = (prefix?: string): ((suffix?: string) => string | undefined) => { + return useCallback( + (suffix: string = ''): string | undefined => { + if (prefix) { + return `${prefix}${suffix ? `-${suffix}` : ''}`; + } + }, + [prefix] + ); +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts index 686edd0c4f4b7..5794d06f16ead 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts @@ -5,36 +5,50 @@ * 2.0. */ -import { RequestHandler } from '@kbn/core/server'; +import { type ElasticsearchClient, RequestHandler } from '@kbn/core/server'; import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types'; - import { errorHandler } from '../error_handler'; +export interface MeteringStats { + name: string; + num_docs: number; + size_in_bytes: number; +} + +interface MeteringStatsResponse { + datastreams: MeteringStats[]; +} + +const getMeteringStats = (client: ElasticsearchClient) => { + return client.transport.request({ + method: 'GET', + path: '/_metering/stats', + }); +}; + export const getDataStreamsHandler = ( dataUsageContext: DataUsageContext ): RequestHandler => { const logger = dataUsageContext.logFactory.get('dataStreamsRoute'); return async (context, _, response) => { - logger.debug(`Retrieving user data streams`); + logger.debug('Retrieving user data streams'); try { const core = await context.core; - const esClient = core.elasticsearch.client.asCurrentUser; - - const { data_streams: dataStreamsResponse } = await esClient.indices.dataStreamsStats({ - name: '*', - expand_wildcards: 'all', - }); + const { datastreams: meteringStats } = await getMeteringStats( + core.elasticsearch.client.asSecondaryAuthUser + ); - const sorted = dataStreamsResponse - .sort((a, b) => b.store_size_bytes - a.store_size_bytes) - .map((dataStream) => ({ - name: dataStream.data_stream, - storageSizeBytes: dataStream.store_size_bytes, + const body = meteringStats + .sort((a, b) => b.size_in_bytes - a.size_in_bytes) + .map((stat) => ({ + name: stat.name, + storageSizeBytes: stat.size_in_bytes, })); + return response.ok({ - body: sorted, + body, }); } catch (error) { return errorHandler(logger, response, error); diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts index 09e9f88721c63..2b68dc3d37a64 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts @@ -11,24 +11,20 @@ import { MetricTypes, UsageMetricsAutoOpsResponseSchema, UsageMetricsAutoOpsResponseSchemaBody, - UsageMetricsRequestSchemaQueryParams, + UsageMetricsRequestBody, UsageMetricsResponseSchemaBody, } from '../../../common/rest_types'; import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types'; import { errorHandler } from '../error_handler'; +import { CustomHttpRequestError } from '../../utils'; const formatStringParams = (value: T | T[]): T[] | MetricTypes[] => typeof value === 'string' ? [value] : value; export const getUsageMetricsHandler = ( dataUsageContext: DataUsageContext -): RequestHandler< - never, - UsageMetricsRequestSchemaQueryParams, - unknown, - DataUsageRequestHandlerContext -> => { +): RequestHandler => { const logger = dataUsageContext.logFactory.get('usageMetricsRoute'); return async (context, request, response) => { @@ -36,8 +32,16 @@ export const getUsageMetricsHandler = ( const core = await context.core; const esClient = core.elasticsearch.client.asCurrentUser; - const { from, to, metricTypes, dataStreams: requestDsNames } = request.query; logger.debug(`Retrieving usage metrics`); + const { from, to, metricTypes, dataStreams: requestDsNames } = request.body; + + if (!requestDsNames?.length) { + return errorHandler( + logger, + response, + new CustomHttpRequestError('[request body.dataStreams]: no data streams selected', 400) + ); + } const { data_streams: dataStreamsResponse }: IndicesGetDataStreamResponse = await esClient.indices.getDataStream({ @@ -52,10 +56,10 @@ export const getUsageMetricsHandler = ( dataStreams: formatStringParams(dataStreamsResponse.map((ds) => ds.name)), }); - const processedMetrics = transformMetricsData(metrics); + const body = transformMetricsData(metrics); return response.ok({ - body: processedMetrics, + body, }); } catch (error) { logger.error(`Error retrieving usage metrics: ${error.message}`); diff --git a/x-pack/plugins/data_usage/server/services/autoops_api.ts b/x-pack/plugins/data_usage/server/services/autoops_api.ts index 5f3a636301f87..a61815a367949 100644 --- a/x-pack/plugins/data_usage/server/services/autoops_api.ts +++ b/x-pack/plugins/data_usage/server/services/autoops_api.ts @@ -12,15 +12,12 @@ import apm from 'elastic-apm-node'; import type { AxiosError, AxiosRequestConfig } from 'axios'; import axios from 'axios'; import { LogMeta } from '@kbn/core/server'; -import { - UsageMetricsRequestSchemaQueryParams, - UsageMetricsResponseSchemaBody, -} from '../../common/rest_types'; +import { UsageMetricsResponseSchemaBody } from '../../common/rest_types'; import { appContextService } from '../app_context'; import { AutoOpsConfig } from '../types'; class AutoOpsAPIService { - public async autoOpsUsageMetricsAPI(requestBody: UsageMetricsRequestSchemaQueryParams) { + public async autoOpsUsageMetricsAPI(requestBody: UsageMetricsResponseSchemaBody) { const logger = appContextService.getLogger().get(); const traceId = apm.currentTransaction?.traceparent; const withRequestIdMessage = (message: string) => `${message} [Request Id: ${traceId}]`; diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index 7ca0276c1f497..6d3818b88b9fe 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -24,11 +24,11 @@ "@kbn/logging", "@kbn/deeplinks-observability", "@kbn/unified-search-plugin", - "@kbn/i18n-react", "@kbn/core-http-browser", "@kbn/core-chrome-browser", "@kbn/features-plugin", "@kbn/index-management-shared-types", + "@kbn/ui-theme", "@kbn/repo-info", "@kbn/cloud-plugin", "@kbn/server-http-tools",