diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts index c6452cf46e0c0..6721262d265f4 100644 --- a/src/legacy/ui/public/agg_types/utils.ts +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -49,7 +49,7 @@ function isValidJson(value: string): boolean { } } -function isValidInterval(value: string, baseInterval: string) { +function isValidInterval(value: string, baseInterval?: string) { if (baseInterval) { return _parseWithBase(value, baseInterval); } else { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index fb3f8774be92a..c9d44b315abd9 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -807,30 +807,38 @@ describe('editor_frame', () => { await waitForPromises(); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( - datasource1State, - expect.anything(), - 'first' + expect.objectContaining({ + state: datasource1State, + setState: expect.anything(), + layerId: 'first', + }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( - datasource2State, - expect.anything(), - 'second' + expect.objectContaining({ + state: datasource2State, + setState: expect.anything(), + layerId: 'second', + }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( - datasource2State, - expect.anything(), - 'third' + expect.objectContaining({ + state: datasource2State, + setState: expect.anything(), + layerId: 'third', + }) ); }); it('should give access to the datasource state in the datasource factory function', async () => { const datasourceState = {}; + const dateRange = { fromDate: 'now-1w', toDate: 'now' }; mockDatasource.initialize.mockResolvedValue(datasourceState); mockDatasource.getLayers.mockReturnValue(['first']); mount( { await waitForPromises(); - expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( - datasourceState, - expect.any(Function), - 'first' - ); + expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ + dateRange, + state: datasourceState, + setState: expect.any(Function), + layerId: 'first', + }); }); it('should re-create the public api after state has been set', async () => { @@ -872,15 +881,17 @@ describe('editor_frame', () => { await waitForPromises(); const updatedState = {}; - const setDatasourceState = mockDatasource.getPublicAPI.mock.calls[0][1]; + const setDatasourceState = mockDatasource.getPublicAPI.mock.calls[0][0].setState; act(() => { setDatasourceState(updatedState); }); expect(mockDatasource.getPublicAPI).toHaveBeenLastCalledWith( - updatedState, - expect.any(Function), - 'first' + expect.objectContaining({ + state: updatedState, + setState: expect.any(Function), + layerId: 'first', + }) ); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 04c0b22c378d7..8e89d8edc9f23 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -89,9 +89,9 @@ export function EditorFrame(props: EditorFrameProps) { const layers = datasource.getLayers(datasourceState); layers.forEach(layer => { - const publicAPI = props.datasourceMap[id].getPublicAPI( - datasourceState, - (newState: unknown) => { + const publicAPI = props.datasourceMap[id].getPublicAPI({ + state: datasourceState, + setState: (newState: unknown) => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', datasourceId: id, @@ -99,8 +99,9 @@ export function EditorFrame(props: EditorFrameProps) { clearStagedPreview: true, }); }, - layer - ); + layerId: layer, + dateRange: props.dateRange, + }); datasourceLayers[layer] = publicAPI; }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index 6aee215d11591..e29ac84486d0b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -367,7 +367,12 @@ function getPreviewExpression( const changedLayers = datasource.getLayers(visualizableState.datasourceState); changedLayers.forEach(layerId => { if (updatedLayerApis[layerId]) { - updatedLayerApis[layerId] = datasource.getPublicAPI(datasourceState, () => {}, layerId); + updatedLayerApis[layerId] = datasource.getPublicAPI({ + layerId, + dateRange: frame.dateRange, + state: datasourceState, + setState: () => {}, + }); } }); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index f349585ce88a4..c3e91c9debcd0 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -53,7 +53,7 @@ export function createMockDatasource(): DatasourceMock { getDatasourceSuggestionsForField: jest.fn((_state, item) => []), getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []), getPersistableState: jest.fn(), - getPublicAPI: jest.fn((_state, _setState, _layerId) => publicAPIMock), + getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts index 359c6d7c35c3a..566e5bece096d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts @@ -10,25 +10,38 @@ import { ExpressionFunction, KibanaContext, } from '../../../../../../src/plugins/expressions/common'; +import { DateRange } from '../../common'; interface LensAutoDateProps { aggConfigs: string; } -export function getAutoInterval(ctx?: KibanaContext | null) { - if (!ctx || !ctx.timeRange) { - return; +export function autoIntervalFromDateRange(dateRange?: DateRange, defaultValue: string = '1h') { + if (!dateRange) { + return defaultValue; } - const { timeRange } = ctx; const buckets = new TimeBuckets(); buckets.setInterval('auto'); buckets.setBounds({ - min: dateMath.parse(timeRange.from), - max: dateMath.parse(timeRange.to, { roundUp: true }), + min: dateMath.parse(dateRange.fromDate), + max: dateMath.parse(dateRange.toDate, { roundUp: true }), }); - return buckets.getInterval(); + return buckets.getInterval().expression; +} + +function autoIntervalFromContext(ctx?: KibanaContext | null) { + if (!ctx || !ctx.timeRange) { + return; + } + + const { timeRange } = ctx; + + return autoIntervalFromDateRange({ + fromDate: timeRange.from, + toDate: timeRange.to, + }); } /** @@ -56,7 +69,7 @@ export const autoDate: ExpressionFunction< }, }, fn(ctx: KibanaContext, args: LensAutoDateProps) { - const interval = getAutoInterval(ctx); + const interval = autoIntervalFromContext(ctx); if (!interval) { return args.aggConfigs; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index a6ccc6361c3d0..81e0a214f93e3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -129,6 +129,7 @@ describe('IndexPatternDimensionPanel', () => { dragDropContext, state, setState, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, columnId: 'col1', layerId: 'first', uniqueLabel: 'stuff', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index a1242947a87d3..fd4acef41f0bf 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -23,6 +23,7 @@ import { changeColumn, deleteColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { DateRange } from '../../../common'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; @@ -34,6 +35,7 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { layerId: string; http: HttpServiceBase; uniqueLabel: string; + dateRange: DateRange; }; export interface OperationFieldSupportMatrix { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index 2a0fcfc214e40..ce30e1195f4ca 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -368,6 +368,7 @@ export function PopoverEditor(props: PopoverEditorProps) { savedObjectsClient={props.savedObjectsClient} layerId={layerId} http={props.http} + dateRange={props.dateRange} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index 2edf9dd0b5acb..9fb7be01642e5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -414,7 +414,15 @@ describe('IndexPattern Data Source', () => { beforeEach(async () => { const initialState = stateFromPersistedState(persistedState); - publicAPI = indexPatternDatasource.getPublicAPI(initialState, () => {}, 'first'); + publicAPI = indexPatternDatasource.getPublicAPI({ + state: initialState, + setState: () => {}, + layerId: 'first', + dateRange: { + fromDate: 'now-30d', + toDate: 'now', + }, + }); }); describe('getTableSpec', () => { @@ -453,8 +461,8 @@ describe('IndexPattern Data Source', () => { suggestedPriority: 2, }, }; - const api = indexPatternDatasource.getPublicAPI( - { + const api = indexPatternDatasource.getPublicAPI({ + state: { ...initialState, layers: { first: { @@ -465,8 +473,12 @@ describe('IndexPattern Data Source', () => { }, }, setState, - 'first' - ); + layerId: 'first', + dateRange: { + fromDate: 'now-1y', + toDate: 'now', + }, + }); api.removeColumnInTableSpec('b'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 1ab6ab6307e8d..bde5ce01aecda 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -16,6 +16,7 @@ import { DatasourceDataPanelProps, Operation, DatasourceLayerPanelProps, + PublicAPIProps, } from '../types'; import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; @@ -196,11 +197,12 @@ export function getIndexPatternDatasource({ ); }, - getPublicAPI( - state: IndexPatternPrivateState, - setState: StateSetter, - layerId: string - ) { + getPublicAPI({ + state, + setState, + layerId, + dateRange, + }: PublicAPIProps) { const columnLabelMap = uniqueLabels(state.layers); return { @@ -237,6 +239,7 @@ export function getIndexPatternDatasource({ layerId={props.layerId} http={core.http} uniqueLabel={columnLabelMap[props.columnId]} + dateRange={dateRange} {...props} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx index 0b2243a01bf0a..d19493d579b64 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { DateHistogramIndexPatternColumn } from './date_histogram'; import { dateHistogramOperation } from '.'; import { shallow } from 'enzyme'; -import { EuiRange, EuiSwitch } from '@elastic/eui'; +import { EuiSwitch } from '@elastic/eui'; import { UiSettingsClientContract, SavedObjectsClientContract, @@ -19,6 +19,26 @@ import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; jest.mock('ui/new_platform'); +jest.mock('ui/chrome', () => ({ + getUiSettingsClient: () => ({ + get(path: string) { + if (path === 'histogram:maxBars') { + return 10; + } + }, + }), +})); + +const defaultOptions = { + storage: {} as IStorageWrapper, + uiSettings: {} as UiSettingsClientContract, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { + fromDate: 'now-1y', + toDate: 'now', + }, + http: {} as HttpServiceBase, +}; describe('date_histogram', () => { let state: IndexPatternPrivateState; @@ -72,7 +92,7 @@ describe('date_histogram', () => { // Private operationType: 'date_histogram', params: { - interval: 'w', + interval: '42w', }, sourceField: 'timestamp', }, @@ -118,6 +138,27 @@ describe('date_histogram', () => { }; }); + function stateWithInterval(interval: string) { + return ({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + interval, + }, + }, + }, + }, + }, + } as unknown) as IndexPatternPrivateState; + } + describe('buildColumn', () => { it('should create column object with auto interval for primary time field', () => { const column = dateHistogramOperation.buildColumn({ @@ -188,7 +229,7 @@ describe('date_histogram', () => { expect(esAggsConfig).toEqual( expect.objectContaining({ params: expect.objectContaining({ - interval: 'w', + interval: '42w', field: 'timestamp', }), }) @@ -217,7 +258,7 @@ describe('date_histogram', () => { expect(column.label).toContain('start_date'); }); - it('should change interval from auto when switching to a non primary time field', () => { + it('should not change interval from auto when switching to a non primary time field', () => { const oldColumn: DateHistogramIndexPatternColumn = { operationType: 'date_histogram', sourceField: 'timestamp', @@ -233,7 +274,7 @@ describe('date_histogram', () => { const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField); expect(column).toHaveProperty('sourceField', 'start_date'); - expect(column).toHaveProperty('params.interval', 'd'); + expect(column).toHaveProperty('params.interval', 'auto'); expect(column.label).toContain('start_date'); }); }); @@ -281,7 +322,7 @@ describe('date_histogram', () => { ); }); - it('should remove time zone param and normalize interval param', () => { + it('should retain interval', () => { const transferedColumn = dateHistogramOperation.transfer!( { dataType: 'date', @@ -309,7 +350,7 @@ describe('date_histogram', () => { expect(transferedColumn).toEqual( expect.objectContaining({ params: { - interval: 'M', + interval: '20s', timeZone: undefined, }, }) @@ -322,55 +363,49 @@ describe('date_histogram', () => { const setStateSpy = jest.fn(); const instance = shallow( ); - expect(instance.find(EuiRange).prop('value')).toEqual(1); + expect(instance.find('[data-test-subj="lensDateHistogramValue"]').prop('value')).toEqual(42); + expect(instance.find('[data-test-subj="lensDateHistogramUnit"]').prop('value')).toEqual('w'); }); it('should render current value for other index pattern', () => { const setStateSpy = jest.fn(); const instance = shallow( ); - expect(instance.find(EuiRange).prop('value')).toEqual(2); + expect(instance.find('[data-test-subj="lensDateHistogramValue"]').prop('value')).toEqual(''); + expect(instance.find('[data-test-subj="lensDateHistogramUnit"]').prop('value')).toEqual('d'); }); - it('should render disabled switch and no time intervals control for auto interval', () => { + it('should render disabled switch and no time interval control for auto interval', () => { const instance = shallow( ); - expect(instance.find(EuiRange).exists()).toBe(false); + expect(instance.find('[data-test-subj="lensDateHistogramValue"]').exists()).toBeFalsy(); + expect(instance.find('[data-test-subj="lensDateHistogramUnit"]').exists()).toBeFalsy(); expect(instance.find(EuiSwitch).prop('checked')).toBe(false); }); @@ -378,15 +413,12 @@ describe('date_histogram', () => { const setStateSpy = jest.fn(); const instance = shallow( ); instance.find(EuiSwitch).prop('onChange')!({ @@ -394,57 +426,124 @@ describe('date_histogram', () => { } as React.ChangeEvent); expect(setStateSpy).toHaveBeenCalled(); const newState = setStateSpy.mock.calls[0][0]; - expect(newState).toHaveProperty('layers.third.columns.col1.params.interval', 'd'); + expect(newState).toHaveProperty('layers.third.columns.col1.params.interval', '30d'); }); - it('should update state with the interval value', () => { + it('should force calendar values to 1', () => { const setStateSpy = jest.fn(); const instance = shallow( ); + instance.find('[data-test-subj="lensDateHistogramValue"]').prop('onChange')!({ + target: { + value: '2', + }, + } as React.ChangeEvent); + expect(setStateSpy).toHaveBeenCalledWith(stateWithInterval('1w')); + }); - instance.find(EuiRange).prop('onChange')!( - { - target: { - value: '2', - }, - } as React.ChangeEvent, - true + it('should display error if an invalid interval is specified', () => { + const setStateSpy = jest.fn(); + const testState = stateWithInterval('4quid'); + const instance = shallow( + ); - expect(setStateSpy).toHaveBeenCalledWith({ - ...state, - layers: { - ...state.layers, - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - ...state.layers.first.columns.col1, - params: { - interval: 'd', - }, - }, - }, - }, + expect(instance.find('[data-test-subj="lensDateHistogramError"]').exists()).toBeTruthy(); + }); + + it('should not display error if interval value is blank', () => { + const setStateSpy = jest.fn(); + const testState = stateWithInterval('d'); + const instance = shallow( + + ); + expect(instance.find('[data-test-subj="lensDateHistogramError"]').exists()).toBeFalsy(); + }); + + it('should display error if interval value is 0', () => { + const setStateSpy = jest.fn(); + const testState = stateWithInterval('0d'); + const instance = shallow( + + ); + expect(instance.find('[data-test-subj="lensDateHistogramError"]').exists()).toBeTruthy(); + }); + + it('should update the unit', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + + ); + instance.find('[data-test-subj="lensDateHistogramUnit"]').prop('onChange')!({ + target: { + value: 'd', }, - }); + } as React.ChangeEvent); + expect(setStateSpy).toHaveBeenCalledWith(stateWithInterval('42d')); + }); + + it('should update the value', () => { + const setStateSpy = jest.fn(); + const testState = stateWithInterval('42d'); + + const instance = shallow( + + ); + instance.find('[data-test-subj="lensDateHistogramValue"]').prop('onChange')!({ + target: { + value: '9', + }, + } as React.ChangeEvent); + expect(setStateSpy).toHaveBeenCalledWith(stateWithInterval('9d')); }); it('should not render options if they are restricted', () => { const setStateSpy = jest.fn(); const instance = shallow( { columnId="col1" layerId="first" currentColumn={state.layers.first.columns.col1 as DateHistogramIndexPatternColumn} - storage={{} as IStorageWrapper} - uiSettings={{} as UiSettingsClientContract} - savedObjectsClient={{} as SavedObjectsClientContract} - http={{} as HttpServiceBase} /> ); - expect(instance.find(EuiRange)).toHaveLength(0); + expect(instance.find('[data-test-subj="lensDateHistogramValue"]').exists()).toBeFalsy(); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx index e5c00542df7d3..017dccab64607 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx @@ -7,31 +7,29 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiForm, EuiFormRow, EuiRange, EuiSwitch } from '@elastic/eui'; + +// TODO: make this new-platform compatible +import { isValidInterval } from 'ui/agg_types/utils'; + +import { + EuiForm, + EuiFormRow, + EuiSwitch, + EuiFieldNumber, + EuiSelect, + EuiFlexItem, + EuiFlexGroup, + EuiTextColor, + EuiSpacer, +} from '@elastic/eui'; import { updateColumnParam } from '../../state_helpers'; import { OperationDefinition } from '.'; import { FieldBasedIndexPatternColumn } from './column_types'; -import { IndexPattern } from '../../types'; - -type PropType = C extends React.ComponentType ? P : unknown; +import { autoIntervalFromDateRange } from '../../auto_date'; +import { AggregationRestrictions } from '../../types'; const autoInterval = 'auto'; -const supportedIntervals = ['M', 'w', 'd', 'h', 'm']; -const defaultCustomInterval = supportedIntervals[2]; - -// Add ticks to EuiRange component props -const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< - PropType & { - ticks?: Array<{ - label: string; - value: number; - }>; - } ->; - -function supportsAutoInterval(fieldName: string, indexPattern: IndexPattern): boolean { - return indexPattern.timeFieldName ? indexPattern.timeFieldName === fieldName : false; -} +const calendarOnlyIntervals = new Set(['w', 'M', 'q', 'y']); export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'date_histogram'; @@ -59,12 +57,11 @@ export const dateHistogramOperation: OperationDefinition { return { ...oldColumn, label: field.name, sourceField: field.name, - params: { - ...oldColumn.params, - // If we have an "auto" interval but the field we're switching to doesn't support auto intervals - // we use the default custom interval instead - interval: - oldColumn.params.interval === 'auto' && !supportsAutoInterval(field.name, indexPattern) - ? defaultCustomInterval - : oldColumn.params.interval, - }, }; }, toEsAggsConfig: (column, columnId) => ({ @@ -157,7 +135,7 @@ export const dateHistogramOperation: OperationDefinition { + paramEditor: ({ state, setState, currentColumn: currentColumn, layerId, dateRange }) => { const field = currentColumn && state.indexPatterns[state.layers[layerId].indexPatternId].fields.find( @@ -166,20 +144,35 @@ export const dateHistogramOperation: OperationDefinition) { - const interval = ev.target.checked ? defaultCustomInterval : autoInterval; + const value = ev.target.checked ? autoIntervalFromDateRange(dateRange) : autoInterval; + setState(updateColumnParam({ state, layerId, currentColumn, paramName: 'interval', value })); + } + + const setInterval = (newInterval: typeof interval) => { + const isCalendarInterval = calendarOnlyIntervals.has(newInterval.unit); + const value = `${isCalendarInterval ? '1' : newInterval.value}${newInterval.unit || 'd'}`; + setState( - updateColumnParam({ state, layerId, currentColumn, paramName: 'interval', value: interval }) + updateColumnParam({ + state, + layerId, + currentColumn, + value, + paramName: 'interval', + }) ); - } + }; return ( @@ -187,7 +180,7 @@ export const dateHistogramOperation: OperationDefinition {intervalIsRestricted ? ( @@ -209,33 +202,101 @@ export const dateHistogramOperation: OperationDefinition ) : ( - ({ - label: interval, - value: index, - }))} - onChange={( - e: React.ChangeEvent | React.MouseEvent - ) => - setState( - updateColumnParam({ - state, - layerId, - currentColumn, - paramName: 'interval', - value: numericToInterval(Number((e.target as HTMLInputElement).value)), - }) - ) - } - aria-label={i18n.translate('xpack.lens.indexPattern.dateHistogram.interval', { - defaultMessage: 'Time intervals', - })} - /> + <> + + + { + setInterval({ + ...interval, + value: e.target.value, + }); + }} + /> + + + { + setInterval({ + ...interval, + unit: e.target.value, + }); + }} + 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', + })} + + + )} + )} )} @@ -243,3 +304,26 @@ export const dateHistogramOperation: OperationDefinition { storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; http: HttpServiceBase; + dateRange: DateRange; } interface BaseOperationDefinitionProps { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index 9630f850dc247..9fba3e205dd45 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -20,6 +20,14 @@ import { IndexPatternPrivateState } from '../../types'; jest.mock('ui/new_platform'); +const defaultProps = { + storage: {} as IStorageWrapper, + uiSettings: {} as UiSettingsClientContract, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + http: {} as HttpServiceBase, +}; + describe('terms', () => { let state: IndexPatternPrivateState; const InlineOptions = termsOperation.paramEditor!; @@ -324,15 +332,12 @@ describe('terms', () => { const setStateSpy = jest.fn(); const instance = shallow( ); @@ -350,15 +355,12 @@ describe('terms', () => { const setStateSpy = jest.fn(); const instance = shallow( ); @@ -398,15 +400,12 @@ describe('terms', () => { const setStateSpy = jest.fn(); const instance = shallow( ); @@ -422,15 +421,12 @@ describe('terms', () => { const setStateSpy = jest.fn(); const instance = shallow( ); @@ -467,15 +463,12 @@ describe('terms', () => { const setStateSpy = jest.fn(); const instance = shallow( ); @@ -486,15 +479,12 @@ describe('terms', () => { const setStateSpy = jest.fn(); const instance = shallow( ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts index 3bf3be41f4c89..9ed5083633314 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts @@ -20,25 +20,27 @@ export interface IndexPattern { >; } +export type AggregationRestrictions = Partial< + Record< + string, + { + agg: string; + interval?: number; + fixed_interval?: string; + calendar_interval?: string; + delay?: string; + time_zone?: string; + } + > +>; + export interface IndexPatternField { name: string; type: string; esTypes?: string[]; aggregatable: boolean; searchable: boolean; - aggregationRestrictions?: Partial< - Record< - string, - { - agg: string; - interval?: number; - fixed_interval?: string; - calendar_interval?: string; - delay?: string; - time_zone?: string; - } - > - >; + aggregationRestrictions?: AggregationRestrictions; } export interface IndexPatternLayer { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 1efa123f4f0a3..93b8f93cbd2ff 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -13,19 +13,24 @@ import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { KibanaDatatable } from '../../../../../src/legacy/core_plugins/interpreter/common'; import { DragContextState } from './drag_drop'; import { Document } from './persistence'; +import { DateRange } from '../common'; // eslint-disable-next-line export interface EditorFrameOptions {} export type ErrorCallback = (e: { message: string }) => void; +export interface PublicAPIProps { + state: T; + setState: StateSetter; + layerId: string; + dateRange: DateRange; +} + export interface EditorFrameProps { onError: ErrorCallback; doc?: Document; - dateRange: { - fromDate: string; - toDate: string; - }; + dateRange: DateRange; query: Query; filters: Filter[]; savedQuery?: SavedQuery; @@ -138,7 +143,7 @@ export interface Datasource { getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; - getPublicAPI: (state: T, setState: StateSetter, layerId: string) => DatasourcePublicAPI; + getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; } /** @@ -171,7 +176,7 @@ export interface DatasourceDataPanelProps { setState: StateSetter; core: Pick; query: Query; - dateRange: FramePublicAPI['dateRange']; + dateRange: DateRange; filters: Filter[]; } @@ -296,10 +301,7 @@ export interface VisualizationSuggestion { export interface FramePublicAPI { datasourceLayers: Record; - dateRange: { - fromDate: string; - toDate: string; - }; + dateRange: DateRange; query: Query; filters: Filter[];