Skip to content

Commit

Permalink
[DataUsage][Serverless] Data usage metrics page enhancements (elastic…
Browse files Browse the repository at this point in the history
…#195556)

## Summary

This PR is a follow-up of elastic/pull/193966 and adds: 

1. Datastreams filter to data usage metrics page.
2. Metrics filter (hidden for now) that lists out metric types to
request.
3. Refactors to make code easier to maintain.
4. Shows a callout if no data stream is selected.

### screen
![Screenshot 2024-10-09 at 17 36
32](https://github.com/user-attachments/assets/a0779c91-25ae-4a64-819e-bc8a626f1f96)

### clip

![latest-metrics-ux](https://github.com/user-attachments/assets/0f4b1a9b-d160-435b-917b-f59c3a5cc9f8)

### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
ashokaditya and kibanamachine authored Oct 14, 2024
1 parent f962cdc commit a7332ad
Show file tree
Hide file tree
Showing 24 changed files with 1,083 additions and 230 deletions.
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 = () => {
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

0 comments on commit a7332ad

Please sign in to comment.