[DataUsage][Serverless] Add missing tests (#198007)

## Summary

Adds missing UX/API tests for changes added in 
- https://github.com/elastic/kibana/pull/193966
- https://github.com/elastic/kibana/pull/197056
- https://github.com/elastic/kibana/pull/195556
- https://github.com/elastic/kibana/pull/196559


### 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:
Ash 2024-11-07 20:57:46 +01:00 committed by GitHub
parent 1e3844619c
commit 207894edd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1341 additions and 212 deletions

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
/* eslint-disable no-console */
export const dataUsageTestQueryClientOptions = {
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
},
};

View file

@ -9,18 +9,21 @@ import { EuiFlexGroup } from '@elastic/eui';
import { MetricTypes } from '../../../common/rest_types';
import { ChartPanel } from './chart_panel';
import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
interface ChartsProps {
data: UsageMetricsResponseSchemaBody;
'data-test-subj'?: string;
}
export const Charts: React.FC<ChartsProps> = ({ data }) => {
export const Charts: React.FC<ChartsProps> = ({ data, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const [popoverOpen, setPopoverOpen] = useState<string | null>(null);
const togglePopover = useCallback((streamName: string | null) => {
setPopoverOpen((prev) => (prev === streamName ? null : streamName));
}, []);
return (
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" data-test-subj={getTestId('charts')}>
{Object.entries(data.metrics).map(([metricType, series], idx) => (
<ChartPanel
key={metricType}

View file

@ -0,0 +1,380 @@
/*
* 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 { render, waitFor } from '@testing-library/react';
import userEvent, { type UserEvent } from '@testing-library/user-event';
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';
jest.mock('../../utils/use_breadcrumbs', () => {
return {
useBreadcrumbs: jest.fn(),
};
});
jest.mock('../../utils/use_kibana', () => {
return {
useKibanaContextForPlugin: () => ({
services: mockServices,
}),
};
});
jest.mock('../../hooks/use_get_usage_metrics', () => {
const original = jest.requireActual('../../hooks/use_get_usage_metrics');
return {
...original,
useGetDataUsageMetrics: jest.fn(original.useGetDataUsageMetrics),
};
});
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('../../hooks/use_get_data_streams', () => {
const original = jest.requireActual('../../hooks/use_get_data_streams');
return {
...original,
useGetDataUsageDataStreams: jest.fn(original.useGetDataUsageDataStreams),
};
});
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',
},
{
from: 'now-30d',
to: 'now',
display: 'Last 30 days',
},
{
from: 'now-90d',
to: 'now',
display: 'Last 90 days',
},
{
from: 'now-1y',
to: 'now',
display: 'Last 1 year',
},
],
};
return x[k];
};
return get(key);
}),
},
},
}),
};
});
const mockUseGetDataUsageMetrics = useGetDataUsageMetrics as jest.Mock;
const mockUseGetDataUsageDataStreams = useGetDataUsageDataStreams as jest.Mock;
const mockServices = mockCore.createStart();
const getBaseMockedDataStreams = () => ({
error: undefined,
data: undefined,
isFetching: false,
refetch: jest.fn(),
});
const getBaseMockedDataUsageMetrics = () => ({
error: undefined,
data: undefined,
isFetching: false,
refetch: jest.fn(),
});
describe('DataUsageMetrics', () => {
let user: UserEvent;
const testId = 'test';
const testIdFilter = `${testId}-filter`;
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(() => {
jest.clearAllMocks();
user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, pointerEventsCheck: 0 });
mockUseGetDataUsageMetrics.mockReturnValue(getBaseMockedDataUsageMetrics);
mockUseGetDataUsageDataStreams.mockReturnValue(getBaseMockedDataStreams);
});
it('renders', () => {
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
expect(getByTestId(`${testId}`)).toBeTruthy();
});
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');
expect(getByTestId(`${testIdFilter}-super-refresh-button`)).toBeTruthy();
});
it('should not show data streams filter while fetching API', () => {
mockUseGetDataUsageDataStreams.mockReturnValue({
...getBaseMockedDataStreams,
isFetching: true,
});
const { queryByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
expect(queryByTestId(`${testIdFilter}-dataStreams-popoverButton`)).not.toBeTruthy();
});
it('should show data streams filter', () => {
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toBeTruthy();
});
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,
},
],
isFetching: false,
});
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
expect(getByTestId(`${testIdFilter}-dataStreams-popoverButton`)).toHaveTextContent(
'Data streams5'
);
});
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,
},
],
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 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'
);
});
it('should not call usage metrics API if no data streams', async () => {
mockUseGetDataUsageDataStreams.mockReturnValue({
...getBaseMockedDataStreams,
data: [],
});
render(<DataUsageMetrics data-test-subj={testId} />);
expect(mockUseGetDataUsageMetrics).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ enabled: false })
);
});
it('should show charts loading if data usage metrics API is fetching', () => {
mockUseGetDataUsageMetrics.mockReturnValue({
...getBaseMockedDataUsageMetrics,
isFetching: true,
});
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
expect(getByTestId(`${testId}-charts-loading`)).toBeTruthy();
});
it('should show charts', () => {
mockUseGetDataUsageMetrics.mockReturnValue({
...getBaseMockedDataUsageMetrics,
isFetched: true,
data: {
metrics: {
ingest_rate: [
{
name: '.ds-1',
data: [{ x: new Date(), y: 1000 }],
},
{
name: '.ds-10',
data: [{ x: new Date(), y: 1100 }],
},
],
storage_retained: [
{
name: '.ds-2',
data: [{ x: new Date(), y: 2000 }],
},
{
name: '.ds-20',
data: [{ x: new Date(), y: 2100 }],
},
],
},
},
});
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
expect(getByTestId(`${testId}-charts`)).toBeTruthy();
});
it('should refetch usage metrics with `Refresh` button click', async () => {
const refetch = jest.fn();
mockUseGetDataUsageMetrics.mockReturnValue({
...getBaseMockedDataUsageMetrics,
data: ['.ds-1', '.ds-2'],
isFetched: true,
});
mockUseGetDataUsageMetrics.mockReturnValue({
...getBaseMockedDataUsageMetrics,
isFetched: true,
refetch,
});
const { getByTestId } = render(<DataUsageMetrics data-test-subj={testId} />);
const refreshButton = getByTestId(`${testIdFilter}-super-refresh-button`);
// click refresh 5 times
for (let i = 0; i < 5; i++) {
await user.click(refreshButton);
}
expect(mockUseGetDataUsageMetrics).toHaveBeenLastCalledWith(
expect.any(Object),
expect.objectContaining({ enabled: false })
);
expect(refetch).toHaveBeenCalledTimes(5);
});
it('should show error toast if usage metrics API fails', async () => {
mockUseGetDataUsageMetrics.mockReturnValue({
...getBaseMockedDataUsageMetrics,
isFetched: true,
error: new Error('Uh oh!'),
});
render(<DataUsageMetrics data-test-subj={testId} />);
await waitFor(() => {
expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({
title: 'Error getting usage metrics',
text: 'Uh oh!',
});
});
});
it('should show error toast if data streams API fails', async () => {
mockUseGetDataUsageDataStreams.mockReturnValue({
...getBaseMockedDataStreams,
isFetched: true,
error: new Error('Uh oh!'),
});
render(<DataUsageMetrics data-test-subj={testId} />);
await waitFor(() => {
expect(mockServices.notifications.toasts.addDanger).toHaveBeenCalledWith({
title: 'Error getting data streams',
text: 'Uh oh!',
});
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -14,11 +14,12 @@ 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 { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams';
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, ChartFiltersProps } from './filters/charts_filters';
import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
const EuiItemCss = css`
width: 100%;
@ -28,181 +29,188 @@ const FlexItemWithCss = ({ children }: { children: React.ReactNode }) => (
<EuiFlexItem css={EuiItemCss}>{children}</EuiFlexItem>
);
export const DataUsageMetrics = () => {
const {
services: { chrome, appParams, notifications },
} = useKibanaContextForPlugin();
useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);
export const DataUsageMetrics = memo(
({ 'data-test-subj': dataTestSubj = 'data-usage-metrics' }: { 'data-test-subj'?: string }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const {
metricTypes: metricTypesFromUrl,
dataStreams: dataStreamsFromUrl,
startDate: startDateFromUrl,
endDate: endDateFromUrl,
setUrlMetricTypesFilter,
setUrlDataStreamsFilter,
setUrlDateRangeFilter,
} = useDataUsageMetricsUrlParams();
const {
services: { chrome, appParams, notifications },
} = useKibanaContextForPlugin();
useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);
const {
error: errorFetchingDataStreams,
data: dataStreams,
isFetching: isFetchingDataStreams,
} = useGetDataUsageDataStreams({
selectedDataStreams: dataStreamsFromUrl,
options: {
enabled: true,
retry: false,
},
});
const {
metricTypes: metricTypesFromUrl,
dataStreams: dataStreamsFromUrl,
startDate: startDateFromUrl,
endDate: endDateFromUrl,
setUrlMetricTypesFilter,
setUrlDataStreamsFilter,
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 (!dataStreamsFromUrl && dataStreams) {
setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(','));
}
if (!startDateFromUrl || !endDateFromUrl) {
setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to });
}
}, [
dataStreams,
dataStreamsFromUrl,
endDateFromUrl,
metricTypesFromUrl,
metricsFilters.dataStreams,
metricsFilters.from,
metricsFilters.metricTypes,
metricsFilters.to,
setUrlDataStreamsFilter,
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: errorFetchingDataUsageMetrics,
data,
isFetching,
isFetched,
refetch: refetchDataUsageMetrics,
} = useGetDataUsageMetrics(
{
...metricsFilters,
from: dateRangePickerState.startDate,
to: dateRangePickerState.endDate,
},
{
retry: false,
enabled: !!metricsFilters.dataStreams.length,
}
);
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]
);
const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => {
const dataStreamsOptions = dataStreams?.reduce<Record<string, number>>((acc, ds) => {
acc[ds.name] = ds.storageSizeBytes;
return acc;
}, {});
return {
dataStreams: {
filterName: 'dataStreams',
options: dataStreamsOptions ? Object.keys(dataStreamsOptions) : metricsFilters.dataStreams,
appendOptions: dataStreamsOptions,
selectedOptions: metricsFilters.dataStreams,
onChangeFilterOptions: onChangeDataStreamsFilter,
isFilterLoading: isFetchingDataStreams,
const {
error: errorFetchingDataStreams,
data: dataStreams,
isFetching: isFetchingDataStreams,
} = useGetDataUsageDataStreams({
selectedDataStreams: dataStreamsFromUrl,
options: {
enabled: true,
retry: false,
},
metricTypes: {
filterName: 'metricTypes',
options: metricsFilters.metricTypes,
onChangeFilterOptions: onChangeMetricTypesFilter,
});
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 (!dataStreamsFromUrl && dataStreams) {
setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(','));
}
if (!startDateFromUrl || !endDateFromUrl) {
setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to });
}
}, [
dataStreams,
dataStreamsFromUrl,
endDateFromUrl,
metricTypesFromUrl,
metricsFilters.dataStreams,
metricsFilters.from,
metricsFilters.metricTypes,
metricsFilters.to,
setUrlDataStreamsFilter,
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: errorFetchingDataUsageMetrics,
data,
isFetching,
isFetched,
refetch: refetchDataUsageMetrics,
} = useGetDataUsageMetrics(
{
...metricsFilters,
from: dateRangePickerState.startDate,
to: dateRangePickerState.endDate,
},
};
}, [
dataStreams,
isFetchingDataStreams,
metricsFilters.dataStreams,
metricsFilters.metricTypes,
onChangeDataStreamsFilter,
onChangeMetricTypesFilter,
]);
{
retry: false,
enabled: !!metricsFilters.dataStreams.length,
}
);
if (errorFetchingDataUsageMetrics) {
notifications.toasts.addDanger({
title: i18n.translate('xpack.dataUsage.getMetrics.addFailure.toast.title', {
defaultMessage: 'Error getting usage metrics',
}),
text: errorFetchingDataUsageMetrics.message,
});
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]
);
const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => {
const dataStreamsOptions = dataStreams?.reduce<Record<string, number>>((acc, ds) => {
acc[ds.name] = ds.storageSizeBytes;
return acc;
}, {});
return {
dataStreams: {
filterName: 'dataStreams',
options: dataStreamsOptions
? Object.keys(dataStreamsOptions)
: metricsFilters.dataStreams,
appendOptions: dataStreamsOptions,
selectedOptions: metricsFilters.dataStreams,
onChangeFilterOptions: onChangeDataStreamsFilter,
isFilterLoading: isFetchingDataStreams,
},
metricTypes: {
filterName: 'metricTypes',
options: metricsFilters.metricTypes,
onChangeFilterOptions: onChangeMetricTypesFilter,
},
};
}, [
dataStreams,
isFetchingDataStreams,
metricsFilters.dataStreams,
metricsFilters.metricTypes,
onChangeDataStreamsFilter,
onChangeMetricTypesFilter,
]);
if (errorFetchingDataUsageMetrics) {
notifications.toasts.addDanger({
title: i18n.translate('xpack.dataUsage.getMetrics.addFailure.toast.title', {
defaultMessage: 'Error getting usage metrics',
}),
text: errorFetchingDataUsageMetrics.message,
});
}
if (errorFetchingDataStreams) {
notifications.toasts.addDanger({
title: i18n.translate('xpack.dataUsage.getDataStreams.addFailure.toast.title', {
defaultMessage: 'Error getting data streams',
}),
text: errorFetchingDataStreams.message,
});
}
return (
<EuiFlexGroup alignItems="flexStart" direction="column" data-test-subj={getTestId()}>
<FlexItemWithCss>
<ChartFilters
dateRangePickerState={dateRangePickerState}
isDataLoading={isFetchingDataStreams}
onClick={refetchDataUsageMetrics}
onRefresh={onRefresh}
onRefreshChange={onRefreshChange}
onTimeChange={onTimeChange}
filterOptions={filterOptions}
showMetricsTypesFilter={false}
data-test-subj={getTestId('filter')}
/>
</FlexItemWithCss>
<FlexItemWithCss>
{isFetched && data?.metrics ? (
<Charts data={data} data-test-subj={dataTestSubj} />
) : isFetching ? (
<EuiLoadingElastic data-test-subj={getTestId('charts-loading')} />
) : null}
</FlexItemWithCss>
</EuiFlexGroup>
);
}
if (errorFetchingDataStreams) {
notifications.toasts.addDanger({
title: i18n.translate('xpack.dataUsage.getDataStreams.addFailure.toast.title', {
defaultMessage: 'Error getting data streams',
}),
text: errorFetchingDataStreams.message,
});
}
return (
<EuiFlexGroup alignItems="flexStart" direction="column">
<FlexItemWithCss>
<ChartFilters
dateRangePickerState={dateRangePickerState}
isDataLoading={isFetchingDataStreams}
onClick={refetchDataUsageMetrics}
onRefresh={onRefresh}
onRefreshChange={onRefreshChange}
onTimeChange={onTimeChange}
filterOptions={filterOptions}
showMetricsTypesFilter={false}
/>
</FlexItemWithCss>
<FlexItemWithCss>
{isFetched && data?.metrics ? (
<Charts data={data} />
) : isFetching ? (
<EuiLoadingElastic />
) : null}
</FlexItemWithCss>
</EuiFlexGroup>
);
};
);

View file

@ -193,13 +193,10 @@ export const ChartsFilter = memo<ChartsFilterProps>(
>
{(list, search) => {
return (
<div
style={{ width: 300 }}
data-test-subj={getTestId(`${filterName}-filter-popoverList`)}
>
<div style={{ width: 300 }} data-test-subj={getTestId(`${filterName}-popoverList`)}>
{isSearchable && (
<EuiPopoverTitle
data-test-subj={getTestId(`${filterName}-filter-search`)}
data-test-subj={getTestId(`${filterName}-search`)}
paddingSize="s"
>
{search}

View file

@ -42,7 +42,7 @@ export const ChartsFilterPopover = memo(
const button = useMemo(
() => (
<EuiFilterButton
data-test-subj={getTestId(`${filterName}-filter-popoverButton`)}
data-test-subj={getTestId(`${filterName}-popoverButton`)}
iconType="arrowDown"
onClick={onButtonClick}
isSelected={isPopoverOpen}

View file

@ -46,13 +46,15 @@ export const ChartFilters = memo<ChartFiltersProps>(
const filters = useMemo(() => {
return (
<>
{showMetricsTypesFilter && <ChartsFilter filterOptions={filterOptions.metricTypes} />}
{showMetricsTypesFilter && (
<ChartsFilter filterOptions={filterOptions.metricTypes} data-test-subj={dataTestSubj} />
)}
{!filterOptions.dataStreams.isFilterLoading && (
<ChartsFilter filterOptions={filterOptions.dataStreams} />
<ChartsFilter filterOptions={filterOptions.dataStreams} data-test-subj={dataTestSubj} />
)}
</>
);
}, [filterOptions, showMetricsTypesFilter]);
}, [dataTestSubj, filterOptions, showMetricsTypesFilter]);
const onClickRefreshButton = useCallback(() => onClick(), [onClick]);
@ -68,6 +70,7 @@ export const ChartFilters = memo<ChartFiltersProps>(
onRefresh={onRefresh}
onRefreshChange={onRefreshChange}
onTimeChange={onTimeChange}
data-test-subj={dataTestSubj}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -15,6 +15,7 @@ import type {
OnRefreshChangeProps,
} from '@elastic/eui/src/components/date_picker/types';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
export interface DateRangePickerValues {
autoRefreshOptions: {
@ -32,10 +33,19 @@ interface UsageMetricsDateRangePickerProps {
onRefresh: () => void;
onRefreshChange: (evt: OnRefreshChangeProps) => void;
onTimeChange: ({ start, end }: DurationRange) => void;
'data-test-subj'?: string;
}
export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps>(
({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => {
({
dateRangePickerState,
isDataLoading,
onRefresh,
onRefreshChange,
onTimeChange,
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { uiSettings } = kibana.services;
const [commonlyUsedRanges] = useState(() => {
@ -54,6 +64,7 @@ export const UsageMetricsDateRangePicker = memo<UsageMetricsDateRangePickerProps
return (
<EuiSuperDatePicker
data-test-subj={getTestId('date-range')}
isLoading={isDataLoading}
dateFormat={uiSettings.get('dateFormat')}
commonlyUsedRanges={commonlyUsedRanges}

View file

@ -0,0 +1,79 @@
/*
* 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 { METRIC_TYPE_VALUES, MetricTypes } from '../../../common/rest_types';
import { getDataUsageMetricsFiltersFromUrlParams } from './use_charts_url_params';
describe('#getDataUsageMetricsFiltersFromUrlParams', () => {
const getMetricTypesAsArray = (): MetricTypes[] => {
return [...METRIC_TYPE_VALUES];
};
it('should not use invalid `metricTypes` values from URL params', () => {
expect(getDataUsageMetricsFiltersFromUrlParams({ metricTypes: 'bar,foo' })).toEqual({});
});
it('should use valid `metricTypes` values from URL params', () => {
expect(
getDataUsageMetricsFiltersFromUrlParams({
metricTypes: `${getMetricTypesAsArray().join()},foo,bar`,
})
).toEqual({
metricTypes: getMetricTypesAsArray().sort(),
});
});
it('should use given `dataStreams` values from URL params', () => {
expect(
getDataUsageMetricsFiltersFromUrlParams({
dataStreams: 'ds-3,ds-1,ds-2',
})
).toEqual({
dataStreams: ['ds-3', 'ds-1', 'ds-2'],
});
});
it('should use valid `metricTypes` along with given `dataStreams` and date values from URL params', () => {
expect(
getDataUsageMetricsFiltersFromUrlParams({
metricTypes: getMetricTypesAsArray().join(),
dataStreams: 'ds-5,ds-1,ds-2',
startDate: '2022-09-12T08:00:00.000Z',
endDate: '2022-09-12T08:30:33.140Z',
})
).toEqual({
metricTypes: getMetricTypesAsArray().sort(),
endDate: '2022-09-12T08:30:33.140Z',
dataStreams: ['ds-5', 'ds-1', 'ds-2'],
startDate: '2022-09-12T08:00:00.000Z',
});
});
it('should use given relative startDate and endDate values URL params', () => {
expect(
getDataUsageMetricsFiltersFromUrlParams({
startDate: 'now-24h/h',
endDate: 'now',
})
).toEqual({
endDate: 'now',
startDate: 'now-24h/h',
});
});
it('should use given absolute startDate and endDate values URL params', () => {
expect(
getDataUsageMetricsFiltersFromUrlParams({
startDate: '2022-09-12T08:00:00.000Z',
endDate: '2022-09-12T08:30:33.140Z',
})
).toEqual({
endDate: '2022-09-12T08:30:33.140Z',
startDate: '2022-09-12T08:00:00.000Z',
});
});
});

View file

@ -0,0 +1,120 @@
/*
* 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, { ReactNode } from 'react';
import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query';
import { renderHook } from '@testing-library/react-hooks';
import { useGetDataUsageDataStreams } from './use_get_data_streams';
import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../common';
import { coreMock as mockCore } from '@kbn/core/public/mocks';
import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options';
const useQueryMock = _useQuery as jest.Mock;
jest.mock('@tanstack/react-query', () => {
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
return {
...actualReactQueryModule,
useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
};
});
const mockServices = mockCore.createStart();
const createWrapper = () => {
const queryClient = new QueryClient(dataUsageTestQueryClientOptions);
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
jest.mock('../utils/use_kibana', () => {
return {
useKibanaContextForPlugin: () => ({
services: mockServices,
}),
};
});
const defaultDataStreamsRequestParams = {
options: { enabled: true },
};
describe('useGetDataUsageDataStreams', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call the correct API', async () => {
await renderHook(() => useGetDataUsageDataStreams(defaultDataStreamsRequestParams), {
wrapper: createWrapper(),
});
expect(mockServices.http.get).toHaveBeenCalledWith(DATA_USAGE_DATA_STREAMS_API_ROUTE, {
signal: expect.any(AbortSignal),
version: '1',
});
});
it('should not send selected data stream names provided in the param when calling the API', async () => {
await renderHook(
() =>
useGetDataUsageDataStreams({
...defaultDataStreamsRequestParams,
selectedDataStreams: ['ds-1'],
}),
{
wrapper: createWrapper(),
}
);
expect(mockServices.http.get).toHaveBeenCalledWith(DATA_USAGE_DATA_STREAMS_API_ROUTE, {
signal: expect.any(AbortSignal),
version: '1',
});
});
it('should not call the API if disabled', async () => {
await renderHook(
() =>
useGetDataUsageDataStreams({
...defaultDataStreamsRequestParams,
options: { enabled: false },
}),
{
wrapper: createWrapper(),
}
);
expect(mockServices.http.get).not.toHaveBeenCalled();
});
it('should allow custom options to be used', async () => {
await renderHook(
() =>
useGetDataUsageDataStreams({
selectedDataStreams: undefined,
options: {
queryKey: ['test-query-key'],
enabled: true,
retry: false,
},
}),
{
wrapper: createWrapper(),
}
);
expect(useQueryMock).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: ['test-query-key'],
enabled: true,
retry: false,
})
);
});
});

View file

@ -31,15 +31,16 @@ export const useGetDataUsageDataStreams = ({
selectedDataStreams?: string[];
options?: UseQueryOptions<GetDataUsageDataStreamsResponse, IHttpFetchError>;
}): UseQueryResult<GetDataUsageDataStreamsResponse, IHttpFetchError> => {
const http = useKibanaContextForPlugin().services.http;
const { http } = useKibanaContextForPlugin().services;
return useQuery<GetDataUsageDataStreamsResponse, IHttpFetchError>({
queryKey: ['get-data-usage-data-streams'],
...options,
keepPreviousData: true,
queryFn: async () => {
queryFn: async ({ signal }) => {
const dataStreamsResponse = await http
.get<GetDataUsageDataStreamsResponse>(DATA_USAGE_DATA_STREAMS_API_ROUTE, {
signal,
version: '1',
})
.catch((error) => {

View file

@ -0,0 +1,102 @@
/*
* 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, { ReactNode } from 'react';
import { QueryClient, QueryClientProvider, useQuery as _useQuery } from '@tanstack/react-query';
import { renderHook } from '@testing-library/react-hooks';
import { useGetDataUsageMetrics } from './use_get_usage_metrics';
import { DATA_USAGE_METRICS_API_ROUTE } from '../../common';
import { coreMock as mockCore } from '@kbn/core/public/mocks';
import { dataUsageTestQueryClientOptions } from '../../common/test_utils/test_query_client_options';
const useQueryMock = _useQuery as jest.Mock;
jest.mock('@tanstack/react-query', () => {
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
return {
...actualReactQueryModule,
useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
};
});
const mockServices = mockCore.createStart();
const createWrapper = () => {
const queryClient = new QueryClient(dataUsageTestQueryClientOptions);
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
jest.mock('../utils/use_kibana', () => {
return {
useKibanaContextForPlugin: () => ({
services: mockServices,
}),
};
});
const defaultUsageMetricsRequestBody = {
from: 'now-15m',
to: 'now',
metricTypes: ['ingest_rate'],
dataStreams: ['ds-1'],
};
describe('useGetDataUsageMetrics', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call the correct API', async () => {
await renderHook(
() => useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { enabled: true }),
{
wrapper: createWrapper(),
}
);
expect(mockServices.http.post).toHaveBeenCalledWith(DATA_USAGE_METRICS_API_ROUTE, {
signal: expect.any(AbortSignal),
version: '1',
body: JSON.stringify(defaultUsageMetricsRequestBody),
});
});
it('should not call the API if disabled', async () => {
await renderHook(
() => useGetDataUsageMetrics(defaultUsageMetricsRequestBody, { enabled: false }),
{
wrapper: createWrapper(),
}
);
expect(mockServices.http.post).not.toHaveBeenCalled();
});
it('should allow custom options to be used', async () => {
await renderHook(
() =>
useGetDataUsageMetrics(defaultUsageMetricsRequestBody, {
queryKey: ['test-query-key'],
enabled: true,
retry: false,
}),
{
wrapper: createWrapper(),
}
);
expect(useQueryMock).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: ['test-query-key'],
enabled: true,
retry: false,
})
);
});
});

View file

@ -21,7 +21,7 @@ export const useGetDataUsageMetrics = (
body: UsageMetricsRequestBody,
options: UseQueryOptions<UsageMetricsResponseSchemaBody, IHttpFetchError<ErrorType>> = {}
): UseQueryResult<UsageMetricsResponseSchemaBody, IHttpFetchError<ErrorType>> => {
const http = useKibanaContextForPlugin().services.http;
const { http } = useKibanaContextForPlugin().services;
return useQuery<UsageMetricsResponseSchemaBody, IHttpFetchError<ErrorType>>({
queryKey: ['get-data-usage-metrics', body],

View file

@ -0,0 +1,24 @@
/*
* 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 { formatBytes } from './format_bytes';
const exponentN = (number: number, exponent: number) => number ** exponent;
describe('formatBytes', () => {
it('should format bytes to human readable format with decimal', () => {
expect(formatBytes(84 + 5)).toBe('89.0 B');
expect(formatBytes(1024 + 256)).toBe('1.3 KB');
expect(formatBytes(1024 + 582)).toBe('1.6 KB');
expect(formatBytes(exponentN(1024, 2) + 582 * 1024)).toBe('1.6 MB');
expect(formatBytes(exponentN(1024, 3) + 582 * exponentN(1024, 2))).toBe('1.6 GB');
expect(formatBytes(exponentN(1024, 4) + 582 * exponentN(1024, 3))).toBe('1.6 TB');
expect(formatBytes(exponentN(1024, 5) + 582 * exponentN(1024, 4))).toBe('1.6 PB');
expect(formatBytes(exponentN(1024, 6) + 582 * exponentN(1024, 5))).toBe('1.6 EB');
expect(formatBytes(exponentN(1024, 7) + 582 * exponentN(1024, 6))).toBe('1.6 ZB');
expect(formatBytes(exponentN(1024, 8) + 582 * exponentN(1024, 7))).toBe('1.6 YB');
});
});

View file

@ -0,0 +1,37 @@
/*
* 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 { loggingSystemMock } from '@kbn/core/server/mocks';
import { DeepReadonly } from 'utility-types';
import { PluginInitializerContext } from '@kbn/core/server';
import { Observable } from 'rxjs';
import { DataUsageContext } from '../types';
import { DataUsageConfigType } from '../config';
export interface MockedDataUsageContext extends DataUsageContext {
logFactory: ReturnType<ReturnType<typeof loggingSystemMock.create>['get']>;
config$?: Observable<DataUsageConfigType>;
configInitialValue: DataUsageConfigType;
serverConfig: DeepReadonly<DataUsageConfigType>;
kibanaInstanceId: PluginInitializerContext['env']['instanceUuid'];
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch'];
}
export const createMockedDataUsageContext = (
context: PluginInitializerContext<DataUsageConfigType>
): MockedDataUsageContext => {
return {
logFactory: loggingSystemMock.create().get(),
config$: context.config.create<DataUsageConfigType>(),
configInitialValue: context.config.get(),
serverConfig: context.config.get(),
kibanaInstanceId: context.env.instanceUuid,
kibanaVersion: context.env.packageInfo.version,
kibanaBranch: context.env.packageInfo.branch,
};
};

View file

@ -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 type { MockedKeys } from '@kbn/utility-types-jest';
import type { CoreSetup } from '@kbn/core/server';
import { registerDataStreamsRoute } from './data_streams';
import { coreMock } from '@kbn/core/server/mocks';
import { httpServerMock } from '@kbn/core/server/mocks';
import { DataUsageService } from '../../services';
import type {
DataUsageRequestHandlerContext,
DataUsageRouter,
DataUsageServerStart,
} from '../../types';
import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../../common';
import { createMockedDataUsageContext } from '../../mocks';
import { getMeteringStats } from '../../utils/get_metering_stats';
import { CustomHttpRequestError } from '../../utils';
jest.mock('../../utils/get_metering_stats');
const mockGetMeteringStats = getMeteringStats as jest.Mock;
describe('registerDataStreamsRoute', () => {
let mockCore: MockedKeys<CoreSetup<{}, DataUsageServerStart>>;
let router: DataUsageRouter;
let dataUsageService: DataUsageService;
let context: DataUsageRequestHandlerContext;
beforeEach(() => {
mockCore = coreMock.createSetup();
router = mockCore.http.createRouter();
context = coreMock.createCustomRequestHandlerContext(
coreMock.createRequestHandlerContext()
) as unknown as DataUsageRequestHandlerContext;
const mockedDataUsageContext = createMockedDataUsageContext(
coreMock.createPluginInitializerContext()
);
dataUsageService = new DataUsageService(mockedDataUsageContext);
registerDataStreamsRoute(router, dataUsageService);
});
it('should request correct API', () => {
expect(router.versioned.get).toHaveBeenCalledTimes(1);
expect(router.versioned.get).toHaveBeenCalledWith({
access: 'internal',
path: DATA_USAGE_DATA_STREAMS_API_ROUTE,
});
});
it('should correctly sort response', async () => {
mockGetMeteringStats.mockResolvedValue({
datastreams: [
{
name: 'datastream1',
size_in_bytes: 100,
},
{
name: 'datastream2',
size_in_bytes: 200,
},
],
});
const mockRequest = httpServerMock.createKibanaRequest({ body: {} });
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls;
await handler(context, mockRequest, mockResponse);
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
body: [
{
name: 'datastream2',
storageSizeBytes: 200,
},
{
name: 'datastream1',
storageSizeBytes: 100,
},
],
});
});
it('should return correct error if metering stats request fails', async () => {
// using custom error for test here to avoid having to import the actual error class
mockGetMeteringStats.mockRejectedValue(
new CustomHttpRequestError('Error getting metring stats!')
);
const mockRequest = httpServerMock.createKibanaRequest({ body: {} });
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls;
await handler(context, mockRequest, mockResponse);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
expect(mockResponse.customError).toHaveBeenCalledWith({
body: new CustomHttpRequestError('Error getting metring stats!'),
statusCode: 500,
});
});
it.each([
['no datastreams', {}, []],
['empty array', { datastreams: [] }, []],
['an empty element', { datastreams: [{}] }, [{ name: undefined, storageSizeBytes: 0 }]],
])('should return empty array when no stats data with %s', async (_, stats, res) => {
mockGetMeteringStats.mockResolvedValue(stats);
const mockRequest = httpServerMock.createKibanaRequest({ body: {} });
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
const [[, handler]] = mockRouter.versioned.get.mock.results[0].value.addVersion.mock.calls;
await handler(context, mockRequest, mockResponse);
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
body: res,
});
});
});

View file

@ -5,27 +5,11 @@
* 2.0.
*/
import { type ElasticsearchClient, RequestHandler } from '@kbn/core/server';
import { RequestHandler } from '@kbn/core/server';
import { DataUsageRequestHandlerContext } from '../../types';
import { errorHandler } from '../error_handler';
import { DataUsageService } from '../../services';
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',
});
};
import { getMeteringStats } from '../../utils/get_metering_stats';
export const getDataStreamsHandler = (
dataUsageService: DataUsageService
@ -41,12 +25,15 @@ export const getDataStreamsHandler = (
core.elasticsearch.client.asSecondaryAuthUser
);
const body = meteringStats
.sort((a, b) => b.size_in_bytes - a.size_in_bytes)
.map((stat) => ({
name: stat.name,
storageSizeBytes: stat.size_in_bytes ?? 0,
}));
const body =
meteringStats && !!meteringStats.length
? meteringStats
.sort((a, b) => b.size_in_bytes - a.size_in_bytes)
.map((stat) => ({
name: stat.name,
storageSizeBytes: stat.size_in_bytes ?? 0,
}))
: [];
return response.ok({
body,

View file

@ -0,0 +1,208 @@
/*
* 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 { MockedKeys } from '@kbn/utility-types-jest';
import type { CoreSetup } from '@kbn/core/server';
import { registerUsageMetricsRoute } from './usage_metrics';
import { coreMock } from '@kbn/core/server/mocks';
import { httpServerMock } from '@kbn/core/server/mocks';
import { DataUsageService } from '../../services';
import type {
DataUsageRequestHandlerContext,
DataUsageRouter,
DataUsageServerStart,
} from '../../types';
import { DATA_USAGE_METRICS_API_ROUTE } from '../../../common';
import { createMockedDataUsageContext } from '../../mocks';
import { CustomHttpRequestError } from '../../utils';
import { AutoOpsError } from '../../services/errors';
describe('registerUsageMetricsRoute', () => {
let mockCore: MockedKeys<CoreSetup<{}, DataUsageServerStart>>;
let router: DataUsageRouter;
let dataUsageService: DataUsageService;
let context: DataUsageRequestHandlerContext;
beforeEach(() => {
mockCore = coreMock.createSetup();
router = mockCore.http.createRouter();
context = coreMock.createCustomRequestHandlerContext(
coreMock.createRequestHandlerContext()
) as unknown as DataUsageRequestHandlerContext;
const mockedDataUsageContext = createMockedDataUsageContext(
coreMock.createPluginInitializerContext()
);
dataUsageService = new DataUsageService(mockedDataUsageContext);
});
it('should request correct API', () => {
registerUsageMetricsRoute(router, dataUsageService);
expect(router.versioned.post).toHaveBeenCalledTimes(1);
expect(router.versioned.post).toHaveBeenCalledWith({
access: 'internal',
path: DATA_USAGE_METRICS_API_ROUTE,
});
});
it('should throw error if no data streams in the request', async () => {
registerUsageMetricsRoute(router, dataUsageService);
const mockRequest = httpServerMock.createKibanaRequest({
body: {
from: 'now-15m',
to: 'now',
metricTypes: ['ingest_rate'],
dataStreams: [],
},
});
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls;
await handler(context, mockRequest, mockResponse);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
expect(mockResponse.customError).toHaveBeenCalledWith({
body: new CustomHttpRequestError('[request body.dataStreams]: no data streams selected'),
statusCode: 400,
});
});
it('should correctly transform response', async () => {
(await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest
.fn()
.mockResolvedValue({
data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }],
});
dataUsageService.getMetrics = jest.fn().mockResolvedValue({
metrics: {
ingest_rate: [
{
name: '.ds-1',
data: [
[1726858530000, 13756849],
[1726862130000, 14657904],
],
},
{
name: '.ds-2',
data: [
[1726858530000, 12894623],
[1726862130000, 14436905],
],
},
],
storage_retained: [
{
name: '.ds-1',
data: [
[1726858530000, 12576413],
[1726862130000, 13956423],
],
},
{
name: '.ds-2',
data: [
[1726858530000, 12894623],
[1726862130000, 14436905],
],
},
],
},
});
registerUsageMetricsRoute(router, dataUsageService);
const mockRequest = httpServerMock.createKibanaRequest({
body: {
from: 'now-15m',
to: 'now',
metricTypes: ['ingest_rate', 'storage_retained'],
dataStreams: ['.ds-1', '.ds-2'],
},
});
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls;
await handler(context, mockRequest, mockResponse);
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
body: {
metrics: {
ingest_rate: [
{
name: '.ds-1',
data: [
{ x: 1726858530000, y: 13756849 },
{ x: 1726862130000, y: 14657904 },
],
},
{
name: '.ds-2',
data: [
{ x: 1726858530000, y: 12894623 },
{ x: 1726862130000, y: 14436905 },
],
},
],
storage_retained: [
{
name: '.ds-1',
data: [
{ x: 1726858530000, y: 12576413 },
{ x: 1726862130000, y: 13956423 },
],
},
{
name: '.ds-2',
data: [
{ x: 1726858530000, y: 12894623 },
{ x: 1726862130000, y: 14436905 },
],
},
],
},
},
});
});
it('should throw error if error on requesting auto ops service', async () => {
(await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest
.fn()
.mockResolvedValue({
data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }],
});
dataUsageService.getMetrics = jest
.fn()
.mockRejectedValue(new AutoOpsError('Uh oh, something went wrong!'));
registerUsageMetricsRoute(router, dataUsageService);
const mockRequest = httpServerMock.createKibanaRequest({
body: {
from: 'now-15m',
to: 'now',
metricTypes: ['ingest_rate'],
dataStreams: ['.ds-1', '.ds-2'],
},
});
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls;
await handler(context, mockRequest, mockResponse);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
expect(mockResponse.customError).toHaveBeenCalledWith({
body: new AutoOpsError('Uh oh, something went wrong!'),
statusCode: 503,
});
});
});

View file

@ -0,0 +1,24 @@
/*
* 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 ElasticsearchClient } from '@kbn/core/server';
export interface MeteringStats {
name: string;
num_docs: number;
size_in_bytes: number;
}
interface MeteringStatsResponse {
datastreams: MeteringStats[];
}
export const getMeteringStats = (client: ElasticsearchClient) => {
return client.transport.request<MeteringStatsResponse>({
method: 'GET',
path: '/_metering/stats',
});
};

View file

@ -31,6 +31,7 @@
"@kbn/repo-info",
"@kbn/cloud-plugin",
"@kbn/server-http-tools",
"@kbn/utility-types-jest",
],
"exclude": ["target/**/*"]
}