Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kbn-management/cards_navigation/src/consts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const appDefinitions: Record<AppId, AppDefinition> = {
description: i18n.translate('management.landing.withCardNavigation.dataUsageDescription', {
defaultMessage: 'View data usage and retention.',
}),
icon: 'documents',
icon: 'stats',
},

[AppIds.RULES]: {
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/data_usage/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export const PLUGIN_ID = 'data_usage';
export const PLUGIN_NAME = i18n.translate('xpack.dataUsage.name', {
defaultMessage: 'Data Usage',
});

export const DATA_USAGE_API_ROUTE_PREFIX = '/api/data_usage/';
export const DATA_USAGE_METRICS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}metrics`;
export const DATA_USAGE_DATA_STREAMS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}data_streams`;
57 changes: 57 additions & 0 deletions x-pack/plugins/data_usage/common/query_client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 { PropsWithChildren } from 'react';
import React, { memo, useMemo } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

type QueryClientOptionsProp = ConstructorParameters<typeof QueryClient>[0];

/**
* Default Query Client for Data Usage.
*/
export class DataUsageQueryClient extends QueryClient {
constructor(options: QueryClientOptionsProp = {}) {
const optionsWithDefaults: QueryClientOptionsProp = {
...options,
defaultOptions: {
...(options.defaultOptions ?? {}),
queries: {
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
refetchOnMount: true,
keepPreviousData: true,
...(options?.defaultOptions?.queries ?? {}),
},
},
};
super(optionsWithDefaults);
}
}

/**
* The default Data Usage Query Client. Can be imported and used from outside of React hooks
* and still benefit from ReactQuery features (like caching, etc)
*
* @see https://tanstack.com/query/v4/docs/reference/QueryClient
*/
export const dataUsageQueryClient = new DataUsageQueryClient();

export type ReactQueryClientProviderProps = PropsWithChildren<{
queryClient?: DataUsageQueryClient;
}>;

export const DataUsageReactQueryClientProvider = memo<ReactQueryClientProviderProps>(
({ queryClient, children }) => {
const client = useMemo(() => {
return queryClient || dataUsageQueryClient;
}, [queryClient]);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}
);

DataUsageReactQueryClientProvider.displayName = 'DataUsageReactQueryClientProvider';
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
* 2.0.
*/

/* eslint-disable @typescript-eslint/no-empty-interface*/
import { schema } from '@kbn/config-schema';

export interface DataUsageSetupDependencies {}

export interface DataUsageStartDependencies {}

export interface DataUsageServerSetup {}

export interface DataUsageServerStart {}
export const DataStreamsResponseSchema = {
body: () =>
schema.arrayOf(
schema.object({
name: schema.string(),
storageSizeBytes: schema.number(),
})
),
};
9 changes: 9 additions & 0 deletions x-pack/plugins/data_usage/common/rest_types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export * from './usage_metrics';
export * from './data_streams';
179 changes: 179 additions & 0 deletions x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* 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 { UsageMetricsRequestSchema } from './usage_metrics';

describe('usage_metrics schemas', () => {
it('should accept valid request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
})
).not.toThrow();
});

it('should accept a single `metricTypes` in request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: 'ingest_rate',
})
).not.toThrow();
});

it('should accept multiple `metricTypes` in request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['ingest_rate', 'storage_retained', 'index_rate'],
})
).not.toThrow();
});

it('should accept a single string as `dataStreams` in request query', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: 'storage_retained',
dataStreams: 'data_stream_1',
})
).not.toThrow();
});

it('should accept `dataStream` list', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
})
).not.toThrow();
});

it('should error if `dataStream` list is empty', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: [],
})
).toThrowError('expected value of type [string] but got [Array]');
});

it('should error if `dataStream` is given an empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: ' ',
})
).toThrow('[dataStreams] must have at least one value');
});

it('should error if `dataStream` is given an empty item in the list', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained'],
dataStreams: ['ds_1', ' '],
})
).toThrow('[dataStreams] list can not contain empty values');
});

