diff --git a/src/plugins/unified_search/public/dataview_picker/mocks/dataview.ts b/src/plugins/unified_search/public/dataview_picker/mocks/dataview.ts index 7b8c1318fae8c..8a608b8a54db5 100644 --- a/src/plugins/unified_search/public/dataview_picker/mocks/dataview.ts +++ b/src/plugins/unified_search/public/dataview_picker/mocks/dataview.ts @@ -121,3 +121,10 @@ export const buildDataViewMock = ({ }; export const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields }); +export const dataViewMockWithTimefield = buildDataViewMock({ + timeFieldName: '@timestamp', + name: 'the-data-view-with-timefield', + fields, +}); + +export const dataViewMockList = [dataViewMock, dataViewMockWithTimefield]; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx index bb9b9e2aa2a18..19d7486cf8308 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx @@ -12,7 +12,10 @@ import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; import { coreMock } from '@kbn/core/public/mocks'; import type { FilterEditorProps } from '.'; import { FilterEditor } from '.'; +import { dataViewMockList } from '../../dataview_picker/mocks/dataview'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +const dataMock = dataPluginMock.createStartContract(); jest.mock('@kbn/code-editor', () => { const original = jest.requireActual('@kbn/code-editor'); @@ -50,6 +53,7 @@ describe('', () => { onCancel: jest.fn(), onSubmit: jest.fn(), docLinks: coreMock.createStart().docLinks, + dataViews: dataMock.dataViews, }; testBed = await registerTestBed(FilterEditor, { defaultProps })(); }); @@ -76,4 +80,72 @@ describe('', () => { expect(find('saveFilter').props().disabled).toBe(false); }); }); + describe('handling data view fallback', () => { + let testBed: TestBed; + + beforeEach(async () => { + dataMock.dataViews.get = jest.fn().mockReturnValue(Promise.resolve(dataViewMockList[1])); + const defaultProps: Omit = { + theme: { + euiTheme: {} as unknown as EuiThemeComputed<{}>, + colorMode: 'DARK', + modifications: [], + } as UseEuiTheme<{}>, + filter: { + meta: { + type: 'phase', + index: dataViewMockList[1].id, + } as any, + }, + indexPatterns: [dataViewMockList[0]], + onCancel: jest.fn(), + onSubmit: jest.fn(), + docLinks: coreMock.createStart().docLinks, + dataViews: dataMock.dataViews, + }; + testBed = await registerTestBed(FilterEditor, { defaultProps })(); + }); + + it('renders the right data view to be selected', async () => { + const { exists, component, find } = testBed; + component.update(); + expect(exists('filterIndexPatternsSelect')).toBe(true); + expect(find('filterIndexPatternsSelect').find('input').props().value).toBe( + dataViewMockList[1].getName() + ); + }); + }); + describe('UI renders when data view fallback promise is rejected', () => { + let testBed: TestBed; + + beforeEach(async () => { + dataMock.dataViews.get = jest.fn().mockReturnValue(Promise.reject()); + const defaultProps: Omit = { + theme: { + euiTheme: {} as unknown as EuiThemeComputed<{}>, + colorMode: 'DARK', + modifications: [], + } as UseEuiTheme<{}>, + filter: { + meta: { + type: 'phase', + index: dataViewMockList[1].id, + } as any, + }, + indexPatterns: [dataViewMockList[0]], + onCancel: jest.fn(), + onSubmit: jest.fn(), + docLinks: coreMock.createStart().docLinks, + dataViews: dataMock.dataViews, + }; + testBed = registerTestBed(FilterEditor, { defaultProps })(); + }); + + it('renders the right data view to be selected', async () => { + const { exists, component, find } = await testBed; + component.update(); + expect(exists('filterIndexPatternsSelect')).toBe(true); + expect(find('filterIndexPatternsSelect').find('input').props().value).toBe(''); + }); + }); }); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx index c3c93edb54ffa..67764134e448a 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx @@ -25,6 +25,7 @@ import { withEuiTheme, EuiTextColor, EuiLink, + EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -43,7 +44,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { DataView } from '@kbn/data-views-plugin/common'; -import { getIndexPatternFromFilter } from '@kbn/data-plugin/public'; +import { DataViewsContract, getIndexPatternFromFilter } from '@kbn/data-plugin/public'; import { CodeEditor } from '@kbn/code-editor'; import { cx } from '@emotion/css'; import { WithEuiThemeProps } from '@elastic/eui/src/services/theme'; @@ -143,42 +144,80 @@ export interface FilterEditorComponentProps { suggestionsAbstraction?: SuggestionsAbstraction; docLinks: DocLinksStart; filtersCount?: number; + dataViews?: DataViewsContract; } export type FilterEditorProps = WithEuiThemeProps & FilterEditorComponentProps; interface State { + indexPatterns: DataView[]; selectedDataView?: DataView; customLabel: string | null; queryDsl: string; isCustomEditorOpen: boolean; localFilter: Filter; + isLoadingDataView?: boolean; } class FilterEditorComponent extends Component { constructor(props: FilterEditorProps) { super(props); - const dataView = this.getIndexPatternFromFilter(); + const dataView = getIndexPatternFromFilter(props.filter, props.indexPatterns); this.state = { + indexPatterns: props.indexPatterns, selectedDataView: dataView, customLabel: props.filter.meta.alias || '', - queryDsl: this.parseFilterToQueryDsl(props.filter), + queryDsl: this.parseFilterToQueryDsl(props.filter, props.indexPatterns), isCustomEditorOpen: this.isUnknownFilterType() || !!this.props.filter?.meta.isMultiIndex, localFilter: dataView ? merge({}, props.filter) : buildEmptyFilter(false), + isLoadingDataView: !Boolean(dataView), }; } componentDidMount() { - const { localFilter, queryDsl, customLabel } = this.state; + const { localFilter, queryDsl, customLabel, selectedDataView } = this.state; this.props.onLocalFilterCreate?.({ filter: localFilter, queryDslFilter: { queryDsl, customLabel }, }); this.props.onLocalFilterUpdate?.(localFilter); + if (!selectedDataView) { + const dataViewId = this.props.filter.meta.index; + if (!dataViewId || !this.props.dataViews) { + this.setState({ isLoadingDataView: false }); + } else { + this.loadDataView(dataViewId, this.props.dataViews); + } + } + } + + /** + * Helper function to load the data view from the index pattern id + * E.g. in Discover there's just one active data view, so filters with different data view id + * Than the currently selected data view need to load the data view from the id to display the filter + * correctly + * @param dataViewId + * @private + */ + private async loadDataView(dataViewId: string, dataViews: DataViewsContract) { + try { + const dataView = await dataViews.get(dataViewId, false); + this.setState({ + selectedDataView: dataView, + isLoadingDataView: false, + indexPatterns: [dataView, ...this.props.indexPatterns], + localFilter: merge({}, this.props.filter), + queryDsl: this.parseFilterToQueryDsl(this.props.filter, this.state.indexPatterns), + }); + } catch (e) { + this.setState({ + isLoadingDataView: false, + }); + } } - private parseFilterToQueryDsl(filter: Filter) { - const dsl = filterToQueryDsl(filter, this.props.indexPatterns); + private parseFilterToQueryDsl(filter: Filter, indexPatterns: DataView[]) { + const dsl = filterToQueryDsl(filter, indexPatterns); return JSON.stringify(dsl, null, 2); } @@ -217,61 +256,67 @@ class FilterEditorComponent extends Component { - + {this.state.isLoadingDataView ? (
- {this.renderIndexPatternInput()} - - {this.state.isCustomEditorOpen - ? this.renderCustomEditor() - : this.renderFiltersBuilderEditor()} - - - - - +
- - - {/* Adding isolation here fixes this bug https://github.com/elastic/kibana/issues/142211 */} - - - - {this.props.mode === 'add' - ? strings.getAddButtonLabel() - : strings.getUpdateButtonLabel()} - - - - - - - - - - -
+ ) : ( + +
+ {this.renderIndexPatternInput()} + + {this.state.isCustomEditorOpen + ? this.renderCustomEditor() + : this.renderFiltersBuilderEditor()} + + + + + +
+ + + {/* Adding isolation here fixes this bug https://github.com/elastic/kibana/issues/142211 */} + + + + {this.props.mode === 'add' + ? strings.getAddButtonLabel() + : strings.getUpdateButtonLabel()} + + + + + + + + + + +
+ )} ); } @@ -283,8 +328,8 @@ class FilterEditorComponent extends Component { } if ( - this.props.indexPatterns.length <= 1 && - this.props.indexPatterns.find( + this.state.indexPatterns.length <= 1 && + this.state.indexPatterns.find( (indexPattern) => indexPattern === this.getIndexPatternFromFilter() ) ) { @@ -296,15 +341,16 @@ class FilterEditorComponent extends Component { return null; } const { selectedDataView } = this.state; + return ( <> indexPattern.getName()} + getLabel={(indexPattern) => indexPattern?.getName()} onChange={this.onIndexPatternChange} isClearable={false} data-test-subj="filterIndexPatternsSelect" @@ -381,7 +427,7 @@ class FilterEditorComponent extends Component { @@ -447,7 +493,7 @@ class FilterEditorComponent extends Component { } private getIndexPatternFromFilter() { - return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); + return getIndexPatternFromFilter(this.props.filter, this.state.indexPatterns); } private isQueryDslValid = (queryDsl: string) => { @@ -526,7 +572,7 @@ class FilterEditorComponent extends Component { return; } - const newIndex = index || this.props.indexPatterns[0].id!; + const newIndex = index || this.state.indexPatterns[0].id!; try { const body = JSON.parse(queryDsl); return buildCustomFilter(newIndex, body, disabled, negate, customLabel || null, $state.store); @@ -592,7 +638,7 @@ class FilterEditorComponent extends Component { const filter = this.props.filter?.meta.type === FILTERS.CUSTOM || // only convert non-custom filters to custom when DSL changes - queryDsl !== this.parseFilterToQueryDsl(this.props.filter) + queryDsl !== this.parseFilterToQueryDsl(this.props.filter, this.state.indexPatterns) ? this.getFilterFromQueryDsl(queryDsl) : { ...this.props.filter, diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index 596a32ea0a2f5..aed639ec76d0d 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -34,7 +34,7 @@ import React, { useCallback, } from 'react'; import type { DocLinksStart, IUiSettingsClient } from '@kbn/core/public'; -import { DataView } from '@kbn/data-views-plugin/public'; +import { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; import { css } from '@emotion/react'; import { getIndexPatternFromFilter, getDisplayValueFromFilter } from '@kbn/data-plugin/public'; import { FilterEditor } from '../filter_editor/filter_editor'; @@ -62,6 +62,7 @@ export interface FilterItemProps extends WithCloseFilterEditorConfirmModalProps readOnly?: boolean; suggestionsAbstraction?: SuggestionsAbstraction; filtersCount?: number; + dataViews?: DataViewsContract; } type FilterPopoverProps = HTMLAttributes & EuiPopoverProps; @@ -399,6 +400,7 @@ function FilterItemComponent(props: FilterItemProps) { suggestionsAbstraction={props.suggestionsAbstraction} docLinks={docLinks} filtersCount={props.filtersCount} + dataViews={props.dataViews} /> , ]} diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx index f0e558f75ba71..941e842d30f6d 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx @@ -47,7 +47,7 @@ export interface FilterItemsProps { const FilterItemsUI = React.memo(function FilterItemsUI(props: FilterItemsProps) { const groupRef = useRef(null); const kibana = useKibana(); - const { appName, usageCollection, uiSettings, docLinks } = kibana.services; + const { appName, data, usageCollection, uiSettings, docLinks } = kibana.services; const { readOnly = false } = props; if (!uiSettings) return null; @@ -84,6 +84,7 @@ const FilterItemsUI = React.memo(function FilterItemsUI(props: FilterItemsProps) readOnly={readOnly} suggestionsAbstraction={props.suggestionsAbstraction} filtersCount={props.filters.length} + dataViews={data?.dataViews} /> )); diff --git a/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx index 6f801b2a32f04..cb3094e66260f 100644 --- a/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx +++ b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx @@ -119,6 +119,7 @@ export const FilterEditorWrapper = React.memo(function FilterEditorWrapper({ filtersForSuggestions={filtersForSuggestions} suggestionsAbstraction={suggestionsAbstraction} docLinks={docLinks} + dataViews={data.dataViews} /> )} diff --git a/x-pack/plugins/exploratory_view/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/exploratory_view/public/components/shared/filter_value_label/filter_value_label.tsx index 934c30c8061f4..af540d4e1f60b 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/filter_value_label/filter_value_label.tsx @@ -77,7 +77,7 @@ export function FilterValueLabel({ const filter = buildFilterLabel({ field, value, label, dataView, negate }); const { - services: { uiSettings, docLinks }, + services: { uiSettings, docLinks, dataViews }, } = useKibana(); return dataView ? ( @@ -101,6 +101,7 @@ export function FilterValueLabel({ 'editFilter', 'disableFilter', ]} + dataViews={dataViews} /> ) : null; }