Skip to content

Commit

Permalink
[FilterEditor] Load DataView in case it's not provided by the consumi…
Browse files Browse the repository at this point in the history
…ng plugin (elastic#173017)

Fix issue with missing data view by loading it, allowing users to edit the filter in the FilterEditor.
  • Loading branch information
kertal authored and delanni committed Jan 11, 2024
1 parent 928e4f0 commit 8f6d173
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -50,6 +53,7 @@ describe('<FilterEditor />', () => {
onCancel: jest.fn(),
onSubmit: jest.fn(),
docLinks: coreMock.createStart().docLinks,
dataViews: dataMock.dataViews,
};
testBed = await registerTestBed(FilterEditor, { defaultProps })();
});
Expand All @@ -76,4 +80,72 @@ describe('<FilterEditor />', () => {
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<FilterEditorProps, 'intl'> = {
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<FilterEditorProps, 'intl'> = {
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('');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
withEuiTheme,
EuiTextColor,
EuiLink,
EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
Expand All @@ -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';
Expand Down Expand Up @@ -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<FilterEditorProps, State> {
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);
}

Expand Down Expand Up @@ -217,61 +256,67 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
</EuiFlexGroup>
</EuiPopoverTitle>

<EuiForm>
{this.state.isLoadingDataView ? (
<div className="globalFilterItem__editorForm">
{this.renderIndexPatternInput()}

{this.state.isCustomEditorOpen
? this.renderCustomEditor()
: this.renderFiltersBuilderEditor()}

<EuiSpacer size="l" />
<EuiFormRow label={strings.getCustomLabel()} fullWidth>
<EuiFieldText
value={`${this.state.customLabel}`}
onChange={this.onCustomLabelChange}
placeholder={strings.getAddCustomLabel()}
fullWidth
/>
</EuiFormRow>
<EuiLoadingSpinner />
</div>

<EuiPopoverFooter paddingSize="s">
{/* Adding isolation here fixes this bug https://github.com/elastic/kibana/issues/142211 */}
<EuiFlexGroup
direction="rowReverse"
alignItems="center"
style={{ isolation: 'isolate' }}
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={this.onSubmit}
isDisabled={!this.isFilterValid()}
data-test-subj="saveFilter"
>
{this.props.mode === 'add'
? strings.getAddButtonLabel()
: strings.getUpdateButtonLabel()}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="right"
onClick={this.props.onCancel}
data-test-subj="cancelSaveFilter"
>
<FormattedMessage
id="unifiedSearch.filter.filterEditor.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>
</EuiPopoverFooter>
</EuiForm>
) : (
<EuiForm>
<div className="globalFilterItem__editorForm">
{this.renderIndexPatternInput()}

{this.state.isCustomEditorOpen
? this.renderCustomEditor()
: this.renderFiltersBuilderEditor()}

<EuiSpacer size="l" />
<EuiFormRow label={strings.getCustomLabel()} fullWidth>
<EuiFieldText
value={`${this.state.customLabel}`}
onChange={this.onCustomLabelChange}
placeholder={strings.getAddCustomLabel()}
fullWidth
/>
</EuiFormRow>
</div>

<EuiPopoverFooter paddingSize="s">
{/* Adding isolation here fixes this bug https://github.com/elastic/kibana/issues/142211 */}
<EuiFlexGroup
direction="rowReverse"
alignItems="center"
style={{ isolation: 'isolate' }}
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={this.onSubmit}
isDisabled={!this.isFilterValid()}
data-test-subj="saveFilter"
>
{this.props.mode === 'add'
? strings.getAddButtonLabel()
: strings.getUpdateButtonLabel()}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="right"
onClick={this.props.onCancel}
data-test-subj="cancelSaveFilter"
>
<FormattedMessage
id="unifiedSearch.filter.filterEditor.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>
</EuiPopoverFooter>
</EuiForm>
)}
</div>
);
}
Expand All @@ -283,8 +328,8 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
}

if (
this.props.indexPatterns.length <= 1 &&
this.props.indexPatterns.find(
this.state.indexPatterns.length <= 1 &&
this.state.indexPatterns.find(
(indexPattern) => indexPattern === this.getIndexPatternFromFilter()
)
) {
Expand All @@ -296,15 +341,16 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
return null;
}
const { selectedDataView } = this.state;

return (
<>
<EuiFormRow fullWidth label={strings.getDataView()}>
<GenericComboBox
fullWidth
placeholder={strings.getSelectDataView()}
options={this.props.indexPatterns}
options={this.state.indexPatterns}
selectedOptions={selectedDataView ? [selectedDataView] : []}
getLabel={(indexPattern) => indexPattern.getName()}
getLabel={(indexPattern) => indexPattern?.getName()}
onChange={this.onIndexPatternChange}
isClearable={false}
data-test-subj="filterIndexPatternsSelect"
Expand Down Expand Up @@ -381,7 +427,7 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
<EuiText size="xs" data-test-subj="filter-preview" css={{ overflowWrap: 'break-word' }}>
<FilterBadgeGroup
filters={[localFilter]}
dataViews={this.props.indexPatterns}
dataViews={this.state.indexPatterns}
booleanRelation={BooleanRelation.AND}
shouldShowBrackets={false}
/>
Expand Down Expand Up @@ -447,7 +493,7 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
}

private getIndexPatternFromFilter() {
return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns);
return getIndexPatternFromFilter(this.props.filter, this.state.indexPatterns);
}

private isQueryDslValid = (queryDsl: string) => {
Expand Down Expand Up @@ -526,7 +572,7 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
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);
Expand Down Expand Up @@ -592,7 +638,7 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
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,
Expand Down
Loading

0 comments on commit 8f6d173

Please sign in to comment.