Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[DataUsage][Serverless] Data usage metrics page enhancements #195556

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion x-pack/plugins/data_usage/common/rest_types/data_streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { schema } from '@kbn/config-schema';
import { schema, TypeOf } from '@kbn/config-schema';

export const DataStreamsResponseSchema = {
body: () =>
Expand All @@ -16,3 +16,5 @@ export const DataStreamsResponseSchema = {
})
),
};

export type DataStreamsResponseBodySchemaBody = TypeOf<typeof DataStreamsResponseSchema.body>;
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ 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(),
to: new Date().toISOString(),
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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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'
);
});

Expand Down
31 changes: 24 additions & 7 deletions x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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);
Expand All @@ -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: () =>
Expand Down
150 changes: 150 additions & 0 deletions x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
Copy link
Contributor

@neptunian neptunian Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useGetDataUsageDataStreams needs to be called somewhere on this page first because we have to get the top 10 by default (looks like nothing is selected by default at the moment) and have them selected by default when the user first lands. dataStreams will be required for the /metrics endpoint so getting the data streams has to happen first so we can pass it to /metrics. Datastreams can instead be passed down as props to the ChartFilters? Does that make sense? Ideally we can call them in parallel but I think we agreed this simplifies things and avoids us having to "get all" data streams in /metrics. Might make more sense after my PR goes in. #195640

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>
);
};
Loading