Skip to content

Commit

Permalink
[Lens] Time scaling without date histogram (#140107)
Browse files Browse the repository at this point in the history
* [Lens] Time scaling without date histogram

Closes: #79656

* cleanup

* fix observability tests

* fix scale_fn jest

* add test

* fix PR comments

* fix PR comment

* remove adjustTimeScaleOnOtherColumnChange

* add reducedTimeRange argument into time_scale

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
alexwizp and kibanamachine authored Sep 15, 2022
1 parent c0c161d commit a60b730
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 215 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import moment from 'moment';
import type { Datatable } from '@kbn/expressions-plugin/common';
import type { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common';

import type { TimeRange } from '@kbn/es-query';
import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks';
Expand All @@ -28,7 +28,12 @@ import { getTimeScale } from './time_scale';
import type { TimeScaleArgs } from './types';

describe('time_scale', () => {
let timeScaleWrapped: (input: Datatable, args: TimeScaleArgs) => Promise<Datatable>;
let timeScaleWrapped: (
input: Datatable,
args: TimeScaleArgs,
context?: ExecutionContext
) => Promise<Datatable>;

const timeScale = getTimeScale(createDatatableUtilitiesMock, () => 'UTC');

const emptyTable: Datatable = {
Expand Down Expand Up @@ -391,6 +396,65 @@ describe('time_scale', () => {
expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]);
});

it('should apply fn for non-histogram fields', async () => {
const result = await timeScaleWrapped(
{
...emptyTable,
rows: [
{
date: moment('2010-01-01T00:00:00.000Z').valueOf(),
metric: 300,
},
],
},
{
inputColumnId: 'metric',
outputColumnId: 'scaledMetric',
targetUnit: 'd',
},
{
getSearchContext: () => ({
timeRange: {
from: '2010-01-01T00:00:00.000Z',
to: '2010-01-05T00:00:00.000Z',
},
}),
} as unknown as ExecutionContext
);

expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([75]);
});

it('should apply fn for non-histogram fields (with Reduced time range)', async () => {
const result = await timeScaleWrapped(
{
...emptyTable,
rows: [
{
date: moment('2010-01-04T00:00:00.000Z').valueOf(),
metric: 300,
},
],
},
{
inputColumnId: 'metric',
outputColumnId: 'scaledMetric',
targetUnit: 'd',
reducedTimeRange: '4d',
},
{
getSearchContext: () => ({
timeRange: {
from: '2009-01-01T00:00:00.000Z',
to: '2010-01-05T00:00:00.000Z',
},
}),
} as unknown as ExecutionContext
);

expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([75]);
});

it('should be sync except for timezone getter to prevent timezone leakage', async () => {
let resolveTimezonePromise: (value: string | PromiseLike<string>) => void;
const timezonePromise = new Promise<string>((res) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const getTimeScale = (
dateColumnId: {
types: ['string'],
help: '',
required: true,
},
inputColumnId: {
types: ['string'],
Expand All @@ -40,6 +39,10 @@ export const getTimeScale = (
help: '',
required: true,
},
reducedTimeRange: {
types: ['string'],
help: '',
},
},
inputTypes: ['datatable'],
async fn(...args) {
Expand Down
176 changes: 105 additions & 71 deletions x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
* 2.0.
*/

import moment from 'moment-timezone';
import moment, { Moment } from 'moment-timezone';
import { i18n } from '@kbn/i18n';
import { buildResultColumns, Datatable, ExecutionContext } from '@kbn/expressions-plugin/common';
import { calculateBounds, DatatableUtilitiesService, parseInterval } from '@kbn/data-plugin/common';
import { buildResultColumns, DatatableRow, ExecutionContext } from '@kbn/expressions-plugin/common';
import {
calculateBounds,
DatatableUtilitiesService,
parseInterval,
TimeRangeBounds,
TimeRange,
} from '@kbn/data-plugin/common';
import type { TimeScaleExpressionFunction, TimeScaleUnit, TimeScaleArgs } from './types';

const unitInMs: Record<TimeScaleUnit, number> = {
Expand All @@ -27,20 +33,79 @@ export const timeScaleFn =
): TimeScaleExpressionFunction['fn'] =>
async (
input,
{ dateColumnId, inputColumnId, outputColumnId, outputColumnName, targetUnit }: TimeScaleArgs,
{
dateColumnId,
inputColumnId,
outputColumnId,
outputColumnName,
targetUnit,
reducedTimeRange,
}: TimeScaleArgs,
context
) => {
const dateColumnDefinition = input.columns.find((column) => column.id === dateColumnId);

if (!dateColumnDefinition) {
throw new Error(
i18n.translate('xpack.lens.functions.timeScale.dateColumnMissingMessage', {
defaultMessage: 'Specified dateColumnId {columnId} does not exist.',
values: {
columnId: dateColumnId,
},
})
);
let timeBounds: TimeRangeBounds | undefined;
const contextTimeZone = await getTimezone(context);

let getStartEndOfBucketMeta: (row: DatatableRow) => {
startOfBucket: Moment;
endOfBucket: Moment;
};

if (dateColumnId) {
const dateColumnDefinition = input.columns.find((column) => column.id === dateColumnId);

if (!dateColumnDefinition) {
throw new Error(
i18n.translate('xpack.lens.functions.timeScale.dateColumnMissingMessage', {
defaultMessage: 'Specified dateColumnId {columnId} does not exist.',
values: {
columnId: dateColumnId,
},
})
);
}
const datatableUtilities = await getDatatableUtilities(context);
const timeInfo = datatableUtilities.getDateHistogramMeta(dateColumnDefinition, {
timeZone: contextTimeZone,
});
const intervalDuration = timeInfo?.interval && parseInterval(timeInfo.interval);
timeBounds = timeInfo?.timeRange && calculateBounds(timeInfo.timeRange);

getStartEndOfBucketMeta = (row) => {
const startOfBucket = moment.tz(row[dateColumnId], timeInfo?.timeZone ?? contextTimeZone);

return {
startOfBucket,
endOfBucket: startOfBucket.clone().add(intervalDuration),
};
};

if (!timeInfo || !intervalDuration) {
throw new Error(
i18n.translate('xpack.lens.functions.timeScale.timeInfoMissingMessage', {
defaultMessage: 'Could not fetch date histogram information',
})
);
}
} else {
const timeRange = context.getSearchContext().timeRange as TimeRange;
const endOfBucket = moment.tz(timeRange.to, contextTimeZone);
let startOfBucket = moment.tz(timeRange.from, contextTimeZone);

if (reducedTimeRange) {
const reducedStartOfBucket = endOfBucket.clone().subtract(parseInterval(reducedTimeRange));

if (reducedStartOfBucket > startOfBucket) {
startOfBucket = reducedStartOfBucket;
}
}

timeBounds = calculateBounds(timeRange);

getStartEndOfBucketMeta = () => ({
startOfBucket,
endOfBucket,
});
}

const resultColumns = buildResultColumns(
Expand All @@ -57,61 +122,30 @@ export const timeScaleFn =
return input;
}

const targetUnitInMs = unitInMs[targetUnit];
const datatableUtilities = await getDatatableUtilities(context);
const timeInfo = datatableUtilities.getDateHistogramMeta(dateColumnDefinition, {
timeZone: await getTimezone(context),
});
const intervalDuration = timeInfo?.interval && parseInterval(timeInfo.interval);

if (!timeInfo || !intervalDuration) {
throw new Error(
i18n.translate('xpack.lens.functions.timeScale.timeInfoMissingMessage', {
defaultMessage: 'Could not fetch date histogram information',
})
);
}
// 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.

// The code between this call and the reset in the finally block is not allowed to get async,
// otherwise the timezone setting can leak out of this function.
const defaultTimezone = moment().zoneName();
let result: Datatable;
try {
moment.tz.setDefault(timeInfo.timeZone);

const timeBounds = timeInfo.timeRange && calculateBounds(timeInfo.timeRange);

result = {
...input,
columns: resultColumns,
rows: input.rows.map((row) => {
const newRow = { ...row };

let startOfBucket = moment(row[dateColumnId]);
let endOfBucket = startOfBucket.clone().add(intervalDuration);
if (timeBounds && timeBounds.min) {
startOfBucket = moment.max(startOfBucket, timeBounds.min);
}
if (timeBounds && timeBounds.max) {
endOfBucket = moment.min(endOfBucket, timeBounds.max);
}
const bucketSize = endOfBucket.diff(startOfBucket);
const factor = bucketSize / targetUnitInMs;

const currentValue = newRow[inputColumnId];
if (currentValue != null) {
newRow[outputColumnId] = Number(currentValue) / factor;
}

return newRow;
}),
};
} finally {
// reset default moment timezone
moment.tz.setDefault(defaultTimezone);
}
return {
...input,
columns: resultColumns,
rows: input.rows.map((row) => {
const newRow = { ...row };

let { startOfBucket, endOfBucket } = getStartEndOfBucketMeta(row);

if (timeBounds && timeBounds.min) {
startOfBucket = moment.max(startOfBucket, timeBounds.min);
}
if (timeBounds && timeBounds.max) {
endOfBucket = moment.min(endOfBucket, timeBounds.max);
}

const bucketSize = endOfBucket.diff(startOfBucket);
const factor = bucketSize / unitInMs[targetUnit];
const currentValue = newRow[inputColumnId];

if (currentValue != null) {
newRow[outputColumnId] = Number(currentValue) / factor;
}

return result;
return newRow;
}),
};
};
3 changes: 2 additions & 1 deletion x-pack/plugins/lens/common/expressions/time_scale/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-p
export type TimeScaleUnit = 's' | 'm' | 'h' | 'd';

export interface TimeScaleArgs {
dateColumnId: string;
inputColumnId: string;
outputColumnId: string;
targetUnit: TimeScaleUnit;
dateColumnId?: string;
outputColumnName?: string;
reducedTimeRange?: string;
}

export type TimeScaleExpressionFunction = ExpressionFunctionDefinition<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,9 @@ export function TimeScaling({
layer: IndexPatternLayer;
updateLayer: (newLayer: IndexPatternLayer) => void;
}) {
const hasDateHistogram = layer.columnOrder.some(
(colId) => layer.columns[colId].operationType === 'date_histogram'
);
const selectedOperation = operationDefinitionMap[selectedColumn.operationType];
if (
!selectedOperation.timeScalingMode ||
selectedOperation.timeScalingMode === 'disabled' ||
!hasDateHistogram
) {

if (!selectedOperation.timeScalingMode || selectedOperation.timeScalingMode === 'disabled') {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ describe('IndexPattern Data Source', () => {
"outputColumnName": Array [
"Count of records",
],
"reducedTimeRange": Array [],
"targetUnit": Array [
"h",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
hasDateField,
checkForDataLayerType,
} from './utils';
import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils';
import { OperationDefinition } from '..';
import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers';
import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils';
Expand Down Expand Up @@ -93,7 +92,6 @@ export const derivativeOperation: OperationDefinition<
isTransferable: (column, newIndexPattern) => {
return hasDateField(newIndexPattern);
},
onOtherColumnChanged: adjustTimeScaleOnOtherColumnChange,
getErrorMessage: (layer: IndexPatternLayer, columnId: string) => {
return combineErrorMessages([
getErrorsForDateReference(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
getFilter,
combineErrorMessages,
} from '../helpers';
import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils';
import type { OperationDefinition, ParamEditorProps } from '..';
import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils';

Expand Down Expand Up @@ -115,7 +114,6 @@ export const movingAverageOperation: OperationDefinition<
isTransferable: (column, newIndexPattern) => {
return hasDateField(newIndexPattern);
},
onOtherColumnChanged: adjustTimeScaleOnOtherColumnChange,
getErrorMessage: (layer: IndexPatternLayer, columnId: string) => {
return combineErrorMessages([
getErrorsForDateReference(
Expand Down
Loading

0 comments on commit a60b730

Please sign in to comment.