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

[Serverless][DataUsage] Data usage UX/API updates #203465

Merged
merged 32 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a9ba306
left align filters
ashokaditya Dec 6, 2024
fc2a98a
update page subtitle
ashokaditya Dec 6, 2024
9a901f6
display invalid date range error
ashokaditya Dec 9, 2024
6d0db09
left align filters
ashokaditya Dec 6, 2024
b8330db
update page subtitle
ashokaditya Dec 6, 2024
6b3bd72
display invalid date range error
ashokaditya Dec 9, 2024
380bc1c
Merge branch 'task/data-usage-feedback-updates' of github.com:ashokad…
ashokaditya Dec 9, 2024
ab69ff7
fix test
ashokaditya Dec 9, 2024
ef6c783
data stream labels/tooltip
ashokaditya Dec 10, 2024
129b861
redundant plugin
ashokaditya Dec 10, 2024
31a1c32
page unit test
ashokaditya Dec 10, 2024
a7aeaea
cleanup
ashokaditya Dec 10, 2024
cb2f4c6
rename
ashokaditya Dec 10, 2024
4314f64
Revert "redundant plugin"
ashokaditya Dec 10, 2024
26d6beb
charts filter tests
ashokaditya Dec 10, 2024
3e40d14
i18n
ashokaditya Dec 10, 2024
7762247
fix type
ashokaditya Dec 10, 2024
5cce71e
fix FTR tests
ashokaditya Dec 10, 2024
04c786e
Merge branch 'main' into task/data-usage-feedback-updates
ashokaditya Dec 10, 2024
87dbe04
Merge branch 'main' into task/data-usage-feedback-updates
ashokaditya Dec 11, 2024
9f93453
cleanup types for data stream
ashokaditya Dec 11, 2024
b9305f0
refactor
ashokaditya Dec 11, 2024
80ab7f5
charts filter test for data streams filter
ashokaditya Dec 11, 2024
90f3bb6
exclude dot datastreams
ashokaditya Dec 11, 2024
44dce1c
Update charts_filter.test.tsx
ashokaditya Dec 11, 2024
4b7774c
Merge branch 'main' into task/data-usage-feedback-updates
ashokaditya Dec 11, 2024
7927b09
Merge branch 'main' into task/data-usage-feedback-updates
ashokaditya Dec 11, 2024
c2aa20e
Merge branch 'main' into task/data-usage-feedback-updates
ashokaditya Dec 12, 2024
e0c30a8
fix select all so it doesn't check group label
ashokaditya Dec 12, 2024
aa159e9
Merge branch 'main' into task/data-usage-feedback-updates
ashokaditya Dec 12, 2024
52cdefa
fix `selectAll`
ashokaditya Dec 12, 2024
4de3bbe
fix tests ids
ashokaditya Dec 12, 2024
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: 2 additions & 2 deletions x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const isDefaultMetricType = (metricType: string) =>
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',
storage_retained: 'Data Retained in Storage',
search_vcu: 'Search VCU',
ingest_vcu: 'Ingest VCU',
ml_vcu: 'ML VCU',
Expand All @@ -40,8 +40,8 @@ export const METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP = Object.freeze<Record<Met
});

