diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx index 222e00b57fe702..d3fcd9671acf76 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx @@ -16,15 +16,18 @@ import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/t import { MetricsExplorerOptions, MetricsExplorerTimeOptions, + MetricsExplorerYAxisMode, + MetricsExplorerChartOptions, } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import euiStyled from '../../../../../common/eui_styled_components'; import { createFormatterForMetric } from './helpers/create_formatter_for_metric'; -import { MetricLineSeries } from './line_series'; +import { MetricExplorerSeriesChart } from './series_chart'; import { MetricsExplorerChartContextMenu } from './chart_context_menu'; import { SourceQuery } from '../../graphql/types'; import { MetricsExplorerEmptyChart } from './empty_chart'; import { MetricsExplorerNoMetrics } from './no_metrics'; import { getChartTheme } from './helpers/get_chart_theme'; +import { calculateDomain } from './helpers/calculate_domain'; interface Props { intl: InjectedIntl; @@ -33,6 +36,7 @@ interface Props { width?: number | string; height?: number | string; options: MetricsExplorerOptions; + chartOptions: MetricsExplorerChartOptions; series: MetricsExplorerSeries; source: SourceQuery.Query['source']['configuration'] | undefined; timeRange: MetricsExplorerTimeOptions; @@ -45,6 +49,7 @@ export const MetricsExplorerChart = injectUICapabilities( ({ source, options, + chartOptions, series, title, onFilter, @@ -66,6 +71,11 @@ export const MetricsExplorerChart = injectUICapabilities( [series.rows] ); const yAxisFormater = useCallback(createFormatterForMetric(first(metrics)), [options]); + const dataDomain = calculateDomain(series, metrics, chartOptions.stack); + const domain = + chartOptions.yAxisMode === MetricsExplorerYAxisMode.fromZero + ? { ...dataDomain, min: 0 } + : dataDomain; return (
{options.groupBy ? ( @@ -80,6 +90,7 @@ export const MetricsExplorerChart = injectUICapabilities( 0 ? ( {metrics.map((metric, id) => ( - + ))} diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx index 0f5547cb0e7932..c5c5a26d8241b2 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { MetricsExplorerChartContextMenu, createNodeDetailLink } from './chart_context_menu'; import { mountWithIntl } from '../../utils/enzyme_helpers'; -import { options, source, timeRange } from '../../utils/fixtures/metrics_explorer'; +import { options, source, timeRange, chartOptions } from '../../utils/fixtures/metrics_explorer'; import { UICapabilities } from 'ui/capabilities'; import { InfraNodeType } from '../../graphql/types'; import DateMath from '@elastic/datemath'; @@ -37,6 +37,7 @@ describe('MetricsExplorerChartContextMenu', () => { options={options} onFilter={onFilter} uiCapabilities={uiCapabilities} + chartOptions={chartOptions} /> ); @@ -57,6 +58,7 @@ describe('MetricsExplorerChartContextMenu', () => { options={customOptions} onFilter={onFilter} uiCapabilities={uiCapabilities} + chartOptions={chartOptions} /> ); component.find('button').simulate('click'); @@ -71,6 +73,7 @@ describe('MetricsExplorerChartContextMenu', () => { series={series} options={options} uiCapabilities={uiCapabilities} + chartOptions={chartOptions} /> ); @@ -89,6 +92,7 @@ describe('MetricsExplorerChartContextMenu', () => { options={customOptions} onFilter={onFilter} uiCapabilities={uiCapabilities} + chartOptions={chartOptions} /> ); @@ -105,6 +109,7 @@ describe('MetricsExplorerChartContextMenu', () => { series={series} options={customOptions} uiCapabilities={uiCapabilities} + chartOptions={chartOptions} /> ); @@ -125,6 +130,7 @@ describe('MetricsExplorerChartContextMenu', () => { options={options} onFilter={onFilter} uiCapabilities={customUICapabilities} + chartOptions={chartOptions} /> ); @@ -144,6 +150,7 @@ describe('MetricsExplorerChartContextMenu', () => { options={customOptions} onFilter={onFilter} uiCapabilities={customUICapabilities} + chartOptions={chartOptions} /> ); expect(component.find('button').length).toBe(0); diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx index 01d44d446392d7..a0e696a20ff9d7 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx @@ -17,6 +17,7 @@ import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/t import { MetricsExplorerOptions, MetricsExplorerTimeOptions, + MetricsExplorerChartOptions, } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import { createTSVBLink } from './helpers/create_tsvb_link'; import { InfraNodeType } from '../../graphql/types'; @@ -31,6 +32,7 @@ interface Props { source?: SourceConfiguration; timeRange: MetricsExplorerTimeOptions; uiCapabilities: UICapabilities; + chartOptions: MetricsExplorerChartOptions; } const fieldToNodeType = (source: SourceConfiguration, field: string): InfraNodeType | undefined => { @@ -66,7 +68,7 @@ export const createNodeDetailLink = ( }; export const MetricsExplorerChartContextMenu = injectI18n( - ({ intl, onFilter, options, series, source, timeRange, uiCapabilities }: Props) => { + ({ intl, onFilter, options, series, source, timeRange, uiCapabilities, chartOptions }: Props) => { const [isPopoverOpen, setPopoverState] = useState(false); const supportFiltering = options.groupBy != null && onFilter != null; const handleFilter = useCallback(() => { @@ -78,7 +80,7 @@ export const MetricsExplorerChartContextMenu = injectI18n( setPopoverState(false); }, [supportFiltering, options.groupBy, series.id, onFilter]); - const tsvbUrl = createTSVBLink(source, options, series, timeRange); + const tsvbUrl = createTSVBLink(source, options, series, timeRange, chartOptions); // Only display the "Add Filter" option if it's supported const filterByItem = supportFiltering diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx new file mode 100644 index 00000000000000..b44ba147784482 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_options.tsx @@ -0,0 +1,166 @@ +/* + * 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 React, { useState, useCallback } from 'react'; +import { InjectedIntl, injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { + EuiRadioGroup, + EuiButtonEmpty, + EuiPopover, + EuiForm, + EuiFormRow, + EuiSwitch, +} from '@elastic/eui'; +import { + MetricsExplorerChartOptions as ChartOptions, + MetricsExplorerYAxisMode, + MetricsExplorerChartType, +} from '../../containers/metrics_explorer/use_metrics_explorer_options'; + +interface Props { + chartOptions: ChartOptions; + onChange: (options: ChartOptions) => void; + intl: InjectedIntl; +} + +export const MetricsExplorerChartOptions = injectI18n(({ chartOptions, onChange, intl }: Props) => { + const [isPopoverOpen, setPopoverState] = useState(false); + + const handleClosePopover = useCallback(() => { + setPopoverState(false); + }, []); + + const handleOpenPopover = useCallback(() => { + setPopoverState(true); + }, []); + + const button = ( + + + + ); + + const yAxisRadios = [ + { + id: MetricsExplorerYAxisMode.auto, + label: intl.formatMessage({ + id: 'xpack.infra.metricsExplorer.chartOptions.autoLabel', + defaultMessage: 'Automatic (Min to Max)', + }), + }, + { + id: MetricsExplorerYAxisMode.fromZero, + label: intl.formatMessage({ + id: 'xpack.infra.metricsExplorer.chartOptions.fromZeroLabel', + defaultMessage: 'From Zero (0 to Max)', + }), + }, + ]; + + const typeRadios = [ + { + id: MetricsExplorerChartType.line, + label: intl.formatMessage({ + id: 'xpack.infra.metricsExplorer.chartOptions.lineLabel', + defaultMessage: 'Line', + }), + }, + { + id: MetricsExplorerChartType.area, + label: intl.formatMessage({ + id: 'xpack.infra.metricsExplorer.chartOptions.areaLabel', + defaultMessage: 'Area', + }), + }, + ]; + + const handleYAxisChange = useCallback( + (id: string) => { + onChange({ + ...chartOptions, + yAxisMode: id as MetricsExplorerYAxisMode, + }); + }, + [chartOptions, onChange] + ); + + const handleTypeChange = useCallback( + (id: string) => { + onChange({ + ...chartOptions, + type: id as MetricsExplorerChartType, + }); + }, + [chartOptions, onChange] + ); + + const handleStackChange = useCallback( + e => { + onChange({ + ...chartOptions, + stack: e.target.checked, + }); + }, + [chartOptions, onChange] + ); + + return ( + + + + + + + + + + + + + + ); +}); diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx index 6ab77fd0d50178..69134383f08f93 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx @@ -11,6 +11,7 @@ import { MetricsExplorerResponse } from '../../../server/routes/metrics_explorer import { MetricsExplorerOptions, MetricsExplorerTimeOptions, + MetricsExplorerChartOptions, } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import { InfraLoadingPanel } from '../loading'; import { NoData } from '../empty_states/no_data'; @@ -20,6 +21,7 @@ import { SourceQuery } from '../../graphql/types'; interface Props { loading: boolean; options: MetricsExplorerOptions; + chartOptions: MetricsExplorerChartOptions; onLoadMore: (afterKey: string | null) => void; onRefetch: () => void; onFilter: (filter: string) => void; @@ -35,6 +37,7 @@ export const MetricsExplorerCharts = injectI18n( data, onLoadMore, options, + chartOptions, onRefetch, intl, onFilter, @@ -77,7 +80,7 @@ export const MetricsExplorerCharts = injectI18n( } return ( - +
{data.series.map(series => ( @@ -85,6 +88,7 @@ export const MetricsExplorerCharts = injectI18n( key={`chart-${series.id}`} onFilter={onFilter} options={options} + chartOptions={chartOptions} title={options.groupBy ? series.id : null} height={data.series.length > 1 ? 200 : 400} series={series} @@ -127,7 +131,7 @@ export const MetricsExplorerCharts = injectI18n( ) : null}
) : null} -
+
); } ); diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts new file mode 100644 index 00000000000000..2f6097a1514fa0 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts @@ -0,0 +1,33 @@ +/* + * 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 { min, max, sum } from 'lodash'; +import { MetricsExplorerSeries } from '../../../../server/routes/metrics_explorer/types'; +import { MetricsExplorerOptionsMetric } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; + +export const calculateDomain = ( + series: MetricsExplorerSeries, + metrics: MetricsExplorerOptionsMetric[], + stacked = false +): { min: number; max: number } => { + const values = series.rows + .reduce( + (acc, row) => { + const rowValues = metrics + .map((m, index) => { + return (row[`metric_${index}`] as number) || null; + }) + .filter(v => v); + const minValue = min(rowValues); + // For stacked domains we want to add 10% head room so the charts have + // enough room to draw the 2 pixel line as well. + const maxValue = stacked ? sum(rowValues) * 1.1 : max(rowValues); + return acc.concat([minValue || null, maxValue || null]); + }, + [] as Array + ) + .filter(v => v); + return { min: min(values) || 0, max: max(values) || 0 }; +}; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts new file mode 100644 index 00000000000000..c335b8b3c31ac1 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { calculateDomain } from './calculate_domain'; +import { + MetricsExplorerSeries, + MetricsExplorerAggregation, + MetricsExplorerColumnType, +} from '../../../../server/routes/metrics_explorer/types'; +import { MetricsExplorerOptionsMetric } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerColor } from '../../../../common/color_palette'; +describe('calculateDomain()', () => { + const series: MetricsExplorerSeries = { + id: 'test-01', + columns: [ + { type: MetricsExplorerColumnType.date, name: 'timestamp' }, + { type: MetricsExplorerColumnType.number, name: 'metric_0' }, + { type: MetricsExplorerColumnType.number, name: 'metric_1' }, + { type: MetricsExplorerColumnType.string, name: 'groupBy' }, + ], + rows: [ + { timestamp: 1562860500000, metric_0: null, metric_1: null }, + { timestamp: 1562860600000, metric_0: 0.1, metric_1: 0.3 }, + { timestamp: 1562860700000, metric_0: 0.5, metric_1: 0.7 }, + { timestamp: 1562860700000, metric_0: 0.4, metric_1: 0.9 }, + { timestamp: 1562860900000, metric_0: 0.01, metric_1: 0.5 }, + ], + }; + const metrics: MetricsExplorerOptionsMetric[] = [ + { + aggregation: MetricsExplorerAggregation.avg, + field: 'system.memory.free', + color: MetricsExplorerColor.color0, + }, + { + aggregation: MetricsExplorerAggregation.avg, + field: 'system.memory.used.bytes', + color: MetricsExplorerColor.color1, + }, + ]; + it('should return the min and max across 2 metrics', () => { + expect(calculateDomain(series, metrics)).toEqual({ min: 0.01, max: 0.9 }); + }); + it('should return the min and combined max across 2 metrics with 10% head room when stacked', () => { + expect(calculateDomain(series, metrics, true)).toEqual({ min: 0.01, max: 1.4300000000000002 }); + }); +}); diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts index 92d91a20439272..d190d09da992ea 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts @@ -5,10 +5,14 @@ */ import { createTSVBLink, createFilterFromOptions } from './create_tsvb_link'; -import { source, options, timeRange } from '../../../utils/fixtures/metrics_explorer'; +import { source, options, timeRange, chartOptions } from '../../../utils/fixtures/metrics_explorer'; import uuid from 'uuid'; import { OutputBuffer } from 'uuid/interfaces'; import { MetricsExplorerAggregation } from '../../../../server/routes/metrics_explorer/types'; +import { + MetricsExplorerYAxisMode, + MetricsExplorerChartType, +} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; jest.mock('uuid'); const mockedUuid = uuid as jest.Mocked; @@ -17,11 +21,12 @@ const series = { id: 'example-01', rows: [], columns: [] }; describe('createTSVBLink()', () => { it('should just work', () => { - const link = createTSVBLink(source, options, series, timeRange); + const link = createTSVBLink(source, options, series, timeRange, chartOptions); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); + it('should work with rates', () => { const customOptions = { ...options, @@ -29,16 +34,16 @@ describe('createTSVBLink()', () => { { aggregation: MetricsExplorerAggregation.rate, field: 'system.network.out.bytes' }, ], }; - const link = createTSVBLink(source, customOptions, series, timeRange); + const link = createTSVBLink(source, customOptions, series, timeRange, chartOptions); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:bytes,id:test-id,label:'rate(system.network.out.bytes)',line_width:2,metrics:!((field:system.network.out.bytes,id:test-id,type:max),(field:test-id,id:test-id,type:derivative,unit:'1s'),(field:test-id,id:test-id,type:positive_only)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}}/s)),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:bytes,id:test-id,label:'rate(system.network.out.bytes)',line_width:2,metrics:!((field:system.network.out.bytes,id:test-id,type:max),(field:test-id,id:test-id,type:derivative,unit:'1s'),(field:test-id,id:test-id,type:positive_only)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}}/s)),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); it('should work with time range', () => { const customTimeRange = { ...timeRange, from: 'now-10m', to: 'now' }; - const link = createTSVBLink(source, options, series, customTimeRange); + const link = createTSVBLink(source, options, series, customTimeRange, chartOptions); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-10m,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-10m,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); it('should work with source', () => { @@ -47,9 +52,9 @@ describe('createTSVBLink()', () => { metricAlias: 'my-beats-*', fields: { ...source.fields, timestamp: 'time' }, }; - const link = createTSVBLink(customSource, options, series, timeRange); + const link = createTSVBLink(customSource, options, series, timeRange, chartOptions); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" ); }); it('should work with filterQuery', () => { @@ -59,9 +64,37 @@ describe('createTSVBLink()', () => { fields: { ...source.fields, timestamp: 'time' }, }; const customOptions = { ...options, filterQuery: 'system.network.name:lo*' }; - const link = createTSVBLink(customSource, customOptions, series, timeRange); + const link = createTSVBLink(customSource, customOptions, series, timeRange, chartOptions); + expect(link).toBe( + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" + ); + }); + + it('should remove axis_min from link', () => { + const customChartOptions = { ...chartOptions, yAxisMode: MetricsExplorerYAxisMode.auto }; + const link = createTSVBLink(source, options, series, timeRange, customChartOptions); + expect(link).toBe( + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + ); + }); + + it('should change series to area', () => { + const customChartOptions = { ...chartOptions, type: MetricsExplorerChartType.area }; + const link = createTSVBLink(source, options, series, timeRange, customChartOptions); + expect(link).toBe( + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0.5,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + ); + }); + + it('should change series to area and stacked', () => { + const customChartOptions = { + ...chartOptions, + type: MetricsExplorerChartType.area, + stack: true, + }; + const link = createTSVBLink(source, options, series, timeRange, customChartOptions); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0.5,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:stacked,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts index 9053002795d45e..788de6a129aa9c 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts @@ -6,6 +6,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; +import { set } from 'lodash'; import { colorTransformer, MetricsExplorerColor } from '../../../../common/color_palette'; import { MetricsExplorerSeries, @@ -15,6 +16,9 @@ import { MetricsExplorerOptions, MetricsExplorerOptionsMetric, MetricsExplorerTimeOptions, + MetricsExplorerChartOptions, + MetricsExplorerYAxisMode, + MetricsExplorerChartType, } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; import { metricToFormat } from './metric_to_format'; import { InfraFormatterType } from '../../../lib/lib'; @@ -55,7 +59,9 @@ export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptions } }; -const mapMetricToSeries = (metric: MetricsExplorerOptionsMetric) => { +const mapMetricToSeries = (chartOptions: MetricsExplorerChartOptions) => ( + metric: MetricsExplorerOptionsMetric +) => { const format = metricToFormat(metric); return { label: createMetricLabel(metric), @@ -65,7 +71,7 @@ const mapMetricToSeries = (metric: MetricsExplorerOptionsMetric) => { (metric.color && colorTransformer(metric.color)) || colorTransformer(MetricsExplorerColor.color0) ), - fill: 0, + fill: chartOptions.type === MetricsExplorerChartType.area ? 0.5 : 0, formatter: format === InfraFormatterType.bits ? InfraFormatterType.bytes : format, value_template: MetricsExplorerAggregation.rate === metric.aggregation ? '{{value}}/s' : '{{value}}', @@ -75,7 +81,7 @@ const mapMetricToSeries = (metric: MetricsExplorerOptionsMetric) => { point_size: 0, separate_axis: 0, split_mode: 'everything', - stacked: 'none', + stacked: chartOptions.stack ? 'stacked' : 'none', }; }; @@ -98,7 +104,8 @@ export const createTSVBLink = ( source: SourceQuery.Query['source']['configuration'] | undefined, options: MetricsExplorerOptions, series: MetricsExplorerSeries, - timeRange: MetricsExplorerTimeOptions + timeRange: MetricsExplorerTimeOptions, + chartOptions: MetricsExplorerChartOptions ) => { const appState = { filters: [], @@ -115,7 +122,7 @@ export const createTSVBLink = ( default_index_pattern: (source && source.metricAlias) || 'metricbeat-*', index_pattern: (source && source.metricAlias) || 'metricbeat-*', interval: 'auto', - series: options.metrics.map(mapMetricToSeries), + series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, show_legend: 1, time_field: (source && source.fields.timestamp) || '@timestamp', @@ -127,6 +134,10 @@ export const createTSVBLink = ( }, }; + if (chartOptions.yAxisMode === MetricsExplorerYAxisMode.fromZero) { + set(appState, 'vis.params.axis_min', 0); + } + const globalState = { refreshInterval: { pause: true, value: 0 }, time: { from: timeRange.from, to: timeRange.to }, diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/line_series.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx similarity index 73% rename from x-pack/legacy/plugins/infra/public/components/metrics_explorer/line_series.tsx rename to x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx index 6e29d943627988..b077d7c17a0f99 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/line_series.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx @@ -6,33 +6,54 @@ import React from 'react'; import { - LineSeries, ScaleType, getSpecId, DataSeriesColorsValues, CustomSeriesColorsMap, + AreaSeries, } from '@elastic/charts'; import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/types'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { createMetricLabel } from './helpers/create_metric_label'; -import { MetricsExplorerOptionsMetric } from '../../containers/metrics_explorer/use_metrics_explorer_options'; +import { + MetricsExplorerOptionsMetric, + MetricsExplorerChartType, +} from '../../containers/metrics_explorer/use_metrics_explorer_options'; interface Props { metric: MetricsExplorerOptionsMetric; id: string | number; series: MetricsExplorerSeries; + type: MetricsExplorerChartType; + stack: boolean; } -export const MetricLineSeries = ({ metric, id, series }: Props) => { +export const MetricExplorerSeriesChart = ({ metric, id, series, type, stack }: Props) => { const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(MetricsExplorerColor.color0); - const seriesLineStyle = { + + const yAccessor = `metric_${id}`; + const specId = getSpecId(yAccessor); + const colors: DataSeriesColorsValues = { + colorValues: [], + specId, + }; + const customColors: CustomSeriesColorsMap = new Map(); + customColors.set(colors, color); + const chartId = `series-${series.id}-${yAccessor}`; + + const seriesAreaStyle = { line: { stroke: color, strokeWidth: 2, visible: true, }, + area: { + fill: color, + opacity: 0.5, + visible: type === MetricsExplorerChartType.area, + }, border: { visible: false, strokeWidth: 2, @@ -46,19 +67,9 @@ export const MetricLineSeries = ({ metric, id, series }: Props) => { opacity: 1, }, }; - - const yAccessor = `metric_${id}`; - const specId = getSpecId(yAccessor); - const colors: DataSeriesColorsValues = { - colorValues: [], - specId, - }; - const customColors: CustomSeriesColorsMap = new Map(); - customColors.set(colors, color); - return ( - { xAccessor="timestamp" yAccessors={[yAccessor]} data={series.rows} - lineSeriesStyle={seriesLineStyle} + stackAccessors={stack ? ['timestamp'] : void 0} + areaSeriesStyle={seriesAreaStyle} customSeriesColors={customColors} /> ); diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/toolbar.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/toolbar.tsx index 8b686bb1de62d7..80d91587811d3d 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/toolbar.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/toolbar.tsx @@ -15,24 +15,28 @@ import { import { MetricsExplorerOptions, MetricsExplorerTimeOptions, + MetricsExplorerChartOptions, } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import { Toolbar } from '../eui/toolbar'; import { MetricsExplorerKueryBar } from './kuery_bar'; import { MetricsExplorerMetrics } from './metrics'; import { MetricsExplorerGroupBy } from './group_by'; import { MetricsExplorerAggregationPicker } from './aggregation'; +import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options'; interface Props { intl: InjectedIntl; derivedIndexPattern: StaticIndexPattern; timeRange: MetricsExplorerTimeOptions; options: MetricsExplorerOptions; + chartOptions: MetricsExplorerChartOptions; onRefresh: () => void; onTimeChange: (start: string, end: string) => void; onGroupByChange: (groupBy: string | null) => void; onFilterQuerySubmit: (query: string) => void; onMetricsChange: (metrics: MetricsExplorerMetric[]) => void; onAggregationChange: (aggregation: MetricsExplorerAggregation) => void; + onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void; } export const MetricsExplorerToolbar = injectI18n( @@ -46,6 +50,8 @@ export const MetricsExplorerToolbar = injectI18n( onFilterQuerySubmit, onMetricsChange, onAggregationChange, + chartOptions, + onChartOptionsChange, }: Props) => { const isDefaultOptions = options.aggregation === MetricsExplorerAggregation.avg && options.metrics.length === 0; @@ -99,6 +105,12 @@ export const MetricsExplorerToolbar = injectI18n( value={options.filterQuery} /> + + + =10s', }; +export const DEFAULT_CHART_OPTIONS: MetricsExplorerChartOptions = { + type: MetricsExplorerChartType.line, + yAxisMode: MetricsExplorerYAxisMode.fromZero, + stack: false, +}; + export const DEFAULT_METRICS: MetricsExplorerOptionsMetric[] = [ { aggregation: MetricsExplorerAggregation.avg, @@ -92,9 +114,15 @@ export const useMetricsExplorerOptions = () => { 'MetricsExplorerTimeRange', DEFAULT_TIMERANGE ); + const [chartOptions, setChartOptions] = useStateWithLocalStorage( + 'MetricsExplorerChartOptions', + DEFAULT_CHART_OPTIONS + ); const [isAutoReloading, setAutoReloading] = useState(false); return { options, + chartOptions, + setChartOptions, currentTimerange, isAutoReloading, setOptions, diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx index d9f3a5d65911dd..027d79d8cb0721 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx @@ -14,17 +14,26 @@ import { MetricsExplorerOptions, MetricsExplorerOptionsContainer, MetricsExplorerTimeOptions, + MetricsExplorerYAxisMode, + MetricsExplorerChartType, + MetricsExplorerChartOptions, } from './use_metrics_explorer_options'; interface MetricsExplorerUrlState { timerange?: MetricsExplorerTimeOptions; options?: MetricsExplorerOptions; + chartOptions?: MetricsExplorerChartOptions; } export const WithMetricsExplorerOptionsUrlState = () => { - const { options, currentTimerange, setOptions: setRawOptions, setTimeRange } = useContext( - MetricsExplorerOptionsContainer.Context - ); + const { + options, + chartOptions, + setChartOptions, + currentTimerange, + setOptions: setRawOptions, + setTimeRange, + } = useContext(MetricsExplorerOptionsContainer.Context); const setOptions = (value: MetricsExplorerOptions) => { setRawOptions(value); @@ -33,32 +42,31 @@ export const WithMetricsExplorerOptionsUrlState = () => { const urlState = useMemo( () => ({ options, + chartOptions, timerange: currentTimerange, }), - [options, currentTimerange] + [options, chartOptions, currentTimerange] ); + const handleChange = (newUrlState: MetricsExplorerUrlState | undefined) => { + if (newUrlState && newUrlState.options) { + setOptions(newUrlState.options); + } + if (newUrlState && newUrlState.timerange) { + setTimeRange(newUrlState.timerange); + } + if (newUrlState && newUrlState.chartOptions) { + setChartOptions(newUrlState.chartOptions); + } + }; + return ( { - if (newUrlState && newUrlState.options) { - setOptions(newUrlState.options); - } - if (newUrlState && newUrlState.timerange) { - setTimeRange(newUrlState.timerange); - } - }} - onInitialize={newUrlState => { - if (newUrlState && newUrlState.options) { - setOptions(newUrlState.options); - } - if (newUrlState && newUrlState.timerange) { - setTimeRange(newUrlState.timerange); - } - }} + onChange={handleChange} + onInitialize={handleChange} /> ); }; @@ -100,6 +108,22 @@ function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOption } } +function isMetricExplorerChartOptions(subject: any): subject is MetricsExplorerChartOptions { + const ChartOptions = t.type({ + yAxisMode: t.union(values(MetricsExplorerYAxisMode).map(v => t.literal(v as string))), + type: t.union(values(MetricsExplorerChartType).map(v => t.literal(v as string))), + stack: t.boolean, + }); + const result = ChartOptions.decode(subject); + + try { + ThrowReporter.report(result); + return true; + } catch (e) { + return false; + } +} + function isMetricExplorerTimeOption(subject: any): subject is MetricsExplorerTimeOptions { const TimeRange = t.type({ from: t.string, @@ -124,6 +148,9 @@ const mapToUrlState = (value: any): MetricsExplorerUrlState | undefined => { if (value.timerange && isMetricExplorerTimeOption(value.timerange)) { set(finalState, 'timerange', value.timerange); } + if (value.chartOptions && isMetricExplorerChartOptions(value.chartOptions)) { + set(finalState, 'chartOptions', value.chartOptions); + } return finalState; } }; diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx index 60268e6d000631..fc497bc90e3213 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx @@ -32,6 +32,8 @@ export const MetricsExplorerPage = injectI18n( data, currentTimerange, options, + chartOptions, + setChartOptions, handleAggregationChange, handleMetricsChange, handleFilterQuerySubmit, @@ -60,12 +62,14 @@ export const MetricsExplorerPage = injectI18n( derivedIndexPattern={derivedIndexPattern} timeRange={currentTimerange} options={options} + chartOptions={chartOptions} onRefresh={handleRefresh} onTimeChange={handleTimeChange} onGroupByChange={handleGroupByChange} onFilterQuerySubmit={handleFilterQuerySubmit} onMetricsChange={handleMetricsChange} onAggregationChange={handleAggregationChange} + onChartOptionsChange={setChartOptions} /> {error ? ( { const [refreshSignal, setRefreshSignal] = useState(0); const [afterKey, setAfterKey] = useState(null); - const { options, currentTimerange, setTimeRange, setOptions } = useContext( - MetricsExplorerOptionsContainer.Context - ); + const { + options, + currentTimerange, + chartOptions, + setChartOptions, + setTimeRange, + setOptions, + } = useContext(MetricsExplorerOptionsContainer.Context); const { loading, error, data } = useMetricsExplorerData( options, source, @@ -101,6 +106,8 @@ export const useMetricsExplorerState = ( data, currentTimerange, options, + chartOptions, + setChartOptions, handleAggregationChange, handleMetricsChange, handleFilterQuerySubmit, diff --git a/x-pack/legacy/plugins/infra/public/utils/fixtures/metrics_explorer.ts b/x-pack/legacy/plugins/infra/public/utils/fixtures/metrics_explorer.ts index b9bcf5f68be316..d553f03c4ed342 100644 --- a/x-pack/legacy/plugins/infra/public/utils/fixtures/metrics_explorer.ts +++ b/x-pack/legacy/plugins/infra/public/utils/fixtures/metrics_explorer.ts @@ -13,6 +13,9 @@ import { import { MetricsExplorerOptions, MetricsExplorerTimeOptions, + MetricsExplorerChartType, + MetricsExplorerYAxisMode, + MetricsExplorerChartOptions, } from '../../containers/metrics_explorer/use_metrics_explorer_options'; export const options: MetricsExplorerOptions = { @@ -38,6 +41,12 @@ export const source = { }, }; +export const chartOptions: MetricsExplorerChartOptions = { + type: MetricsExplorerChartType.line, + yAxisMode: MetricsExplorerYAxisMode.fromZero, + stack: false, +}; + export const derivedIndexPattern = { title: 'metricbeat-*', fields: [] }; export const timeRange: MetricsExplorerTimeOptions = {