diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 4ffd0db52d374..c4df3c17db3b7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -400,6 +400,8 @@ export function DimensionEditor(props: DimensionEditorProps) { { isFullscreen: false, toggleFullscreen: jest.fn(), setIsCloseable: jest.fn(), + layerId: '1', }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index b0cdf96f928f9..af8c8d7d1bf28 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -35,11 +35,14 @@ import { hasField } from '../utils'; import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { VisualizationDimensionGroupConfig } from '../../types'; +import { IndexPatternDimensionEditorProps } from './dimension_panel'; const operationPanels = getOperationDisplay(); export interface ReferenceEditorProps { layer: IndexPatternLayer; + layerId: string; + activeData?: IndexPatternDimensionEditorProps['activeData']; selectionStyle: 'full' | 'field' | 'hidden'; validation: RequiredReference; columnId: string; @@ -67,6 +70,8 @@ export interface ReferenceEditorProps { export function ReferenceEditor(props: ReferenceEditorProps) { const { layer, + layerId, + activeData, columnId, updateLayer, currentIndexPattern, @@ -350,6 +355,8 @@ export function ReferenceEditor(props: ReferenceEditorProps) { updateLayer={updateLayer} currentColumn={column} layer={layer} + layerId={layerId} + activeData={activeData} columnId={columnId} indexPattern={currentIndexPattern} dateRange={dateRange} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx index 0ac02c15b34a5..ba9525ac53fc5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx @@ -8,21 +8,22 @@ import { EuiButtonIcon } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; import { Query } from 'src/plugins/data/public'; -import { search } from '../../../../../../src/plugins/data/public'; import { parseTimeShift } from '../../../../../../src/plugins/data/common'; import { adjustTimeScaleLabelSuffix, IndexPatternColumn, operationDefinitionMap, } from '../operations'; -import { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; +import { IndexPattern, IndexPatternLayer } from '../types'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; -import { FramePublicAPI } from '../../types'; +import { + getDateHistogramInterval, + getLayerTimeShiftChecks, + timeShiftOptions, +} from '../time_shift_utils'; // to do: get the language from uiSettings export const defaultFilter: Query = { @@ -59,75 +60,6 @@ export function setTimeShift( }; } -const timeShiftOptions = [ - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', { - defaultMessage: '1 hour (1h)', - }), - value: '1h', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', { - defaultMessage: '3 hours (3h)', - }), - value: '3h', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', { - defaultMessage: '6 hours (6h)', - }), - value: '6h', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', { - defaultMessage: '12 hours (12h)', - }), - value: '12h', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.day', { - defaultMessage: '1 day (1d)', - }), - value: '1d', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.week', { - defaultMessage: '1 week (1w)', - }), - value: '1w', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.month', { - defaultMessage: '1 month (1M)', - }), - value: '1M', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', { - defaultMessage: '3 months (3M)', - }), - value: '3M', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', { - defaultMessage: '6 months (6M)', - }), - value: '6M', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.year', { - defaultMessage: '1 year (1y)', - }), - value: '1y', - }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { - defaultMessage: 'Previous', - }), - value: 'previous', - }, -]; - export function TimeShift({ selectedColumn, columnId, @@ -157,38 +89,12 @@ export function TimeShift({ return null; } - let dateHistogramInterval: null | moment.Duration = null; - const dateHistogramColumn = layer.columnOrder.find( - (colId) => layer.columns[colId].operationType === 'date_histogram' + const { isValueTooSmall, isValueNotMultiple, canShift } = getLayerTimeShiftChecks( + getDateHistogramInterval(layer, indexPattern, activeData, layerId) ); - if (!dateHistogramColumn && !indexPattern.timeFieldName) { - return null; - } - if (dateHistogramColumn && activeData && activeData[layerId] && activeData[layerId]) { - const column = activeData[layerId].columns.find((col) => col.id === dateHistogramColumn); - if (column) { - dateHistogramInterval = search.aggs.parseInterval( - search.aggs.getDateHistogramMetaDataByDatatableColumn(column)?.interval || '' - ); - } - } - - function isValueTooSmall(parsedValue: ReturnType) { - return ( - dateHistogramInterval && - parsedValue && - typeof parsedValue === 'object' && - parsedValue.asMilliseconds() < dateHistogramInterval.asMilliseconds() - ); - } - function isValueNotMultiple(parsedValue: ReturnType) { - return ( - dateHistogramInterval && - parsedValue && - typeof parsedValue === 'object' && - !Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval.asMilliseconds()) - ); + if (!canShift) { + return null; } const parsedLocalValue = localValue && parseTimeShift(localValue); @@ -305,90 +211,3 @@ export function TimeShift({ ); } - -export function getTimeShiftWarningMessages( - state: IndexPatternPrivateState, - { activeData }: FramePublicAPI -) { - if (!state) return; - const warningMessages: React.ReactNode[] = []; - Object.entries(state.layers).forEach(([layerId, layer]) => { - let dateHistogramInterval: null | string = null; - const dateHistogramColumn = layer.columnOrder.find( - (colId) => layer.columns[colId].operationType === 'date_histogram' - ); - if (!dateHistogramColumn) { - return; - } - if (dateHistogramColumn && activeData && activeData[layerId]) { - const column = activeData[layerId].columns.find((col) => col.id === dateHistogramColumn); - if (column) { - dateHistogramInterval = - search.aggs.getDateHistogramMetaDataByDatatableColumn(column)?.interval || null; - } - } - if (dateHistogramInterval === null) { - return; - } - const shiftInterval = search.aggs.parseInterval(dateHistogramInterval)!.asMilliseconds(); - let timeShifts: number[] = []; - const timeShiftMap: Record = {}; - Object.entries(layer.columns).forEach(([columnId, column]) => { - if (column.isBucketed) return; - let duration: number = 0; - if (column.timeShift) { - const parsedTimeShift = parseTimeShift(column.timeShift); - if (parsedTimeShift === 'previous' || parsedTimeShift === 'invalid') { - return; - } - duration = parsedTimeShift.asMilliseconds(); - } - timeShifts.push(duration); - if (!timeShiftMap[duration]) { - timeShiftMap[duration] = []; - } - timeShiftMap[duration].push(columnId); - }); - timeShifts = uniq(timeShifts); - - if (timeShifts.length < 2) { - return; - } - - timeShifts.forEach((timeShift) => { - if (timeShift === 0) return; - if (timeShift < shiftInterval) { - timeShiftMap[timeShift].forEach((columnId) => { - warningMessages.push( - {layer.columns[columnId].label}, - interval: {dateHistogramInterval}, - columnTimeShift: {layer.columns[columnId].timeShift}, - }} - /> - ); - }); - } else if (!Number.isInteger(timeShift / shiftInterval)) { - timeShiftMap[timeShift].forEach((columnId) => { - warningMessages.push( - {layer.columns[columnId].label}, - interval: dateHistogramInterval, - columnTimeShift: layer.columns[columnId].timeShift!, - }} - /> - ); - }); - } - }); - }); - return warningMessages; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index bc2184bd9edb7..5f17ea74ae992 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -55,7 +55,7 @@ import { deleteColumn, isReferenced } from './operations'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; import { DraggingIdentifier } from '../drag_drop'; -import { getTimeShiftWarningMessages } from './dimension_panel/time_shift'; +import { getStateTimeShiftWarningMessages } from './time_shift_utils'; export { OperationType, IndexPatternColumn, deleteColumn } from './operations'; @@ -462,7 +462,7 @@ export function getIndexPatternDatasource({ }); return messages.length ? messages : undefined; }, - getWarningMessages: getTimeShiftWarningMessages, + getWarningMessages: getStateTimeShiftWarningMessages, checkIntegrity: (state) => { const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId); return ids.filter((id) => !state.indexPatterns[id]); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index 396eae9b39c41..3f539b18896c4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -94,7 +94,7 @@ export const counterRateOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale, - timeShift: previousColumn?.timeShift, + timeShift: columnParams?.shift || previousColumn?.timeShift, filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index f39e5587b398c..0bdef4cc7678d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -90,7 +90,7 @@ export const cumulativeSumOperation: OperationDefinition< operationType: 'cumulative_sum', isBucketed: false, scale: 'ratio', - timeShift: previousColumn?.timeShift, + timeShift: columnParams?.shift || previousColumn?.timeShift, filter: getFilter(previousColumn, columnParams), references: referenceIds, params: getFormatFromPreviousColumn(previousColumn), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index e103acd9ab677..857b719b0f411 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -82,7 +82,7 @@ export const derivativeOperation: OperationDefinition< references: referenceIds, timeScale: previousColumn?.timeScale, filter: getFilter(previousColumn, columnParams), - timeShift: previousColumn?.timeShift, + timeShift: columnParams?.shift || previousColumn?.timeShift, params: getFormatFromPreviousColumn(previousColumn), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index ee305bc043f1b..aea203c70a928 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -98,9 +98,9 @@ export const movingAverageOperation: OperationDefinition< isBucketed: false, scale: 'ratio', references: referenceIds, - timeScale: previousColumn?.timeScale, + timeShift: columnParams?.shift || previousColumn?.timeShift, filter: getFilter(previousColumn, columnParams), - timeShift: previousColumn?.timeShift, + timeScale: previousColumn?.timeScale, params: { window, ...getFormatFromPreviousColumn(previousColumn), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 4da8a3ca0eb24..41b90cfc6173a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -95,7 +95,7 @@ export const cardinalityOperation: OperationDefinition) => { + const dateHistogramInterval = getDateHistogramInterval( + rest.layer, + rest.indexPattern, + activeData, + rest.layerId + ); + return ; +}; -export const MemoizedFormulaEditor = React.memo(FormulaEditor); +const MemoizedFormulaEditor = React.memo(FormulaEditor); export function FormulaEditor({ layer, @@ -61,7 +75,10 @@ export function FormulaEditor({ toggleFullscreen, isFullscreen, setIsCloseable, -}: ParamEditorProps) { + dateHistogramInterval, +}: Omit, 'activeData'> & { + dateHistogramInterval: ReturnType; +}) { const [text, setText] = useState(currentColumn.params.formula); const [warnings, setWarnings] = useState< Array<{ severity: monaco.MarkerSeverity; message: string }> @@ -78,6 +95,13 @@ export function FormulaEditor({ operationDefinitionMap, ]); + const baseInterval = + 'interval' in dateHistogramInterval + ? dateHistogramInterval.interval?.asMilliseconds() + : undefined; + const baseIntervalRef = useRef(baseInterval); + baseIntervalRef.current = baseInterval; + // The Monaco editor needs to have the overflowDiv in the first render. Using an effect // requires a second render to work, so we are using an if statement to guarantee it happens // on first render @@ -227,6 +251,7 @@ export function FormulaEditor({ const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns); const markers: monaco.editor.IMarkerData[] = managedColumns .flatMap(([id, column]) => { + const newWarnings: monaco.editor.IMarkerData[] = []; if (locations[id]) { const def = visibleOperationsMap[column.operationType]; if (def.getErrorMessage) { @@ -239,20 +264,32 @@ export function FormulaEditor({ if (messages) { const startPosition = offsetToRowColumn(text, locations[id].min); const endPosition = offsetToRowColumn(text, locations[id].max); - return [ - { - message: messages.join(', '), - startColumn: startPosition.column + 1, - startLineNumber: startPosition.lineNumber, - endColumn: endPosition.column + 1, - endLineNumber: endPosition.lineNumber, - severity: monaco.MarkerSeverity.Warning, - }, - ]; + newWarnings.push({ + message: messages.join(', '), + startColumn: startPosition.column + 1, + startLineNumber: startPosition.lineNumber, + endColumn: endPosition.column + 1, + endLineNumber: endPosition.lineNumber, + severity: monaco.MarkerSeverity.Warning, + }); } } + if (def.shiftable && column.timeShift) { + 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, + })) + ); + } } - return []; + return newWarnings; }) .filter((marker) => marker); setWarnings(markers.map(({ severity, message }) => ({ severity, message }))); @@ -313,6 +350,7 @@ export function FormulaEditor({ indexPattern, operationDefinitionMap: visibleOperationsMap, data, + dateHistogramInterval: baseIntervalRef.current, }); } } else { @@ -323,6 +361,7 @@ export function FormulaEditor({ indexPattern, operationDefinitionMap: visibleOperationsMap, data, + dateHistogramInterval: baseIntervalRef.current, }); } @@ -332,7 +371,7 @@ export function FormulaEditor({ ), }; }, - [indexPattern, visibleOperationsMap, data] + [indexPattern, visibleOperationsMap, data, baseIntervalRef] ); const provideSignatureHelp = useCallback( @@ -417,7 +456,9 @@ export function FormulaEditor({ !tokenInfo || typeof tokenInfo.ast === 'number' || tokenInfo.ast.type !== 'namedArgument' || - (tokenInfo.ast.name !== 'kql' && tokenInfo.ast.name !== 'lucene') || + (tokenInfo.ast.name !== 'kql' && + tokenInfo.ast.name !== 'lucene' && + tokenInfo.ast.name !== 'shift') || (tokenInfo.ast.value !== 'LENS_MATH_MARKER' && !isSingleQuoteCase.test(tokenInfo.ast.value)) ) { @@ -437,7 +478,7 @@ export function FormulaEditor({ text: `''`, }; } - if (char === "'") { + if (char === "'" && tokenInfo.ast.name !== 'shift') { editOperation = { range: { ...currentPosition, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx index afe5471666b22..aa08dfbe7ea33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx @@ -391,6 +391,14 @@ export function getFunctionSignatureLabel( defaultMessage: '[kql]?: string, [lucene]?: string', }); } + if (def.filterable && def.shiftable) { + extraArgs += ', '; + } + if (def.shiftable) { + extraArgs += i18n.translate('xpack.lens.formula.shiftExtraArguments', { + defaultMessage: '[shift]?: string', + }); + } return `${name}(${def.documentation?.signature}${extraArgs})`; } return ''; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts index df747e532b38a..815df943cdba3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -25,12 +25,15 @@ import { tinymathFunctions, groupArgsByType } from '../util'; import type { GenericOperationDefinition } from '../..'; import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help'; import { hasFunctionFieldArgument } from '../validation'; +import { timeShiftOptions, timeShiftOptionOrder } from '../../../../time_shift_utils'; +import { parseTimeShift } from '../../../../../../../../../src/plugins/data/common'; export enum SUGGESTION_TYPE { FIELD = 'field', NAMED_ARGUMENT = 'named_argument', FUNCTIONS = 'functions', KQL = 'kql', + SHIFTS = 'shifts', } export type LensMathSuggestion = @@ -116,6 +119,7 @@ export async function suggest({ indexPattern, operationDefinitionMap, data, + dateHistogramInterval, }: { expression: string; zeroIndexedOffset: number; @@ -123,6 +127,7 @@ export async function suggest({ indexPattern: IndexPattern; operationDefinitionMap: Record; data: DataPublicPluginStart; + dateHistogramInterval?: number; }): Promise<{ list: LensMathSuggestion[]; type: SUGGESTION_TYPE }> { const text = expression.substr(0, zeroIndexedOffset) + MARKER + expression.substr(zeroIndexedOffset); @@ -143,6 +148,7 @@ export async function suggest({ ast: tokenAst as TinymathNamedArgument, data, indexPattern, + dateHistogramInterval, }); } else if (tokenInfo?.parent) { return getArgumentSuggestions( @@ -231,13 +237,19 @@ function getArgumentSuggestions( const { namedArguments } = groupArgsByType(ast.args); const list = []; if (operation.filterable) { - if (!namedArguments.find((arg) => arg.name === 'kql')) { + const hasFilterArgument = namedArguments.find( + (arg) => arg.name === 'kql' || arg.name === 'lucene' + ); + if (!hasFilterArgument) { list.push('kql'); - } - if (!namedArguments.find((arg) => arg.name === 'lucene')) { list.push('lucene'); } } + if (operation.shiftable) { + if (!namedArguments.find((arg) => arg.name === 'shift')) { + list.push('shift'); + } + } if ('operationParams' in operation) { // Exclude any previously used named args list.push( @@ -308,11 +320,28 @@ export async function getNamedArgumentSuggestions({ ast, data, indexPattern, + dateHistogramInterval, }: { ast: TinymathNamedArgument; indexPattern: IndexPattern; data: DataPublicPluginStart; + dateHistogramInterval?: number; }) { + if (ast.name === 'shift') { + return { + list: timeShiftOptions + .filter(({ value }) => { + if (typeof dateHistogramInterval === 'undefined') return true; + const parsedValue = parseTimeShift(value); + return ( + typeof parsedValue === 'string' || + Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval) + ); + }) + .map(({ value }) => value), + type: SUGGESTION_TYPE.SHIFTS, + }; + } if (ast.name !== 'kql' && ast.name !== 'lucene') { return { list: [], type: SUGGESTION_TYPE.KQL }; } @@ -363,6 +392,9 @@ export function getSuggestion( const filterText: string = label; switch (type) { + case SUGGESTION_TYPE.SHIFTS: + sortText = String(timeShiftOptionOrder[label]).padStart(4, '0'); + break; case SUGGESTION_TYPE.FIELD: kind = monaco.languages.CompletionItemKind.Value; break; @@ -387,7 +419,7 @@ export function getSuggestion( break; case SUGGESTION_TYPE.NAMED_ARGUMENT: kind = monaco.languages.CompletionItemKind.Keyword; - if (label === 'kql' || label === 'lucene') { + if (label === 'kql' || label === 'lucene' || label === 'shift') { command = TRIGGER_SUGGESTION_COMMAND; insertText = `${label}='$0'`; insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; @@ -550,7 +582,10 @@ export function getSignatureHelp( } catch (e) { // do nothing } - return { value: { signatures: [], activeParameter: 0, activeSignature: 0 }, dispose: () => {} }; + return { + value: { signatures: [], activeParameter: 0, activeSignature: 0 }, + dispose: () => {}, + }; } export function getHover( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index e1c722fd9cb38..e6aa29ea4d763 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -430,7 +430,7 @@ describe('formula', () => { customLabel: true, dataType: 'number', isBucketed: false, - label: 'col1X0', + label: 'Part of average(bytes)', operationType: 'average', scale: 'ratio', sourceField: 'bytes', @@ -440,7 +440,7 @@ describe('formula', () => { customLabel: true, dataType: 'number', isBucketed: false, - label: 'col1X1', + label: 'Part of average(bytes)', operationType: 'math', params: { tinymathAst: 'col1X0', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx index 6d3ab6a7f3082..994f7280c3286 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -10,7 +10,7 @@ import { OperationDefinition } from '../index'; import { ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPattern } from '../../../types'; import { runASTValidation, tryToParse } from './validation'; -import { MemoizedFormulaEditor } from './editor'; +import { WrappedFormulaEditor } from './editor'; import { regenerateLayerFromAst } from './parse'; import { generateFormula } from './generate'; import { filterByVisibleOperation } from './util'; @@ -160,5 +160,5 @@ export const formulaOperation: OperationDefinition< return newLayer; }, - paramEditor: MemoizedFormulaEditor, + paramEditor: WrappedFormulaEditor, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts index e44cd50ae9c41..a5c19c537acee 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts @@ -6,16 +6,23 @@ */ import { isObject } from 'lodash'; -import { GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { + FieldBasedIndexPatternColumn, + GenericOperationDefinition, + IndexPatternColumn, +} from '../index'; import { ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; // Just handle two levels for now type OperationParams = Record>; -export function getSafeFieldName(fieldName: string | undefined) { - // clean up the "Records" field for now - if (!fieldName || fieldName === 'Records') { +export function getSafeFieldName({ + sourceField: fieldName, + operationType, +}: FieldBasedIndexPatternColumn) { + // return empty for the records field + if (!fieldName || operationType === 'count') { return ''; } return fieldName; @@ -30,15 +37,13 @@ export function generateFormula( if ('references' in previousColumn) { const metric = layer.columns[previousColumn.references[0]]; if (metric && 'sourceField' in metric && metric.dataType === 'number') { - const fieldName = getSafeFieldName(metric.sourceField); + const fieldName = getSafeFieldName(metric); // TODO need to check the input type from the definition previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`; } } else { if (previousColumn && 'sourceField' in previousColumn && previousColumn.dataType === 'number') { - previousFormula += `${previousColumn.operationType}(${getSafeFieldName( - previousColumn?.sourceField - )}`; + previousFormula += `${previousColumn.operationType}(${getSafeFieldName(previousColumn)}`; } } const formulaNamedArgs = extractParamsForFormula(previousColumn, operationDefinitionMap); @@ -54,6 +59,12 @@ export function generateFormula( (previousColumn.filter.language === 'kuery' ? 'kql=' : 'lucene=') + `'${previousColumn.filter.query.replace(/'/g, `\\'`)}'`; // replace all } + if (previousColumn.timeShift) { + if (previousColumn.operationType !== 'count' || previousColumn.filter) { + previousFormula += ', '; + } + previousFormula += `shift='${previousColumn.timeShift}'`; + } if (previousFormula) { // close the formula at the end previousFormula += ')'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index 517cf5f1bbf45..8b726d06f4602 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -31,7 +31,8 @@ function parseAndExtract( layer: IndexPatternLayer, columnId: string, indexPattern: IndexPattern, - operationDefinitionMap: Record + operationDefinitionMap: Record, + label?: string ) { const { root, error } = tryToParse(text, operationDefinitionMap); if (error || !root) { @@ -45,7 +46,17 @@ function parseAndExtract( /* { name: 'add', args: [ { name: 'abc', args: [5] }, 5 ] } */ - const extracted = extractColumns(columnId, operationDefinitionMap, root, layer, indexPattern); + const extracted = extractColumns( + columnId, + operationDefinitionMap, + root, + layer, + indexPattern, + i18n.translate('xpack.lens.indexPattern.formulaPartLabel', { + defaultMessage: 'Part of {label}', + values: { label: label || text }, + }) + ); return { extracted, isValid: true }; } @@ -54,7 +65,8 @@ function extractColumns( operations: Record, ast: TinymathAST, layer: IndexPatternLayer, - indexPattern: IndexPattern + indexPattern: IndexPattern, + label: string ): Array<{ column: IndexPatternColumn; location?: TinymathLocation }> { const columns: Array<{ column: IndexPatternColumn; location?: TinymathLocation }> = []; @@ -102,7 +114,7 @@ function extractColumns( ); const newColId = getManagedId(idPrefix, columns.length); newCol.customLabel = true; - newCol.label = newColId; + newCol.label = label; columns.push({ column: newCol, location: node.location }); // replace by new column id return newColId; @@ -121,7 +133,7 @@ function extractColumns( mathColumn.params.tinymathAst = consumedParam!; columns.push({ column: mathColumn }); mathColumn.customLabel = true; - mathColumn.label = getManagedId(idPrefix, columns.length - 1); + mathColumn.label = label; const mappedParams = getOperationParams(nodeOperation, namedArguments || []); const newCol = (nodeOperation as OperationDefinition< @@ -137,12 +149,13 @@ function extractColumns( ); const newColId = getManagedId(idPrefix, columns.length); newCol.customLabel = true; - newCol.label = newColId; + newCol.label = label; columns.push({ column: newCol, location: node.location }); // replace by new column id return newColId; } } + const root = parseNode(ast); if (root === undefined) { return []; @@ -154,9 +167,8 @@ function extractColumns( }); mathColumn.references = variables.map(({ value }) => value); mathColumn.params.tinymathAst = root!; - const newColId = getManagedId(idPrefix, columns.length); mathColumn.customLabel = true; - mathColumn.label = newColId; + mathColumn.label = label; columns.push({ column: mathColumn }); return columns; } @@ -174,7 +186,8 @@ export function regenerateLayerFromAst( layer, columnId, indexPattern, - filterByVisibleOperation(operationDefinitionMap) + filterByVisibleOperation(operationDefinitionMap), + currentColumn.customLabel ? currentColumn.label : undefined ); const columns = { ...layer.columns }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts index dd95ebdec5b8a..d29682eafa329 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -62,6 +62,9 @@ export function getOperationParams( if (operation.filterable && (name === 'kql' || name === 'lucene')) { args[name] = value; } + if (operation.shiftable && name === 'shift') { + args[name] = value; + } return args; }, {}); } @@ -170,7 +173,7 @@ ${'`multiply(sum(price), 1.2)`'} Divides the first number by the second number. Also works with ${'`/`'} symbol -Example: Calculate profit margin +Example: Calculate profit margin ${'`sum(profit) / sum(revenue)`'} Example: ${'`divide(sum(bytes), 2)`'} @@ -214,7 +217,7 @@ ${'`cbrt(last_value(volume))`'} help: ` Ceiling of value, rounds up. -Example: Round up price to the next dollar +Example: Round up price to the next dollar ${'`ceil(sum(price))`'} `, }, @@ -437,6 +440,7 @@ export function findMathNodes(root: TinymathAST | string): TinymathFunction[] { } return [node, ...node.args.flatMap(flattenMathNodes)].filter(Boolean); } + return flattenMathNodes(root); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts index 992b8ee2422e9..5b7a9beaa4e32 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts @@ -23,6 +23,7 @@ import { import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index'; import type { IndexPattern, IndexPatternLayer } from '../../../types'; import type { TinymathNodeTypes } from './types'; +import { parseTimeShift } from '../../../../../../../../src/plugins/data/common'; interface ValidationErrors { missingField: { message: string; type: { variablesLength: number; variablesList: string } }; @@ -63,6 +64,7 @@ interface ValidationErrors { type: {}; }; } + type ErrorTypes = keyof ValidationErrors; type ErrorValues = ValidationErrors[K]['type']; @@ -83,6 +85,7 @@ function findFunctionNodes(root: TinymathAST | string): TinymathFunction[] { } return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(Boolean); } + return flattenFunctionNodes(root); } @@ -410,6 +413,19 @@ 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], + }); + } + } }); return errors; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index c38475f85f47e..246959913c39e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -181,6 +181,7 @@ export interface ParamEditorProps { setIsCloseable: (isCloseable: boolean) => void; isFullscreen: boolean; columnId: string; + layerId: string; indexPattern: IndexPattern; uiSettings: IUiSettingsClient; storage: IStorageWrapper; @@ -381,7 +382,11 @@ interface FieldBasedOperationDefinition { field: IndexPatternField; previousColumn?: IndexPatternColumn; }, - columnParams?: (IndexPatternColumn & C)['params'] & { kql?: string; lucene?: string } + columnParams?: (IndexPatternColumn & C)['params'] & { + kql?: string; + lucene?: string; + shift?: string; + } ) => C; /** * This method will be called if the user changes the field of an operation. @@ -487,6 +492,7 @@ interface FullReferenceOperationDefinition { columnParams?: (ReferenceBasedIndexPatternColumn & C)['params'] & { kql?: string; lucene?: string; + shift?: string; } ) => ReferenceBasedIndexPatternColumn & C; /** diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 2ad91a7ba91a1..2db4d5e4b7742 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -33,6 +33,7 @@ const defaultProps = { isFullscreen: false, toggleFullscreen: jest.fn(), setIsCloseable: jest.fn(), + layerId: '1', }; describe('last_value', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index bfc5ce39bc939..4914ca0bd0f88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -175,7 +175,7 @@ export const lastValueOperation: OperationDefinition>({ scale: 'ratio', timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, filter: getFilter(previousColumn, columnParams), - timeShift: previousColumn?.timeShift, + timeShift: columnParams?.shift || previousColumn?.timeShift, params: getFormatFromPreviousColumn(previousColumn), } as T; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 0a3462ef20f3f..118405baebc8b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -35,6 +35,7 @@ const defaultProps = { isFullscreen: false, toggleFullscreen: jest.fn(), setIsCloseable: jest.fn(), + layerId: '1', }; describe('percentile', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index 39b876050c2ea..bdc4743e4f56d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -109,7 +109,7 @@ export const percentileOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 32b7dfee828fc..f5540732953ac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -38,6 +38,7 @@ const defaultProps = { isFullscreen: false, toggleFullscreen: jest.fn(), setIsCloseable: jest.fn(), + layerId: '1', }; describe('terms', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 1ae2f4421a0bc..34765217da664 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -117,7 +117,7 @@ describe('state_helpers', () => { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'formulaX2', + label: 'Part of moving_average(sum(bytes), window=5)', operationType: 'math' as const, params: { tinymathAst: 'formulaX2' }, references: ['formulaX2'], @@ -126,7 +126,7 @@ describe('state_helpers', () => { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'formulaX0', + label: 'Part of moving_average(sum(bytes), window=5)', operationType: 'sum' as const, scale: 'ratio' as const, sourceField: 'bytes', @@ -135,7 +135,7 @@ describe('state_helpers', () => { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'formulaX2', + label: 'Part of moving_average(sum(bytes), window=5)', operationType: 'moving_average' as const, params: { window: 5 }, references: ['formulaX0'], @@ -152,7 +152,7 @@ describe('state_helpers', () => { formulaX2: movingAvg, formulaX3: { ...math, - label: 'formulaX3', + label: 'Part of moving_average(sum(bytes), window=5)', references: ['formulaX2'], params: { tinymathAst: 'formulaX2' }, }, @@ -185,26 +185,24 @@ describe('state_helpers', () => { formulaX2: movingAvg, formulaX3: { ...math, - label: 'formulaX3', references: ['formulaX2'], params: { tinymathAst: 'formulaX2' }, }, copy: expect.objectContaining({ ...source, references: ['copyX3'] }), - copyX0: expect.objectContaining({ ...sum, label: 'copyX0' }), + copyX0: expect.objectContaining({ + ...sum, + }), copyX1: expect.objectContaining({ ...math, - label: 'copyX1', references: ['copyX0'], params: { tinymathAst: 'copyX0' }, }), copyX2: expect.objectContaining({ ...movingAvg, - label: 'copyX2', references: ['copyX1'], }), copyX3: expect.objectContaining({ ...math, - label: 'copyX3', references: ['copyX2'], params: { tinymathAst: 'copyX2' }, }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx new file mode 100644 index 0000000000000..14ba6b9189e6b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { uniq } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + IndexPattern, + IndexPatternColumn, + IndexPatternLayer, + IndexPatternPrivateState, +} from './types'; +import { Datatable } from '../../../../../src/plugins/expressions'; +import { search } from '../../../../../src/plugins/data/public'; +import { parseTimeShift } from '../../../../../src/plugins/data/common'; +import { FramePublicAPI } from '../types'; + +export const timeShiftOptions = [ + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', { + defaultMessage: '1 hour (1h)', + }), + value: '1h', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', { + defaultMessage: '3 hours (3h)', + }), + value: '3h', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', { + defaultMessage: '6 hours (6h)', + }), + value: '6h', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', { + defaultMessage: '12 hours (12h)', + }), + value: '12h', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.day', { + defaultMessage: '1 day (1d)', + }), + value: '1d', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.week', { + defaultMessage: '1 week (1w)', + }), + value: '1w', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.month', { + defaultMessage: '1 month (1M)', + }), + value: '1M', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', { + defaultMessage: '3 months (3M)', + }), + value: '3M', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', { + defaultMessage: '6 months (6M)', + }), + value: '6M', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.year', { + defaultMessage: '1 year (1y)', + }), + value: '1y', + }, + { + label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { + defaultMessage: 'Previous', + }), + value: 'previous', + }, +]; + +export const timeShiftOptionOrder = timeShiftOptions.reduce<{ [key: string]: number }>( + (optionMap, { value }, index) => ({ + ...optionMap, + [value]: index, + }), + {} +); + +export function getDateHistogramInterval( + layer: IndexPatternLayer, + indexPattern: IndexPattern, + activeData: Record | undefined, + layerId: string +) { + const dateHistogramColumn = layer.columnOrder.find( + (colId) => layer.columns[colId].operationType === 'date_histogram' + ); + if (!dateHistogramColumn && !indexPattern.timeFieldName) { + return { canShift: false }; + } + if (dateHistogramColumn && activeData && activeData[layerId] && activeData[layerId]) { + const column = activeData[layerId].columns.find((col) => col.id === dateHistogramColumn); + if (column) { + const expression = + search.aggs.getDateHistogramMetaDataByDatatableColumn(column)?.interval || ''; + return { + interval: search.aggs.parseInterval(expression), + expression, + canShift: true, + }; + } + } + return { canShift: true }; +} + +export function getLayerTimeShiftChecks({ + interval: dateHistogramInterval, + canShift, +}: ReturnType) { + return { + canShift, + isValueTooSmall: (parsedValue: ReturnType) => { + return ( + dateHistogramInterval && + parsedValue && + typeof parsedValue === 'object' && + parsedValue.asMilliseconds() < dateHistogramInterval.asMilliseconds() + ); + }, + isValueNotMultiple: (parsedValue: ReturnType) => { + return ( + dateHistogramInterval && + parsedValue && + typeof parsedValue === 'object' && + !Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval.asMilliseconds()) + ); + }, + }; +} + +export function getStateTimeShiftWarningMessages( + state: IndexPatternPrivateState, + { activeData }: FramePublicAPI +) { + if (!state) return; + const warningMessages: React.ReactNode[] = []; + Object.entries(state.layers).forEach(([layerId, layer]) => { + const dateHistogramInterval = getDateHistogramInterval( + layer, + state.indexPatterns[layer.indexPatternId], + activeData, + layerId + ); + if (!dateHistogramInterval.interval) { + return; + } + const dateHistogramIntervalExpression = dateHistogramInterval.expression; + const shiftInterval = dateHistogramInterval.interval.asMilliseconds(); + let timeShifts: number[] = []; + const timeShiftMap: Record = {}; + Object.entries(layer.columns).forEach(([columnId, column]) => { + if (column.isBucketed) return; + let duration: number = 0; + if (column.timeShift) { + const parsedTimeShift = parseTimeShift(column.timeShift); + if (parsedTimeShift === 'previous' || parsedTimeShift === 'invalid') { + return; + } + duration = parsedTimeShift.asMilliseconds(); + } + timeShifts.push(duration); + if (!timeShiftMap[duration]) { + timeShiftMap[duration] = []; + } + timeShiftMap[duration].push(columnId); + }); + timeShifts = uniq(timeShifts); + + if (timeShifts.length < 2) { + return; + } + + timeShifts.forEach((timeShift) => { + if (timeShift === 0) return; + if (timeShift < shiftInterval) { + timeShiftMap[timeShift].forEach((columnId) => { + warningMessages.push( + {layer.columns[columnId].label}, + interval: {dateHistogramIntervalExpression}, + columnTimeShift: {layer.columns[columnId].timeShift}, + }} + /> + ); + }); + } else if (!Number.isInteger(timeShift / shiftInterval)) { + timeShiftMap[timeShift].forEach((columnId) => { + warningMessages.push( + {layer.columns[columnId].label}, + interval: {dateHistogramIntervalExpression}, + columnTimeShift: {layer.columns[columnId].timeShift!}, + }} + /> + ); + }); + } + }); + }); + return warningMessages; +} + +export function getColumnTimeShiftWarnings( + dateHistogramInterval: ReturnType, + column: IndexPatternColumn +) { + const { isValueTooSmall, isValueNotMultiple } = getLayerTimeShiftChecks(dateHistogramInterval); + + const warnings: string[] = []; + + const parsedLocalValue = column.timeShift && parseTimeShift(column.timeShift); + const localValueTooSmall = parsedLocalValue && isValueTooSmall(parsedLocalValue); + const localValueNotMultiple = parsedLocalValue && isValueNotMultiple(parsedLocalValue); + if (localValueTooSmall) { + warnings.push( + 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', + }) + ); + } else if (localValueNotMultiple) { + warnings.push( + 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', + }) + ); + } + return warnings; +}