export const METRIC_TYPE_UI_OPTIONS_VALUES_TO_API_MAP = Object.freeze<Record<string, MetricTypes>>({
'Data Retained in Storage': 'storage_retained',
'Data Ingested': 'ingest_rate',
'Data Retained in Storage': 'storage_retained',
'Search VCU': 'search_vcu',
'Ingest VCU': 'ingest_vcu',
'ML VCU': 'ml_vcu',
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/data_usage/common/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { isDateRangeValid } from './utils';
describe('isDateRangeValid', () => {
describe('Valid ranges', () => {
it.each([
['both start and end date is `now`', { start: 'now', end: 'now' }],
['start date is `now-10s` and end date is `now`', { start: 'now-10s', end: 'now' }],
['bounded within the min and max date range', { start: 'now-8d', end: 'now-4s' }],
])('should return true if %s', (_, { start, end }) => {
Expand All @@ -20,8 +19,10 @@ describe('isDateRangeValid', () => {

describe('Invalid ranges', () => {
it.each([
['both start and end date is `now`', { start: 'now', end: 'now' }],
['starts before the min date', { start: 'now-11d', end: 'now-5s' }],
['ends after the max date', { start: 'now-9d', end: 'now+2s' }],
['ends after the max date in seconds', { start: 'now-9d', end: 'now+2s' }],
['ends after the max date in days', { start: 'now-6d', end: 'now+6d' }],
[
'end date is before the start date even when both are within min and max date range',
{ start: 'now-3s', end: 'now-10s' },
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/data_usage/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const DEFAULT_DATE_RANGE_OPTIONS = Object.freeze({
recentlyUsedDateRanges: [],
});

export type ParsedDate = ReturnType<typeof momentDateParser>;
export const momentDateParser = (date: string) => dateMath.parse(date);
export const transformToUTCtime = ({
start,
Expand Down Expand Up @@ -50,6 +51,6 @@ export const isDateRangeValid = ({ start, end }: { start: string; end: string })
return (
startDate.isSameOrAfter(minDate, 's') &&
endDate.isSameOrBefore(maxDate, 's') &&
startDate <= endDate
startDate.isBefore(endDate, 's')
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DataUsageMetrics } from './data_usage_metrics';
import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics';
import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams';
import { coreMock as mockCore } from '@kbn/core/public/mocks';
import { mockUseKibana, generateDataStreams } from '../mocks';

jest.mock('../../utils/use_breadcrumbs', () => {
return {
Expand Down Expand Up @@ -60,60 +61,10 @@ jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...original,
useKibana: () => ({
services: {
uiSettings: {
get: jest.fn().mockImplementation((key) => {
const get = (k: 'dateFormat' | 'timepicker:quickRanges') => {
const x = {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
'timepicker:quickRanges': [
{
from: 'now/d',
to: 'now/d',
display: 'Today',
},
{
from: 'now/w',
to: 'now/w',
display: 'This week',
},
{
from: 'now-15m',
to: 'now',
display: 'Last 15 minutes',
},
{
from: 'now-30m',
to: 'now',
display: 'Last 30 minutes',
},
{
from: 'now-1h',
to: 'now',
display: 'Last 1 hour',
},
{
from: 'now-24h',
to: 'now',
display: 'Last 24 hours',
},
{
from: 'now-7d',
to: 'now',
display: 'Last 7 days',
},
],
};
return x[k];
};
return get(key);
}),
},
},
}),
useKibana: () => mockUseKibana,
};
});

const mockUseGetDataUsageMetrics = useGetDataUsageMetrics as jest.Mock;
const mockUseGetDataUsageDataStreams = useGetDataUsageDataStreams as jest.Mock;
const mockServices = mockCore.createStart();
Expand All @@ -131,13 +82,6 @@ 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';
Expand Down Expand Up @@ -228,14 +172,14 @@ describe('DataUsageMetrics', () => {
expect(toggleFilterButton).toHaveTextContent('Data streams10');
await user.click(toggleFilterButton);
const allFilterOptions = getAllByTestId('dataStreams-filter-option');
// deselect 9 options
for (let i = 0; i < allFilterOptions.length; i++) {
// deselect 3 options
for (let i = 0; i < 3; i++) {
await user.click(allFilterOptions[i]);
}

expect(toggleFilterButton).toHaveTextContent('Data streams1');
expect(toggleFilterButton).toHaveTextContent('Data streams7');
expect(within(toggleFilterButton).getByRole('marquee').getAttribute('aria-label')).toEqual(
'1 active filters'
'7 active filters'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,9 @@ import { PLUGIN_NAME } from '../../translations';
import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics';
import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams';
import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params';
import {
DEFAULT_DATE_RANGE_OPTIONS,
transformToUTCtime,
isDateRangeValid,
} from '../../../common/utils';
import { DEFAULT_DATE_RANGE_OPTIONS, transformToUTCtime } from '../../../common/utils';
import { useDateRangePicker } from '../hooks/use_date_picker';
import { ChartFilters, ChartFiltersProps } from './filters/charts_filters';
import { ChartsFilters, ChartsFiltersProps } from './filters/charts_filters';
import { ChartsLoading } from './charts_loading';
import { NoDataCallout } from './no_data_callout';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
Expand Down Expand Up @@ -114,16 +110,8 @@ export const DataUsageMetrics = memo(
}));
}, [metricTypesFromUrl, dataStreamsFromUrl, startDateFromUrl, endDateFromUrl]);

const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker();

const isValidDateRange = useMemo(
() =>
isDateRangeValid({
start: dateRangePickerState.startDate,
end: dateRangePickerState.endDate,
}),
[dateRangePickerState.endDate, dateRangePickerState.startDate]
);
const { dateRangePickerState, isValidDateRange, onRefreshChange, onTimeChange } =
useDateRangePicker();

const enableFetchUsageMetricsData = useMemo(
() =>
Expand Down Expand Up @@ -187,8 +175,10 @@ export const DataUsageMetrics = memo(
[setMetricsFilters]
);

const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => {
const dataStreamsOptions = dataStreams?.reduce<Record<string, number>>((acc, ds) => {
const filterOptions: ChartsFiltersProps['filterOptions'] = useMemo(() => {
const dataStreamsOptions = dataStreams?.reduce<
Required<ChartsFiltersProps['filterOptions']['dataStreams']>['appendOptions']
>((acc, ds) => {
acc[ds.name] = ds.storageSizeBytes;
return acc;
}, {});
Expand Down Expand Up @@ -239,10 +229,11 @@ export const DataUsageMetrics = memo(
return (
<EuiFlexGroup alignItems="flexStart" direction="column" data-test-subj={getTestId()}>
<FlexItemWithCss>
<ChartFilters
<ChartsFilters
dateRangePickerState={dateRangePickerState}
isDataLoading={isFetchingDataStreams}
isUpdateDisabled={!enableFetchUsageMetricsData}
isValidDateRange={isValidDateRange}
onClick={refetchDataUsageMetrics}
onRefresh={onRefresh}
onRefreshChange={onRefreshChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 { TestProvider } from '../../../../common/test_utils';
import { render, type RenderResult } from '@testing-library/react';
import userEvent, { type UserEvent } from '@testing-library/user-event';
import { ChartsFilter, type ChartsFilterProps } from './charts_filter';
import { FilterName } from '../../hooks';
import { mockUseKibana, generateDataStreams } from '../../mocks';

const mockUseLocation = jest.fn(() => ({ pathname: '/' }));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => mockUseLocation(),
useHistory: jest.fn().mockReturnValue({
push: jest.fn(),
listen: jest.fn(),
location: {
search: '',
},
}),
}));

jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...original,
useKibana: () => mockUseKibana,
};
});

describe('Charts Filters', () => {
let user: UserEvent;
const testId = 'test';
const testIdFilter = `${testId}-filter`;

const defaultProps = {
filterOptions: {
filterName: 'dataStreams' as FilterName,
isFilterLoading: false,
appendOptions: {},
selectedOptions: [],
options: generateDataStreams(8).map((ds) => ds.name),
onChangeFilterOptions: jest.fn(),
},
};

let renderComponent: (props: ChartsFilterProps) => RenderResult;

beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

beforeEach(() => {
jest.clearAllMocks();
renderComponent = (props: ChartsFilterProps) =>
render(
<TestProvider>
<ChartsFilter data-test-subj={testIdFilter} {...props} />
</TestProvider>
);
user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, pointerEventsCheck: 0 });
});

it('renders data streams filter with all options selected', async () => {
const { getByTestId, getAllByTestId } = renderComponent(defaultProps);
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy();

const filterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`);
expect(filterButton).toBeTruthy();
await user.click(filterButton);
const allFilterOptions = getAllByTestId('dataStreams-filter-option');

// checked options
const checkedOptions = allFilterOptions.filter(
(option) => option.getAttribute('aria-checked') === 'true'
);
expect(checkedOptions).toHaveLength(8);
});

it('renders data streams filter with 50 options selected when more than 50 items in the filter', async () => {
const { getByTestId } = renderComponent({
...defaultProps,
filterOptions: {
...defaultProps.filterOptions,
options: generateDataStreams(55).map((ds) => ds.name),
},
});
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy();

const toggleFilterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`);
expect(toggleFilterButton).toBeTruthy();
expect(toggleFilterButton).toHaveTextContent('Data streams50');
expect(
toggleFilterButton.querySelector('.euiNotificationBadge')?.getAttribute('aria-label')
).toBe('50 active filters');
});

it('renders data streams filter with no options selected and select all is disabled', async () => {
const { getByTestId, queryByTestId } = renderComponent({
...defaultProps,
filterOptions: {
...defaultProps.filterOptions,
options: [],
},
});
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy();

const filterButton = getByTestId(`${testIdFilter}-dataStreams-popoverButton`);
expect(filterButton).toBeTruthy();
await user.click(filterButton);
expect(queryByTestId('dataStreams-filter-option')).toBeFalsy();
expect(getByTestId('dataStreams-group-label')).toBeTruthy();
expect(getByTestId(`${testIdFilter}-dataStreams-selectAllButton`)).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export const ChartsFilter = memo<ChartsFilterProps>(
},
});

const isSelectAllDisabled = useMemo(
() => options.length === 0 || (hasActiveFilters && numFilters === 0),
[hasActiveFilters, numFilters, options.length]
);

const addHeightToPopover = useMemo(
() => isDataStreamsFilter && numFilters + numActiveFilters > 15,
[isDataStreamsFilter, numFilters, numActiveFilters]
Expand All @@ -107,7 +112,12 @@ export const ChartsFilter = memo<ChartsFilterProps>(
const sortedDataStreamsFilterOptions = useMemo(() => {
if (shouldPinSelectedDataStreams() || areDataStreamsSelectedOnMount) {
// pin checked items to the top
return orderBy('checked', 'asc', items);
const sorted = orderBy(
'checked',
'asc',
items.filter((item) => !item.isGroupLabel)
);
return [...items.filter((item) => item.isGroupLabel), ...sorted];
}
// return options as are for other filters
return items;
Expand Down Expand Up @@ -260,7 +270,7 @@ export const ChartsFilter = memo<ChartsFilterProps>(
data-test-subj={getTestId(`${filterName}-selectAllButton`)}
icon="check"
label={UX_LABELS.filterSelectAll}
isDisabled={hasActiveFilters && numFilters === 0}
isDisabled={isSelectAllDisabled}
onClick={onSelectAll}
/>
</EuiFlexItem>
Expand Down
Loading