From 3eded8e680b8f2c3b7e9c5c12fb7fd238ce6926f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 24 Jan 2022 11:08:16 +0100 Subject: [PATCH] [TSVB] Handle ignore daylight time correctly and fix shift problem (#123398) --- .../components/vis_types/timeseries/vis.js | 1 + .../vis_types/timeseries/vis.test.js | 60 ++++++++++++++++++- .../visualizations/views/timeseries/index.js | 5 +- .../response_processors/series/time_shift.js | 30 +++++++++- .../series/time_shift.test.js | 55 +++++++++++++++++ .../vis_data/series/handle_response_body.ts | 3 +- 6 files changed, 146 insertions(+), 8 deletions(-) diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js index b177ef632e210..2790130c553b5 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js @@ -266,6 +266,7 @@ class TimeseriesVisualization extends Component { legend={Boolean(model.show_legend)} legendPosition={model.legend_position} truncateLegend={Boolean(model.truncate_legend)} + ignoreDaylightTime={Boolean(model.ignore_daylight_time)} maxLegendLines={model.max_lines_legend} tooltipMode={model.tooltip_mode} xAxisFormatter={this.xAxisFormatter(interval)} diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js index cf4c327df3d77..12ae70cca1036 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js @@ -10,11 +10,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TimeSeries } from '../../../visualizations/views/timeseries'; import TimeseriesVisualization from './vis'; -import { setFieldFormats } from '../../../../services'; +import { setFieldFormats, setCharts, setUISettings } from '../../../../services'; import { createFieldFormatter } from '../../lib/create_field_formatter'; import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; import { METRIC_TYPES } from '../../../../../../../data/common'; import { getFieldFormatsRegistry } from '../../../../../../../data/public/test_utils'; +import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../../../charts/public'; jest.mock('../../../../../../../data/public/services', () => ({ getUiSettings: () => ({ get: jest.fn() }), @@ -35,12 +36,47 @@ describe('TimeseriesVisualization', () => { }) ); - const setupTimeSeriesProps = (formatters, valueTemplates) => { + setCharts({ + theme: { + useChartsTheme: () => ({ + axes: { + tickLabel: { + padding: { + inner: 0, + }, + }, + }, + }), + useChartsBaseTheme: () => ({ + axes: { + tickLabel: { + padding: { + inner: 0, + }, + }, + }, + }), + }, + activeCursor: {}, + }); + + setUISettings({ + get: () => ({}), + isDefault: () => true, + }); + + const renderShallow = (formatters, valueTemplates, modelOverwrites) => { const series = formatters.map((formatter, index) => ({ id: id + index, + label: '', formatter, value_template: valueTemplates?.[index], data: [], + lines: { + show: true, + }, + points: {}, + color: '#000000', metrics: [ { type: METRIC_TYPES.AVG, @@ -63,6 +99,7 @@ describe('TimeseriesVisualization', () => { id, series, use_kibana_indexes: true, + ...modelOverwrites, }} visData={{ [id]: { @@ -75,9 +112,26 @@ describe('TimeseriesVisualization', () => { /> ); - return timeSeriesVisualization.find(TimeSeries).props(); + return timeSeriesVisualization; + }; + + const setupTimeSeriesProps = (formatters, valueTemplates) => { + return renderShallow(formatters, valueTemplates).find(TimeSeries).props(); }; + test('should enable new time axis if ignore daylight time setting is switched off', () => { + const component = renderShallow(['byte'], undefined, { ignore_daylight_time: false }); + console.log(component.find('TimeSeries').dive().debug()); + const xAxis = component.find('TimeSeries').dive().find('[id="bottom"]'); + expect(xAxis.prop('style')).toEqual(MULTILAYER_TIME_AXIS_STYLE); + }); + + test('should disable new time axis for ignore daylight time setting', () => { + const component = renderShallow(['byte'], undefined, { ignore_daylight_time: true }); + const xAxis = component.find('TimeSeries').dive().find('[id="bottom"]'); + expect(xAxis.prop('style')).toBeUndefined(); + }); + test('should return byte formatted value from yAxis formatter for single byte series', () => { const timeSeriesProps = setupTimeSeriesProps(['byte']); diff --git a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js index 9dfddd3457d44..c8e845ce6b54c 100644 --- a/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js @@ -77,6 +77,7 @@ export const TimeSeries = ({ interval, isLastBucketDropped, useLegacyTimeAxis, + ignoreDaylightTime, }) => { // If the color isn't configured by the user, use the color mapping service // to assign a color from the Kibana palette. Colors will be shared across the @@ -152,7 +153,9 @@ export const TimeSeries = ({ const shouldUseNewTimeAxis = series.every( ({ stack, bars, lines }) => (bars?.show && stack !== STACKED_OPTIONS.NONE) || lines?.show - ) && !useLegacyTimeAxis; + ) && + !useLegacyTimeAxis && + !ignoreDaylightTime; return ( diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js index 109e552ce89a1..429050fab36cc 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js @@ -7,9 +7,18 @@ */ import { startsWith } from 'lodash'; -import moment from 'moment'; +import moment from 'moment-timezone'; -export function timeShift(resp, panel, series) { +export function timeShift( + resp, + panel, + series, + meta, + extractFields, + fieldFormatService, + cachedIndexPatternFetcher, + timezone +) { return (next) => (results) => { if (/^([+-]?[\d]+)([shmdwMy]|ms)$/.test(series.offset_time)) { const matches = series.offset_time.match(/^([+-]?[\d]+)([shmdwMy]|ms)$/); @@ -18,14 +27,29 @@ export function timeShift(resp, panel, series) { const offsetValue = matches[1]; const offsetUnit = matches[2]; + let defaultTimezone; + if (!panel.ignore_daylight_time) { + // the datemath plugin always parses dates by using the current default moment time zone. + // to use the configured time zone, we are switching just for the bounds calculation. + defaultTimezone = moment().zoneName(); + moment.tz.setDefault(timezone); + } + results.forEach((item) => { if (startsWith(item.id, series.id)) { item.data = item.data.map((row) => [ - moment.utc(row[0]).add(offsetValue, offsetUnit).valueOf(), + (panel.ignore_daylight_time ? moment.utc : moment)(row[0]) + .add(offsetValue, offsetUnit) + .valueOf(), row[1], ]); } }); + + if (!panel.ignore_daylight_time) { + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + } } } diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js index e00f4aad24130..7fff2603cf47a 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js @@ -73,4 +73,59 @@ describe('timeShift(resp, panel, series)', () => { [1483225210000 + 3600000, 2], ]); }); + + test('shifts in right timezone', async () => { + series.offset_time = '1d'; + const dateBeforeDST = new Date('2022-03-26T12:00:00.000Z').valueOf(); + const dateAfterDST = new Date('2022-03-28T12:00:00.000Z').valueOf(); + resp.aggregations.test.timeseries.buckets[0].key = dateBeforeDST; + resp.aggregations.test.timeseries.buckets[1].key = dateAfterDST; + const next = await timeShift( + resp, + panel, + series, + {}, + undefined, + undefined, + undefined, + 'Europe/Berlin' + )((results) => results); + const results = await stdMetric(resp, panel, series, {})(next)([]); + + expect(results).toHaveLength(1); + expect(results[0].data).toEqual([ + // only 23h in a day because it goes over the DST switch + [dateBeforeDST + 1000 * 60 * 60 * 23, 1], + // regular 24h in a day + [dateAfterDST + 1000 * 60 * 60 * 24, 2], + ]); + }); + + test('shifts in utc if ignore daylight time is set', async () => { + series.offset_time = '1d'; + panel.ignore_daylight_time = 1; + const dateBeforeDST = new Date('2022-03-26T12:00:00.000Z').valueOf(); + const dateAfterDST = new Date('2022-03-28T12:00:00.000Z').valueOf(); + resp.aggregations.test.timeseries.buckets[0].key = dateBeforeDST; + resp.aggregations.test.timeseries.buckets[1].key = dateAfterDST; + const next = await timeShift( + resp, + panel, + series, + {}, + undefined, + undefined, + undefined, + 'Europe/Berlin' + )((results) => results); + const results = await stdMetric(resp, panel, series, {})(next)([]); + + expect(results).toHaveLength(1); + expect(results[0].data).toEqual([ + // still 24h shift because DST is ignored + [dateBeforeDST + 1000 * 60 * 60 * 24, 1], + // regular 24h in a day + [dateAfterDST + 1000 * 60 * 60 * 24, 2], + ]); + }); }); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts index 415844abeedaf..5244bca66a5b3 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts @@ -59,7 +59,8 @@ export function handleResponseBody( meta, extractFields, fieldFormatService, - services.cachedIndexPatternFetcher + services.cachedIndexPatternFetcher, + req.body.timerange.timezone ); return await processor([]);