mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
1e3844619c
commit
207894edd5
20 changed files with 1341 additions and 212 deletions
|
@ -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: () => {},
|
||||
},
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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!',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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],
|
||||
|
|
24
x-pack/plugins/data_usage/public/utils/format_bytes.test.ts
Normal file
24
x-pack/plugins/data_usage/public/utils/format_bytes.test.ts
Normal 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');
|
||||
});
|
||||
});
|
37
x-pack/plugins/data_usage/server/mocks/index.ts
Normal file
37
x-pack/plugins/data_usage/server/mocks/index.ts
Normal 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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
24
x-pack/plugins/data_usage/server/utils/get_metering_stats.ts
Normal file
24
x-pack/plugins/data_usage/server/utils/get_metering_stats.ts
Normal 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',
|
||||
});
|
||||
};
|
|
@ -31,6 +31,7 @@
|
|||
"@kbn/repo-info",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/server-http-tools",
|
||||
"@kbn/utility-types-jest",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue