mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[DataUsage][Serverless] data stream filter and date range filter fixes (#200731)
## Summary Adds changes that: 1. Shows more than 50 datastream options if present and pre-selects at most 50 on initial load. 2. Limits date range selection to 10 days from now. 3. Allows date/time selection to hours and minutes for ease of use. 4. Pre-selects start/end time range based on timezone offset from UTC on first load ### Checklist - [x] [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a85127605c
commit
e0129e0ea3
10 changed files with 67 additions and 93 deletions
|
@ -12,6 +12,8 @@ export const PLUGIN_NAME = i18n.translate('xpack.dataUsage.name', {
|
|||
defaultMessage: 'Data Usage',
|
||||
});
|
||||
|
||||
export const DEFAULT_SELECTED_OPTIONS = 50 as const;
|
||||
|
||||
export const DATA_USAGE_API_ROUTE_PREFIX = '/api/data_usage/';
|
||||
export const DATA_USAGE_METRICS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}metrics`;
|
||||
export const DATA_USAGE_DATA_STREAMS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}data_streams`;
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import userEvent, { type UserEvent } from '@testing-library/user-event';
|
||||
import { DataUsageMetrics } from './data_usage_metrics';
|
||||
|
@ -147,6 +145,13 @@ const getBaseMockedDataUsageMetrics = () => ({
|
|||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const generateDataStreams = (count: number) => {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
name: `.ds-${i}`,
|
||||
storageSizeBytes: 1024 ** 2 * (22 / 7),
|
||||
}));
|
||||
};
|
||||
|
||||
describe('DataUsageMetrics', () => {
|
||||
let user: UserEvent;
|
||||
const testId = 'test';
|
||||
|
@ -174,8 +179,9 @@ describe('DataUsageMetrics', () => {
|
|||
|
||||
it('should show date filter', () => {
|
||||
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
|
||||
expect(getByTestId(`${testIdFilter}-date-range`)).toBeTruthy();
|
||||
expect(getByTestId(`${testIdFilter}-date-range`).textContent).toContain('Last 24 hours');
|
||||
const dateFilter = getByTestId(`${testIdFilter}-date-range`);
|
||||
expect(dateFilter).toBeTruthy();
|
||||
expect(dateFilter.textContent).toContain('to');
|
||||
expect(getByTestId(`${testIdFilter}-super-refresh-button`)).toBeTruthy();
|
||||
});
|
||||
|
||||
|
@ -196,28 +202,7 @@ describe('DataUsageMetrics', () => {
|
|||
it('should show selected data streams on the filter', () => {
|
||||
mockUseGetDataUsageDataStreams.mockReturnValue({
|
||||
error: undefined,
|
||||
data: [
|
||||
{
|
||||
name: '.ds-1',
|
||||
storageSizeBytes: 10000,
|
||||
},
|
||||
{
|
||||
name: '.ds-2',
|
||||
storageSizeBytes: 20000,
|
||||
},
|
||||
{
|
||||
name: '.ds-3',
|
||||
storageSizeBytes: 10300,
|
||||
},
|
||||
{
|
||||
name: '.ds-4',
|
||||
storageSizeBytes: 23000,
|
||||
},
|
||||
{
|
||||
name: '.ds-5',
|
||||
storageSizeBytes: 23200,
|
||||
},
|
||||
],
|
||||
data: generateDataStreams(5),
|
||||
isFetching: false,
|
||||
});
|
||||
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
|
||||
|
@ -226,46 +211,35 @@ describe('DataUsageMetrics', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should show at most 50 selected data streams on the filter', async () => {
|
||||
mockUseGetDataUsageDataStreams.mockReturnValue({
|
||||
error: undefined,
|
||||
data: generateDataStreams(100),
|
||||
isFetching: false,
|
||||
});
|
||||
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
|
||||
const toggleFilterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`);
|
||||
|
||||
expect(toggleFilterButton).toHaveTextContent('Data streams50');
|
||||
});
|
||||
|
||||
it('should allow de-selecting all but one data stream option', async () => {
|
||||
mockUseGetDataUsageDataStreams.mockReturnValue({
|
||||
error: undefined,
|
||||
data: [
|
||||
{
|
||||
name: '.ds-1',
|
||||
storageSizeBytes: 10000,
|
||||
},
|
||||
{
|
||||
name: '.ds-2',
|
||||
storageSizeBytes: 20000,
|
||||
},
|
||||
{
|
||||
name: '.ds-3',
|
||||
storageSizeBytes: 10300,
|
||||
},
|
||||
{
|
||||
name: '.ds-4',
|
||||
storageSizeBytes: 23000,
|
||||
},
|
||||
{
|
||||
name: '.ds-5',
|
||||
storageSizeBytes: 23200,
|
||||
},
|
||||
],
|
||||
data: generateDataStreams(5),
|
||||
isFetching: false,
|
||||
});
|
||||
const { getByTestId, getAllByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
|
||||
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent(
|
||||
'Data streams5'
|
||||
);
|
||||
await user.click(getByTestId(`${testIdFilter}-dataStreams-popoverButton`));
|
||||
const toggleFilterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`);
|
||||
|
||||
expect(toggleFilterButton).toHaveTextContent('Data streams5');
|
||||
await user.click(toggleFilterButton);
|
||||
const allFilterOptions = getAllByTestId('dataStreams-filter-option');
|
||||
for (let i = 0; i < allFilterOptions.length - 1; i++) {
|
||||
await user.click(allFilterOptions[i]);
|
||||
}
|
||||
|
||||
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent(
|
||||
'Data streams1'
|
||||
);
|
||||
expect(toggleFilterButton).toHaveTextContent('Data streams1');
|
||||
});
|
||||
|
||||
it('should not call usage metrics API if no data streams', async () => {
|
||||
|
|
|
@ -72,7 +72,9 @@ export const DataUsageMetrics = memo(
|
|||
setUrlMetricTypesFilter(metricsFilters.metricTypes.join(','));
|
||||
}
|
||||
if (!dataStreamsFromUrl && dataStreams) {
|
||||
setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(','));
|
||||
const hasMoreThan50 = dataStreams.length > 50;
|
||||
const _dataStreams = hasMoreThan50 ? dataStreams.slice(0, 50) : dataStreams;
|
||||
setUrlDataStreamsFilter(_dataStreams.map((ds) => ds.name).join(','));
|
||||
}
|
||||
if (!startDateFromUrl || !endDateFromUrl) {
|
||||
setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to });
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
OnRefreshChangeProps,
|
||||
} from '@elastic/eui/src/components/date_picker/types';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import moment from 'moment';
|
||||
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
|
||||
|
||||
export interface DateRangePickerValues {
|
||||
|
@ -66,7 +67,7 @@ export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps
|
|||
<EuiSuperDatePicker
|
||||
data-test-subj={getTestId('date-range')}
|
||||
isLoading={isDataLoading}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
dateFormat={'MMM D, YYYY @ HH:mm'}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
end={dateRangePickerState.endDate}
|
||||
isPaused={!dateRangePickerState.autoRefreshOptions.enabled}
|
||||
|
@ -77,7 +78,11 @@ export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps
|
|||
recentlyUsedRanges={dateRangePickerState.recentlyUsedDateRanges}
|
||||
start={dateRangePickerState.startDate}
|
||||
showUpdateButton={false}
|
||||
timeFormat={'HH:mm'}
|
||||
updateButtonProps={{ iconOnly: false, fill: false }}
|
||||
utcOffset={moment().utcOffset() / 60}
|
||||
maxDate={moment()}
|
||||
minDate={moment().subtract(9, 'days').startOf('day')}
|
||||
width="auto"
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP,
|
||||
METRIC_TYPE_VALUES,
|
||||
} from '../../../common/rest_types';
|
||||
import { DEFAULT_SELECTED_OPTIONS } from '../../../common';
|
||||
import { FILTER_NAMES } from '../translations';
|
||||
import { useDataUsageMetricsUrlParams } from './use_charts_url_params';
|
||||
import { formatBytes } from '../../utils/format_bytes';
|
||||
|
@ -77,7 +78,7 @@ export const useChartsFilter = ({
|
|||
'data-test-subj': `${filterOptions.filterName}-filter-option`,
|
||||
}))
|
||||
: isDataStreamsFilter && !!filterOptions.options.length
|
||||
? filterOptions.options?.map((filterOption) => ({
|
||||
? filterOptions.options?.map((filterOption, i) => ({
|
||||
key: filterOption,
|
||||
label: filterOption,
|
||||
append: formatBytes(filterOptions.appendOptions?.[filterOption] ?? 0),
|
||||
|
@ -85,7 +86,9 @@ export const useChartsFilter = ({
|
|||
? selectedDataStreamsFromUrl.includes(filterOption)
|
||||
? 'on'
|
||||
: undefined
|
||||
: 'on',
|
||||
: i < DEFAULT_SELECTED_OPTIONS
|
||||
? 'on'
|
||||
: undefined,
|
||||
'data-test-subj': `${filterOptions.filterName}-filter-option`,
|
||||
}))
|
||||
: []
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { METRIC_TYPE_VALUES, MetricTypes } from '../../../common/rest_types';
|
||||
import { getDataUsageMetricsFiltersFromUrlParams } from './use_charts_url_params';
|
||||
|
||||
|
@ -56,12 +57,12 @@ describe('#getDataUsageMetricsFiltersFromUrlParams', () => {
|
|||
it('should use given relative startDate and endDate values URL params', () => {
|
||||
expect(
|
||||
getDataUsageMetricsFiltersFromUrlParams({
|
||||
startDate: 'now-24h/h',
|
||||
endDate: 'now',
|
||||
startDate: moment().subtract(24, 'hours').toISOString(),
|
||||
endDate: moment().toISOString(),
|
||||
})
|
||||
).toEqual({
|
||||
endDate: 'now',
|
||||
startDate: 'now-24h/h',
|
||||
endDate: moment().toISOString(),
|
||||
startDate: moment().subtract(24, 'hours').toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type {
|
||||
DurationRange,
|
||||
|
@ -18,8 +19,8 @@ export const DEFAULT_DATE_RANGE_OPTIONS = Object.freeze({
|
|||
enabled: false,
|
||||
duration: 10000,
|
||||
},
|
||||
startDate: 'now-24h/h',
|
||||
endDate: 'now',
|
||||
startDate: moment().subtract(24, 'hours').startOf('day').toISOString(),
|
||||
endDate: moment().toISOString(),
|
||||
recentlyUsedDateRanges: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +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 { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../common';
|
||||
import { DATA_USAGE_DATA_STREAMS_API_ROUTE, DEFAULT_SELECTED_OPTIONS } from '../../common';
|
||||
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||
|
||||
type GetDataUsageDataStreamsResponse = Array<{
|
||||
|
@ -17,11 +17,6 @@ type GetDataUsageDataStreamsResponse = Array<{
|
|||
selected: boolean;
|
||||
}>;
|
||||
|
||||
const PAGING_PARAMS = Object.freeze({
|
||||
default: 50,
|
||||
all: 10000,
|
||||
});
|
||||
|
||||
export const useGetDataUsageDataStreams = ({
|
||||
selectedDataStreams,
|
||||
options = {
|
||||
|
@ -51,14 +46,14 @@ export const useGetDataUsageDataStreams = ({
|
|||
selected: GetDataUsageDataStreamsResponse;
|
||||
rest: GetDataUsageDataStreamsResponse;
|
||||
}>(
|
||||
(acc, ds) => {
|
||||
(acc, ds, i) => {
|
||||
const item = {
|
||||
name: ds.name,
|
||||
storageSizeBytes: ds.storageSizeBytes,
|
||||
selected: ds.selected,
|
||||
};
|
||||
|
||||
if (selectedDataStreams?.includes(ds.name)) {
|
||||
if (selectedDataStreams?.includes(ds.name) && i < DEFAULT_SELECTED_OPTIONS) {
|
||||
acc.selected.push({ ...item, selected: true });
|
||||
} else {
|
||||
acc.rest.push({ ...item, selected: false });
|
||||
|
@ -69,20 +64,10 @@ export const useGetDataUsageDataStreams = ({
|
|||
{ 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
|
||||
);
|
||||
];
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
@ -41,8 +42,8 @@ jest.mock('../utils/use_kibana', () => {
|
|||
});
|
||||
|
||||
const defaultUsageMetricsRequestBody = {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
from: moment().subtract(15, 'minutes').toISOString(),
|
||||
to: moment().toISOString(),
|
||||
metricTypes: ['ingest_rate'],
|
||||
dataStreams: ['ds-1'],
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import type { MockedKeys } from '@kbn/utility-types-jest';
|
||||
import type { CoreSetup } from '@kbn/core/server';
|
||||
import { registerUsageMetricsRoute } from './usage_metrics';
|
||||
|
@ -56,8 +56,8 @@ describe('registerUsageMetricsRoute', () => {
|
|||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
from: moment().subtract(15, 'minutes').toISOString(),
|
||||
to: moment().toISOString(),
|
||||
metricTypes: ['ingest_rate'],
|
||||
dataStreams: [],
|
||||
},
|
||||
|
@ -123,8 +123,8 @@ describe('registerUsageMetricsRoute', () => {
|
|||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
from: moment().subtract(15, 'minutes').toISOString(),
|
||||
to: moment().toISOString(),
|
||||
metricTypes: ['ingest_rate', 'storage_retained'],
|
||||
dataStreams: ['.ds-1', '.ds-2'],
|
||||
},
|
||||
|
@ -191,8 +191,8 @@ describe('registerUsageMetricsRoute', () => {
|
|||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
from: moment().subtract(15, 'minutes').toISOString(),
|
||||
to: moment().toISOString(),
|
||||
metricTypes: ['ingest_rate'],
|
||||
dataStreams: ['.ds-1', '.ds-2'],
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue