diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index eaa2014717714..a115dfbc30474 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -297,6 +297,9 @@ image::images/lens_time_shift.png[Line chart with week-over-week sales compariso . Click *Save and return*. +Time shifts can be used on any metric. The special shift *previous* will show the time window preceding the currently selected one in the time picker in the top right, spanning the same duration. +For example, if *Last 7 days* is selected in the time picker, *previous* will show data from 14 days ago to 7 days ago. This mode can't be used together with date histograms. + [float] [[compare-time-as-percent]] ==== Analyze the percent change between time ranges diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx index 1baab99c99b09..b7563f28aac5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx @@ -89,16 +89,16 @@ export function TimeShift({ return null; } - const { isValueTooSmall, isValueNotMultiple, canShift } = getLayerTimeShiftChecks( - getDateHistogramInterval(layer, indexPattern, activeData, layerId) - ); + const dateHistogramInterval = getDateHistogramInterval(layer, indexPattern, activeData, layerId); + const { isValueTooSmall, isValueNotMultiple, isInvalid, canShift } = + getLayerTimeShiftChecks(dateHistogramInterval); if (!canShift) { return null; } const parsedLocalValue = localValue && parseTimeShift(localValue); - const isLocalValueInvalid = Boolean(parsedLocalValue === 'invalid'); + const isLocalValueInvalid = Boolean(parsedLocalValue && isInvalid(parsedLocalValue)); const localValueTooSmall = parsedLocalValue && isValueTooSmall(parsedLocalValue); const localValueNotMultiple = parsedLocalValue && isValueNotMultiple(parsedLocalValue); @@ -167,7 +167,10 @@ export function TimeShift({ options={timeShiftOptions.filter(({ value }) => { const parsedValue = parseTimeShift(value); return ( - parsedValue && !isValueTooSmall(parsedValue) && !isValueNotMultiple(parsedValue) + parsedValue && + !isValueTooSmall(parsedValue) && + !isValueNotMultiple(parsedValue) && + !(parsedValue === 'previous' && dateHistogramInterval.interval) ); })} selectedOptions={getSelectedOption()} @@ -175,7 +178,7 @@ export function TimeShift({ isInvalid={isLocalValueInvalid} onCreateOption={(val) => { const parsedVal = parseTimeShift(val); - if (parsedVal !== 'invalid') { + if (!isInvalid(parsedVal)) { updateLayer(setTimeShift(columnId, layer, val)); } else { setLocalValue(val); @@ -190,7 +193,7 @@ export function TimeShift({ const choice = choices[0].value as string; const parsedVal = parseTimeShift(choice); - if (parsedVal !== 'invalid') { + if (!isInvalid(parsedVal)) { updateLayer(setTimeShift(columnId, layer, choice)); } else { setLocalValue(choice); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index 8cc6139fedc0a..3f051286f3da9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -18,7 +18,8 @@ import { } from './utils'; import { DEFAULT_TIME_SCALE } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn, getFilter } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers'; +import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.CounterRateOf', { @@ -104,13 +105,16 @@ export const counterRateOperation: OperationDefinition< return hasDateField(newIndexPattern); }, getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { - return getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.counterRate', { - defaultMessage: 'Counter rate', - }) - ); + return combineErrorMessages([ + getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.counterRate', { + defaultMessage: 'Counter rate', + }) + ), + getDisallowedPreviousShiftMessage(layer, columnId), + ]); }, getDisabledStatus(indexPattern, layer, layerType) { const opName = i18n.translate('xpack.lens.indexPattern.counterRate', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index a59491cfc8a6b..6a05c80702797 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -17,7 +17,8 @@ import { checkForDataLayerType, } from './utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn, getFilter } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers'; +import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', { @@ -101,13 +102,16 @@ export const cumulativeSumOperation: OperationDefinition< return true; }, getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { - return getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.cumulativeSum', { - defaultMessage: 'Cumulative sum', - }) - ); + return combineErrorMessages([ + getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.cumulativeSum', { + defaultMessage: 'Cumulative sum', + }) + ), + getDisallowedPreviousShiftMessage(layer, columnId), + ]); }, getDisabledStatus(indexPattern, layer, layerType) { const opName = i18n.translate('xpack.lens.indexPattern.cumulativeSum', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index 730067e9c5577..31b21327958d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -18,7 +18,8 @@ import { } from './utils'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn, getFilter } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers'; +import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; const OPERATION_NAME = 'differences'; @@ -92,13 +93,16 @@ export const derivativeOperation: OperationDefinition< }, onOtherColumnChanged: adjustTimeScaleOnOtherColumnChange, getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { - return getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.derivative', { - defaultMessage: 'Differences', - }) - ); + return combineErrorMessages([ + getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.derivative', { + defaultMessage: 'Differences', + }) + ), + getDisallowedPreviousShiftMessage(layer, columnId), + ]); }, getDisabledStatus(indexPattern, layer, layerType) { const opName = i18n.translate('xpack.lens.indexPattern.derivative', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 026b1bf7fd64a..1a8519e6a60a1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -21,10 +21,16 @@ import { checkForDataLayerType, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { getFormatFromPreviousColumn, isValidNumber, getFilter } from '../helpers'; +import { + getFormatFromPreviousColumn, + isValidNumber, + getFilter, + combineErrorMessages, +} from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; import type { OperationDefinition, ParamEditorProps } from '..'; +import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.movingAverageOf', { @@ -114,13 +120,16 @@ export const movingAverageOperation: OperationDefinition< }, onOtherColumnChanged: adjustTimeScaleOnOtherColumnChange, getErrorMessage: (layer: IndexPatternLayer, columnId: string) => { - return getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.movingAverage', { - defaultMessage: 'Moving average', - }) - ); + return combineErrorMessages([ + getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.movingAverage', { + defaultMessage: 'Moving average', + }) + ), + getDisallowedPreviousShiftMessage(layer, columnId), + ]); }, getHelpMessage: () => , getDisabledStatus(indexPattern, layer, layerType) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 72c1362896ac0..c97447803524d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -16,8 +16,10 @@ import { getInvalidFieldMessage, getSafeName, getFilter, + combineErrorMessages, } from './helpers'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; +import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; const supportedTypes = new Set([ 'string', @@ -71,7 +73,10 @@ export const cardinalityOperation: OperationDefinition - getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + combineErrorMessages([ + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + getDisallowedPreviousShiftMessage(layer, columnId), + ]), isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.getFieldByName(column.sourceField); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 6290abac77844..a35f8fbc08acf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -11,11 +11,17 @@ import { buildExpressionFunction } from '../../../../../../../src/plugins/expres import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; -import { getInvalidFieldMessage, getFilter, isColumnFormatted } from './helpers'; +import { + getInvalidFieldMessage, + getFilter, + isColumnFormatted, + combineErrorMessages, +} from './helpers'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, } from '../time_scale_utils'; +import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { defaultMessage: 'Count of records', @@ -34,7 +40,10 @@ export const countOperation: OperationDefinition - getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + combineErrorMessages([ + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + getDisallowedPreviousShiftMessage(layer, columnId), + ]), onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts index 28e762e7dff0f..ece41a95130bf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -347,8 +347,9 @@ export async function getNamedArgumentSuggestions({ if (typeof dateHistogramInterval === 'undefined') return true; const parsedValue = parseTimeShift(value); return ( - typeof parsedValue === 'string' || - Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval) + parsedValue !== 'previous' && + (parsedValue === 'invalid' || + Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval)) ); }) .map(({ value }) => value), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 9b22ef02fb3b5..73e0e61a68950 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -67,6 +67,13 @@ export function getInvalidFieldMessage( return undefined; } +export function combineErrorMessages( + errorMessages: Array +): string[] | undefined { + const messages = (errorMessages.filter(Boolean) as string[][]).flat(); + return messages.length ? messages : undefined; +} + export function getSafeName(name: string, indexPattern: IndexPattern): string { const field = indexPattern.getFieldByName(name); return field diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 46e1b368fc913..7a73a1a0b6c24 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -22,6 +22,7 @@ import { getFilter, } from './helpers'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; +import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; function ofName(name: string, timeShift: string | undefined) { return adjustTimeScaleLabelSuffix( @@ -152,6 +153,7 @@ export const lastValueOperation: OperationDefinition = FormattedIndexPatternColumn & FieldBasedIndexPatternColumn & { @@ -132,7 +134,13 @@ function buildMetricOperation>({ }).toAst(); }, getErrorMessage: (layer, columnId, indexPattern) => - getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + combineErrorMessages([ + getInvalidFieldMessage( + layer.columns[columnId] as FieldBasedIndexPatternColumn, + indexPattern + ), + getDisallowedPreviousShiftMessage(layer, columnId), + ]), filterable: true, documentation: { section: 'elasticsearch', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index d1ce42696ea68..73536ae4893a6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -18,10 +18,12 @@ import { isValidNumber, getFilter, isColumnOfType, + combineErrorMessages, } from './helpers'; import { FieldBasedIndexPatternColumn } from './column_types'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; import { useDebouncedValue } from '../../../shared_components'; +import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'percentile'; @@ -142,7 +144,10 @@ export const percentileOperation: OperationDefinition< ).toAst(); }, getErrorMessage: (layer, columnId, indexPattern) => - getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + combineErrorMessages([ + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + getDisallowedPreviousShiftMessage(layer, columnId), + ]), paramEditor: function PercentileParamEditor({ layer, updateLayer, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.test.tsx new file mode 100644 index 0000000000000..4cb251e3820be --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getDisallowedPreviousShiftMessage } from './time_shift_utils'; +import { IndexPatternLayer } from './types'; + +describe('time_shift_utils', () => { + describe('getDisallowedPreviousShiftMessage', () => { + const layer: IndexPatternLayer = { + indexPatternId: '', + columnOrder: [], + columns: { + a: { + operationType: 'date_histogram', + dataType: 'date', + isBucketed: true, + label: '', + references: [], + sourceField: 'timestamp', + }, + b: { + operationType: 'count', + dataType: 'number', + isBucketed: false, + label: 'non shifted', + references: [], + sourceField: 'records', + }, + c: { + operationType: 'count', + dataType: 'number', + isBucketed: false, + label: 'shifted', + timeShift: '1d', + references: [], + sourceField: 'records', + }, + }, + }; + + it('shoud not produce an error for no shift', () => { + expect(getDisallowedPreviousShiftMessage(layer, 'b')).toBeUndefined(); + }); + + it('shoud not produce an error for non-previous shift', () => { + expect(getDisallowedPreviousShiftMessage(layer, 'c')).toBeUndefined(); + }); + + it('shoud produce an error for previous shift with date histogram', () => { + expect( + getDisallowedPreviousShiftMessage( + { + ...layer, + columns: { ...layer.columns, c: { ...layer.columns.c, timeShift: 'previous' } }, + }, + 'c' + ) + ).toHaveLength(1); + }); + + it('shoud not produce an error for previous shift without date histogram', () => { + expect( + getDisallowedPreviousShiftMessage( + { + ...layer, + columns: { + ...layer.columns, + a: { ...layer.columns.a, operationType: 'terms' }, + c: { ...layer.columns.c, timeShift: 'previous' }, + }, + }, + 'c' + ) + ).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index 4e18e2f376c70..90353ac087436 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -81,6 +81,12 @@ export const timeShiftOptions = [ }), value: '1y', }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { + defaultMessage: 'Previous time range', + }), + value: 'previous', + }, ]; export const timeShiftOptionOrder = timeShiftOptions.reduce<{ [key: string]: number }>( @@ -101,7 +107,7 @@ export function getDateHistogramInterval( (colId) => layer.columns[colId].operationType === 'date_histogram' ); if (!dateHistogramColumn && !indexPattern.timeFieldName) { - return { canShift: false }; + return { canShift: false, hasDateHistogram: false }; } if (dateHistogramColumn && activeData && activeData[layerId] && activeData[layerId]) { const column = activeData[layerId].columns.find((col) => col.id === dateHistogramColumn); @@ -112,14 +118,16 @@ export function getDateHistogramInterval( interval: search.aggs.parseInterval(expression), expression, canShift: true, + hasDateHistogram: true, }; } } - return { canShift: true }; + return { canShift: true, hasDateHistogram: Boolean(dateHistogramColumn) }; } export function getLayerTimeShiftChecks({ interval: dateHistogramInterval, + hasDateHistogram, canShift, }: ReturnType) { return { @@ -140,9 +148,41 @@ export function getLayerTimeShiftChecks({ !Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval.asMilliseconds()) ); }, + isInvalid: (parsedValue: ReturnType) => { + return Boolean( + parsedValue === 'invalid' || (hasDateHistogram && parsedValue && parsedValue === 'previous') + ); + }, }; } +export function getDisallowedPreviousShiftMessage( + layer: IndexPatternLayer, + columnId: string +): string[] | undefined { + const currentColumn = layer.columns[columnId]; + const hasPreviousShift = + currentColumn.timeShift && parseTimeShift(currentColumn.timeShift) === 'previous'; + if (!hasPreviousShift) { + return; + } + const hasDateHistogram = Object.values(layer.columns).some( + (column) => column.operationType === 'date_histogram' + ); + if (!hasDateHistogram) { + return; + } + return [ + i18n.translate('xpack.lens.indexPattern.dateHistogramTimeShift', { + defaultMessage: + 'In a single layer, you are unable to combine previous time range shift with date histograms. Either use an explicit time shift duration in "{column}" or replace the date histogram.', + values: { + column: currentColumn.label, + }, + }), + ]; +} + export function getStateTimeShiftWarningMessages( state: IndexPatternPrivateState, { activeData }: FramePublicAPI