[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 { 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<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);
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)) {

View file

@ -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<typeof metricsExplorerOptionsMetricRT>;
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<typeof metricsExplorerChartOptionsRT>;
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<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 = {
from: 'now-1h',