mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] Absolute time shift support in formula (#144564)
## Summary Closes #102493 This PR introduces the ability to define an absolute time shift as a new type of time shift in Lens. The idea is to avoid to push down the absolute logic to the agg level, rather translate as soon as possible the absolute shift into a relative one (in seconds, the minimal time unit allowed) and perform all checks in this format. **Note**: The feature is currently enabled as formula-only and using it in a Quick function context will validate it as an invalid shift. **Details**: The used format for anchoring right now is: * `startAt(2022-11-03T18:30:27.278Z)` for start anchoring (from the given date used as `start` anchor forward to a dynamic end date), and <img width="1172" alt="Screenshot 2022-11-15 at 15 13 36" src="https://user-images.githubusercontent.com/924948/201942522-8515f845-b065-448b-bc3b-da72ea8f9de3.png"> * `endAt(2022-11-03T18:30:27.278Z)` for end anchoring (from the given date backward to a start date dynamically computed) <img width="1168" alt="Screenshot 2022-11-15 at 15 13 21" src="https://user-images.githubusercontent.com/924948/201942569-91175316-1cb6-45af-8d32-83eb91d5ad9a.png"> * when an absolute time shift is detected in formula then the shifts suggestions are translated into absolute shifts <img width="304" alt="Screenshot 2022-11-15 at 15 18 08" src="https://user-images.githubusercontent.com/924948/201942631-b4906a11-9de0-4ce0-94f8-16d9576247bf.png"> <img width="281" alt="Screenshot 2022-11-15 at 15 18 17" src="https://user-images.githubusercontent.com/924948/201942633-20c52277-450e-4191-94a7-74f21a6255af.png"> ### Basics * [x] Add a Absolute time shift validation function * [x] Add a strict shift parser to duration (in seconds) * [x] Add a function to extract tokens from raw string * Enable the feature in Lens * [x] formula * [x] parsing * [x] formula validation * [x] suggestion adapted to work with the Absolute shift * [x] errors * [x] warnings * interval-based validations are skipped for absolute shifts as issues are resolved at expression translation time anyway * [x] via API (formula helper) ### Implementation details While the range is computed correctly based on the current interval, the actual shift is rounded to prevent issues on data fetching. The interval is computed with an algorithm similar to the one used by ES and the shift is offset to the closest multiple of that interval (i.e. if the current shift is `1.78` times the interval, then the shift is rounded to `2` intervals in a way to include the given date). ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
cf2bbcd957
commit
528f3bd3a5
36 changed files with 1032 additions and 227 deletions
|
@ -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
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import {
|
||||
isAbsoluteTimeShift,
|
||||
parseAbsoluteTimeShift,
|
||||
parseTimeShift,
|
||||
REASON_IDS,
|
||||
validateAbsoluteTimeShift,
|
||||
} from './parse_time_shift';
|
||||
|
||||
describe('parse time shifts', () => {
|
||||
describe('relative time shifts', () => {
|
||||
it('should return valid duration for valid units', () => {
|
||||
for (const unit of ['s', 'm', 'h', 'd', 'w', 'M', 'y']) {
|
||||
expect(moment.isDuration(parseTimeShift(`1${unit}`))).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return previous for the previous string', () => {
|
||||
expect(parseTimeShift('previous')).toBe('previous');
|
||||
});
|
||||
|
||||
it('should return "invalid" for anything else', () => {
|
||||
for (const value of ['1a', 's', 'non-valid-string']) {
|
||||
expect(parseTimeShift(value)).toBe('invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('absolute time shifts', () => {
|
||||
const dateString = '2022-11-02T00:00:00.000Z';
|
||||
const futureDateString = '3022-11-02T00:00:00.000Z';
|
||||
|
||||
function applyTimeZone(zone: string = 'Z') {
|
||||
return dateString.replace('Z', zone);
|
||||
}
|
||||
describe('isAbsoluteTimeShift', () => {
|
||||
it('should return true for a valid absoluteTimeShift string', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
expect(isAbsoluteTimeShift(`${anchor}(${dateString})`)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return false for no string passed', () => {
|
||||
expect(isAbsoluteTimeShift()).toBeFalsy();
|
||||
});
|
||||
|
||||
// that's ok, the function is used to distinguish from the relative shifts
|
||||
it('should perform only a shallow check on the string', () => {
|
||||
expect(isAbsoluteTimeShift('startAt(aaaaa)')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAbsoluteTimeShift', () => {
|
||||
it('should return no error for valid time shifts', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
expect(
|
||||
validateAbsoluteTimeShift(`${anchor}(${dateString})`, {
|
||||
from: moment(dateString).add('5', 'd').toISOString(),
|
||||
to: moment(dateString).add('6', 'd').toISOString(),
|
||||
})
|
||||
).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return no error for valid time shifts if no time range is passed', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
expect(validateAbsoluteTimeShift(`${anchor}(${dateString})`)).toBeUndefined();
|
||||
// This will pass as the range checks are relaxed without the second argument passed
|
||||
expect(validateAbsoluteTimeShift(`${anchor}(${futureDateString})`)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if the string value is not an absolute time shift', () => {
|
||||
for (const val of ['startAt()', 'endAt()', '1d', 'aaa']) {
|
||||
expect(validateAbsoluteTimeShift(val)).toBe(REASON_IDS.notAbsoluteTimeShift);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if the passed date is invalid', () => {
|
||||
for (const val of [
|
||||
'startAt(a)',
|
||||
'endAt(a)',
|
||||
'startAt(2022)',
|
||||
'startAt(2022-11-02T00:00:00.000)',
|
||||
'endAt(2022-11-02)',
|
||||
]) {
|
||||
expect(validateAbsoluteTimeShift(val)).toBe(REASON_IDS.invalidDate);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if dateRange is passed and the shift is after that', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
expect(
|
||||
validateAbsoluteTimeShift(`${anchor}(${futureDateString})`, {
|
||||
from: moment(dateString).subtract('1', 'd').toISOString(),
|
||||
to: moment(dateString).add('1', 'd').toISOString(),
|
||||
})
|
||||
).toBe(REASON_IDS.shiftAfterTimeRange);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return no error for dates with non-UTC offset', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
for (const offset of ['Z', '+01:00', '-12:00', '-05']) {
|
||||
expect(
|
||||
validateAbsoluteTimeShift(`${anchor}(${applyTimeZone(offset)})`, {
|
||||
from: moment(dateString).add('5', 'd').toISOString(),
|
||||
to: moment(dateString).add('6', 'd').toISOString(),
|
||||
})
|
||||
).toBeUndefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAbsoluteTimeShift', () => {
|
||||
it('should return an error if no time range is passed', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
expect(parseAbsoluteTimeShift(`${anchor}(${dateString})`, undefined)).toEqual({
|
||||
value: 'invalid',
|
||||
reason: REASON_IDS.missingTimerange,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error for invalid dates', () => {
|
||||
for (const invalidDates of [
|
||||
'startAt(a)',
|
||||
'endAt(a)',
|
||||
'startAt(2022)',
|
||||
'startAt(2022-11-02T00:00:00.000)',
|
||||
'endAt(2022-11-02)',
|
||||
'1d',
|
||||
'aaa',
|
||||
`startAt(${futureDateString})`,
|
||||
]) {
|
||||
expect(
|
||||
parseAbsoluteTimeShift(invalidDates, {
|
||||
from: moment(dateString).add('5', 'd').toISOString(),
|
||||
to: moment(dateString).add('6', 'd').toISOString(),
|
||||
}).value
|
||||
).toBe('invalid');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return no reason for a valid absolute time shift', () => {
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
expect(
|
||||
parseAbsoluteTimeShift(`${anchor}(${dateString})`, {
|
||||
from: moment(dateString).add('5', 'd').toISOString(),
|
||||
to: moment(dateString).add('6', 'd').toISOString(),
|
||||
}).reason
|
||||
).toBe(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,24 +6,140 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import { TimeRange } from '../../../types';
|
||||
|
||||
const allowedUnits = ['s', 'm', 'h', 'd', 'w', 'M', 'y'] as const;
|
||||
type AllowedUnit = typeof allowedUnits[number];
|
||||
const ALLOWED_UNITS = ['s', 'm', 'h', 'd', 'w', 'M', 'y'] as const;
|
||||
const ANCHORED_TIME_SHIFT_REGEXP = /^(startAt|endAt)\((.+)\)$/;
|
||||
const DURATION_REGEXP = /^(\d+)\s*(\w)$/;
|
||||
const INVALID_DATE = 'invalid';
|
||||
const PREVIOUS_DATE = 'previous';
|
||||
const START_AT_ANCHOR = 'startAt';
|
||||
|
||||
type AllowedUnit = typeof ALLOWED_UNITS[number];
|
||||
type PreviousDateType = typeof PREVIOUS_DATE;
|
||||
type InvalidDateType = typeof INVALID_DATE;
|
||||
|
||||
// The ISO8601 format supports also partial date strings as described here:
|
||||
// https://momentjs.com/docs/#/parsing/string/
|
||||
// But in this specific case we want to enforce the full version of the ISO8601
|
||||
// which is build in this case with moment special HTML5 format + the timezone support
|
||||
const LONG_ISO8601_LIKE_FORMAT = moment.HTML5_FMT.DATETIME_LOCAL_MS + 'Z';
|
||||
|
||||
/**
|
||||
* This method parses a string into a time shift duration.
|
||||
* If parsing fails, 'invalid' is returned.
|
||||
* Allowed values are the string 'previous' and an integer followed by the units s,m,h,d,w,M,y
|
||||
* */
|
||||
export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'invalid' => {
|
||||
export const parseTimeShift = (
|
||||
val: string
|
||||
): moment.Duration | PreviousDateType | InvalidDateType => {
|
||||
const trimmedVal = val.trim();
|
||||
if (trimmedVal === 'previous') {
|
||||
return 'previous';
|
||||
if (trimmedVal === PREVIOUS_DATE) {
|
||||
return PREVIOUS_DATE;
|
||||
}
|
||||
const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || [];
|
||||
const [, amount, unit] = trimmedVal.match(DURATION_REGEXP) || [];
|
||||
const parsedAmount = Number(amount);
|
||||
if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) {
|
||||
return 'invalid';
|
||||
if (Number.isNaN(parsedAmount) || !ALLOWED_UNITS.includes(unit as AllowedUnit)) {
|
||||
return INVALID_DATE;
|
||||
}
|
||||
return moment.duration(Number(amount), unit as AllowedUnit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check function to detect an absolute time shift.
|
||||
* The check is performed only on the string format and the timestamp is not validated:
|
||||
* use the validateAbsoluteTimeShift fucntion to perform more in depth checks
|
||||
* @param val the string to parse (it assumes it has been trimmed already)
|
||||
* @returns true if an absolute time shift
|
||||
*/
|
||||
export const isAbsoluteTimeShift = (val?: string) => {
|
||||
return val != null && ANCHORED_TIME_SHIFT_REGEXP.test(val);
|
||||
};
|
||||
|
||||
export const REASON_IDS = {
|
||||
missingTimerange: 'missingTimerange',
|
||||
notAbsoluteTimeShift: 'notAbsoluteTimeShift',
|
||||
invalidDate: 'invalidDate',
|
||||
shiftAfterTimeRange: 'shiftAfterTimeRange',
|
||||
} as const;
|
||||
|
||||
export type REASON_ID_TYPES = keyof typeof REASON_IDS;
|
||||
|
||||
/**
|
||||
* Parses an absolute time shift string and returns its equivalent duration
|
||||
* @param val the string to parse
|
||||
* @param timeRange the current date histogram interval
|
||||
* @returns
|
||||
*/
|
||||
export const parseAbsoluteTimeShift = (
|
||||
val: string,
|
||||
timeRange: TimeRange | undefined
|
||||
):
|
||||
| { value: moment.Duration; reason: null }
|
||||
| { value: InvalidDateType; reason: REASON_ID_TYPES } => {
|
||||
const trimmedVal = val.trim();
|
||||
if (timeRange == null) {
|
||||
return {
|
||||
value: INVALID_DATE,
|
||||
reason: REASON_IDS.missingTimerange,
|
||||
};
|
||||
}
|
||||
const error = validateAbsoluteTimeShift(trimmedVal, timeRange);
|
||||
if (error) {
|
||||
return {
|
||||
value: INVALID_DATE,
|
||||
reason: error,
|
||||
};
|
||||
}
|
||||
const { anchor, timestamp } = extractTokensFromAbsTimeShift(trimmedVal);
|
||||
// the regexp test above will make sure anchor and timestamp are both strings
|
||||
// now be very strict on the format
|
||||
const tsMoment = moment(timestamp, LONG_ISO8601_LIKE_FORMAT, true);
|
||||
// workout how long is the ref time range
|
||||
const duration = moment(timeRange.to).diff(moment(timeRange.from));
|
||||
// pick the end of the absolute range now
|
||||
const absRangeEnd = anchor === START_AT_ANCHOR ? tsMoment.add(duration) : tsMoment;
|
||||
// return (ref end date - shift end date)
|
||||
return { value: moment.duration(moment(timeRange.to).diff(absRangeEnd)), reason: null };
|
||||
};
|
||||
|
||||
/**
|
||||
* Fucntion to extract the anchor and timestamp tokens from an absolute time shift
|
||||
* @param val absolute time shift string
|
||||
* @returns the anchor and timestamp strings
|
||||
*/
|
||||
function extractTokensFromAbsTimeShift(val: string) {
|
||||
const [, anchor, timestamp] = val.match(ANCHORED_TIME_SHIFT_REGEXP) || [];
|
||||
return { anchor, timestamp };
|
||||
}
|
||||
/**
|
||||
* Relaxed version of the parsing validation
|
||||
* This version of the validation applies the timeRange validation only when passed
|
||||
* @param val
|
||||
* @param timeRange
|
||||
* @returns the reason id if the absolute shift is not valid, undefined otherwise
|
||||
*/
|
||||
export function validateAbsoluteTimeShift(
|
||||
val: string,
|
||||
timeRange?: TimeRange
|
||||
): REASON_ID_TYPES | undefined {
|
||||
const trimmedVal = val.trim();
|
||||
if (!isAbsoluteTimeShift(trimmedVal)) {
|
||||
return REASON_IDS.notAbsoluteTimeShift;
|
||||
}
|
||||
const { anchor, timestamp } = extractTokensFromAbsTimeShift(trimmedVal);
|
||||
// the regexp test above will make sure anchor and timestamp are both strings
|
||||
// now be very strict on the format
|
||||
const tsMoment = moment(timestamp, LONG_ISO8601_LIKE_FORMAT, true);
|
||||
if (!tsMoment.isValid()) {
|
||||
return REASON_IDS.invalidDate;
|
||||
}
|
||||
if (timeRange) {
|
||||
const duration = moment(timeRange.to).diff(moment(timeRange.from));
|
||||
if (
|
||||
(anchor === START_AT_ANCHOR && tsMoment.isAfter(timeRange.from)) ||
|
||||
tsMoment.subtract(duration).isAfter(timeRange.from)
|
||||
)
|
||||
return REASON_IDS.shiftAfterTimeRange;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -328,6 +328,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
field: currentField || undefined,
|
||||
filterOperations: props.filterOperations,
|
||||
visualizationGroups: dimensionGroups,
|
||||
dateRange,
|
||||
}),
|
||||
disabledStatus:
|
||||
definition.getDisabledStatus &&
|
||||
|
|
|
@ -1571,6 +1571,23 @@ describe('FormBasedDimensionEditor', () => {
|
|||
.prop('error')
|
||||
).toBe('Time shift value is not valid.');
|
||||
});
|
||||
|
||||
it('should mark absolute time shift as invalid', () => {
|
||||
const props = getProps({
|
||||
timeShift: 'startAt(2022-11-02T00:00:00.000Z)',
|
||||
});
|
||||
wrapper = mount(<FormBasedDimensionEditorComponent {...props} />);
|
||||
|
||||
expect(wrapper.find(TimeShift).find(EuiComboBox).prop('isInvalid')).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(TimeShift)
|
||||
.find('[data-test-subj="indexPattern-dimension-time-shift-row"]')
|
||||
.first()
|
||||
.prop('error')
|
||||
).toBe('Time shift value is not valid.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
|
|
|
@ -52,14 +52,13 @@ function wrapOnDot(str?: string) {
|
|||
export const FormBasedDimensionTriggerComponent = function FormBasedDimensionTrigger(
|
||||
props: FormBasedDimensionTriggerProps
|
||||
) {
|
||||
const layerId = props.layerId;
|
||||
const { columnId, uniqueLabel, invalid, invalidMessage, hideTooltip, layerId, dateRange } = props;
|
||||
const layer = props.state.layers[layerId];
|
||||
const currentIndexPattern = props.indexPatterns[layer.indexPatternId];
|
||||
const { columnId, uniqueLabel, invalid, invalidMessage, hideTooltip } = props;
|
||||
|
||||
const currentColumnHasErrors = useMemo(
|
||||
() => invalid || isColumnInvalid(layer, columnId, currentIndexPattern),
|
||||
[layer, columnId, currentIndexPattern, invalid]
|
||||
() => invalid || isColumnInvalid(layer, columnId, currentIndexPattern, dateRange),
|
||||
[layer, columnId, currentIndexPattern, invalid, dateRange]
|
||||
);
|
||||
|
||||
const selectedColumn: GenericIndexPatternColumn | null = layer.columns[props.columnId] ?? null;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
|||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DatatableUtilitiesService, parseTimeShift } from '@kbn/data-plugin/common';
|
||||
import { type DatatableUtilitiesService, parseTimeShift } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
adjustTimeScaleLabelSuffix,
|
||||
GenericIndexPatternColumn,
|
||||
|
@ -21,6 +21,7 @@ import {
|
|||
getDateHistogramInterval,
|
||||
getLayerTimeShiftChecks,
|
||||
timeShiftOptions,
|
||||
getColumnTimeShiftWarnings,
|
||||
} from '../time_shift_utils';
|
||||
import type { IndexPattern } from '../../../types';
|
||||
|
||||
|
@ -90,7 +91,7 @@ export function TimeShift({
|
|||
activeData,
|
||||
layerId
|
||||
);
|
||||
const { isValueTooSmall, isValueNotMultiple, isInvalid, canShift } =
|
||||
const { canShift, isValueTooSmall, isValueNotMultiple, isInvalid } =
|
||||
getLayerTimeShiftChecks(dateHistogramInterval);
|
||||
|
||||
if (!canShift) {
|
||||
|
@ -99,8 +100,7 @@ export function TimeShift({
|
|||
|
||||
const parsedLocalValue = localValue && parseTimeShift(localValue);
|
||||
const isLocalValueInvalid = Boolean(parsedLocalValue && isInvalid(parsedLocalValue));
|
||||
const localValueTooSmall = parsedLocalValue && isValueTooSmall(parsedLocalValue);
|
||||
const localValueNotMultiple = parsedLocalValue && isValueNotMultiple(parsedLocalValue);
|
||||
const warnings = getColumnTimeShiftWarnings(dateHistogramInterval, localValue);
|
||||
|
||||
function getSelectedOption() {
|
||||
const goodPick = timeShiftOptions.filter(({ value }) => value === localValue);
|
||||
|
@ -130,22 +130,13 @@ export function TimeShift({
|
|||
defaultMessage: 'Enter the time shift number and unit',
|
||||
})}
|
||||
error={
|
||||
(localValueTooSmall &&
|
||||
i18n.translate('xpack.lens.indexPattern.timeShift.tooSmallHelp', {
|
||||
defaultMessage:
|
||||
'Time shift should to be larger than the date histogram interval. Either increase time shift or specify smaller interval in date histogram',
|
||||
})) ||
|
||||
(localValueNotMultiple &&
|
||||
i18n.translate('xpack.lens.indexPattern.timeShift.noMultipleHelp', {
|
||||
defaultMessage:
|
||||
'Time shift should be a multiple of the date histogram interval. Either adjust time shift or date histogram interval',
|
||||
})) ||
|
||||
warnings[0] ||
|
||||
(isLocalValueInvalid &&
|
||||
i18n.translate('xpack.lens.indexPattern.timeShift.genericInvalidHelp', {
|
||||
defaultMessage: 'Time shift value is not valid.',
|
||||
}))
|
||||
}
|
||||
isInvalid={Boolean(isLocalValueInvalid || localValueTooSmall || localValueNotMultiple)}
|
||||
isInvalid={Boolean(isLocalValueInvalid || warnings.length)}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -176,6 +176,10 @@ const expectedIndexPatterns = {
|
|||
};
|
||||
|
||||
const indexPatterns = expectedIndexPatterns;
|
||||
const dateRange = {
|
||||
fromDate: '2022-03-17T08:25:00.000Z',
|
||||
toDate: '2022-04-17T08:25:00.000Z',
|
||||
};
|
||||
|
||||
describe('IndexPattern Data Source', () => {
|
||||
let baseState: FormBasedPrivateState;
|
||||
|
@ -316,7 +320,7 @@ describe('IndexPattern Data Source', () => {
|
|||
it('should generate an empty expression when no columns are selected', async () => {
|
||||
const state = FormBasedDatasource.initialize();
|
||||
expect(
|
||||
FormBasedDatasource.toExpression(state, 'first', indexPatterns, 'testing-seed')
|
||||
FormBasedDatasource.toExpression(state, 'first', indexPatterns, dateRange, 'testing-seed')
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
|
@ -341,7 +345,13 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
FormBasedDatasource.toExpression(queryBaseState, 'first', indexPatterns, 'testing-seed')
|
||||
FormBasedDatasource.toExpression(
|
||||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
)
|
||||
).toEqual({
|
||||
chain: [
|
||||
{
|
||||
|
@ -390,7 +400,13 @@ describe('IndexPattern Data Source', () => {
|
|||
};
|
||||
|
||||
expect(
|
||||
FormBasedDatasource.toExpression(queryBaseState, 'first', indexPatterns, 'testing-seed')
|
||||
FormBasedDatasource.toExpression(
|
||||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
|
@ -575,6 +591,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
|
||||
|
@ -615,6 +632,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect((ast.chain[1].arguments.aggs[1] as Ast).chain[0].arguments.timeShift).toEqual(['1d']);
|
||||
|
@ -827,6 +845,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const count = (ast.chain[1].arguments.aggs[1] as Ast).chain[0];
|
||||
|
@ -896,6 +915,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.aggs[0]).toMatchInlineSnapshot(`
|
||||
|
@ -1025,6 +1045,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const timeScaleCalls = ast.chain.filter((fn) => fn.function === 'lens_time_scale');
|
||||
|
@ -1095,6 +1116,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const filteredMetricAgg = (ast.chain[1].arguments.aggs[0] as Ast).chain[0].arguments;
|
||||
|
@ -1151,6 +1173,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const formatIndex = ast.chain.findIndex((fn) => fn.function === 'lens_format_column');
|
||||
|
@ -1204,6 +1227,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.metricsAtAllLevels).toEqual([false]);
|
||||
|
@ -1248,6 +1272,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.timeFields).toEqual(['timestamp']);
|
||||
|
@ -1306,7 +1331,13 @@ describe('IndexPattern Data Source', () => {
|
|||
|
||||
const optimizeMock = jest.spyOn(operationDefinitionMap.percentile, 'optimizeEsAggs');
|
||||
|
||||
FormBasedDatasource.toExpression(queryBaseState, 'first', indexPatterns, 'testing-seed');
|
||||
FormBasedDatasource.toExpression(
|
||||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
);
|
||||
|
||||
expect(operationDefinitionMap.percentile.optimizeEsAggs).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
@ -1378,6 +1409,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
|
||||
|
@ -1447,6 +1479,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
|
||||
|
@ -1557,6 +1590,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
// @ts-expect-error we can't isolate just the reference type
|
||||
|
@ -1595,6 +1629,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
|
||||
|
@ -1687,6 +1722,7 @@ describe('IndexPattern Data Source', () => {
|
|||
queryBaseState,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const chainLength = ast.chain.length;
|
||||
|
|
|
@ -416,8 +416,8 @@ export function getFormBasedDatasource({
|
|||
return fields;
|
||||
},
|
||||
|
||||
toExpression: (state, layerId, indexPatterns, searchSessionId) =>
|
||||
toExpression(state, layerId, indexPatterns, uiSettings, searchSessionId),
|
||||
toExpression: (state, layerId, indexPatterns, dateRange, searchSessionId) =>
|
||||
toExpression(state, layerId, indexPatterns, uiSettings, dateRange, searchSessionId),
|
||||
|
||||
renderLayerSettings(
|
||||
domElement: Element,
|
||||
|
@ -513,10 +513,10 @@ export function getFormBasedDatasource({
|
|||
return columnLabelMap;
|
||||
},
|
||||
|
||||
isValidColumn: (state, indexPatterns, layerId, columnId) => {
|
||||
isValidColumn: (state, indexPatterns, layerId, columnId, dateRange) => {
|
||||
const layer = state.layers[layerId];
|
||||
|
||||
return !isColumnInvalid(layer, columnId, indexPatterns[layer.indexPatternId]);
|
||||
return !isColumnInvalid(layer, columnId, indexPatterns[layer.indexPatternId], dateRange);
|
||||
},
|
||||
|
||||
renderDimensionTrigger: (
|
||||
|
|
|
@ -107,6 +107,7 @@ export function FormulaEditor({
|
|||
setIsCloseable,
|
||||
dateHistogramInterval,
|
||||
hasData,
|
||||
dateRange,
|
||||
}: Omit<ParamEditorProps<FormulaIndexPatternColumn>, 'activeData'> & {
|
||||
dateHistogramInterval: ReturnType<typeof getDateHistogramInterval>;
|
||||
hasData: boolean;
|
||||
|
@ -189,6 +190,7 @@ export function FormulaEditor({
|
|||
{
|
||||
indexPattern,
|
||||
operations: operationDefinitionMap,
|
||||
dateRange,
|
||||
}
|
||||
).layer
|
||||
);
|
||||
|
@ -218,6 +220,7 @@ export function FormulaEditor({
|
|||
{
|
||||
indexPattern,
|
||||
operations: operationDefinitionMap,
|
||||
dateRange,
|
||||
}
|
||||
).layer
|
||||
);
|
||||
|
@ -237,7 +240,8 @@ export function FormulaEditor({
|
|||
layer,
|
||||
indexPattern,
|
||||
visibleOperationsMap,
|
||||
currentColumn
|
||||
currentColumn,
|
||||
dateRange
|
||||
);
|
||||
if (validationErrors.length) {
|
||||
errors = validationErrors;
|
||||
|
@ -267,6 +271,7 @@ export function FormulaEditor({
|
|||
{
|
||||
indexPattern,
|
||||
operations: operationDefinitionMap,
|
||||
dateRange,
|
||||
}
|
||||
).layer
|
||||
);
|
||||
|
@ -332,6 +337,7 @@ export function FormulaEditor({
|
|||
{
|
||||
indexPattern,
|
||||
operations: operationDefinitionMap,
|
||||
dateRange,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -348,6 +354,7 @@ export function FormulaEditor({
|
|||
newLayer,
|
||||
id,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
visibleOperationsMap
|
||||
);
|
||||
if (messages) {
|
||||
|
@ -367,14 +374,16 @@ export function FormulaEditor({
|
|||
const startPosition = offsetToRowColumn(text, locations[id].min);
|
||||
const endPosition = offsetToRowColumn(text, locations[id].max);
|
||||
newWarnings.push(
|
||||
...getColumnTimeShiftWarnings(dateHistogramInterval, column).map((message) => ({
|
||||
message,
|
||||
startColumn: startPosition.column + 1,
|
||||
startLineNumber: startPosition.lineNumber,
|
||||
endColumn: endPosition.column + 1,
|
||||
endLineNumber: endPosition.lineNumber,
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
}))
|
||||
...getColumnTimeShiftWarnings(dateHistogramInterval, column.timeShift).map(
|
||||
(message) => ({
|
||||
message,
|
||||
startColumn: startPosition.column + 1,
|
||||
startLineNumber: startPosition.lineNumber,
|
||||
endColumn: endPosition.column + 1,
|
||||
endLineNumber: endPosition.lineNumber,
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -442,6 +451,7 @@ export function FormulaEditor({
|
|||
unifiedSearch,
|
||||
dataViews,
|
||||
dateHistogramInterval: baseIntervalRef.current,
|
||||
dateRange,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -454,6 +464,7 @@ export function FormulaEditor({
|
|||
unifiedSearch,
|
||||
dataViews,
|
||||
dateHistogramInterval: baseIntervalRef.current,
|
||||
dateRange,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -469,7 +480,7 @@ export function FormulaEditor({
|
|||
),
|
||||
};
|
||||
},
|
||||
[indexPattern, visibleOperationsMap, unifiedSearch, dataViews, baseIntervalRef]
|
||||
[indexPattern, visibleOperationsMap, unifiedSearch, dataViews, baseIntervalRef, dateRange]
|
||||
);
|
||||
|
||||
const provideSignatureHelp = useCallback(
|
||||
|
|
|
@ -202,21 +202,41 @@ describe('math completion', () => {
|
|||
});
|
||||
|
||||
describe('autocomplete', () => {
|
||||
it('should list all valid functions at the top level (fake test)', async () => {
|
||||
// This test forces an invalid scenario, since the autocomplete actually requires
|
||||
// some typing
|
||||
const results = await suggest({
|
||||
expression: '',
|
||||
zeroIndexedOffset: 1,
|
||||
const dateRange = { fromDate: '2022-11-01T00:00:00.000Z', toDate: '2022-11-03T00:00:00.000Z' };
|
||||
|
||||
function getSuggestionArgs({
|
||||
expression,
|
||||
zeroIndexedOffset,
|
||||
triggerCharacter,
|
||||
}: {
|
||||
expression: string;
|
||||
zeroIndexedOffset: number;
|
||||
triggerCharacter: string;
|
||||
}) {
|
||||
return {
|
||||
expression,
|
||||
zeroIndexedOffset,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
triggerCharacter: '',
|
||||
triggerCharacter,
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
dateRange,
|
||||
};
|
||||
}
|
||||
it('should list all valid functions at the top level (fake test)', async () => {
|
||||
// This test forces an invalid scenario, since the autocomplete actually requires
|
||||
// some typing
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: '',
|
||||
zeroIndexedOffset: 1,
|
||||
triggerCharacter: '',
|
||||
})
|
||||
);
|
||||
expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length);
|
||||
['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => {
|
||||
expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'operation' }]));
|
||||
|
@ -227,18 +247,13 @@ describe('math completion', () => {
|
|||
});
|
||||
|
||||
it('should list all valid sub-functions for a fullReference', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'moving_average()',
|
||||
zeroIndexedOffset: 15,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'moving_average()',
|
||||
zeroIndexedOffset: 15,
|
||||
triggerCharacter: '(',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toHaveLength(2);
|
||||
['sum', 'last_value'].forEach((key) => {
|
||||
expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'operation' }]));
|
||||
|
@ -246,50 +261,35 @@ describe('math completion', () => {
|
|||
});
|
||||
|
||||
it('should list all valid named arguments for a fullReference', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'moving_average(count(),)',
|
||||
zeroIndexedOffset: 23,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'moving_average(count(),)',
|
||||
zeroIndexedOffset: 23,
|
||||
triggerCharacter: ',',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toEqual(['window']);
|
||||
});
|
||||
|
||||
it('should not list named arguments when they are already in use', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'moving_average(count(), window=5, )',
|
||||
zeroIndexedOffset: 34,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'moving_average(count(), window=5, )',
|
||||
zeroIndexedOffset: 34,
|
||||
triggerCharacter: ',',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toEqual([]);
|
||||
});
|
||||
|
||||
it('should list all valid positional arguments for a tinymath function used by name', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'divide(count(), )',
|
||||
zeroIndexedOffset: 16,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'divide(count(), )',
|
||||
zeroIndexedOffset: 16,
|
||||
triggerCharacter: ',',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length);
|
||||
['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => {
|
||||
expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'math' }]));
|
||||
|
@ -300,18 +300,13 @@ describe('math completion', () => {
|
|||
});
|
||||
|
||||
it('should list all valid positional arguments for a tinymath function used with alias', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'count() / ',
|
||||
zeroIndexedOffset: 10,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'count() / ',
|
||||
zeroIndexedOffset: 10,
|
||||
triggerCharacter: ',',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length);
|
||||
['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => {
|
||||
expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'math' }]));
|
||||
|
@ -322,52 +317,87 @@ describe('math completion', () => {
|
|||
});
|
||||
|
||||
it('should not autocomplete any fields for the count function', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'count()',
|
||||
zeroIndexedOffset: 6,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'count()',
|
||||
zeroIndexedOffset: 6,
|
||||
triggerCharacter: '(',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should autocomplete and validate the right type of field', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'sum()',
|
||||
zeroIndexedOffset: 4,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'sum()',
|
||||
zeroIndexedOffset: 4,
|
||||
triggerCharacter: '(',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toEqual(['bytes', 'memory']);
|
||||
});
|
||||
|
||||
it('should autocomplete only operations that provide numeric or date output', async () => {
|
||||
const results = await suggest({
|
||||
expression: 'last_value()',
|
||||
zeroIndexedOffset: 11,
|
||||
context: {
|
||||
triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter,
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: 'last_value()',
|
||||
zeroIndexedOffset: 11,
|
||||
triggerCharacter: '(',
|
||||
},
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
operationDefinitionMap,
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
});
|
||||
})
|
||||
);
|
||||
expect(results.list).toEqual(['bytes', 'memory', 'timestamp', 'start_date']);
|
||||
});
|
||||
|
||||
it('should autocomplete shift parameter with relative suggestions and a couple of abs ones', async () => {
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: `count(shift='')`,
|
||||
zeroIndexedOffset: 13,
|
||||
triggerCharacter: '=',
|
||||
})
|
||||
);
|
||||
expect(results.list).toEqual([
|
||||
'',
|
||||
'1h',
|
||||
'3h',
|
||||
'6h',
|
||||
'12h',
|
||||
'1d',
|
||||
'1w',
|
||||
'1M',
|
||||
'3M',
|
||||
'6M',
|
||||
'1y',
|
||||
'previous',
|
||||
'startAt(2022-11-01T00:00:00.000Z)',
|
||||
'endAt(2022-11-03T00:00:00.000Z)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should autocomplete shift parameter with absolute suggestions once detected', async () => {
|
||||
const results = await suggest(
|
||||
getSuggestionArgs({
|
||||
expression: `count(shift='endAt(')`,
|
||||
zeroIndexedOffset: 19,
|
||||
triggerCharacter: '=',
|
||||
})
|
||||
);
|
||||
expect(results.list).toEqual([
|
||||
'2022-11-03T00:00:00.000Z)',
|
||||
'2022-11-02T23:00:00.000Z)',
|
||||
'2022-11-02T21:00:00.000Z)',
|
||||
'2022-11-02T18:00:00.000Z)',
|
||||
'2022-11-02T12:00:00.000Z)',
|
||||
'2022-11-02T00:00:00.000Z)',
|
||||
'2022-10-27T00:00:00.000Z)',
|
||||
'2022-10-03T00:00:00.000Z)',
|
||||
'2022-08-03T00:00:00.000Z)',
|
||||
'2022-05-03T00:00:00.000Z)',
|
||||
'2021-11-03T00:00:00.000Z)',
|
||||
'2022-11-03T00:00:00.000Z)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('offsetToRowColumn', () => {
|
||||
|
|
|
@ -22,6 +22,8 @@ import type {
|
|||
} from '@kbn/unified-search-plugin/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { parseTimeShift } from '@kbn/data-plugin/common';
|
||||
import moment from 'moment';
|
||||
import { DateRange } from '../../../../../../../common/types';
|
||||
import type { IndexPattern } from '../../../../../../types';
|
||||
import { memoizedGetAvailableOperationsByMetadata } from '../../../operations';
|
||||
import { tinymathFunctions, groupArgsByType, unquotedStringRegex, nonNullable } from '../util';
|
||||
|
@ -146,6 +148,7 @@ export async function suggest({
|
|||
dataViews,
|
||||
unifiedSearch,
|
||||
dateHistogramInterval,
|
||||
dateRange,
|
||||
}: {
|
||||
expression: string;
|
||||
zeroIndexedOffset: number;
|
||||
|
@ -155,6 +158,7 @@ export async function suggest({
|
|||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
dateHistogramInterval?: number;
|
||||
dateRange: DateRange;
|
||||
}): Promise<LensMathSuggestions> {
|
||||
const text =
|
||||
expression.substr(0, zeroIndexedOffset) + MARKER + expression.substr(zeroIndexedOffset);
|
||||
|
@ -177,6 +181,7 @@ export async function suggest({
|
|||
dataViews,
|
||||
indexPattern,
|
||||
dateHistogramInterval,
|
||||
dateRange,
|
||||
});
|
||||
} else if (tokenInfo?.parent) {
|
||||
return getArgumentSuggestions(
|
||||
|
@ -362,32 +367,55 @@ function getArgumentSuggestions(
|
|||
return { list: [], type: SUGGESTION_TYPE.FIELD };
|
||||
}
|
||||
|
||||
const anchoredAbsoluteTimeShiftRegexp = /^(start|end)At\(/;
|
||||
|
||||
function computeAbsSuggestion(dateRange: DateRange, prefix: string, value: string) {
|
||||
const refDate = prefix.startsWith('s') ? dateRange.fromDate : dateRange.toDate;
|
||||
return moment(refDate).subtract(parseTimeShift(value), 'ms').toISOString();
|
||||
}
|
||||
|
||||
export async function getNamedArgumentSuggestions({
|
||||
ast,
|
||||
unifiedSearch,
|
||||
dataViews,
|
||||
indexPattern,
|
||||
dateHistogramInterval,
|
||||
dateRange,
|
||||
}: {
|
||||
ast: TinymathNamedArgument;
|
||||
indexPattern: IndexPattern;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
dateHistogramInterval?: number;
|
||||
dateRange: DateRange;
|
||||
}) {
|
||||
if (ast.name === 'shift') {
|
||||
const validTimeShiftOptions = timeShiftOptions
|
||||
.filter(({ value }) => {
|
||||
if (dateHistogramInterval == null) return true;
|
||||
const parsedValue = parseTimeShift(value);
|
||||
return (
|
||||
parsedValue !== 'previous' &&
|
||||
(parsedValue === 'invalid' ||
|
||||
Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval))
|
||||
);
|
||||
})
|
||||
.map(({ value }) => value);
|
||||
const absShift = ast.value.split(MARKER)[0];
|
||||
// Translate the relative time shifts into absolute ones
|
||||
if (anchoredAbsoluteTimeShiftRegexp.test(absShift)) {
|
||||
return {
|
||||
list: validTimeShiftOptions.map(
|
||||
(value) => `${computeAbsSuggestion(dateRange, absShift, value)})`
|
||||
),
|
||||
type: SUGGESTION_TYPE.SHIFTS,
|
||||
};
|
||||
}
|
||||
const extraAbsSuggestions = ['startAt', 'endAt'].map(
|
||||
(prefix) => `${prefix}(${computeAbsSuggestion(dateRange, prefix, validTimeShiftOptions[0])})`
|
||||
);
|
||||
return {
|
||||
list: timeShiftOptions
|
||||
.filter(({ value }) => {
|
||||
if (typeof dateHistogramInterval === 'undefined') return true;
|
||||
const parsedValue = parseTimeShift(value);
|
||||
return (
|
||||
parsedValue !== 'previous' &&
|
||||
(parsedValue === 'invalid' ||
|
||||
Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval))
|
||||
);
|
||||
})
|
||||
.map(({ value }) => value),
|
||||
list: validTimeShiftOptions.concat(extraAbsSuggestions),
|
||||
type: SUGGESTION_TYPE.SHIFTS,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,7 +27,14 @@ jest.mock('../../layer_helpers', () => {
|
|||
getColumnOrder: jest.fn(({ columns }: { columns: Record<string, GenericIndexPatternColumn> }) =>
|
||||
Object.keys(columns)
|
||||
),
|
||||
getManagedColumnsFrom: jest.fn().mockReturnValue([]),
|
||||
getManagedColumnsFrom: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
id: string,
|
||||
columns: Record<string, Extract<GenericIndexPatternColumn, { references: string[] }>>
|
||||
) => columns[id].references?.map((colId) => [colId, {}]) || []
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -893,7 +900,7 @@ describe('formula', () => {
|
|||
function getNewLayerWithFormula(
|
||||
formula: string,
|
||||
isBroken = true,
|
||||
columnParams: Partial<Pick<FormulaIndexPatternColumn, 'filter'>> = {}
|
||||
columnParams: Partial<Pick<FormulaIndexPatternColumn, 'filter' | 'references'>> = {}
|
||||
): FormBasedLayer {
|
||||
return {
|
||||
columns: {
|
||||
|
@ -922,6 +929,7 @@ describe('formula', () => {
|
|||
getNewLayerWithFormula('count()'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -933,6 +941,7 @@ describe('formula', () => {
|
|||
getNewLayerWithFormula(`count(kql='*')`, false),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -944,6 +953,7 @@ describe('formula', () => {
|
|||
getNewLayerWithFormula(`count(kql='invalid: "')`, false),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -959,6 +969,7 @@ invalid: "
|
|||
getNewLayerWithFormula('average(bytes)'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -968,6 +979,7 @@ invalid: "
|
|||
getNewLayerWithFormula('average("bytes")'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -979,6 +991,7 @@ invalid: "
|
|||
getNewLayerWithFormula('derivative(average(bytes))'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -988,6 +1001,7 @@ invalid: "
|
|||
getNewLayerWithFormula('derivative(average("bytes"))'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -999,6 +1013,7 @@ invalid: "
|
|||
getNewLayerWithFormula('moving_average(average(bytes), window=7)'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1010,6 +1025,7 @@ invalid: "
|
|||
getNewLayerWithFormula('bytes'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The field bytes cannot be used without operation`]);
|
||||
|
@ -1019,6 +1035,7 @@ invalid: "
|
|||
getNewLayerWithFormula('bytes + bytes'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The operation add does not accept any field as argument`]);
|
||||
|
@ -1040,6 +1057,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The Formula ${formula} cannot be parsed`]);
|
||||
|
@ -1060,6 +1078,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Field noField not found']);
|
||||
|
@ -1075,6 +1094,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Fields noField, noField2 not found']);
|
||||
|
@ -1090,6 +1110,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Operation noFn not found']);
|
||||
|
@ -1103,6 +1124,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Operations noFn, noFnTwo not found']);
|
||||
|
@ -1118,6 +1140,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Operation formula not found']);
|
||||
|
@ -1131,6 +1154,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Operation math not found']);
|
||||
|
@ -1154,6 +1178,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(
|
||||
|
@ -1173,6 +1198,7 @@ invalid: "
|
|||
getNewLayerWithFormula('count()'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1184,6 +1210,7 @@ invalid: "
|
|||
getNewLayerWithFormula('moving_average(average(bytes))'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1195,6 +1222,7 @@ invalid: "
|
|||
getNewLayerWithFormula('moving_average(average(bytes), myparam=7)'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1208,6 +1236,7 @@ invalid: "
|
|||
getNewLayerWithFormula('average(bytes, myparam=7)'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['The operation average does not accept any parameter']);
|
||||
|
@ -1228,6 +1257,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(
|
||||
|
@ -1258,6 +1288,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(
|
||||
|
@ -1276,6 +1307,7 @@ invalid: "
|
|||
getNewLayerWithFormula('moving_average(average(bytes), window="m")'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1295,6 +1327,7 @@ invalid: "
|
|||
`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1315,6 +1348,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1353,6 +1387,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(expect.arrayContaining([expect.stringMatching(`Single quotes are required`)]));
|
||||
|
@ -1383,6 +1418,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The Formula ${formula} cannot be parsed`]);
|
||||
|
@ -1401,6 +1437,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1413,6 +1450,7 @@ invalid: "
|
|||
getNewLayerWithFormula(`count(kql='category.keyword: *', lucene='category.keyword: *')`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Use only one of kql= or lucene=, not both']);
|
||||
|
@ -1425,6 +1463,7 @@ invalid: "
|
|||
getNewLayerWithFormula(`${fn}()`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The first argument for ${fn} should be a field name. Found no field`]);
|
||||
|
@ -1434,6 +1473,7 @@ invalid: "
|
|||
getNewLayerWithFormula(`sum(kql='category.keyword: *')`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The first argument for sum should be a field name. Found category.keyword: *`]);
|
||||
|
@ -1446,6 +1486,7 @@ invalid: "
|
|||
getNewLayerWithFormula(`${fn}()`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([`The first argument for ${fn} should be a operation name. Found no operation`]);
|
||||
|
@ -1453,9 +1494,11 @@ invalid: "
|
|||
});
|
||||
|
||||
it('returns an error if the formula is fully static and there is at least one bucket dimension', () => {
|
||||
const formulaLayer = getNewLayerWithFormula('5 + 3 * 7');
|
||||
const formulaLayer = getNewLayerWithFormula('5 + 3 * 7', false, { references: ['col1X'] });
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
// this became a little bit more tricker as now it takes into account the number of references
|
||||
// for the error
|
||||
{
|
||||
...formulaLayer,
|
||||
columns: {
|
||||
|
@ -1473,6 +1516,7 @@ invalid: "
|
|||
},
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1486,6 +1530,7 @@ invalid: "
|
|||
getNewLayerWithFormula('5 + 3 * 7'),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1502,6 +1547,7 @@ invalid: "
|
|||
getNewLayerWithFormula(`ifelse(${formula}, 1, 5)`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1520,6 +1566,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1538,6 +1585,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
@ -1560,6 +1608,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1633,6 +1682,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toContain(errors[i](fn));
|
||||
|
@ -1653,6 +1703,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1697,6 +1748,7 @@ invalid: "
|
|||
getNewLayerWithFormula(`${fn}(${cond}, ${left}, ${right})`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(
|
||||
|
@ -1723,6 +1775,7 @@ invalid: "
|
|||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([errorsWithSuggestions[i]]);
|
||||
|
@ -1751,6 +1804,7 @@ invalid: "
|
|||
}),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1771,6 +1825,7 @@ invalid: "
|
|||
),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
|
@ -1795,6 +1850,7 @@ invalid: "
|
|||
}),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
|
|
@ -62,7 +62,7 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
getDisabledStatus(indexPattern: IndexPattern) {
|
||||
return undefined;
|
||||
},
|
||||
getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap) {
|
||||
getErrorMessage(layer, columnId, indexPattern, dateRange, operationDefinitionMap) {
|
||||
const column = layer.columns[columnId] as FormulaIndexPatternColumn;
|
||||
if (!column.params.formula || !operationDefinitionMap) {
|
||||
return;
|
||||
|
@ -74,7 +74,14 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
return error?.message ? [error.message] : [];
|
||||
}
|
||||
|
||||
const errors = runASTValidation(root, layer, indexPattern, visibleOperationsMap, column);
|
||||
const errors = runASTValidation(
|
||||
root,
|
||||
layer,
|
||||
indexPattern,
|
||||
visibleOperationsMap,
|
||||
column,
|
||||
dateRange
|
||||
);
|
||||
|
||||
if (errors.length) {
|
||||
// remove duplicates
|
||||
|
@ -86,7 +93,13 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
.flatMap(([id, col]) => {
|
||||
const def = visibleOperationsMap[col.operationType];
|
||||
if (def?.getErrorMessage) {
|
||||
const messages = def.getErrorMessage(layer, id, indexPattern, visibleOperationsMap);
|
||||
const messages = def.getErrorMessage(
|
||||
layer,
|
||||
id,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
visibleOperationsMap
|
||||
);
|
||||
return messages ? { message: messages.join(', ') } : [];
|
||||
}
|
||||
return [];
|
||||
|
@ -102,8 +115,13 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
col.operationType !== 'formula'
|
||||
);
|
||||
});
|
||||
// What happens when it transition from an error state to a new valid state?
|
||||
// the "hasOtherMetrics" might be false as the formula hasn't had time to
|
||||
// populate all the referenced columns yet. So check if there are managedColumns
|
||||
// (if no error is present, there's at least one other math column present)
|
||||
const hasBeenEvaluated = !errors.length && managedColumns.length;
|
||||
|
||||
if (hasBuckets && !hasOtherMetrics) {
|
||||
if (hasBuckets && !hasOtherMetrics && hasBeenEvaluated) {
|
||||
innerErrors.push({
|
||||
message: i18n.translate('xpack.lens.indexPattern.noRealMetricError', {
|
||||
defaultMessage:
|
||||
|
|
|
@ -11,10 +11,18 @@ import { createFormulaPublicApi, FormulaPublicApi } from './formula_public_api';
|
|||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DateHistogramIndexPatternColumn, PersistedIndexPatternLayer } from '../../../types';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../../../../../data_views_service/loader';
|
||||
import moment from 'moment';
|
||||
import type { FormulaIndexPatternColumn } from './formula';
|
||||
|
||||
jest.mock('./parse', () => ({
|
||||
insertOrReplaceFormulaColumn: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
jest.mock('./parse', () => {
|
||||
const original = jest.requireActual('./parse');
|
||||
return {
|
||||
...original,
|
||||
insertOrReplaceFormulaColumn: jest.fn((...args) =>
|
||||
original.insertOrReplaceFormulaColumn(...args)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../data_views_service/loader', () => ({
|
||||
convertDataViewIntoLensIndexPattern: jest.fn((v) => v),
|
||||
|
@ -153,4 +161,80 @@ describe('createFormulaPublicApi', () => {
|
|||
{ indexPattern: {} }
|
||||
);
|
||||
});
|
||||
|
||||
test('should accept an absolute time shift for shiftable operations', () => {
|
||||
const baseLayer = getBaseLayer();
|
||||
|
||||
const dateString = '2022-11-02T00:00:00.000Z';
|
||||
// shift by 2 days + 2500 s (to get a shift which is not a multiple of the given interval)
|
||||
const shiftedDate = moment(dateString).subtract(175300, 's').toISOString();
|
||||
|
||||
const result = publicApiHelper.insertOrReplaceFormulaColumn(
|
||||
'col',
|
||||
{
|
||||
formula: `count(shift='startAt(${shiftedDate})') - count(shift='endAt(${shiftedDate})')`,
|
||||
},
|
||||
baseLayer,
|
||||
dataView
|
||||
);
|
||||
expect((result?.columns.col as FormulaIndexPatternColumn).params.isFormulaBroken).toBe(false);
|
||||
});
|
||||
|
||||
test('should perform more validations for absolute time shifts if dateRange is passed', () => {
|
||||
const baseLayer = getBaseLayer();
|
||||
|
||||
const dateString = '2022-11-02T00:00:00.000Z';
|
||||
// date in the future
|
||||
const shiftedDate = '3022-11-02T00:00:00.000Z';
|
||||
|
||||
const dateRange = {
|
||||
fromDate: moment(dateString).subtract('1', 'd').toISOString(),
|
||||
toDate: moment(dateString).add('1', 'd').toISOString(),
|
||||
};
|
||||
|
||||
const result = publicApiHelper.insertOrReplaceFormulaColumn(
|
||||
'col',
|
||||
{
|
||||
formula: `count(shift='startAt(${shiftedDate})') - count(shift='endAt(${shiftedDate})')`,
|
||||
},
|
||||
baseLayer,
|
||||
dataView,
|
||||
dateRange
|
||||
);
|
||||
|
||||
expect((result?.columns.col as FormulaIndexPatternColumn).params.isFormulaBroken).toBe(true);
|
||||
});
|
||||
|
||||
test('should perform format-only validation if no date range is passed', () => {
|
||||
const baseLayer = getBaseLayer();
|
||||
|
||||
const result = publicApiHelper.insertOrReplaceFormulaColumn(
|
||||
'col',
|
||||
{
|
||||
formula: `count(shift='startAt(invalid)') - count(shift='endAt(3022)')`,
|
||||
},
|
||||
baseLayer,
|
||||
dataView
|
||||
);
|
||||
|
||||
expect((result?.columns.col as FormulaIndexPatternColumn).params.isFormulaBroken).toBe(true);
|
||||
});
|
||||
|
||||
test('should not detect date in the future error if no date range is passed', () => {
|
||||
const baseLayer = getBaseLayer();
|
||||
|
||||
// date in the future
|
||||
const shiftedDate = '3022-11-02T00:00:00.000Z';
|
||||
|
||||
const result = publicApiHelper.insertOrReplaceFormulaColumn(
|
||||
'col',
|
||||
{
|
||||
formula: `count(shift='startAt(${shiftedDate})') - count(shift='endAt(${shiftedDate})')`,
|
||||
},
|
||||
baseLayer,
|
||||
dataView
|
||||
);
|
||||
|
||||
expect((result?.columns.col as FormulaIndexPatternColumn).params.isFormulaBroken).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import type { DateRange } from '../../../../../../common';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../../../../../data_views_service/loader';
|
||||
import type { IndexPattern } from '../../../../../types';
|
||||
import type { PersistedIndexPatternLayer } from '../../../types';
|
||||
|
@ -44,7 +45,8 @@ export interface FormulaPublicApi {
|
|||
};
|
||||
},
|
||||
layer: PersistedIndexPatternLayer,
|
||||
dataView: DataView
|
||||
dataView: DataView,
|
||||
dateRange?: DateRange
|
||||
) => PersistedIndexPatternLayer | undefined;
|
||||
}
|
||||
|
||||
|
@ -67,7 +69,8 @@ export const createFormulaPublicApi = (): FormulaPublicApi => {
|
|||
id,
|
||||
{ formula, label, format, filter, reducedTimeRange, timeScale },
|
||||
layer,
|
||||
dataView
|
||||
dataView,
|
||||
dateRange
|
||||
) => {
|
||||
const indexPattern = getCachedLensIndexPattern(dataView);
|
||||
|
||||
|
@ -89,7 +92,7 @@ export const createFormulaPublicApi = (): FormulaPublicApi => {
|
|||
},
|
||||
},
|
||||
{ ...layer, indexPatternId: indexPattern.id },
|
||||
{ indexPattern }
|
||||
{ indexPattern, dateRange }
|
||||
).layer;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { isObject } from 'lodash';
|
||||
import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath';
|
||||
import type { DateRange } from '../../../../../../common/types';
|
||||
import type { IndexPattern } from '../../../../../types';
|
||||
import {
|
||||
OperationDefinition,
|
||||
|
@ -41,6 +42,7 @@ function parseAndExtract(
|
|||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
operations: Record<string, GenericOperationDefinition>,
|
||||
dateRange: DateRange | undefined,
|
||||
label?: string
|
||||
) {
|
||||
const { root, error } = tryToParse(text, operations);
|
||||
|
@ -48,7 +50,14 @@ function parseAndExtract(
|
|||
return { extracted: [], isValid: false };
|
||||
}
|
||||
// before extracting the data run the validation task and throw if invalid
|
||||
const errors = runASTValidation(root, layer, indexPattern, operations, layer.columns[columnId]);
|
||||
const errors = runASTValidation(
|
||||
root,
|
||||
layer,
|
||||
indexPattern,
|
||||
operations,
|
||||
layer.columns[columnId],
|
||||
dateRange
|
||||
);
|
||||
if (errors.length) {
|
||||
return { extracted: [], isValid: false };
|
||||
}
|
||||
|
@ -210,6 +219,8 @@ function extractColumns(
|
|||
interface ExpandColumnProperties {
|
||||
indexPattern: IndexPattern;
|
||||
operations?: Record<string, GenericOperationDefinition>;
|
||||
dateRange?: DateRange;
|
||||
strictShiftValidation?: boolean;
|
||||
}
|
||||
|
||||
const getEmptyColumnsWithFormulaMeta = (): {
|
||||
|
@ -228,7 +239,7 @@ function generateFormulaColumns(
|
|||
id: string,
|
||||
column: FormulaIndexPatternColumn,
|
||||
layer: FormBasedLayer,
|
||||
{ indexPattern, operations = operationDefinitionMap }: ExpandColumnProperties
|
||||
{ indexPattern, operations = operationDefinitionMap, dateRange }: ExpandColumnProperties
|
||||
) {
|
||||
const { columns, meta } = getEmptyColumnsWithFormulaMeta();
|
||||
const formula = column.params.formula || '';
|
||||
|
@ -239,6 +250,7 @@ function generateFormulaColumns(
|
|||
id,
|
||||
indexPattern,
|
||||
filterByVisibleOperation(operations),
|
||||
dateRange,
|
||||
column.customLabel ? column.label : undefined
|
||||
);
|
||||
|
||||
|
|
|
@ -11,7 +11,14 @@ import { parse, TinymathLocation, TinymathVariable } from '@kbn/tinymath';
|
|||
import type { TinymathAST, TinymathFunction, TinymathNamedArgument } from '@kbn/tinymath';
|
||||
import { luceneStringToDsl, toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { parseTimeShift } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
isAbsoluteTimeShift,
|
||||
parseTimeShift,
|
||||
REASON_IDS,
|
||||
REASON_ID_TYPES,
|
||||
validateAbsoluteTimeShift,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { DateRange } from '../../../../../../common/types';
|
||||
import {
|
||||
findMathNodes,
|
||||
findVariables,
|
||||
|
@ -437,12 +444,13 @@ export function runASTValidation(
|
|||
layer: FormBasedLayer,
|
||||
indexPattern: IndexPattern,
|
||||
operations: Record<string, GenericOperationDefinition>,
|
||||
currentColumn: GenericIndexPatternColumn
|
||||
currentColumn: GenericIndexPatternColumn,
|
||||
dateRange?: DateRange
|
||||
) {
|
||||
return [
|
||||
...checkMissingVariableOrFunctions(ast, layer, indexPattern, operations),
|
||||
...checkTopNodeReturnType(ast),
|
||||
...runFullASTValidation(ast, layer, indexPattern, operations, currentColumn),
|
||||
...runFullASTValidation(ast, layer, indexPattern, operations, dateRange, currentColumn),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -508,9 +516,31 @@ function checkMissingVariableOrFunctions(
|
|||
return [...missingErrors, ...invalidVariableErrors];
|
||||
}
|
||||
|
||||
function getAbsoluteTimeShiftErrorMessage(reason: REASON_ID_TYPES) {
|
||||
switch (reason) {
|
||||
case REASON_IDS.missingTimerange:
|
||||
return i18n.translate('xpack.lens.indexPattern.absoluteMissingTimeRange', {
|
||||
defaultMessage: 'Invalid time shift. No time range found as reference',
|
||||
});
|
||||
case REASON_IDS.invalidDate:
|
||||
return i18n.translate('xpack.lens.indexPattern.absoluteInvalidDate', {
|
||||
defaultMessage: 'Invalid time shift. The date is not of the correct format',
|
||||
});
|
||||
case REASON_IDS.shiftAfterTimeRange:
|
||||
return i18n.translate('xpack.lens.indexPattern.absoluteAfterTimeRange', {
|
||||
defaultMessage: 'Invalid time shift. The provided date is after the current time range',
|
||||
});
|
||||
case REASON_IDS.notAbsoluteTimeShift:
|
||||
return i18n.translate('xpack.lens.indexPattern.notAbsoluteTimeShift', {
|
||||
defaultMessage: 'Invalid time shift.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryValidationErrors(
|
||||
namedArguments: TinymathNamedArgument[] | undefined,
|
||||
indexPattern: IndexPattern
|
||||
indexPattern: IndexPattern,
|
||||
dateRange: DateRange | undefined
|
||||
): ErrorWrapper[] {
|
||||
const errors: ErrorWrapper[] = [];
|
||||
(namedArguments ?? []).forEach((arg) => {
|
||||
|
@ -530,16 +560,34 @@ function getQueryValidationErrors(
|
|||
if (arg.name === 'shift') {
|
||||
const parsedShift = parseTimeShift(arg.value);
|
||||
if (parsedShift === 'invalid') {
|
||||
errors.push({
|
||||
message: i18n.translate('xpack.lens.indexPattern.invalidTimeShift', {
|
||||
defaultMessage:
|
||||
'Invalid time shift. Enter positive integer amount followed by one of the units s, m, h, d, w, M, y. For example 3h for 3 hours',
|
||||
}),
|
||||
locations: [arg.location],
|
||||
});
|
||||
if (isAbsoluteTimeShift(arg.value)) {
|
||||
// try to parse as absolute time shift
|
||||
const error = validateAbsoluteTimeShift(
|
||||
arg.value,
|
||||
dateRange
|
||||
? {
|
||||
from: dateRange.fromDate,
|
||||
to: dateRange.toDate,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
if (error) {
|
||||
errors.push({
|
||||
message: getAbsoluteTimeShiftErrorMessage(error),
|
||||
locations: [arg.location],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
message: i18n.translate('xpack.lens.indexPattern.invalidTimeShift', {
|
||||
defaultMessage:
|
||||
'Invalid time shift. Enter positive integer amount followed by one of the units s, m, h, d, w, M, y. For example 3h for 3 hours',
|
||||
}),
|
||||
locations: [arg.location],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (arg.name === 'reducedTimeRange') {
|
||||
const parsedReducedTimeRange = parseTimeShift(arg.value || '');
|
||||
if (parsedReducedTimeRange === 'invalid' || parsedReducedTimeRange === 'previous') {
|
||||
|
@ -600,7 +648,8 @@ function validateNameArguments(
|
|||
| OperationDefinition<GenericIndexPatternColumn, 'field'>
|
||||
| OperationDefinition<GenericIndexPatternColumn, 'fullReference'>,
|
||||
namedArguments: TinymathNamedArgument[] | undefined,
|
||||
indexPattern: IndexPattern
|
||||
indexPattern: IndexPattern,
|
||||
dateRange: DateRange | undefined
|
||||
) {
|
||||
const errors = [];
|
||||
const missingParams = getMissingParams(nodeOperation, namedArguments);
|
||||
|
@ -642,7 +691,7 @@ function validateNameArguments(
|
|||
})
|
||||
);
|
||||
}
|
||||
const queryValidationErrors = getQueryValidationErrors(namedArguments, indexPattern);
|
||||
const queryValidationErrors = getQueryValidationErrors(namedArguments, indexPattern, dateRange);
|
||||
if (queryValidationErrors.length) {
|
||||
errors.push(...queryValidationErrors);
|
||||
}
|
||||
|
@ -685,6 +734,7 @@ function runFullASTValidation(
|
|||
layer: FormBasedLayer,
|
||||
indexPattern: IndexPattern,
|
||||
operations: Record<string, GenericOperationDefinition>,
|
||||
dateRange?: DateRange,
|
||||
currentColumn?: GenericIndexPatternColumn
|
||||
): ErrorWrapper[] {
|
||||
const missingVariables = findVariables(ast).filter(
|
||||
|
@ -783,7 +833,8 @@ function runFullASTValidation(
|
|||
node,
|
||||
nodeOperation,
|
||||
namedArguments,
|
||||
indexPattern
|
||||
indexPattern,
|
||||
dateRange
|
||||
);
|
||||
|
||||
const filtersErrors = validateFiltersArguments(
|
||||
|
@ -860,7 +911,8 @@ function runFullASTValidation(
|
|||
node,
|
||||
nodeOperation,
|
||||
namedArguments,
|
||||
indexPattern
|
||||
indexPattern,
|
||||
dateRange
|
||||
);
|
||||
const filtersErrors = validateFiltersArguments(
|
||||
node,
|
||||
|
|
|
@ -310,6 +310,7 @@ interface BaseOperationDefinitionProps<
|
|||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
dateRange?: DateRange,
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition>
|
||||
) =>
|
||||
| Array<
|
||||
|
|
|
@ -32,6 +32,10 @@ jest.mock('lodash', () => {
|
|||
});
|
||||
|
||||
const uiSettingsMock = {} as IUiSettingsClient;
|
||||
const dateRange = {
|
||||
fromDate: '2022-03-17T08:25:00.000Z',
|
||||
toDate: '2022-04-17T08:25:00.000Z',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
storage: {} as IStorageWrapper,
|
||||
|
@ -152,7 +156,8 @@ describe('static_value', () => {
|
|||
staticValueOperation.getErrorMessage!(
|
||||
getLayerWithStaticValue('23'),
|
||||
'col2',
|
||||
createMockedIndexPattern()
|
||||
createMockedIndexPattern(),
|
||||
dateRange
|
||||
)
|
||||
).toBeUndefined();
|
||||
// test for potential falsy value
|
||||
|
@ -160,7 +165,8 @@ describe('static_value', () => {
|
|||
staticValueOperation.getErrorMessage!(
|
||||
getLayerWithStaticValue('0'),
|
||||
'col2',
|
||||
createMockedIndexPattern()
|
||||
createMockedIndexPattern(),
|
||||
dateRange
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
@ -172,7 +178,8 @@ describe('static_value', () => {
|
|||
staticValueOperation.getErrorMessage!(
|
||||
getLayerWithStaticValue(value),
|
||||
'col2',
|
||||
createMockedIndexPattern()
|
||||
createMockedIndexPattern(),
|
||||
dateRange
|
||||
)
|
||||
).toEqual(expect.arrayContaining([expect.stringMatching('is not a valid number')]));
|
||||
}
|
||||
|
@ -183,7 +190,8 @@ describe('static_value', () => {
|
|||
staticValueOperation.getErrorMessage!(
|
||||
getLayerWithStaticValue(value),
|
||||
'col2',
|
||||
createMockedIndexPattern()
|
||||
createMockedIndexPattern(),
|
||||
dateRange
|
||||
)
|
||||
).toBe(undefined);
|
||||
});
|
||||
|
|
|
@ -42,6 +42,9 @@ import { IndexPattern } from '../../../types';
|
|||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
dataMock.query.timefilter.timefilter.getAbsoluteTime = jest
|
||||
.fn()
|
||||
.mockReturnValue({ from: '2022-11-01T00:00:00.000Z', to: '2022-11-03T00:00:00.000Z' });
|
||||
|
||||
jest.mock('.');
|
||||
jest.mock('../../../id_generator');
|
||||
|
@ -3202,6 +3205,10 @@ describe('state_helpers', () => {
|
|||
},
|
||||
'col1',
|
||||
indexPattern,
|
||||
{
|
||||
fromDate: '2022-11-01T00:00:00.000Z',
|
||||
toDate: '2022-11-03T00:00:00.000Z',
|
||||
},
|
||||
operationDefinitionMap
|
||||
);
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { CoreStart } from '@kbn/core/public';
|
|||
import type { Query } from '@kbn/es-query';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DateRange } from '../../../../common';
|
||||
import type {
|
||||
DatasourceFixAction,
|
||||
FrameDatasourceAPI,
|
||||
|
@ -903,8 +904,10 @@ export function canTransition({
|
|||
indexPattern,
|
||||
filterOperations,
|
||||
visualizationGroups,
|
||||
dateRange,
|
||||
}: ColumnChange & {
|
||||
filterOperations: (meta: OperationMetadata) => boolean;
|
||||
dateRange: DateRange;
|
||||
}): boolean {
|
||||
const previousColumn = layer.columns[columnId];
|
||||
if (!previousColumn) {
|
||||
|
@ -930,7 +933,7 @@ export function canTransition({
|
|||
Boolean(newColumn) &&
|
||||
!newLayer.incompleteColumns?.[columnId] &&
|
||||
filterOperations(newColumn) &&
|
||||
!newDefinition.getErrorMessage?.(newLayer, columnId, indexPattern)?.length
|
||||
!newDefinition.getErrorMessage?.(newLayer, columnId, indexPattern, dateRange)?.length
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
@ -1574,7 +1577,14 @@ export function getErrorMessages(
|
|||
}
|
||||
const def = operationDefinitionMap[column.operationType];
|
||||
if (def.getErrorMessage) {
|
||||
return def.getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap);
|
||||
const currentTimeRange = data.query.timefilter.timefilter.getAbsoluteTime();
|
||||
return def.getErrorMessage(
|
||||
layer,
|
||||
columnId,
|
||||
indexPattern,
|
||||
{ fromDate: currentTimeRange.from, toDate: currentTimeRange.to },
|
||||
operationDefinitionMap
|
||||
);
|
||||
}
|
||||
})
|
||||
.map((errorMessage) => {
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getDisallowedPreviousShiftMessage } from './time_shift_utils';
|
||||
import moment from 'moment';
|
||||
import { getDisallowedPreviousShiftMessage, resolveTimeShift } from './time_shift_utils';
|
||||
import { FormBasedLayer } from './types';
|
||||
|
||||
describe('time_shift_utils', () => {
|
||||
|
@ -78,4 +79,51 @@ describe('time_shift_utils', () => {
|
|||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTimeShift', () => {
|
||||
const dateString = '2022-11-02T00:00:00.000Z';
|
||||
// shift by 2 days + 2500 s (to get a shift which is not a multiple of the given interval)
|
||||
const shiftedDate = moment(dateString).subtract(175300, 's').toISOString();
|
||||
|
||||
function getDateRange(val = dateString) {
|
||||
return {
|
||||
fromDate: moment(val).subtract('1', 'd').toISOString(),
|
||||
toDate: moment(val).add('1', 'd').toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
it('should not change a relative time shift', () => {
|
||||
for (const val of ['1d', 'previous']) {
|
||||
expect(resolveTimeShift(val, getDateRange(), 100)).toBe(val);
|
||||
}
|
||||
});
|
||||
|
||||
it('should change absolute values to relative in seconds (rounded) with start anchor', () => {
|
||||
expect(resolveTimeShift(`startAt(${shiftedDate})`, getDateRange(), 100))
|
||||
// the raw value is 88900s, but that's not a multiple of the range interval
|
||||
// so it will be rounded to the next interval multiple, then decremented by 1 interval unit (1800s)
|
||||
// in order to include the provided date
|
||||
.toBe('90000s');
|
||||
});
|
||||
|
||||
it('should change absolute values to relative in seconds (rounded) with end anchor', () => {
|
||||
expect(resolveTimeShift(`endAt(${shiftedDate})`, getDateRange(), 100))
|
||||
// the raw value is 261700s, but that's not a multiple of the range interval
|
||||
// so it will be rounded to the next interval multiple
|
||||
.toBe('261000s');
|
||||
});
|
||||
|
||||
it('should always include the passed date in the computed interval', () => {
|
||||
const dateRange = getDateRange();
|
||||
for (const anchor of ['startAt', 'endAt']) {
|
||||
const [shift] = resolveTimeShift(`${anchor}(${shiftedDate})`, dateRange, 100)!.split('s');
|
||||
expect(
|
||||
moment(shiftedDate).isBetween(
|
||||
moment(dateRange.fromDate).subtract(Number(shift), 's'),
|
||||
moment(dateRange.toDate).subtract(Number(shift), 's')
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,15 +7,28 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { uniq } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
|
||||
import moment from 'moment';
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { search } from '@kbn/data-plugin/public';
|
||||
import { parseTimeShift } from '@kbn/data-plugin/common';
|
||||
import type { GenericIndexPatternColumn, FormBasedLayer, FormBasedPrivateState } from './types';
|
||||
import {
|
||||
calcAutoIntervalNear,
|
||||
DatatableUtilitiesService,
|
||||
isAbsoluteTimeShift,
|
||||
parseAbsoluteTimeShift,
|
||||
parseTimeShift,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import type { DateRange } from '../../../common/types';
|
||||
import type { FormBasedLayer, FormBasedPrivateState } from './types';
|
||||
import type { FramePublicAPI, IndexPattern } from '../../types';
|
||||
|
||||
export function parseTimeShiftWrapper(timeShiftString: string, dateRange: DateRange) {
|
||||
return isAbsoluteTimeShift(timeShiftString.trim())
|
||||
? parseAbsoluteTimeShift(timeShiftString, { from: dateRange.fromDate, to: dateRange.toDate })
|
||||
.value
|
||||
: parseTimeShift(timeShiftString);
|
||||
}
|
||||
|
||||
export const timeShiftOptions = [
|
||||
{
|
||||
label: i18n.translate('xpack.lens.indexPattern.timeShift.none', {
|
||||
|
@ -134,7 +147,7 @@ export function getLayerTimeShiftChecks({
|
|||
}: ReturnType<typeof getDateHistogramInterval>) {
|
||||
return {
|
||||
canShift,
|
||||
isValueTooSmall: (parsedValue: ReturnType<typeof parseTimeShift>) => {
|
||||
isValueTooSmall: (parsedValue: ReturnType<typeof parseTimeShiftWrapper>) => {
|
||||
return (
|
||||
dateHistogramInterval &&
|
||||
parsedValue &&
|
||||
|
@ -142,7 +155,7 @@ export function getLayerTimeShiftChecks({
|
|||
parsedValue.asMilliseconds() < dateHistogramInterval.asMilliseconds()
|
||||
);
|
||||
},
|
||||
isValueNotMultiple: (parsedValue: ReturnType<typeof parseTimeShift>) => {
|
||||
isValueNotMultiple: (parsedValue: ReturnType<typeof parseTimeShiftWrapper>) => {
|
||||
return (
|
||||
dateHistogramInterval &&
|
||||
parsedValue &&
|
||||
|
@ -150,7 +163,7 @@ export function getLayerTimeShiftChecks({
|
|||
!Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval.asMilliseconds())
|
||||
);
|
||||
},
|
||||
isInvalid: (parsedValue: ReturnType<typeof parseTimeShift>) => {
|
||||
isInvalid: (parsedValue: ReturnType<typeof parseTimeShiftWrapper>) => {
|
||||
return Boolean(
|
||||
parsedValue === 'invalid' || (hasDateHistogram && parsedValue && parsedValue === 'previous')
|
||||
);
|
||||
|
@ -164,7 +177,9 @@ export function getDisallowedPreviousShiftMessage(
|
|||
): string[] | undefined {
|
||||
const currentColumn = layer.columns[columnId];
|
||||
const hasPreviousShift =
|
||||
currentColumn.timeShift && parseTimeShift(currentColumn.timeShift) === 'previous';
|
||||
currentColumn.timeShift &&
|
||||
!isAbsoluteTimeShift(currentColumn.timeShift) &&
|
||||
parseTimeShift(currentColumn.timeShift) === 'previous';
|
||||
if (!hasPreviousShift) {
|
||||
return;
|
||||
}
|
||||
|
@ -209,27 +224,28 @@ export function getStateTimeShiftWarningMessages(
|
|||
}
|
||||
const dateHistogramIntervalExpression = dateHistogramInterval.expression;
|
||||
const shiftInterval = dateHistogramInterval.interval.asMilliseconds();
|
||||
let timeShifts: number[] = [];
|
||||
const timeShifts = new Set<number>();
|
||||
const timeShiftMap: Record<number, string[]> = {};
|
||||
Object.entries(layer.columns).forEach(([columnId, column]) => {
|
||||
if (column.isBucketed) return;
|
||||
let duration: number = 0;
|
||||
if (column.timeShift) {
|
||||
// skip absolute time shifts as underneath it will be converted to be round
|
||||
// and avoid this type of issues
|
||||
if (column.timeShift && !isAbsoluteTimeShift(column.timeShift)) {
|
||||
const parsedTimeShift = parseTimeShift(column.timeShift);
|
||||
if (parsedTimeShift === 'previous' || parsedTimeShift === 'invalid') {
|
||||
return;
|
||||
}
|
||||
duration = parsedTimeShift.asMilliseconds();
|
||||
}
|
||||
timeShifts.push(duration);
|
||||
timeShifts.add(duration);
|
||||
if (!timeShiftMap[duration]) {
|
||||
timeShiftMap[duration] = [];
|
||||
}
|
||||
timeShiftMap[duration].push(columnId);
|
||||
});
|
||||
timeShifts = uniq(timeShifts);
|
||||
|
||||
if (timeShifts.length < 2) {
|
||||
if (timeShifts.size < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -273,13 +289,16 @@ export function getStateTimeShiftWarningMessages(
|
|||
|
||||
export function getColumnTimeShiftWarnings(
|
||||
dateHistogramInterval: ReturnType<typeof getDateHistogramInterval>,
|
||||
column: GenericIndexPatternColumn
|
||||
timeShift: string | undefined
|
||||
) {
|
||||
const { isValueTooSmall, isValueNotMultiple } = getLayerTimeShiftChecks(dateHistogramInterval);
|
||||
|
||||
const warnings: string[] = [];
|
||||
if (isAbsoluteTimeShift(timeShift)) {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
const parsedLocalValue = column.timeShift && parseTimeShift(column.timeShift);
|
||||
const parsedLocalValue = timeShift && parseTimeShift(timeShift);
|
||||
const localValueTooSmall = parsedLocalValue && isValueTooSmall(parsedLocalValue);
|
||||
const localValueNotMultiple = parsedLocalValue && isValueNotMultiple(parsedLocalValue);
|
||||
if (localValueTooSmall) {
|
||||
|
@ -299,3 +318,38 @@ export function getColumnTimeShiftWarnings(
|
|||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function closestMultipleOfInterval(duration: number, interval: number) {
|
||||
if (duration % interval === 0) {
|
||||
return duration;
|
||||
}
|
||||
return Math.ceil(duration / interval) * interval;
|
||||
}
|
||||
|
||||
function roundAbsoluteInterval(timeShift: string, dateRange: DateRange, targetBars: number) {
|
||||
// workout the interval (most probably matching the ES one)
|
||||
const interval = calcAutoIntervalNear(
|
||||
targetBars,
|
||||
moment(dateRange.toDate).diff(moment(dateRange.fromDate))
|
||||
);
|
||||
const duration = parseTimeShiftWrapper(timeShift, dateRange);
|
||||
if (typeof duration !== 'string') {
|
||||
const roundingOffset = timeShift.startsWith('end') ? interval.asMilliseconds() : 0;
|
||||
return `${
|
||||
(closestMultipleOfInterval(duration.asMilliseconds(), interval.asMilliseconds()) -
|
||||
roundingOffset) /
|
||||
1000
|
||||
}s`;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTimeShift(
|
||||
timeShift: string | undefined,
|
||||
dateRange: DateRange,
|
||||
targetBars: number
|
||||
) {
|
||||
if (timeShift && isAbsoluteTimeShift(timeShift)) {
|
||||
return roundAbsoluteInterval(timeShift, dateRange, targetBars);
|
||||
}
|
||||
return timeShift;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { partition, uniq } from 'lodash';
|
||||
import seedrandom from 'seedrandom';
|
||||
import {
|
||||
import type {
|
||||
AggFunctionsMapping,
|
||||
EsaggsExpressionFunctionDefinition,
|
||||
IndexPatternLoadExpressionFunctionDefinition,
|
||||
|
@ -21,6 +21,7 @@ import {
|
|||
ExpressionAstExpressionBuilder,
|
||||
ExpressionAstFunction,
|
||||
} from '@kbn/expressions-plugin/public';
|
||||
import type { DateRange } from '../../../common/types';
|
||||
import { GenericIndexPatternColumn } from './form_based';
|
||||
import { operationDefinitionMap } from './operations';
|
||||
import { FormBasedPrivateState, FormBasedLayer } from './types';
|
||||
|
@ -29,6 +30,7 @@ import { FormattedIndexPatternColumn } from './operations/definitions/column_typ
|
|||
import { isColumnFormatted, isColumnOfType } from './operations/definitions/helpers';
|
||||
import type { IndexPattern, IndexPatternMap } from '../../types';
|
||||
import { dedupeAggs } from './dedupe_aggs';
|
||||
import { resolveTimeShift } from './time_shift_utils';
|
||||
|
||||
export type OriginalColumn = { id: string } & GenericIndexPatternColumn;
|
||||
|
||||
|
@ -54,6 +56,7 @@ function getExpressionForLayer(
|
|||
layer: FormBasedLayer,
|
||||
indexPattern: IndexPattern,
|
||||
uiSettings: IUiSettingsClient,
|
||||
dateRange: DateRange,
|
||||
searchSessionId?: string
|
||||
): ExpressionAstExpression | null {
|
||||
const { columnOrder } = layer;
|
||||
|
@ -120,7 +123,10 @@ function getExpressionForLayer(
|
|||
operationDefinitionMap[col.operationType]?.input === 'fullReference' ||
|
||||
operationDefinitionMap[col.operationType]?.input === 'managedReference'
|
||||
);
|
||||
const hasDateHistogram = columnEntries.some(([, c]) => c.operationType === 'date_histogram');
|
||||
const firstDateHistogramColumn = columnEntries.find(
|
||||
([, col]) => col.operationType === 'date_histogram'
|
||||
);
|
||||
const hasDateHistogram = Boolean(firstDateHistogramColumn);
|
||||
|
||||
if (referenceEntries.length || esAggEntries.length) {
|
||||
let aggs: ExpressionAstExpressionBuilder[] = [];
|
||||
|
@ -137,6 +143,7 @@ function getExpressionForLayer(
|
|||
const orderedColumnIds = esAggEntries.map(([colId]) => colId);
|
||||
let esAggsIdMap: Record<string, OriginalColumn[]> = {};
|
||||
const aggExpressionToEsAggsIdMap: Map<ExpressionAstExpressionBuilder, string> = new Map();
|
||||
const histogramBarsTarget = uiSettings.get('histogram:barTarget');
|
||||
esAggEntries.forEach(([colId, col], index) => {
|
||||
const def = operationDefinitionMap[col.operationType];
|
||||
if (def.input !== 'fullReference' && def.input !== 'managedReference') {
|
||||
|
@ -149,7 +156,10 @@ function getExpressionForLayer(
|
|||
col.reducedTimeRange &&
|
||||
indexPattern.timeFieldName;
|
||||
let aggAst = def.toEsAggsFn(
|
||||
col,
|
||||
{
|
||||
...col,
|
||||
timeShift: resolveTimeShift(col.timeShift, dateRange, histogramBarsTarget),
|
||||
},
|
||||
wrapInFilter || wrapInTimeFilter ? `${aggId}-metric` : aggId,
|
||||
indexPattern,
|
||||
layer,
|
||||
|
@ -171,11 +181,11 @@ function getExpressionForLayer(
|
|||
schema: 'bucket',
|
||||
filter: col.filter && queryToAst(col.filter),
|
||||
timeWindow: wrapInTimeFilter ? col.reducedTimeRange : undefined,
|
||||
timeShift: col.timeShift,
|
||||
timeShift: resolveTimeShift(col.timeShift, dateRange, histogramBarsTarget),
|
||||
}),
|
||||
]),
|
||||
customMetric: buildExpression({ type: 'expression', chain: [aggAst] }),
|
||||
timeShift: col.timeShift,
|
||||
timeShift: resolveTimeShift(col.timeShift, dateRange, histogramBarsTarget),
|
||||
}
|
||||
).toAst();
|
||||
}
|
||||
|
@ -310,10 +320,6 @@ function getExpressionForLayer(
|
|||
return base;
|
||||
});
|
||||
|
||||
const firstDateHistogramColumn = columnEntries.find(
|
||||
([, col]) => col.operationType === 'date_histogram'
|
||||
);
|
||||
|
||||
const columnsWithTimeScale = columnEntries.filter(
|
||||
([, col]) =>
|
||||
col.timeScale &&
|
||||
|
@ -446,6 +452,7 @@ export function toExpression(
|
|||
layerId: string,
|
||||
indexPatterns: IndexPatternMap,
|
||||
uiSettings: IUiSettingsClient,
|
||||
dateRange: DateRange,
|
||||
searchSessionId?: string
|
||||
) {
|
||||
if (state.layers[layerId]) {
|
||||
|
@ -453,6 +460,7 @@ export function toExpression(
|
|||
state.layers[layerId],
|
||||
indexPatterns[state.layers[layerId].indexPatternId],
|
||||
uiSettings,
|
||||
dateRange,
|
||||
searchSessionId
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from '@kbn/data-plugin/public';
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import type { DateRange } from '../../../common/types';
|
||||
import type { FramePublicAPI, IndexPattern, StateSetter } from '../../types';
|
||||
import { renewIDs } from '../../utils';
|
||||
import type { FormBasedLayer, FormBasedPersistedState, FormBasedPrivateState } from './types';
|
||||
|
@ -54,7 +55,8 @@ import { isQueryValid } from '../../shared_components';
|
|||
export function isColumnInvalid(
|
||||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern
|
||||
indexPattern: IndexPattern,
|
||||
dateRange: DateRange | undefined
|
||||
) {
|
||||
const column: GenericIndexPatternColumn | undefined = layer.columns[columnId];
|
||||
if (!column || !indexPattern) return;
|
||||
|
@ -64,11 +66,17 @@ export function isColumnInvalid(
|
|||
const referencesHaveErrors =
|
||||
true &&
|
||||
'references' in column &&
|
||||
Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length);
|
||||
Boolean(getReferencesErrors(layer, column, indexPattern, dateRange).filter(Boolean).length);
|
||||
|
||||
const operationErrorMessages =
|
||||
operationDefinition &&
|
||||
operationDefinition.getErrorMessage?.(layer, columnId, indexPattern, operationDefinitionMap);
|
||||
operationDefinition.getErrorMessage?.(
|
||||
layer,
|
||||
columnId,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
operationDefinitionMap
|
||||
);
|
||||
|
||||
const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false;
|
||||
|
||||
|
@ -82,7 +90,8 @@ export function isColumnInvalid(
|
|||
function getReferencesErrors(
|
||||
layer: FormBasedLayer,
|
||||
column: ReferenceBasedIndexPatternColumn,
|
||||
indexPattern: IndexPattern
|
||||
indexPattern: IndexPattern,
|
||||
dateRange: DateRange | undefined
|
||||
) {
|
||||
return column.references?.map((referenceId: string) => {
|
||||
const referencedOperation = layer.columns[referenceId]?.operationType;
|
||||
|
@ -91,6 +100,7 @@ function getReferencesErrors(
|
|||
layer,
|
||||
referenceId,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
operationDefinitionMap
|
||||
);
|
||||
});
|
||||
|
|
|
@ -76,6 +76,10 @@ const expectedIndexPatterns = {
|
|||
};
|
||||
|
||||
const indexPatterns = expectedIndexPatterns;
|
||||
const dateRange = {
|
||||
fromDate: '2022-03-17T08:25:00.000Z',
|
||||
toDate: '2022-04-17T08:25:00.000Z',
|
||||
};
|
||||
|
||||
describe('Textbased Data Source', () => {
|
||||
let baseState: TextBasedPrivateState;
|
||||
|
@ -622,7 +626,9 @@ describe('Textbased Data Source', () => {
|
|||
describe('#toExpression', () => {
|
||||
it('should generate an empty expression when no columns are selected', async () => {
|
||||
const state = TextBasedDatasource.initialize();
|
||||
expect(TextBasedDatasource.toExpression(state, 'first', indexPatterns)).toEqual(null);
|
||||
expect(TextBasedDatasource.toExpression(state, 'first', indexPatterns, dateRange)).toEqual(
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate an expression for an SQL query', async () => {
|
||||
|
@ -672,7 +678,7 @@ describe('Textbased Data Source', () => {
|
|||
],
|
||||
} as unknown as TextBasedPrivateState;
|
||||
|
||||
expect(TextBasedDatasource.toExpression(queryBaseState, 'a', indexPatterns))
|
||||
expect(TextBasedDatasource.toExpression(queryBaseState, 'a', indexPatterns, dateRange))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
|
|
|
@ -566,7 +566,8 @@ export function LayerPanel(
|
|||
layerDatasourceState,
|
||||
dataViews.indexPatterns,
|
||||
layerId,
|
||||
columnId
|
||||
columnId,
|
||||
dateRange
|
||||
)
|
||||
}
|
||||
>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { Ast, fromExpression } from '@kbn/interpreter';
|
||||
import type { DateRange } from '../../../common/types';
|
||||
import { DatasourceStates } from '../../state_management';
|
||||
import { Visualization, DatasourceMap, DatasourceLayers, IndexPatternMap } from '../../types';
|
||||
|
||||
|
@ -12,6 +13,7 @@ export function getDatasourceExpressionsByLayers(
|
|||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: DatasourceStates,
|
||||
indexPatterns: IndexPatternMap,
|
||||
dateRange: DateRange,
|
||||
searchSessionId?: string
|
||||
): null | Record<string, Ast> {
|
||||
const datasourceExpressions: Array<[string, Ast | string]> = [];
|
||||
|
@ -25,7 +27,13 @@ export function getDatasourceExpressionsByLayers(
|
|||
const layers = datasource.getLayers(state);
|
||||
|
||||
layers.forEach((layerId) => {
|
||||
const result = datasource.toExpression(state, layerId, indexPatterns, searchSessionId);
|
||||
const result = datasource.toExpression(
|
||||
state,
|
||||
layerId,
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
searchSessionId
|
||||
);
|
||||
if (result) {
|
||||
datasourceExpressions.push([layerId, result]);
|
||||
}
|
||||
|
@ -54,6 +62,7 @@ export function buildExpression({
|
|||
title,
|
||||
description,
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
searchSessionId,
|
||||
}: {
|
||||
title?: string;
|
||||
|
@ -65,6 +74,7 @@ export function buildExpression({
|
|||
datasourceLayers: DatasourceLayers;
|
||||
indexPatterns: IndexPatternMap;
|
||||
searchSessionId?: string;
|
||||
dateRange: DateRange;
|
||||
}): Ast | null {
|
||||
if (visualization === null) {
|
||||
return null;
|
||||
|
@ -74,6 +84,7 @@ export function buildExpression({
|
|||
datasourceMap,
|
||||
datasourceStates,
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
searchSessionId
|
||||
);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { difference } from 'lodash';
|
|||
import type { DataViewsContract, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import type { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
Datasource,
|
||||
DatasourceLayers,
|
||||
|
@ -321,6 +322,7 @@ export async function persistedStateToExpression(
|
|||
uiSettings: IUiSettingsClient;
|
||||
storage: IStorageWrapper;
|
||||
dataViews: DataViewsContract;
|
||||
timefilter: TimefilterContract;
|
||||
}
|
||||
): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> {
|
||||
const {
|
||||
|
@ -412,6 +414,7 @@ export async function persistedStateToExpression(
|
|||
visualizationState,
|
||||
{ datasourceLayers, dataViews: { indexPatterns } as DataViewsState }
|
||||
);
|
||||
const currentTimeRange = services.timefilter.getAbsoluteTime();
|
||||
|
||||
return {
|
||||
ast: buildExpression({
|
||||
|
@ -423,6 +426,7 @@ export async function persistedStateToExpression(
|
|||
datasourceStates,
|
||||
datasourceLayers,
|
||||
indexPatterns,
|
||||
dateRange: { fromDate: currentTimeRange.from, toDate: currentTimeRange.to },
|
||||
}),
|
||||
errors: validationResult,
|
||||
};
|
||||
|
|
|
@ -526,7 +526,8 @@ function getPreviewExpression(
|
|||
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
|
||||
datasources,
|
||||
datasourceStates,
|
||||
frame.dataViews.indexPatterns
|
||||
frame.dataViews.indexPatterns,
|
||||
frame.dateRange
|
||||
);
|
||||
|
||||
return visualization.toPreviewExpression(
|
||||
|
|
|
@ -310,7 +310,14 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
framePublicAPI
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[activeVisualization, visualization.state, activeDatasourceId, datasourceMap, datasourceStates]
|
||||
[
|
||||
activeVisualization,
|
||||
visualization.state,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
framePublicAPI.dateRange,
|
||||
]
|
||||
);
|
||||
|
||||
// if the expression is undefined, it means we hit an error that should be displayed to the user
|
||||
|
@ -324,6 +331,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceStates,
|
||||
datasourceLayers,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
dateRange: framePublicAPI.dateRange,
|
||||
searchSessionId,
|
||||
});
|
||||
|
||||
|
@ -368,6 +376,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceLayers,
|
||||
dataViews.indexPatterns,
|
||||
searchSessionId,
|
||||
framePublicAPI.dateRange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
DataViewsContract,
|
||||
TimefilterContract,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
|
||||
|
@ -55,6 +56,7 @@ export interface EditorFramePlugins {
|
|||
dataViews: DataViewsContract;
|
||||
uiSettings: IUiSettingsClient;
|
||||
storage: IStorageWrapper;
|
||||
timefilter: TimefilterContract;
|
||||
}
|
||||
|
||||
async function collectAsyncDefinitions<T extends { id: string }>(
|
||||
|
|
|
@ -42,7 +42,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
initialize: jest.fn((_state?) => {}),
|
||||
renderDataPanel: jest.fn(),
|
||||
renderLayerPanel: jest.fn(),
|
||||
toExpression: jest.fn((_frame, _state, _indexPatterns) => null),
|
||||
toExpression: jest.fn((_frame, _state, _indexPatterns, dateRange) => null),
|
||||
insertLayer: jest.fn((_state, _newLayerId) => ({})),
|
||||
removeLayer: jest.fn((state, layerId) => ({ newState: state, removedLayerIds: [layerId] })),
|
||||
cloneLayer: jest.fn((_state, _layerId, _newLayerId, getNewId) => {}),
|
||||
|
|
|
@ -300,6 +300,7 @@ export class LensPlugin {
|
|||
dataViews: plugins.dataViews,
|
||||
storage: new Storage(localStorage),
|
||||
uiSettings: core.uiSettings,
|
||||
timefilter: plugins.data.query.timefilter.timefilter,
|
||||
}),
|
||||
injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
visualizationMap,
|
||||
|
|
|
@ -389,6 +389,7 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
state: T,
|
||||
layerId: string,
|
||||
indexPatterns: IndexPatternMap,
|
||||
dateRange: DateRange,
|
||||
searchSessionId?: string
|
||||
) => ExpressionAstExpression | string | null;
|
||||
|
||||
|
@ -470,7 +471,8 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
state: T,
|
||||
indexPatterns: IndexPatternMap,
|
||||
layerId: string,
|
||||
columnId: string
|
||||
columnId: string,
|
||||
dateRange?: DateRange
|
||||
) => boolean;
|
||||
/**
|
||||
* Are these datasources equivalent?
|
||||
|
@ -611,6 +613,7 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
|
|||
onRemove?: (accessor: string) => void;
|
||||
state: T;
|
||||
activeData?: Record<string, Datatable>;
|
||||
dateRange: DateRange;
|
||||
indexPatterns: IndexPatternMap;
|
||||
hideTooltip?: boolean;
|
||||
invalid?: boolean;
|
||||
|
|
|
@ -60,7 +60,8 @@ describe('#toExpression', () => {
|
|||
const datasourceExpression = mockDatasource.toExpression(
|
||||
frame.datasourceLayers.first,
|
||||
'first',
|
||||
frame.dataViews.indexPatterns
|
||||
frame.dataViews.indexPatterns,
|
||||
frame.dateRange
|
||||
) ?? {
|
||||
type: 'expression',
|
||||
chain: [],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue