diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index fc141b8c89c18..498691c06285d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index b12ad94017fbb..a0c94187910b0 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1743,8 +1743,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx new file mode 100644 index 0000000000000..27f924d98e6eb --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { NoDataPopover } from './no_data_popover'; +import { EuiTourStep } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; + +describe('NoDataPopover', () => { + const createMockStorage = () => ({ + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + + it('should hide popover if showNoDataPopover is set to false', () => { + const Child = () => ; + const instance = mount( + + + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + expect(instance.find(EuiTourStep).find(Child)).toHaveLength(1); + }); + + it('should hide popover if showNoDataPopover is set to true, but local storage flag is set', () => { + const child = ; + const storage = createMockStorage(); + storage.get.mockReturnValue(true); + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should render popover if showNoDataPopover is set to true and local storage flag is not set', () => { + const child = ; + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(true); + }); + + it('should hide popover if it is closed', async () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('closePopover')!(); + }); + instance.setProps({ ...props }); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should set local storage flag and hide on closing with button', () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('footerAction')!.props.onClick(); + }); + instance.setProps({ ...props }); + expect(props.storage.set).toHaveBeenCalledWith(expect.any(String), true); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); +}); diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx new file mode 100644 index 0000000000000..302477a5fff5e --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ReactElement, useEffect, useState } from 'react'; +import React from 'react'; +import { EuiButtonEmpty, EuiText, EuiTourStep } from '@elastic/eui'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { i18n } from '@kbn/i18n'; + +const NO_DATA_POPOVER_STORAGE_KEY = 'data.noDataPopover'; + +export function NoDataPopover({ + showNoDataPopover, + storage, + children, +}: { + showNoDataPopover?: boolean; + storage: IStorageWrapper; + children: ReactElement; +}) { + const [noDataPopoverDismissed, setNoDataPopoverDismissed] = useState(() => + Boolean(storage.get(NO_DATA_POPOVER_STORAGE_KEY)) + ); + const [noDataPopoverVisible, setNoDataPopoverVisible] = useState(false); + + useEffect(() => { + if (showNoDataPopover && !noDataPopoverDismissed) { + setNoDataPopoverVisible(true); + } + }, [noDataPopoverDismissed, showNoDataPopover]); + + return ( + {}} + closePopover={() => { + setNoDataPopoverVisible(false); + }} + content={ + +

+ {i18n.translate('data.noDataPopover.content', { + defaultMessage: + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts", + })} +

+
+ } + minWidth={300} + anchorPosition="downCenter" + step={1} + stepsTotal={1} + isStepOpen={noDataPopoverVisible} + subtitle={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Tip' })} + title="" + footerAction={ + { + storage.set(NO_DATA_POPOVER_STORAGE_KEY, true); + setNoDataPopoverDismissed(true); + setNoDataPopoverVisible(false); + }} + > + {i18n.translate('data.noDataPopover.dismissAction', { + defaultMessage: "Don't show again", + })} + + } + > +
{ + setNoDataPopoverVisible(false); + }} + > + {children} +
+
+ ); +} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index f65bf97e391e2..4b0dc579c39ce 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -40,6 +40,7 @@ import { useKibana, toMountPoint } from '../../../../kibana_react/public'; import { QueryStringInput } from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; +import { NoDataPopover } from './no_data_popover'; interface Props { query?: Query; @@ -63,6 +64,7 @@ interface Props { customSubmitButton?: any; isDirty: boolean; timeHistory?: TimeHistoryContract; + indicateNoData?: boolean; } export function QueryBarTopRow(props: Props) { @@ -230,10 +232,12 @@ export function QueryBarTopRow(props: Props) { } return ( - - {renderDatePicker()} - {button} - + + + {renderDatePicker()} + {button} + + ); } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 81e84e3198072..a0df7604f23aa 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -198,6 +198,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) showSaveQuery={props.showSaveQuery} screenTitle={props.screenTitle} indexPatterns={props.indexPatterns} + indicateNoData={props.indicateNoData} timeHistory={data.query.timefilter.history} dateRangeFrom={timeRange.from} dateRangeTo={timeRange.to} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index a5ac227559115..2f740cc476087 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -75,6 +75,7 @@ export interface SearchBarOwnProps { onClearSavedQuery?: () => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; + indicateNoData?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -402,6 +403,7 @@ class SearchBarUI extends Component { this.props.customSubmitButton ? this.props.customSubmitButton : undefined } dataTestSubj={this.props.dataTestSubj} + indicateNoData={this.props.indicateNoData} /> ); } diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 20ae89fc1a8d0..7ef291c8c7005 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -52,6 +52,13 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo await this.setAbsoluteRange(this.defaultStartTime, this.defaultEndTime); } + async ensureHiddenNoDataPopover() { + const isVisible = await testSubjects.exists('noDataPopoverDismissButton'); + if (isVisible) { + await testSubjects.click('noDataPopoverDismissButton'); + } + } + /** * the provides a quicker way to set the timepicker to the default range, saves a few seconds */ diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index cd6fbf96d6750..3bd12a87456a0 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -226,6 +226,7 @@ describe('Lens App', () => { "query": "", }, "savedQuery": undefined, + "showNoDataPopover": [Function], }, ], ] diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 0ab547bed6d37..9b8b9a8531cf0 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -40,6 +40,7 @@ import { } from '../../../../../src/plugins/data/public'; interface State { + indicateNoData: boolean; isLoading: boolean; isSaveModalVisible: boolean; indexPatternsForTopNav: IndexPatternInstance[]; @@ -97,9 +98,27 @@ export function App({ toDate: currentRange.to, }, filters: [], + indicateNoData: false, }; }); + const showNoDataPopover = useCallback(() => { + setState((prevState) => ({ ...prevState, indicateNoData: true })); + }, [setState]); + + useEffect(() => { + if (state.indicateNoData) { + setState((prevState) => ({ ...prevState, indicateNoData: false })); + } + }, [ + setState, + state.indicateNoData, + state.query, + state.filters, + state.dateRange, + state.indexPatternsForTopNav, + ]); + const { lastKnownDoc } = state; const isSaveable = @@ -458,6 +477,7 @@ export function App({ query={state.query} dateRangeFrom={state.dateRange.fromDate} dateRangeTo={state.dateRange.toDate} + indicateNoData={state.indicateNoData} /> @@ -472,6 +492,7 @@ export function App({ savedQuery: state.savedQuery, doc: state.persistedDoc, onError, + showNoDataPopover, onChange: ({ filterableIndexPatterns, doc }) => { if (!_.isEqual(state.persistedDoc, doc)) { setState((s) => ({ ...s, lastKnownDoc: doc })); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index afb2719f28e89..0f74abe97c418 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -19,6 +19,7 @@ interface DataPanelWrapperProps { activeDatasource: string | null; datasourceIsLoading: boolean; dispatch: (action: Action) => void; + showNoDataPopover: () => void; core: DatasourceDataPanelProps['core']; query: Query; dateRange: FramePublicAPI['dateRange']; @@ -46,6 +47,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { query: props.query, dateRange: props.dateRange, filters: props.filters, + showNoDataPopover: props.showNoDataPopover, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index ff9e24f95d1e2..ad4f6e74c9e92 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -56,6 +56,7 @@ function getDefaultProps() { data: dataPluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), }, + showNoDataPopover: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index af3d0ed068d2f..bcceb1222ce03 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -48,6 +48,7 @@ export interface EditorFrameProps { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; }) => void; + showNoDataPopover: () => void; } export function EditorFrame(props: EditorFrameProps) { @@ -255,6 +256,7 @@ export function EditorFrame(props: EditorFrameProps) { query={props.query} dateRange={props.dateRange} filters={props.filters} + showNoDataPopover={props.showNoDataPopover} /> } configPanel={ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index e1151b92aac51..969467b5789ec 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -35,6 +35,7 @@ describe('editor_frame state management', () => { dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }; }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index fbd65c5044d51..7b1d091c1c8fe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -51,6 +51,7 @@ describe('editor_frame service', () => { dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }); instance.unmount(); })() @@ -70,6 +71,7 @@ describe('editor_frame service', () => { dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }); instance.unmount(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index f57acf3bef62d..47339373b6d1a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -102,7 +102,10 @@ export class EditorFrameService { ]); return { - mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => { + mount: ( + element, + { doc, onError, dateRange, query, filters, savedQuery, onChange, showNoDataPopover } + ) => { domElement = element; const firstDatasourceId = Object.keys(resolvedDatasources)[0]; const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; @@ -127,6 +130,7 @@ export class EditorFrameService { filters={filters} savedQuery={savedQuery} onChange={onChange} + showNoDataPopover={showNoDataPopover} /> , domElement diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index f2fedda1fa353..ca5fe706985f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -19,6 +19,7 @@ export function loadInitialState() { [restricted.id]: restricted, }, layers: {}, + isFirstExistenceFetch: false, }; return result; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 7653dab2c9b84..f70df855fe0cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -204,12 +204,15 @@ const initialState: IndexPatternPrivateState = { ], }, }, + isFirstExistenceFetch: false, }; const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } }; describe('IndexPattern Data Panel', () => { - let defaultProps: Parameters[0]; + let defaultProps: Parameters[0] & { + showNoDataPopover: () => void; + }; let core: ReturnType; beforeEach(() => { @@ -229,6 +232,7 @@ describe('IndexPattern Data Panel', () => { }, query: { query: '', language: 'lucene' }, filters: [], + showNoDataPopover: jest.fn(), }; }); @@ -301,6 +305,7 @@ describe('IndexPattern Data Panel', () => { state: { indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, currentIndexPatternId: 'a', indexPatterns: { a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index b72f87e243dcd..87fbf81fceba0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -82,6 +82,7 @@ export function IndexPatternDataPanel({ filters, dateRange, changeIndexPattern, + showNoDataPopover, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( @@ -116,6 +117,9 @@ export function IndexPatternDataPanel({ syncExistingFields({ dateRange, setState, + isFirstExistenceFetch: state.isFirstExistenceFetch, + currentIndexPatternTitle: indexPatterns[currentIndexPatternId].title, + showNoDataPopover, indexPatterns: indexPatternList, fetchJson: core.http.post, dslQuery, @@ -210,7 +214,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ core, data, existingFields, -}: Pick> & { +}: Omit & { data: DataPublicPluginStart; currentIndexPatternId: string; indexPatternRefs: IndexPatternRef[]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index ee9b6778650ef..a1c084f83e447 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -79,6 +79,7 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternRefs: [], indexPatterns: expectedIndexPatterns, currentIndexPatternId: '1', + isFirstExistenceFetch: false, existingFields: { 'my-fake-index-pattern': { timestamp: true, @@ -1257,6 +1258,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { myLayer: { indexPatternId: 'foo', 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 a69d7c055eaa7..6a79ce450cd9a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -127,6 +127,7 @@ function stateFromPersistedState( indexPatterns: expectedIndexPatterns, indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, }; } @@ -401,6 +402,7 @@ describe('IndexPattern Data Source', () => { }, }, currentIndexPatternId: '1', + isFirstExistenceFetch: false, }; expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({ ...state, @@ -421,6 +423,7 @@ describe('IndexPattern Data Source', () => { const state = { indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { @@ -455,6 +458,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource.getLayers({ indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { @@ -480,6 +484,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource.getMetaData({ indexPatternRefs: [], existingFields: {}, + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 87d91b56d2a5c..b6246c6e91e7e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -146,6 +146,7 @@ function testInitialState(): IndexPatternPrivateState { }, }, }, + isFirstExistenceFetch: false, }; } @@ -304,6 +305,7 @@ describe('IndexPattern Data Source suggestions', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: { 1: { id: '1', @@ -508,6 +510,7 @@ describe('IndexPattern Data Source suggestions', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: { 1: { id: '1', @@ -1046,6 +1049,7 @@ describe('IndexPattern Data Source suggestions', () => { it('returns no suggestions if there are no columns', () => { expect( getDatasourceSuggestionsFromCurrentState({ + isFirstExistenceFetch: false, indexPatternRefs: [], existingFields: {}, indexPatterns: expectedIndexPatterns, @@ -1351,6 +1355,7 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -1470,6 +1475,7 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -1523,6 +1529,7 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -1553,6 +1560,7 @@ describe('IndexPattern Data Source suggestions', () => { existingFields: {}, currentIndexPatternId: '1', indexPatterns: expectedIndexPatterns, + isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 9cbd624b42d3e..f9a74ee477d57 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -22,6 +22,7 @@ const initialState: IndexPatternPrivateState = { ], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index e8c8c5762bb83..5776691fbcc7f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/public'; +import { HttpHandler, SavedObjectsClientContract } from 'kibana/public'; import _ from 'lodash'; import { loadInitialState, @@ -429,6 +429,7 @@ describe('loader', () => { indexPatterns: {}, existingFields: {}, layers: {}, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -463,6 +464,7 @@ describe('loader', () => { existingFields: {}, indexPatterns: {}, layers: {}, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -520,6 +522,7 @@ describe('loader', () => { indexPatternId: 'a', }, }, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'a' }); @@ -588,6 +591,7 @@ describe('loader', () => { indexPatternId: 'a', }, }, + isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -625,7 +629,7 @@ describe('loader', () => { it('should call once for each index pattern', async () => { const setState = jest.fn(); - const fetchJson = jest.fn((path: string) => { + const fetchJson = (jest.fn((path: string) => { const indexPatternTitle = _.last(path.split('/')); return { indexPatternTitle, @@ -633,15 +637,17 @@ describe('loader', () => { (fieldName) => `${indexPatternTitle}_${fieldName}` ), }; - }); + }) as unknown) as HttpHandler; await syncExistingFields({ dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fetchJson: fetchJson as any, + fetchJson, indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], setState, dslQuery, + showNoDataPopover: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, }); expect(fetchJson).toHaveBeenCalledTimes(3); @@ -655,6 +661,7 @@ describe('loader', () => { expect(newState).toEqual({ foo: 'bar', + isFirstExistenceFetch: false, existingFields: { a: { a_field_1: true, a_field_2: true }, b: { b_field_1: true, b_field_2: true }, @@ -662,5 +669,38 @@ describe('loader', () => { }, }); }); + + it('should call showNoDataPopover callback if current index pattern returns no fields', async () => { + const setState = jest.fn(); + const showNoDataPopover = jest.fn(); + const fetchJson = (jest.fn((path: string) => { + const indexPatternTitle = _.last(path.split('/')); + return { + indexPatternTitle, + existingFieldNames: + indexPatternTitle === 'a' + ? ['field_1', 'field_2'].map((fieldName) => `${indexPatternTitle}_${fieldName}`) + : [], + }; + }) as unknown) as HttpHandler; + + const args = { + dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, + fetchJson, + indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + setState, + dslQuery, + showNoDataPopover: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, + }; + + await syncExistingFields(args); + + expect(showNoDataPopover).not.toHaveBeenCalled(); + + await syncExistingFields({ ...args, isFirstExistenceFetch: true }); + expect(showNoDataPopover).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 6c57988dfc7b6..101f536993365 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -119,6 +119,7 @@ export async function loadInitialState({ indexPatternRefs, indexPatterns, existingFields: {}, + isFirstExistenceFetch: true, }; } @@ -128,6 +129,7 @@ export async function loadInitialState({ indexPatterns, layers: {}, existingFields: {}, + isFirstExistenceFetch: true, }; } @@ -238,13 +240,19 @@ export async function syncExistingFields({ dateRange, fetchJson, setState, + isFirstExistenceFetch, + currentIndexPatternTitle, dslQuery, + showNoDataPopover, }: { dateRange: DateRange; indexPatterns: Array<{ id: string; timeFieldName?: string | null }>; fetchJson: HttpSetup['post']; setState: SetState; + isFirstExistenceFetch: boolean; + currentIndexPatternTitle: string; dslQuery: object; + showNoDataPopover: () => void; }) { const emptinessInfo = await Promise.all( indexPatterns.map((pattern) => { @@ -264,8 +272,18 @@ export async function syncExistingFields({ }) ); + if (isFirstExistenceFetch) { + const fieldsCurrentIndexPattern = emptinessInfo.find( + (info) => info.indexPatternTitle === currentIndexPatternTitle + ); + if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { + showNoDataPopover(); + } + } + setState((state) => ({ ...state, + isFirstExistenceFetch: false, existingFields: emptinessInfo.reduce((acc, info) => { acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames); return acc; 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 d0c7af42114e3..1a094a36f68e3 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 @@ -51,6 +51,7 @@ describe('date_histogram', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: { 1: { id: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index 1e1d83a0a5c4c..d7f00e185a5bb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -34,6 +34,7 @@ describe('terms', () => { indexPatterns: {}, existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index a73f6e13d94c5..1a37e5e4cf6a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -147,6 +147,7 @@ describe('getOperationTypesForField', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, indexPatterns: expectedIndexPatterns, layers: { first: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 65a2401fd689a..d778749ef3940 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -42,6 +42,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -95,6 +96,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -145,6 +147,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -185,6 +188,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -218,6 +222,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -279,6 +284,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -331,6 +337,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -410,6 +417,7 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', + isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 35a82d8774130..b7beb67196add 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -51,6 +51,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { * indexPatternId -> fieldName -> boolean */ existingFields: Record>; + isFirstExistenceFetch: boolean; }; export interface IndexPatternRef { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d451e312446bd..c7bda65cd1327 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -47,6 +47,7 @@ export interface EditorFrameProps { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; }) => void; + showNoDataPopover: () => void; } export interface EditorFrameInstance { mount: (element: Element, props: EditorFrameProps) => void; @@ -186,6 +187,7 @@ export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; setState: StateSetter; + showNoDataPopover: () => void; core: Pick; query: Query; dateRange: DateRange; diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index 00d9208772798..b980116c581da 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'header', 'timePicker']); + const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'timePicker']); const browser = getService('browser'); const filterBar = getService('filterBar'); const appsMenu = getService('appsMenu'); @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should carry over time range and pinned filters to discover', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); - await PageObjects.timePicker.setAbsoluteRange( + await PageObjects.lens.goToTimeRange( 'Sep 06, 2015 @ 06:31:44.000', 'Sep 18, 2025 @ 06:31:44.000' ); @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should remember time range and pinned filters from discover', async () => { - await PageObjects.timePicker.setAbsoluteRange( + await PageObjects.lens.goToTimeRange( 'Sep 07, 2015 @ 06:31:44.000', 'Sep 19, 2025 @ 06:31:44.000' ); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index bae11e1ea8a90..ce621d4471d0f 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -38,10 +38,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Move the date filter to the specified time range, defaults to * a range that has data in our dataset. */ - goToTimeRange(fromTime?: string, toTime?: string) { + async goToTimeRange(fromTime?: string, toTime?: string) { + await PageObjects.timePicker.ensureHiddenNoDataPopover(); fromTime = fromTime || PageObjects.timePicker.defaultStartTime; toTime = toTime || PageObjects.timePicker.defaultEndTime; - return PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }, /**