diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.test.ts b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.test.ts new file mode 100644 index 000000000000..8be34b4498c3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { omit } from 'lodash'; +import { mapToUrlState } from './with_metrics_explorer_options_url_state'; + +describe('WithMetricsExplorerOptionsUrlState', () => { + describe('mapToUrlState', () => { + it('loads a valid URL state', () => { + expect(mapToUrlState(validState)).toEqual(validState); + }); + it('discards invalid properties and loads valid properties into the URL', () => { + expect(mapToUrlState(invalidState)).toEqual(omit(invalidState, 'options')); + }); + }); +}); + +const validState = { + chartOptions: { + stack: false, + type: 'line', + yAxisMode: 'fromZero', + }, + options: { + aggregation: 'avg', + filterQuery: '', + groupBy: ['host.hostname'], + metrics: [ + { + aggregation: 'avg', + color: 'color0', + field: 'system.cpu.user.pct', + }, + { + aggregation: 'avg', + color: 'color1', + field: 'system.load.1', + }, + ], + source: 'url', + }, + timerange: { + from: 'now-1h', + interval: '>=10s', + to: 'now', + }, +}; + +const invalidState = { + chartOptions: { + stack: false, + type: 'line', + yAxisMode: 'fromZero', + }, + options: { + aggregation: 'avg', + filterQuery: '', + groupBy: ['host.hostname'], + metrics: 'this is the wrong data type', + source: 'url', + }, + timerange: { + from: 'now-1h', + interval: '>=10s', + to: 'now', + }, +}; diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx index 35fb66b2620d..c263d0f68a45 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx +++ b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx @@ -5,19 +5,17 @@ */ import { set } from '@elastic/safer-lodash-set'; -import { values } from 'lodash'; import React, { useContext, useMemo } from 'react'; -import * as t from 'io-ts'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; -import { MetricsExplorerColor } from '../../../common/color_palette'; import { UrlStateContainer } from '../../utils/url_state'; import { MetricsExplorerOptions, MetricsExplorerOptionsContainer, MetricsExplorerTimeOptions, - MetricsExplorerYAxisMode, - MetricsExplorerChartType, MetricsExplorerChartOptions, + metricExplorerOptionsRT, + metricsExplorerChartOptionsRT, + metricsExplorerTimeOptionsRT, } from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; interface MetricsExplorerUrlState { @@ -74,36 +72,7 @@ export const WithMetricsExplorerOptionsUrlState = () => { }; function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOptions { - const MetricRequired = t.type({ - aggregation: t.string, - }); - - const MetricOptional = t.partial({ - field: t.string, - rate: t.boolean, - color: t.keyof( - Object.fromEntries(values(MetricsExplorerColor).map((c) => [c, null])) as Record - ), - label: t.string, - }); - - const Metric = t.intersection([MetricRequired, MetricOptional]); - - const OptionsRequired = t.type({ - aggregation: t.string, - metrics: t.array(Metric), - }); - - const OptionsOptional = t.partial({ - limit: t.number, - groupBy: t.string, - filterQuery: t.string, - source: t.string, - }); - - const Options = t.intersection([OptionsRequired, OptionsOptional]); - - const result = Options.decode(subject); + const result = metricExplorerOptionsRT.decode(subject); try { ThrowReporter.report(result); @@ -114,22 +83,7 @@ function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOption } function isMetricExplorerChartOptions(subject: any): subject is MetricsExplorerChartOptions { - const ChartOptions = t.type({ - yAxisMode: t.keyof( - Object.fromEntries(values(MetricsExplorerYAxisMode).map((v) => [v, null])) as Record< - string, - null - > - ), - type: t.keyof( - Object.fromEntries(values(MetricsExplorerChartType).map((v) => [v, null])) as Record< - string, - null - > - ), - stack: t.boolean, - }); - const result = ChartOptions.decode(subject); + const result = metricsExplorerChartOptionsRT.decode(subject); try { ThrowReporter.report(result); @@ -140,12 +94,7 @@ function isMetricExplorerChartOptions(subject: any): subject is MetricsExplorerC } function isMetricExplorerTimeOption(subject: any): subject is MetricsExplorerTimeOptions { - const TimeRange = t.type({ - from: t.string, - to: t.string, - interval: t.string, - }); - const result = TimeRange.decode(subject); + const result = metricsExplorerTimeOptionsRT.decode(subject); try { ThrowReporter.report(result); return true; @@ -154,7 +103,7 @@ function isMetricExplorerTimeOption(subject: any): subject is MetricsExplorerTim } } -const mapToUrlState = (value: any): MetricsExplorerUrlState | undefined => { +export const mapToUrlState = (value: any): MetricsExplorerUrlState | undefined => { const finalState = {}; if (value) { if (value.options && isMetricExplorerOptions(value.options)) { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index fa103ce15e3e..299231f1821f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -4,19 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as t from 'io-ts'; +import { values } from 'lodash'; import createContainer from 'constate'; import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react'; import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { MetricsExplorerColor } from '../../../../../common/color_palette'; -import { - MetricsExplorerAggregation, - MetricsExplorerMetric, -} from '../../../../../common/http_api/metrics_explorer'; +import { metricsExplorerMetricRT } from '../../../../../common/http_api/metrics_explorer'; -export type MetricsExplorerOptionsMetric = MetricsExplorerMetric & { - color?: MetricsExplorerColor; - label?: string; -}; +const metricsExplorerOptionsMetricRT = t.intersection([ + metricsExplorerMetricRT, + t.partial({ + rate: t.boolean, + color: t.keyof( + Object.fromEntries(values(MetricsExplorerColor).map((c) => [c, null])) as Record< + MetricsExplorerColor, + null + > + ), + label: t.string, + }), +]); + +export type MetricsExplorerOptionsMetric = t.TypeOf; export enum MetricsExplorerChartType { line = 'line', @@ -29,28 +39,50 @@ export enum MetricsExplorerYAxisMode { auto = 'auto', } -export interface MetricsExplorerChartOptions { - type: MetricsExplorerChartType; - yAxisMode: MetricsExplorerYAxisMode; - stack: boolean; -} +export const metricsExplorerChartOptionsRT = t.type({ + yAxisMode: t.keyof( + Object.fromEntries(values(MetricsExplorerYAxisMode).map((v) => [v, null])) as Record< + MetricsExplorerYAxisMode, + null + > + ), + type: t.keyof( + Object.fromEntries(values(MetricsExplorerChartType).map((v) => [v, null])) as Record< + MetricsExplorerChartType, + null + > + ), + stack: t.boolean, +}); -export interface MetricsExplorerOptions { - metrics: MetricsExplorerOptionsMetric[]; - limit?: number; - groupBy?: string | string[]; - filterQuery?: string; - aggregation: MetricsExplorerAggregation; - forceInterval?: boolean; - dropLastBucket?: boolean; - source?: string; -} +export type MetricsExplorerChartOptions = t.TypeOf; -export interface MetricsExplorerTimeOptions { - from: string; - to: string; - interval: string; -} +const metricExplorerOptionsRequiredRT = t.type({ + aggregation: t.string, + metrics: t.array(metricsExplorerOptionsMetricRT), +}); + +const metricExplorerOptionsOptionalRT = t.partial({ + limit: t.number, + groupBy: t.union([t.string, t.array(t.string)]), + filterQuery: t.string, + source: t.string, + forceInterval: t.boolean, + dropLastBucket: t.boolean, +}); +export const metricExplorerOptionsRT = t.intersection([ + metricExplorerOptionsRequiredRT, + metricExplorerOptionsOptionalRT, +]); + +export type MetricsExplorerOptions = t.TypeOf; + +export const metricsExplorerTimeOptionsRT = t.type({ + from: t.string, + to: t.string, + interval: t.string, +}); +export type MetricsExplorerTimeOptions = t.TypeOf; export const DEFAULT_TIMERANGE: MetricsExplorerTimeOptions = { from: 'now-1h',