diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index c1e026064fdfb..0adb06d91d268 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -202,9 +202,6 @@ export const HeatmapComponent: FC = memo( const cell = e[0][0]; const { x, y } = cell.datum; - const xAxisFieldName = xAxisColumn?.meta?.field; - const timeFieldName = isTimeBasedSwimLane ? xAxisFieldName : ''; - const points = [ { row: table.rows.findIndex((r) => r[xAxisColumn.id] === x), @@ -229,35 +226,21 @@ export const HeatmapComponent: FC = memo( value: point.value, table, })), - timeFieldName, }; onClickValue(context); }, - [ - isTimeBasedSwimLane, - onClickValue, - table, - xAxisColumn?.id, - xAxisColumn?.meta?.field, - xAxisColumnIndex, - yAxisColumn, - yAxisColumnIndex, - ] + [onClickValue, table, xAxisColumn?.id, xAxisColumnIndex, yAxisColumn, yAxisColumnIndex] ); const onBrushEnd = useCallback( (e: HeatmapBrushEvent) => { const { x, y } = e; - const xAxisFieldName = xAxisColumn?.meta?.field; - const timeFieldName = isTimeBasedSwimLane ? xAxisFieldName : ''; - if (isTimeBasedSwimLane) { const context: BrushEvent['data'] = { range: x as number[], table, column: xAxisColumnIndex, - timeFieldName, }; onSelectRange(context); } else { @@ -289,7 +272,6 @@ export const HeatmapComponent: FC = memo( value: point.value, table, })), - timeFieldName, }; onClickValue(context); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts index bcce2fa2f6f69..515f5a40fd58a 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts @@ -82,7 +82,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: 'a', }); }); @@ -102,7 +101,6 @@ describe('Table actions', () => { }, ], negate: true, - timeFieldName: 'a', }); }); @@ -122,7 +120,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: 'a', }); }); @@ -142,7 +139,6 @@ describe('Table actions', () => { }, ], negate: true, - timeFieldName: undefined, }); }); }); @@ -173,7 +169,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: 'a', }); }); @@ -202,7 +197,6 @@ describe('Table actions', () => { }, ], negate: true, - timeFieldName: undefined, }); }); @@ -274,7 +268,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: undefined, }); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts index 3c1297e864553..c37ab22002c1c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -75,10 +75,6 @@ export const createGridFilterHandler = onClickValue: (data: LensFilterEvent['data']) => void ) => (field: string, value: unknown, colIndex: number, rowIndex: number, negate: boolean = false) => { - const col = tableRef.current.columns[colIndex]; - const isDate = col.meta?.type === 'date'; - const timeFieldName = negate && isDate ? undefined : col?.meta?.field; - const data: LensFilterEvent['data'] = { negate, data: [ @@ -89,7 +85,6 @@ export const createGridFilterHandler = table: tableRef.current, }, ], - timeFieldName, }; onClickValue(data); @@ -106,11 +101,6 @@ export const createTransposeColumnFilterHandler = ) => { if (!untransposedDataRef.current) return; const originalTable = Object.values(untransposedDataRef.current.tables)[0]; - const timeField = bucketValues.find( - ({ originalBucketColumn }) => originalBucketColumn.meta.type === 'date' - )?.originalBucketColumn; - const isDate = Boolean(timeField); - const timeFieldName = negate && isDate ? undefined : timeField?.meta?.field; const data: LensFilterEvent['data'] = { negate, @@ -126,7 +116,6 @@ export const createTransposeColumnFilterHandler = table: originalTable, }; }), - timeFieldName, }; onClickValue(data); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 8c75ee9efcc6b..6cab22cd08ccd 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -210,7 +210,6 @@ describe('DatatableComponent', () => { }, ], negate: true, - timeFieldName: 'a', }, }); }); @@ -256,7 +255,6 @@ describe('DatatableComponent', () => { }, ], negate: false, - timeFieldName: 'b', }, }); }); @@ -341,7 +339,6 @@ describe('DatatableComponent', () => { }, ], negate: false, - timeFieldName: 'a', }, }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 5d475be7bb83f..ccd9e8aace2ab 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -204,11 +204,11 @@ describe('workspace_panel', () => { const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; - const eventData = {}; + const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 }; onEvent({ name: 'brush', data: eventData }); expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); - expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); + expect(trigger.exec).toHaveBeenCalledWith({ data: { ...eventData, timeFieldName: undefined } }); }); it('should push add current data table to state on data$ emitting value', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 3554f77047577..a26d72f1b4fc2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -68,6 +68,7 @@ import { selectSearchSessionId, } from '../../../state_management'; import type { LensInspector } from '../../../lens_inspector_service'; +import { inferTimeField } from '../../../utils'; export interface WorkspacePanelProps { visualizationMap: VisualizationMap; @@ -250,12 +251,18 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ } if (isLensBrushEvent(event)) { plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: inferTimeField(event.data), + }, }); } if (isLensFilterEvent(event)) { plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: inferTimeField(event.data), + }, }); } if (isLensEditEvent(event) && activeVisualization?.onEditAction) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 5ae3cb571bdbb..c1c86367ee211 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -821,12 +821,15 @@ describe('embeddable', () => { const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; - const eventData = {}; + const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 }; onEvent({ name: 'brush', data: eventData }); expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); expect(trigger.exec).toHaveBeenCalledWith( - expect.objectContaining({ data: eventData, embeddable: expect.anything() }) + expect.objectContaining({ + data: { ...eventData, timeFieldName: undefined }, + embeddable: expect.anything(), + }) ); }); @@ -1006,7 +1009,10 @@ describe('embeddable', () => { expressionRenderer = jest.fn(({ onEvent }) => { setTimeout(() => { - onEvent?.({ name: 'filter', data: { pings: false } }); + onEvent?.({ + name: 'filter', + data: { pings: false, table: { rows: [], columns: [] }, column: 0 }, + }); }, 10); return null; @@ -1048,7 +1054,7 @@ describe('embeddable', () => { await new Promise((resolve) => setTimeout(resolve, 20)); - expect(onFilter).toHaveBeenCalledWith({ pings: false }); + expect(onFilter).toHaveBeenCalledWith(expect.objectContaining({ pings: false })); expect(onFilter).toHaveBeenCalledTimes(1); }); @@ -1057,7 +1063,10 @@ describe('embeddable', () => { expressionRenderer = jest.fn(({ onEvent }) => { setTimeout(() => { - onEvent?.({ name: 'brush', data: { range: [0, 1] } }); + onEvent?.({ + name: 'brush', + data: { range: [0, 1], table: { rows: [], columns: [] }, column: 0 }, + }); }, 10); return null; @@ -1099,7 +1108,7 @@ describe('embeddable', () => { await new Promise((resolve) => setTimeout(resolve, 20)); - expect(onBrushEnd).toHaveBeenCalledWith({ range: [0, 1] }); + expect(onBrushEnd).toHaveBeenCalledWith(expect.objectContaining({ range: [0, 1] })); expect(onBrushEnd).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 712e9f9f7f476..aa0a9de248c1b 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; import { Filter } from '@kbn/es-query'; -import type { +import { ExecutionContextSearch, Query, TimefilterContract, @@ -70,6 +70,7 @@ import type { ErrorMessage } from '../editor_frame_service/types'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; import { SharingSavedObjectProps } from '../types'; import type { SpacesPluginStart } from '../../../spaces/public'; +import { inferTimeField } from '../utils'; export type LensSavedObjectAttributes = Omit; @@ -529,7 +530,10 @@ export class Embeddable } if (isLensBrushEvent(event)) { this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: event.data.timeFieldName || inferTimeField(event.data), + }, embeddable: this, }); @@ -539,7 +543,10 @@ export class Embeddable } if (isLensFilterEvent(event)) { this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: event.data.timeFieldName || inferTimeField(event.data), + }, embeddable: this, }); if (this.input.onFilter) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 404c31010278b..2c74f8468e52b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -491,13 +491,13 @@ describe('IndexPattern Data Source', () => { `); }); - it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { + it('should put all time fields used in date_histograms to the esaggs timeFields parameter if not ignoring global time range', async () => { const queryBaseState: DataViewBaseState = { currentIndexPatternId: '1', layers: { first: { indexPatternId: '1', - columnOrder: ['col1', 'col2', 'col3'], + columnOrder: ['col1', 'col2', 'col3', 'col4'], columns: { col1: { label: 'Count of records', @@ -526,6 +526,17 @@ describe('IndexPattern Data Source', () => { interval: 'auto', }, } as DateHistogramIndexPatternColumn, + col4: { + label: 'Date 3', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'yet_another_datefield', + params: { + interval: '2d', + ignoreTimeRange: true, + }, + } as DateHistogramIndexPatternColumn, }, }, }, @@ -1633,6 +1644,63 @@ describe('IndexPattern Data Source', () => { }); expect(indexPatternDatasource.isTimeBased(state)).toEqual(true); }); + it('should return false if date histogram exists but is detached from global time range in every layer', () => { + const state = enrichBaseState({ + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records2', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + }, + }, + second: { + indexPatternId: '1', + columnOrder: ['bucket1', 'bucket2', 'metric2'], + columns: { + metric2: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + bucket1: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + ignoreTimeRange: true, + }, + } as DateHistogramIndexPatternColumn, + bucket2: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'geo.src', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 10, + }, + } as TermsIndexPatternColumn, + }, + }, + }, + }); + expect(indexPatternDatasource.isTimeBased(state)).toEqual(false); + }); it('should return false if date histogram does not exist in any layer', () => { const state = enrichBaseState({ currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2a44550af2b58..0ac77696d5987 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -48,7 +48,12 @@ import { import { getVisualDefaultsForLayer, isColumnInvalid } from './utils'; import { normalizeOperationDataType, isDraggedField } from './pure_utils'; import { LayerPanel } from './layerpanel'; -import { GenericIndexPatternColumn, getErrorMessages, insertNewColumn } from './operations'; +import { + DateHistogramIndexPatternColumn, + GenericIndexPatternColumn, + getErrorMessages, + insertNewColumn, +} from './operations'; import { IndexPatternField, IndexPatternPrivateState, @@ -70,6 +75,7 @@ import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/wor import { DraggingIdentifier } from '../drag_drop'; import { getStateTimeShiftWarningMessages } from './time_shift_utils'; import { getPrecisionErrorWarningMessages } from './utils'; +import { isColumnOfType } from './operations/definitions/helpers'; export type { OperationType, GenericIndexPatternColumn } from './operations'; export { deleteColumn } from './operations'; @@ -561,7 +567,13 @@ export function getIndexPatternDatasource({ Boolean(layers) && Object.values(layers).some((layer) => { const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed); - return buckets.some((colId) => layer.columns[colId].operationType === 'date_histogram'); + return buckets.some((colId) => { + const column = layer.columns[colId]; + return ( + isColumnOfType('date_histogram', column) && + !column.params.ignoreTimeRange + ); + }); }) ); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 26cbd2a990978..beca7cfa4c39f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -423,6 +423,92 @@ describe('date_histogram', () => { expect(newLayer).toHaveProperty('columns.col1.params.interval', '30d'); }); + it('should allow turning off time range sync', () => { + const thirdLayer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1h', + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + }, + }; + + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + instance + .find(EuiSwitch) + .at(1) + .simulate('change', { + target: { checked: false }, + }); + expect(updateLayerSpy).toHaveBeenCalled(); + const newLayer = updateLayerSpy.mock.calls[0][0]; + expect(newLayer).toHaveProperty('columns.col1.params.ignoreTimeRange', true); + }); + + it('turns off time range ignore on switching to auto interval', () => { + const thirdLayer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1h', + ignoreTimeRange: true, + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + }, + }; + + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + instance + .find(EuiSwitch) + .at(0) + .simulate('change', { + target: { checked: false }, + }); + expect(updateLayerSpy).toHaveBeenCalled(); + const newLayer = updateLayerSpy.mock.calls[0][0]; + expect(newLayer).toHaveProperty('columns.col1.params.ignoreTimeRange', false); + expect(newLayer).toHaveProperty('columns.col1.params.interval', 'auto'); + }); + it('should force calendar values to 1', () => { const updateLayerSpy = jest.fn(); const instance = shallow( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index ea43766464cf5..e269778b5ad53 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -16,6 +16,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiIconTip, EuiSelect, EuiSpacer, EuiSwitch, @@ -46,6 +47,7 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC operationType: 'date_histogram'; params: { interval: string; + ignoreTimeRange?: boolean; }; } @@ -189,7 +191,14 @@ export const dateHistogramOperation: OperationDefinition< const value = ev.target.checked ? data.search.aggs.calculateAutoTimeExpression({ from: fromDate, to: toDate }) || '1h' : autoInterval; - updateLayer(updateColumnParam({ layer, columnId, paramName: 'interval', value })); + updateLayer( + updateColumnParam({ + layer: updateColumnParam({ layer, columnId, paramName: 'interval', value }), + columnId, + paramName: 'ignoreTimeRange', + value: false, + }) + ); } const setInterval = (newInterval: typeof interval) => { @@ -214,128 +223,176 @@ export const dateHistogramOperation: OperationDefinition< )} {currentColumn.params.interval !== autoInterval && ( - - {intervalIsRestricted ? ( - - ) : ( - <> - - - + + {intervalIsRestricted ? ( + + ) : ( + <> + + + { + const newInterval = { + ...interval, + value: e.target.value, + }; + setInterval(newInterval); + }} + step={1} + /> + + + { + const newInterval = { + ...interval, + unit: e.target.value, + }; + setInterval(newInterval); + }} + isInvalid={!isValid} + options={[ + { + value: 'ms', + text: i18n.translate( + 'xpack.lens.indexPattern.dateHistogram.milliseconds', + { + defaultMessage: 'milliseconds', + } + ), + }, + { + value: 's', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.seconds', { + defaultMessage: 'seconds', + }), + }, + { + value: 'm', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.minutes', { + defaultMessage: 'minutes', + }), + }, + { + value: 'h', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.hours', { + defaultMessage: 'hours', + }), + }, + { + value: 'd', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.days', { + defaultMessage: 'days', + }), + }, + { + value: 'w', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.week', { + defaultMessage: 'week', + }), + }, + { + value: 'M', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.month', { + defaultMessage: 'month', + }), + }, + // Quarterly intervals appear to be unsupported by esaggs + { + value: 'y', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.year', { + defaultMessage: 'year', + }), + }, + ]} + /> + + + {!isValid && ( + <> + + + {i18n.translate('xpack.lens.indexPattern.invalidInterval', { + defaultMessage: 'Invalid interval value', + })} + + + )} + + )} + + + + {i18n.translate( + 'xpack.lens.indexPattern.dateHistogram.bindToGlobalTimePicker', + { + defaultMessage: 'Bind to global time picker', } - disabled={calendarOnlyIntervals.has(interval.unit)} - isInvalid={!isValid} - onChange={(e) => { - const newInterval = { - ...interval, - value: e.target.value, - }; - setInterval(newInterval); - }} - step={1} - /> - - - { - const newInterval = { - ...interval, - unit: e.target.value, - }; - setInterval(newInterval); - }} - isInvalid={!isValid} - options={[ - { - value: 'ms', - text: i18n.translate( - 'xpack.lens.indexPattern.dateHistogram.milliseconds', - { - defaultMessage: 'milliseconds', - } - ), - }, - { - value: 's', - text: i18n.translate('xpack.lens.indexPattern.dateHistogram.seconds', { - defaultMessage: 'seconds', - }), - }, + )}{' '} + - - - {!isValid && ( - <> - - - {i18n.translate('xpack.lens.indexPattern.invalidInterval', { - defaultMessage: 'Invalid interval value', - })} - - )} - - )} - + } + disabled={indexPattern.timeFieldName === field?.name} + checked={ + indexPattern.timeFieldName === field?.name || + !currentColumn.params.ignoreTimeRange + } + onChange={() => { + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'ignoreTimeRange', + value: !currentColumn.params.ignoreTimeRange, + }) + ); + }} + compressed + /> + + )} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 0d8b57a5502ad..f9fe8701949e1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -263,7 +263,8 @@ function getExpressionForLayer( const allDateHistogramFields = Object.values(columns) .map((column) => - isColumnOfType('date_histogram', column) + isColumnOfType('date_histogram', column) && + !column.params.ignoreTimeRange ? column.sourceField : null ) diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts new file mode 100644 index 0000000000000..857f30e692305 --- /dev/null +++ b/x-pack/plugins/lens/public/utils.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { Datatable } from 'src/plugins/expressions/public'; +import { inferTimeField } from './utils'; + +const table: Datatable = { + type: 'datatable', + rows: [], + columns: [ + { + id: '1', + name: '', + meta: { + type: 'date', + field: 'abc', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2021-01-01', + to: '2022-01-01', + }, + }, + }, + }, + ], +}; + +const tableWithoutAppliedTimeRange = { + ...table, + columns: [ + { + ...table.columns[0], + meta: { + ...table.columns[0].meta, + sourceParams: { + ...table.columns[0].meta.sourceParams, + appliedTimeRange: undefined, + }, + }, + }, + ], +}; + +describe('utils', () => { + describe('inferTimeField', () => { + test('infer time field for brush event', () => { + expect( + inferTimeField({ + table, + column: 0, + range: [1, 2], + }) + ).toEqual('abc'); + }); + + test('do not return time field if time range is not bound', () => { + expect( + inferTimeField({ + table: tableWithoutAppliedTimeRange, + column: 0, + range: [1, 2], + }) + ).toEqual(undefined); + }); + + test('infer time field for click event', () => { + expect( + inferTimeField({ + data: [ + { + table, + column: 0, + row: 0, + value: 1, + }, + ], + }) + ).toEqual('abc'); + }); + + test('do not return time field for negated click event', () => { + expect( + inferTimeField({ + data: [ + { + table, + column: 0, + row: 0, + value: 1, + }, + ], + negate: true, + }) + ).toEqual(undefined); + }); + + test('do not return time field for click event without bound time field', () => { + expect( + inferTimeField({ + data: [ + { + table: tableWithoutAppliedTimeRange, + column: 0, + row: 0, + value: 1, + }, + ], + }) + ).toEqual(undefined); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 921cc8fb364a2..f71f7a128934a 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -16,7 +16,14 @@ import type { import type { IUiSettingsClient } from 'kibana/public'; import type { SavedObjectReference } from 'kibana/public'; import type { Document } from './persistence/saved_object_store'; -import type { Datasource, DatasourceMap, Visualization } from './types'; +import type { + Datasource, + DatasourceMap, + LensBrushEvent, + LensFilterEvent, + Visualization, +} from './types'; +import { search } from '../../../../src/plugins/data/public'; import type { DatasourceStates, VisualizationState } from './state_management'; export function getVisualizeGeoFieldMessage(fieldType: string) { @@ -107,3 +114,24 @@ export function getRemoveOperation( // fallback to generic count check return layerCount === 1 ? 'clear' : 'remove'; } + +export function inferTimeField(context: LensBrushEvent['data'] | LensFilterEvent['data']) { + const tablesAndColumns = + 'table' in context + ? [{ table: context.table, column: context.column }] + : !context.negate + ? context.data + : // if it's a negated filter, never respect bound time field + []; + return tablesAndColumns + .map(({ table, column }) => { + const tableColumn = table.columns[column]; + const hasTimeRange = Boolean( + tableColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(tableColumn)?.timeRange + ); + if (hasTimeRange) { + return tableColumn.meta.field; + } + }) + .find(Boolean); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index bc57547bc0ee6..6bee021b36de6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -134,6 +134,10 @@ const dateHistogramData: LensMultiTable = { sourceParams: { indexPatternId: 'indexPatternId', type: 'date_histogram', + appliedTimeRange: { + from: '2020-04-01T16:14:16.246Z', + to: '2020-04-01T17:15:41.263Z', + }, params: { field: 'order_date', timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, @@ -582,9 +586,29 @@ describe('xy_expression', () => { {...defaultProps} data={{ ...data, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), + tables: { + first: { + ...data.tables.first, + columns: data.tables.first.columns.map((c) => + c.id !== 'c' + ? c + : { + ...c, + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2019-01-02T05:00:00.000Z', + to: '2019-01-03T05:00:00.000Z', + }, + }, + }, + } + ), + }, }, }} args={{ @@ -612,25 +636,13 @@ describe('xy_expression', () => { }, }; - const component = shallow( - - ); + const component = shallow(); // real auto interval is 30mins = 1800000 expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` Object { - "max": 1546491600000, - "min": 1546405200000, + "max": NaN, + "min": NaN, "minInterval": 50, } `); @@ -749,14 +761,36 @@ describe('xy_expression', () => { }); describe('endzones', () => { const { args } = sampleArgs(); + const table = createSampleDatatableWithRows([ + { a: 1, b: 2, c: new Date('2021-04-22').valueOf(), d: 'Foo' }, + { a: 1, b: 2, c: new Date('2021-04-23').valueOf(), d: 'Foo' }, + { a: 1, b: 2, c: new Date('2021-04-24').valueOf(), d: 'Foo' }, + ]); const data: LensMultiTable = { type: 'lens_multitable', tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: new Date('2021-04-22').valueOf(), d: 'Foo' }, - { a: 1, b: 2, c: new Date('2021-04-23').valueOf(), d: 'Foo' }, - { a: 1, b: 2, c: new Date('2021-04-24').valueOf(), d: 'Foo' }, - ]), + first: { + ...table, + columns: table.columns.map((c) => + c.id !== 'c' + ? c + : { + ...c, + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2021-04-22T12:00:00.000Z', + to: '2021-04-24T12:00:00.000Z', + }, + }, + }, + } + ), + }, }, dateRange: { // first and last bucket are partial @@ -1187,7 +1221,6 @@ describe('xy_expression', () => { column: 0, table: dateHistogramData.tables.timeLayer, range: [1585757732783, 1585758880838], - timeFieldName: 'order_date', }); }); @@ -1267,7 +1300,6 @@ describe('xy_expression', () => { column: 0, table: numberHistogramData.tables.numberLayer, range: [5, 8], - timeFieldName: undefined, }); }); @@ -1398,7 +1430,6 @@ describe('xy_expression', () => { value: 1585758120000, }, ], - timeFieldName: 'order_date', }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 9594d8920515b..ea0e336ff2f08 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -106,7 +106,7 @@ export type XYChartRenderProps = XYChartProps & { export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) return; - const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); + const isTimeViz = filteredLayers.every((l) => l.xScaleType === 'time'); const xColumn = data.tables[filteredLayers[0].layerId].columns.find( (column) => column.id === filteredLayers[0].xAccessor ); @@ -315,7 +315,7 @@ export function XYChart({ filteredBarLayers.some((layer) => layer.accessors.length > 1) || filteredBarLayers.some((layer) => layer.splitAccessor); - const isTimeViz = Boolean(data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time')); + const isTimeViz = Boolean(filteredLayers.every((l) => l.xScaleType === 'time')); const isHistogramViz = filteredLayers.every((l) => l.isHistogram); const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( @@ -512,10 +512,6 @@ export function XYChart({ value: pointValue, }); } - const currentColumnMeta = table.columns.find((el) => el.id === layer.xAccessor)?.meta; - const xAxisFieldName = currentColumnMeta?.field; - const isDateField = currentColumnMeta?.type === 'date'; - const context: LensFilterEvent['data'] = { data: points.map((point) => ({ row: point.row, @@ -523,7 +519,6 @@ export function XYChart({ value: point.value, table, })), - timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined, }; onClickValue(context); }; @@ -541,13 +536,10 @@ export function XYChart({ const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor); - const timeFieldName = isTimeViz ? table.columns[xAxisColumnIndex]?.meta?.field : undefined; - const context: LensBrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex, - timeFieldName, }; onSelectRange(context); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx b/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx index d5eb8ac1e92ba..81037418a8143 100644 --- a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx @@ -7,9 +7,11 @@ import { uniq } from 'lodash'; import React from 'react'; +import moment from 'moment'; import { Endzones } from '../../../../../src/plugins/charts/public'; import type { LensMultiTable } from '../../common'; import type { LayerArgs } from '../../common/expressions'; +import { search } from '../../../../../src/plugins/data/public'; export interface XDomain { min?: number; @@ -17,6 +19,23 @@ export interface XDomain { minInterval?: number; } +export const getAppliedTimeRange = (layers: LayerArgs[], data: LensMultiTable) => { + return Object.entries(data.tables) + .map(([tableId, table]) => { + const layer = layers.find((l) => l.layerId === tableId); + const xColumn = table.columns.find((col) => col.id === layer?.xAccessor); + const timeRange = + xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange; + if (timeRange) { + return { + timeRange, + field: xColumn.meta.field, + }; + } + }) + .find(Boolean); +}; + export const getXDomain = ( layers: LayerArgs[], data: LensMultiTable, @@ -24,10 +43,13 @@ export const getXDomain = ( isTimeViz: boolean, isHistogram: boolean ) => { + const appliedTimeRange = getAppliedTimeRange(layers, data)?.timeRange; + const from = appliedTimeRange?.from; + const to = appliedTimeRange?.to; const baseDomain = isTimeViz ? { - min: data.dateRange?.fromDate.getTime() ?? NaN, - max: data.dateRange?.toDate.getTime() ?? NaN, + min: from ? moment(from).valueOf() : NaN, + max: to ? moment(to).valueOf() : NaN, minInterval, } : isHistogram