it('should error if `metricTypes` is empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ' ',
})
).toThrow();
});

it('should error if `metricTypes` is empty item', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: [' ', 'storage_retained'],
})
).toThrow('[metricTypes] list can not contain empty values');
});

it('should error if `metricTypes` is not a valid value', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: 'foo',
})
).toThrow(
'[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate'
);
});

it('should error if `metricTypes` is not a valid list', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: new Date().toISOString(),
metricTypes: ['storage_retained', 'foo'],
})
).toThrow(
'[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate'
);
});

it('should error if `from` is not a valid input', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: 1010,
to: new Date().toISOString(),
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[from]: expected value of type [string] but got [number]');
});

it('should error if `to` is not a valid input', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: 1010,
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[to]: expected value of type [string] but got [number]');
});

it('should error if `from` is empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: ' ',
to: new Date().toISOString(),
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[from]: Date ISO string must not be empty');
});

it('should error if `to` is empty string', () => {
expect(() =>
UsageMetricsRequestSchema.query.validate({
from: new Date().toISOString(),
to: ' ',
metricTypes: ['storage_retained', 'foo'],
})
).toThrow('[to]: Date ISO string must not be empty');
});
});
102 changes: 102 additions & 0 deletions x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
Original file line number Diff line number Diff line change
@@ -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 { schema, type TypeOf } from '@kbn/config-schema';

const METRIC_TYPE_VALUES = [
'storage_retained',
'ingest_rate',
'search_vcu',
'ingest_vcu',
'ml_vcu',
'index_latency',
'index_rate',
'search_latency',
'search_rate',
] as const;

export type MetricTypes = (typeof METRIC_TYPE_VALUES)[number];

// type guard for MetricTypes
export const isMetricType = (type: string): type is MetricTypes =>
METRIC_TYPE_VALUES.includes(type as MetricTypes);

// @ts-ignore
const isValidMetricType = (value: string) => METRIC_TYPE_VALUES.includes(value);

const DateSchema = schema.string({
minLength: 1,
validate: (v) => (v.trim().length ? undefined : 'Date ISO string must not be empty'),
});

const metricTypesSchema = schema.oneOf(
// @ts-expect-error TS2769: No overload matches this call
METRIC_TYPE_VALUES.map((metricType) => schema.literal(metricType)) // Create a oneOf schema for the keys
);
export const UsageMetricsRequestSchema = {
query: schema.object({
from: DateSchema,
to: DateSchema,
metricTypes: schema.oneOf([
schema.arrayOf(schema.string(), {
minSize: 1,
validate: (values) => {
if (values.map((v) => v.trim()).some((v) => !v.length)) {
return '[metricTypes] list can not contain empty values';
} else if (values.map((v) => v.trim()).some((v) => !isValidMetricType(v))) {
return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
}
},
}),
schema.string({
validate: (v) => {
if (!v.trim().length) {
return '[metricTypes] must have at least one value';
} else if (!isValidMetricType(v)) {
return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
}
},
}),
]),
dataStreams: schema.maybe(
schema.oneOf([
schema.arrayOf(schema.string(), {
minSize: 1,
validate: (values) => {
if (values.map((v) => v.trim()).some((v) => !v.length)) {
return '[dataStreams] list can not contain empty values';
}
},
}),
schema.string({
validate: (v) =>
v.trim().length ? undefined : '[dataStreams] must have at least one value',
}),
])
),
}),
};

export type UsageMetricsRequestSchemaQueryParams = TypeOf<typeof UsageMetricsRequestSchema.query>;

export const UsageMetricsResponseSchema = {
body: () =>
schema.object({
metrics: schema.recordOf(
metricTypesSchema,
schema.arrayOf(
schema.object({
name: schema.string(),
data: schema.arrayOf(
schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 }) // Each data point is an array of 2 numbers
),
})
)
),
}),
};
export type UsageMetricsResponseSchemaBody = TypeOf<typeof UsageMetricsResponseSchema.body>;
Loading