mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[DataUsage][Serverless] Data usage metrics page enhancements (#195556)](https://github.com/elastic/kibana/pull/195556) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Ash","email":"1849116+ashokaditya@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-14T09:54:22Z","message":"[DataUsage][Serverless] Data usage metrics page enhancements (#195556)\n\n## Summary\r\n\r\nThis PR is a follow-up of elastic/kibana/pull/193966 and adds: \r\n\r\n1. Datastreams filter to data usage metrics page.\r\n2. Metrics filter (hidden for now) that lists out metric types to\r\nrequest.\r\n3. Refactors to make code easier to maintain.\r\n4. Shows a callout if no data stream is selected.\r\n\r\n### screen\r\n\r\n\r\n### clip\r\n\r\n\r\n\r\n### Checklist\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [ ] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [x] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [x] This renders correctly on smaller devices using a responsive\r\nlayout. (You can test this [in your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n- [x] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"a7332ad11611d224a16f2bb3c0d3f207cf746065","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","v8.16.0","backport:version"],"number":195556,"url":"https://github.com/elastic/kibana/pull/195556","mergeCommit":{"message":"[DataUsage][Serverless] Data usage metrics page enhancements (#195556)\n\n## Summary\r\n\r\nThis PR is a follow-up of elastic/kibana/pull/193966 and adds: \r\n\r\n1. Datastreams filter to data usage metrics page.\r\n2. Metrics filter (hidden for now) that lists out metric types to\r\nrequest.\r\n3. Refactors to make code easier to maintain.\r\n4. Shows a callout if no data stream is selected.\r\n\r\n### screen\r\n\r\n\r\n### clip\r\n\r\n\r\n\r\n### Checklist\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [ ] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [x] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [x] This renders correctly on smaller devices using a responsive\r\nlayout. (You can test this [in your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n- [x] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"a7332ad11611d224a16f2bb3c0d3f207cf746065"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195556","number":195556,"mergeCommit":{"message":"[DataUsage][Serverless] Data usage metrics page enhancements (#195556)\n\n## Summary\r\n\r\nThis PR is a follow-up of elastic/kibana/pull/193966 and adds: \r\n\r\n1. Datastreams filter to data usage metrics page.\r\n2. Metrics filter (hidden for now) that lists out metric types to\r\nrequest.\r\n3. Refactors to make code easier to maintain.\r\n4. Shows a callout if no data stream is selected.\r\n\r\n### screen\r\n\r\n\r\n### clip\r\n\r\n\r\n\r\n### Checklist\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [ ] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [x] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [x] This renders correctly on smaller devices using a responsive\r\nlayout. (You can test this [in your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n- [x] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"a7332ad11611d224a16f2bb3c0d3f207cf746065"}},{"branch":"8.x","label":"v8.16.0","labelRegex":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
84faa5c7e9
commit
06a2faa6a9
24 changed files with 1083 additions and 230 deletions
|
@ -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<typeof DataStreamsResponseSchema.body>;
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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<Record<MetricTypes, string>>({
|
||||
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<typeof UsageMetricsRequestSchema>;
|
||||
export type UsageMetricsRequestBody = TypeOf<typeof UsageMetricsRequestSchema>;
|
||||
|
||||
export const UsageMetricsResponseSchema = {
|
||||
body: () =>
|
||||
|
|
|
@ -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 }) => (
|
||||
<EuiFlexItem css={EuiItemCss}>{children}</EuiFlexItem>
|
||||
));
|
||||
|
||||
export const DataUsageMetrics = () => {
|
||||
const {
|
||||
services: { chrome, appParams },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const {
|
||||
metricTypes: metricTypesFromUrl,
|
||||
dataStreams: dataStreamsFromUrl,
|
||||
startDate: startDateFromUrl,
|
||||
endDate: endDateFromUrl,
|
||||
setUrlMetricTypesFilter,
|
||||
setUrlDateRangeFilter,
|
||||
} = useDataUsageMetricsUrlParams();
|
||||
|
||||
const [metricsFilters, setMetricsFilters] = useState<UsageMetricsRequestBody>({
|
||||
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 (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column">
|
||||
<FlexItemWithCss>
|
||||
<ChartFilters
|
||||
dateRangePickerState={dateRangePickerState}
|
||||
isDataLoading={isFetching}
|
||||
onClick={refetchDataUsageMetrics}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={onRefreshChange}
|
||||
onTimeChange={onTimeChange}
|
||||
onChangeDataStreamsFilter={onChangeDataStreamsFilter}
|
||||
onChangeMetricTypesFilter={onChangeMetricTypesFilter}
|
||||
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} />
|
||||
) : isFetching ? (
|
||||
<EuiLoadingElastic />
|
||||
) : null}
|
||||
</FlexItemWithCss>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -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<string[]>((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 (
|
||||
<ChartsFilterPopover
|
||||
closePopover={onClosePopover}
|
||||
filterName={filterName}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
numActiveFilters={numActiveFilters}
|
||||
numFilters={numFilters}
|
||||
onButtonClick={onPopoverButtonClick}
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiSelectable
|
||||
aria-label={`${filterName}`}
|
||||
emptyMessage={UX_LABELS.filterEmptyMessage(filterName)}
|
||||
isLoading={isLoading}
|
||||
onChange={onOptionsChange}
|
||||
options={sortedHostsFilterOptions}
|
||||
searchable={isSearchable ? true : undefined}
|
||||
searchProps={{
|
||||
placeholder: getSearchPlaceholder(filterName),
|
||||
compressed: true,
|
||||
onChange: (searchValue) => setSearchString(searchValue.trim()),
|
||||
}}
|
||||
>
|
||||
{(list, search) => {
|
||||
return (
|
||||
<div
|
||||
style={{ width: 300 }}
|
||||
data-test-subj={getTestId(`${filterName}-filter-popoverList`)}
|
||||
>
|
||||
{isSearchable && (
|
||||
<EuiPopoverTitle
|
||||
data-test-subj={getTestId(`${filterName}-filter-search`)}
|
||||
paddingSize="s"
|
||||
>
|
||||
{search}
|
||||
</EuiPopoverTitle>
|
||||
)}
|
||||
{list}
|
||||
{!isMetricsFilter && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<ClearAllButton
|
||||
data-test-subj={getTestId(`${filterName}-filter-clearAllButton`)}
|
||||
isDisabled={!hasActiveFilters}
|
||||
onClick={onClearAll}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</EuiSelectable>
|
||||
</ChartsFilterPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChartsFilter.displayName = 'ChartsFilter';
|
|
@ -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(
|
||||
() => (
|
||||
<EuiFilterButton
|
||||
data-test-subj={getTestId(`${filterName}-filter-popoverButton`)}
|
||||
iconType="arrowDown"
|
||||
onClick={onButtonClick}
|
||||
isSelected={isPopoverOpen}
|
||||
numFilters={numFilters}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
numActiveFilters={numActiveFilters}
|
||||
>
|
||||
{FILTER_NAMES[filterName]}
|
||||
</EuiFilterButton>
|
||||
),
|
||||
[
|
||||
filterName,
|
||||
getTestId,
|
||||
hasActiveFilters,
|
||||
isPopoverOpen,
|
||||
numActiveFilters,
|
||||
numFilters,
|
||||
onButtonClick,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
closePopover={closePopover}
|
||||
id={filterGroupPopoverId}
|
||||
isOpen={isPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
{children}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChartsFilterPopover.displayName = 'ChartsFilterPopover';
|
|
@ -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<typeof useGetDataUsageMetrics>['refetch'];
|
||||
showMetricsTypesFilter?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const ChartFilters = memo<ChartFiltersProps>(
|
||||
({
|
||||
dateRangePickerState,
|
||||
isDataLoading,
|
||||
onClick,
|
||||
onChangeMetricTypesFilter,
|
||||
onChangeDataStreamsFilter,
|
||||
onRefresh,
|
||||
onRefreshChange,
|
||||
onTimeChange,
|
||||
showMetricsTypesFilter = false,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
|
||||
const filters = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{showMetricsTypesFilter && (
|
||||
<ChartsFilter
|
||||
filterName={'metricTypes'}
|
||||
onChangeFilterOptions={onChangeMetricTypesFilter}
|
||||
/>
|
||||
)}
|
||||
<ChartsFilter
|
||||
filterName={'dataStreams'}
|
||||
onChangeFilterOptions={onChangeDataStreamsFilter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [onChangeDataStreamsFilter, onChangeMetricTypesFilter, showMetricsTypesFilter]);
|
||||
|
||||
const onClickRefreshButton = useCallback(() => onClick(), [onClick]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup responsive gutterSize="m">
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiFilterGroup>{filters}</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<UsageMetricsDateRangePicker
|
||||
dateRangePickerState={dateRangePickerState}
|
||||
isDataLoading={isDataLoading}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={onRefreshChange}
|
||||
onTimeChange={onTimeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperUpdateButton
|
||||
data-test-subj={getTestId('super-refresh-button')}
|
||||
fill={false}
|
||||
isLoading={isDataLoading}
|
||||
onClick={onClickRefreshButton}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChartFilters.displayName = 'ChartFilters';
|
|
@ -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 (
|
||||
<EuiButtonEmpty
|
||||
css={buttonCss}
|
||||
data-test-subj={dataTestSubj}
|
||||
isDisabled={isDisabled}
|
||||
onClick={onClick}
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
>
|
||||
{UX_LABELS.filterClearAll}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ClearAllButton.displayName = 'ClearAllButton';
|
|
@ -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<UsageMetricsDateRangePickerProps>(
|
||||
({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
const { uiSettings } = kibana.services;
|
||||
const [commonlyUsedRanges] = useState(() => {
|
||||
|
@ -55,32 +53,22 @@ export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps
|
|||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
padding-bottom: ${euiTheme.size.l};
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" direction="row" responsive={false} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiSuperDatePicker
|
||||
isLoading={isDataLoading}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
end={dateRangePickerState.endDate}
|
||||
isPaused={!dateRangePickerState.autoRefreshOptions.enabled}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefreshChange={onRefreshChange}
|
||||
refreshInterval={dateRangePickerState.autoRefreshOptions.duration}
|
||||
onRefresh={onRefresh}
|
||||
recentlyUsedRanges={dateRangePickerState.recentlyUsedDateRanges}
|
||||
start={dateRangePickerState.startDate}
|
||||
showUpdateButton
|
||||
updateButtonProps={{ iconOnly: false, fill: false }}
|
||||
width="auto"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
<EuiSuperDatePicker
|
||||
isLoading={isDataLoading}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
end={dateRangePickerState.endDate}
|
||||
isPaused={!dateRangePickerState.autoRefreshOptions.enabled}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefreshChange={onRefreshChange}
|
||||
refreshInterval={dateRangePickerState.autoRefreshOptions.duration}
|
||||
onRefresh={onRefresh}
|
||||
recentlyUsedRanges={dateRangePickerState.recentlyUsedDateRanges}
|
||||
start={dateRangePickerState.startDate}
|
||||
showUpdateButton={false}
|
||||
updateButtonProps={{ iconOnly: false, fill: false }}
|
||||
width="auto"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
69
x-pack/plugins/data_usage/public/app/components/page.tsx
Normal file
69
x-pack/plugins/data_usage/public/app/components/page.tsx
Normal file
|
@ -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<PropsWithChildren<DataUsagePageProps & CommonProps>>(
|
||||
({ title, subtitle, children, restrictWidth = false, hasBottomBorder = true, ...otherProps }) => {
|
||||
const header = useMemo(() => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<span data-test-subj="dataUsage-page-title">{title}</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [, title]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
return subtitle ? (
|
||||
<span data-test-subj="dataUsage-page-description">{subtitle}</span>
|
||||
) : undefined;
|
||||
}, [subtitle]);
|
||||
|
||||
return (
|
||||
<div {...otherProps}>
|
||||
<>
|
||||
<EuiPageHeader
|
||||
pageTitle={header}
|
||||
description={description}
|
||||
bottomBorder={hasBottomBorder}
|
||||
restrictWidth={restrictWidth}
|
||||
data-test-subj={'dataUsage-page-header'}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
<EuiPageSection paddingSize="none" color="transparent" restrictWidth={restrictWidth}>
|
||||
{children}
|
||||
</EuiPageSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DataUsagePage.displayName = 'DataUsagePage';
|
|
@ -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<UsageMetricsRequestSchemaQueryParams>({
|
||||
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 <div>{error.body.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="l">
|
||||
<h2>
|
||||
{i18n.translate('xpack.dataUsage.pageTitle', {
|
||||
defaultMessage: 'Data Usage',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPageSection paddingSize="none">
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.dataUsage.description"
|
||||
defaultMessage="Monitor data ingested and retained by data streams."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UsageMetricsDateRangePicker
|
||||
dateRangePickerState={dateRangePickerState}
|
||||
isDataLoading={isFetching}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={onRefreshChange}
|
||||
onTimeChange={onTimeChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
{isFetched && data ? <Charts data={data} /> : <EuiLoadingElastic />}
|
||||
</EuiPageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<DataUsagePage
|
||||
data-test-subj="DataUsagePage"
|
||||
title={DATA_USAGE_PAGE.title}
|
||||
subtitle={DATA_USAGE_PAGE.subTitle}
|
||||
>
|
||||
<DataUsageMetrics />
|
||||
</DataUsagePage>
|
||||
);
|
||||
};
|
||||
|
||||
DataUsageMetricsPage.displayName = 'DataUsageMetricsPage';
|
|
@ -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';
|
123
x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx
Normal file
123
x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx
Normal file
|
@ -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<React.SetStateAction<FilterItems>>;
|
||||
hasActiveFilters: boolean;
|
||||
numActiveFilters: number;
|
||||
numFilters: number;
|
||||
setAreDataStreamsSelectedOnMount: (value: React.SetStateAction<boolean>) => 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<boolean>(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<FilterItems>(
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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: {
|
||||
|
|
54
x-pack/plugins/data_usage/public/app/translations.tsx
Normal file
54
x-pack/plugins/data_usage/public/app/translations.tsx
Normal file
|
@ -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.',
|
||||
}),
|
||||
});
|
|
@ -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 = ({
|
|||
<Router history={params.history}>
|
||||
<PerformanceContextProvider>
|
||||
<Routes>
|
||||
<Route path="/" exact={true} component={DataUsage} />
|
||||
<Route path="/" exact={true} component={DataUsageMetricsPage} />
|
||||
</Routes>
|
||||
</PerformanceContextProvider>
|
||||
</Router>
|
||||
|
|
|
@ -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<GetDataUsageDataStreamsResponse, IHttpFetchError>;
|
||||
}): UseQueryResult<GetDataUsageDataStreamsResponse, IHttpFetchError> => {
|
||||
const http = useKibanaContextForPlugin().services.http;
|
||||
|
||||
return useQuery<GetDataUsageDataStreamsResponse, IHttpFetchError>({
|
||||
queryKey: ['get-data-usage-data-streams'],
|
||||
...options,
|
||||
keepPreviousData: true,
|
||||
queryFn: async () => {
|
||||
const dataStreamsResponse = await http.get<GetDataUsageDataStreamsResponse>(
|
||||
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
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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<UsageMetricsResponseSchemaBody, IHttpFetchError<ErrorType>> = {}
|
||||
): UseQueryResult<UsageMetricsResponseSchemaBody, IHttpFetchError<ErrorType>> => {
|
||||
const http = useKibanaContextForPlugin().services.http;
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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<MeteringStatsResponse>({
|
||||
method: 'GET',
|
||||
path: '/_metering/stats',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDataStreamsHandler = (
|
||||
dataUsageContext: DataUsageContext
|
||||
): RequestHandler<never, unknown, DataUsageRequestHandlerContext> => {
|
||||
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 { datastreams: meteringStats } = await getMeteringStats(
|
||||
core.elasticsearch.client.asSecondaryAuthUser
|
||||
);
|
||||
|
||||
const { data_streams: dataStreamsResponse } = await esClient.indices.dataStreamsStats({
|
||||
name: '*',
|
||||
expand_wildcards: 'all',
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
|
@ -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 = <T extends string>(value: T | T[]): T[] | MetricTypes[] =>
|
||||
typeof value === 'string' ? [value] : value;
|
||||
|
||||
export const getUsageMetricsHandler = (
|
||||
dataUsageContext: DataUsageContext
|
||||
): RequestHandler<
|
||||
never,
|
||||
UsageMetricsRequestSchemaQueryParams,
|
||||
unknown,
|
||||
DataUsageRequestHandlerContext
|
||||
> => {
|
||||
): RequestHandler<never, unknown, UsageMetricsRequestBody, DataUsageRequestHandlerContext> => {
|
||||
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}`);
|
||||
|
|
|
@ -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}]`;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue