[Metrics UI] Fix validating Metrics Explorer URL (#74311)
This commit is contained in:
parent
1c428ffed7
commit
0737241dec
|
@ -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',
|
||||||
|
},
|
||||||
|
};
|
|
@ -5,19 +5,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { set } from '@elastic/safer-lodash-set';
|
import { set } from '@elastic/safer-lodash-set';
|
||||||
import { values } from 'lodash';
|
|
||||||
import React, { useContext, useMemo } from 'react';
|
import React, { useContext, useMemo } from 'react';
|
||||||
import * as t from 'io-ts';
|
|
||||||
import { ThrowReporter } from 'io-ts/lib/ThrowReporter';
|
import { ThrowReporter } from 'io-ts/lib/ThrowReporter';
|
||||||
import { MetricsExplorerColor } from '../../../common/color_palette';
|
|
||||||
import { UrlStateContainer } from '../../utils/url_state';
|
import { UrlStateContainer } from '../../utils/url_state';
|
||||||
import {
|
import {
|
||||||
MetricsExplorerOptions,
|
MetricsExplorerOptions,
|
||||||
MetricsExplorerOptionsContainer,
|
MetricsExplorerOptionsContainer,
|
||||||
MetricsExplorerTimeOptions,
|
MetricsExplorerTimeOptions,
|
||||||
MetricsExplorerYAxisMode,
|
|
||||||
MetricsExplorerChartType,
|
|
||||||
MetricsExplorerChartOptions,
|
MetricsExplorerChartOptions,
|
||||||
|
metricExplorerOptionsRT,
|
||||||
|
metricsExplorerChartOptionsRT,
|
||||||
|
metricsExplorerTimeOptionsRT,
|
||||||
} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||||
|
|
||||||
interface MetricsExplorerUrlState {
|
interface MetricsExplorerUrlState {
|
||||||
|
@ -74,36 +72,7 @@ export const WithMetricsExplorerOptionsUrlState = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOptions {
|
function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOptions {
|
||||||
const MetricRequired = t.type({
|
const result = metricExplorerOptionsRT.decode(subject);
|
||||||
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<string, null>
|
|
||||||
),
|
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ThrowReporter.report(result);
|
ThrowReporter.report(result);
|
||||||
|
@ -114,22 +83,7 @@ function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOption
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMetricExplorerChartOptions(subject: any): subject is MetricsExplorerChartOptions {
|
function isMetricExplorerChartOptions(subject: any): subject is MetricsExplorerChartOptions {
|
||||||
const ChartOptions = t.type({
|
const result = metricsExplorerChartOptionsRT.decode(subject);
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ThrowReporter.report(result);
|
ThrowReporter.report(result);
|
||||||
|
@ -140,12 +94,7 @@ function isMetricExplorerChartOptions(subject: any): subject is MetricsExplorerC
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMetricExplorerTimeOption(subject: any): subject is MetricsExplorerTimeOptions {
|
function isMetricExplorerTimeOption(subject: any): subject is MetricsExplorerTimeOptions {
|
||||||
const TimeRange = t.type({
|
const result = metricsExplorerTimeOptionsRT.decode(subject);
|
||||||
from: t.string,
|
|
||||||
to: t.string,
|
|
||||||
interval: t.string,
|
|
||||||
});
|
|
||||||
const result = TimeRange.decode(subject);
|
|
||||||
try {
|
try {
|
||||||
ThrowReporter.report(result);
|
ThrowReporter.report(result);
|
||||||
return true;
|
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 = {};
|
const finalState = {};
|
||||||
if (value) {
|
if (value) {
|
||||||
if (value.options && isMetricExplorerOptions(value.options)) {
|
if (value.options && isMetricExplorerOptions(value.options)) {
|
||||||
|
|
|
@ -4,19 +4,29 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 createContainer from 'constate';
|
||||||
import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react';
|
import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react';
|
||||||
import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill';
|
import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill';
|
||||||
import { MetricsExplorerColor } from '../../../../../common/color_palette';
|
import { MetricsExplorerColor } from '../../../../../common/color_palette';
|
||||||
import {
|
import { metricsExplorerMetricRT } from '../../../../../common/http_api/metrics_explorer';
|
||||||
MetricsExplorerAggregation,
|
|
||||||
MetricsExplorerMetric,
|
|
||||||
} from '../../../../../common/http_api/metrics_explorer';
|
|
||||||
|
|
||||||
export type MetricsExplorerOptionsMetric = MetricsExplorerMetric & {
|
const metricsExplorerOptionsMetricRT = t.intersection([
|
||||||
color?: MetricsExplorerColor;
|
metricsExplorerMetricRT,
|
||||||
label?: string;
|
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<typeof metricsExplorerOptionsMetricRT>;
|
||||||
|
|
||||||
export enum MetricsExplorerChartType {
|
export enum MetricsExplorerChartType {
|
||||||
line = 'line',
|
line = 'line',
|
||||||
|
@ -29,28 +39,50 @@ export enum MetricsExplorerYAxisMode {
|
||||||
auto = 'auto',
|
auto = 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricsExplorerChartOptions {
|
export const metricsExplorerChartOptionsRT = t.type({
|
||||||
type: MetricsExplorerChartType;
|
yAxisMode: t.keyof(
|
||||||
yAxisMode: MetricsExplorerYAxisMode;
|
Object.fromEntries(values(MetricsExplorerYAxisMode).map((v) => [v, null])) as Record<
|
||||||
stack: boolean;
|
MetricsExplorerYAxisMode,
|
||||||
}
|
null
|
||||||
|
>
|
||||||
|
),
|
||||||
|
type: t.keyof(
|
||||||
|
Object.fromEntries(values(MetricsExplorerChartType).map((v) => [v, null])) as Record<
|
||||||
|
MetricsExplorerChartType,
|
||||||
|
null
|
||||||
|
>
|
||||||
|
),
|
||||||
|
stack: t.boolean,
|
||||||
|
});
|
||||||
|
|
||||||
export interface MetricsExplorerOptions {
|
export type MetricsExplorerChartOptions = t.TypeOf<typeof metricsExplorerChartOptionsRT>;
|
||||||
metrics: MetricsExplorerOptionsMetric[];
|
|
||||||
limit?: number;
|
|
||||||
groupBy?: string | string[];
|
|
||||||
filterQuery?: string;
|
|
||||||
aggregation: MetricsExplorerAggregation;
|
|
||||||
forceInterval?: boolean;
|
|
||||||
dropLastBucket?: boolean;
|
|
||||||
source?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MetricsExplorerTimeOptions {
|
const metricExplorerOptionsRequiredRT = t.type({
|
||||||
from: string;
|
aggregation: t.string,
|
||||||
to: string;
|
metrics: t.array(metricsExplorerOptionsMetricRT),
|
||||||
interval: string;
|
});
|
||||||
}
|
|
||||||
|
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<typeof metricExplorerOptionsRT>;
|
||||||
|
|
||||||
|
export const metricsExplorerTimeOptionsRT = t.type({
|
||||||
|
from: t.string,
|
||||||
|
to: t.string,
|
||||||
|
interval: t.string,
|
||||||
|
});
|
||||||
|
export type MetricsExplorerTimeOptions = t.TypeOf<typeof metricsExplorerTimeOptionsRT>;
|
||||||
|
|
||||||
export const DEFAULT_TIMERANGE: MetricsExplorerTimeOptions = {
|
export const DEFAULT_TIMERANGE: MetricsExplorerTimeOptions = {
|
||||||
from: 'now-1h',
|
from: 'now-1h',
|
||||||
|
|
Loading…
Reference in a new issue