[Metrics UI] Fix validating Metrics Explorer URL (#74311)

This commit is contained in:
Zacqary Adam Xeper 2020-08-05 13:13:22 -05:00 committed by GitHub
parent 1c428ffed7
commit 0737241dec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 86 deletions

View file

@ -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',
},
};

View file

@ -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)) {

View file

@ -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',