[Lens] Add previous time shift back (#121284)

This commit is contained in:
Joe Reuter 2022-01-12 11:53:56 +01:00 committed by GitHub
parent c3bf4e05df
commit 339f721a86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 233 additions and 48 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: () => <MovingAveragePopup />,
getDisabledStatus(indexPattern, layer, layerType) {

View file

@ -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<CardinalityIndexPatternCo
}
},
getErrorMessage: (layer, columnId, indexPattern) =>
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);

View file

@ -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<CountIndexPatternColumn, 'field
}),
input: 'field',
getErrorMessage: (layer, columnId, indexPattern) =>
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
combineErrorMessages([
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
getDisallowedPreviousShiftMessage(layer, columnId),
]),
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,

View file

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

View file

@ -67,6 +67,13 @@ export function getInvalidFieldMessage(
return undefined;
}
export function combineErrorMessages(
errorMessages: Array<string[] | undefined>
): 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

View file

@ -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<LastValueIndexPatternColumn
if (invalidSortFieldMessage) {
errorMessages = [invalidSortFieldMessage];
}
errorMessages.push(...(getDisallowedPreviousShiftMessage(layer, columnId) || []));
return errorMessages.length ? errorMessages : undefined;
},
buildColumn({ field, previousColumn, indexPattern }, columnParams) {

View file

@ -13,6 +13,7 @@ import {
getInvalidFieldMessage,
getSafeName,
getFilter,
combineErrorMessages,
} from './helpers';
import {
FormattedIndexPatternColumn,
@ -23,6 +24,7 @@ import {
adjustTimeScaleLabelSuffix,
adjustTimeScaleOnOtherColumnChange,
} from '../time_scale_utils';
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
type MetricColumn<T> = FormattedIndexPatternColumn &
FieldBasedIndexPatternColumn & {
@ -132,7 +134,13 @@ function buildMetricOperation<T extends MetricColumn<string>>({
}).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',

View file

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

View file

@ -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();
});
});
});

View file

@ -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<typeof getDateHistogramInterval>) {
return {
@ -140,9 +148,41 @@ export function getLayerTimeShiftChecks({
!Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval.asMilliseconds())
);
},
isInvalid: (parsedValue: ReturnType<typeof parseTimeShift>) => {
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