Skip to content

Commit

Permalink
[Dashboard][Controls] Make Text Field Based Options List Controls Cas…
Browse files Browse the repository at this point in the history
…e Insensitive (#131198)

* Adds case insensitive search and run past timeout option
  • Loading branch information
ThomThomson authored May 12, 2022
1 parent eda92d4 commit 11ae98d
Show file tree
Hide file tree
Showing 13 changed files with 691 additions and 148 deletions.
37 changes: 35 additions & 2 deletions src/plugins/controls/common/control_types/options_list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,60 @@
* Side Public License, v 1.
*/

import { BoolQuery } from '@kbn/es-query';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import { TimeRange } from '@kbn/data-plugin/common';
import { Filter, Query, BoolQuery } from '@kbn/es-query';
import { FieldSpec, DataView, DataViewField } from '@kbn/data-views-plugin/common';

import { DataControlInput } from '../../types';

export const OPTIONS_LIST_CONTROL = 'optionsListControl';

export interface OptionsListEmbeddableInput extends DataControlInput {
selectedOptions?: string[];
runPastTimeout?: boolean;
textFieldName?: string;
singleSelect?: boolean;
loading?: boolean;
}

export type OptionsListField = DataViewField & {
textFieldName?: string;
parentFieldName?: string;
childFieldName?: string;
};

/**
* The Options list response is returned from the serverside Options List route.
*/
export interface OptionsListResponse {
suggestions: string[];
totalCardinality: number;
invalidSelections?: string[];
}

/**
* The Options list request type taken in by the public Options List service.
*/
export type OptionsListRequest = Omit<
OptionsListRequestBody,
'filters' | 'fieldName' | 'fieldSpec' | 'textFieldName'
> & {
timeRange?: TimeRange;
field: OptionsListField;
runPastTimeout?: boolean;
dataView: DataView;
filters?: Filter[];
query?: Query;
};

/**
* The Options list request body is sent to the serverside Options List route and is used to create the ES query.
*/
export interface OptionsListRequestBody {
filters?: Array<{ bool: BoolQuery }>;
selectedOptions?: string[];
runPastTimeout?: boolean;
textFieldName?: string;
searchString?: string;
fieldSpec?: FieldSpec;
fieldName: string;
Expand Down
7 changes: 3 additions & 4 deletions src/plugins/controls/public/__stories__/controls.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ import { decorators } from './decorators';
import { ControlsPanels } from '../control_group/types';
import { ControlGroupContainer } from '../control_group';
import { pluginServices, registry } from '../services/storybook';
import { replaceValueSuggestionMethod } from '../services/storybook/unified_search';
import { injectStorybookDataView } from '../services/storybook/data_views';
import { populateStorybookControlFactories } from './storybook_control_factories';
import { OptionsListRequest } from '../services/options_list';
import { OptionsListResponse } from '../control_types/options_list/types';
import { replaceOptionsListMethod } from '../services/storybook/options_list';
import { populateStorybookControlFactories } from './storybook_control_factories';
import { replaceValueSuggestionMethod } from '../services/storybook/unified_search';
import { OptionsListResponse, OptionsListRequest } from '../control_types/options_list/types';

export default {
title: 'Controls',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@

import useMount from 'react-use/lib/useMount';
import React, { useEffect, useState } from 'react';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';

import { DataViewListItem, DataView } from '@kbn/data-views-plugin/common';
import {
LazyDataViewPicker,
LazyFieldPicker,
withSuspense,
} from '@kbn/presentation-util-plugin/public';
import { IFieldSubTypeMulti } from '@kbn/es-query';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { DataViewListItem, DataView } from '@kbn/data-views-plugin/common';

import { pluginServices } from '../../services';
import { ControlEditorProps } from '../../types';
import { OptionsListEmbeddableInput } from './types';
import { OptionsListStrings } from './options_list_strings';

import { OptionsListEmbeddableInput, OptionsListField } from './types';
interface OptionsListEditorState {
singleSelect?: boolean;

runPastTimeout?: boolean;
dataViewListItems: DataViewListItem[];

fieldsMap?: { [key: string]: OptionsListField };
dataView?: DataView;
fieldName?: string;
}
Expand All @@ -48,6 +49,7 @@ export const OptionsListEditor = ({
const [state, setState] = useState<OptionsListEditorState>({
fieldName: initialInput?.fieldName,
singleSelect: initialInput?.singleSelect,
runPastTimeout: initialInput?.runPastTimeout,
dataViewListItems: [],
});

Expand All @@ -64,13 +66,54 @@ export const OptionsListEditor = ({
dataView = await get(initialId);
}
if (!mounted) return;
setState((s) => ({ ...s, dataView, dataViewListItems }));
setState((s) => ({ ...s, dataView, dataViewListItems, fieldsMap: {} }));
})();
return () => {
mounted = false;
};
});

useEffect(() => {
if (!state.dataView) return;

// double link the parent-child relationship so that we can filter in fields which are multi-typed to text / keyword
const doubleLinkedFields: OptionsListField[] = state.dataView?.fields.getAll();
for (const field of doubleLinkedFields) {
const parentFieldName = (field.subType as IFieldSubTypeMulti)?.multi?.parent;
if (parentFieldName) {
(field as OptionsListField).parentFieldName = parentFieldName;
const parentField = state.dataView?.getFieldByName(parentFieldName);
(parentField as OptionsListField).childFieldName = field.name;
}
}

const newFieldsMap: OptionsListEditorState['fieldsMap'] = {};
for (const field of doubleLinkedFields) {
if (field.type === 'boolean') {
newFieldsMap[field.name] = field;
}

// field type is keyword, check if this field is related to a text mapped field and include it.
else if (field.aggregatable && field.type === 'string') {
const childField =
(field.childFieldName && state.dataView?.fields.getByName(field.childFieldName)) ||
undefined;
const parentField =
(field.parentFieldName && state.dataView?.fields.getByName(field.parentFieldName)) ||
undefined;

const textFieldName = childField?.esTypes?.includes('text')
? childField.name
: parentField?.esTypes?.includes('text')
? parentField.name
: undefined;

newFieldsMap[field.name] = { ...field, textFieldName } as OptionsListField;
}
}
setState((s) => ({ ...s, fieldsMap: newFieldsMap }));
}, [state.dataView]);

useEffect(
() => setValidState(Boolean(state.fieldName) && Boolean(state.dataView)),
[state.fieldName, setValidState, state.dataView]
Expand Down Expand Up @@ -100,14 +143,16 @@ export const OptionsListEditor = ({
</EuiFormRow>
<EuiFormRow label={OptionsListStrings.editor.getFieldTitle()}>
<FieldPicker
filterPredicate={(field) =>
(field.aggregatable && field.type === 'string') || field.type === 'boolean'
}
filterPredicate={(field) => Boolean(state.fieldsMap?.[field.name])}
selectedFieldName={fieldName}
dataView={dataView}
onSelectField={(field) => {
setDefaultTitle(field.displayName ?? field.name);
onChange({ fieldName: field.name });
const textFieldName = state.fieldsMap?.[field.name].textFieldName;
onChange({
fieldName: field.name,
textFieldName,
});
setState((s) => ({ ...s, fieldName: field.name }));
}}
/>
Expand All @@ -122,6 +167,16 @@ export const OptionsListEditor = ({
}}
/>
</EuiFormRow>
<EuiFormRow>
<EuiSwitch
label={OptionsListStrings.editor.getRunPastTimeoutTitle()}
checked={Boolean(state.runPastTimeout)}
onChange={() => {
onChange({ runPastTimeout: !state.runPastTimeout });
setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout }));
}}
/>
</EuiFormRow>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ import deepEqual from 'fast-deep-equal';
import { merge, Subject, Subscription, BehaviorSubject } from 'rxjs';
import { tap, debounceTime, map, distinctUntilChanged, skip } from 'rxjs/operators';

import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
import {
withSuspense,
LazyReduxEmbeddableWrapper,
ReduxEmbeddableWrapperPropsWithChildren,
} from '@kbn/presentation-util-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';

import { OptionsListEmbeddableInput, OptionsListField, OPTIONS_LIST_CONTROL } from './types';
import { OptionsListComponent, OptionsListComponentState } from './options_list_component';
import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from './types';
import { ControlsOptionsListService } from '../../services/options_list';
import { ControlsDataViewsService } from '../../services/data_views';
import { optionsListReducers } from './options_list_reducers';
import { OptionsListStrings } from './options_list_strings';
import { ControlInput, ControlOutput } from '../..';
import { pluginServices } from '../../services';
import { ControlsOptionsListService } from '../../services/options_list';

const OptionsListReduxWrapper = withSuspense<
ReduxEmbeddableWrapperPropsWithChildren<OptionsListEmbeddableInput>
Expand Down Expand Up @@ -76,7 +77,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
private typeaheadSubject: Subject<string> = new Subject<string>();
private abortController?: AbortController;
private dataView?: DataView;
private field?: DataViewField;
private field?: OptionsListField;
private searchString = '';

// State to be passed down to component
Expand Down Expand Up @@ -176,9 +177,9 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput

private getCurrentDataViewAndField = async (): Promise<{
dataView: DataView;
field: DataViewField;
field: OptionsListField;
}> => {
const { dataViewId, fieldName } = this.getInput();
const { dataViewId, fieldName, textFieldName } = this.getInput();
if (!this.dataView || this.dataView.id !== dataViewId) {
this.dataView = await this.dataViewsService.get(dataViewId);
if (this.dataView === undefined) {
Expand All @@ -190,7 +191,10 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
}

if (!this.field || this.field.name !== fieldName) {
this.field = this.dataView.getFieldByName(fieldName);
const originalField = this.dataView.getFieldByName(fieldName);
(originalField as OptionsListField).textFieldName = textFieldName;
this.field = originalField;

if (this.field === undefined) {
this.onFatalError(new Error(OptionsListStrings.errors.getDataViewNotFoundError(fieldName)));
}
Expand All @@ -212,7 +216,8 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
const { dataView, field } = await this.getCurrentDataViewAndField();
this.updateComponentState({ loading: true });
this.updateOutput({ loading: true, dataViews: [dataView] });
const { ignoreParentSettings, filters, query, selectedOptions, timeRange } = this.getInput();
const { ignoreParentSettings, filters, query, selectedOptions, timeRange, runPastTimeout } =
this.getInput();

if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
Expand All @@ -224,6 +229,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
filters,
dataView,
timeRange,
runPastTimeout,
selectedOptions,
searchString: this.searchString,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const OptionsListStrings = {
i18n.translate('controls.optionsList.editor.allowMultiselectTitle', {
defaultMessage: 'Allow multiple selections in dropdown',
}),
getRunPastTimeoutTitle: () =>
i18n.translate('controls.optionsList.editor.runPastTimeout', {
defaultMessage: 'Run past timeout',
}),
},
popover: {
getLoadingMessage: () =>
Expand Down
16 changes: 11 additions & 5 deletions src/plugins/controls/public/services/kibana/options_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
*/

import { memoize } from 'lodash';

import dateMath from '@kbn/datemath';
import { buildEsQuery } from '@kbn/es-query';

import { TimeRange } from '@kbn/data-plugin/public';
import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { ControlsOptionsListService, OptionsListRequest } from '../options_list';

import {
OptionsListRequestBody,
OptionsListRequest,
OptionsListResponse,
OptionsListRequestBody,
OptionsListField,
} from '../../control_types/options_list/types';
import { ControlsPluginStartDeps } from '../../types';
import { ControlsDataService } from '../data';
import { ControlsHTTPService } from '../http';
import { ControlsDataService } from '../data';
import { ControlsPluginStartDeps } from '../../types';
import { ControlsOptionsListService } from '../options_list';

class OptionsListService implements ControlsOptionsListService {
private data: ControlsDataService;
Expand All @@ -40,6 +43,7 @@ class OptionsListService implements ControlsOptionsListService {
filters,
timeRange,
searchString,
runPastTimeout,
selectedOptions,
field: { name: fieldName },
dataView: { title: dataViewTitle },
Expand All @@ -50,6 +54,7 @@ class OptionsListService implements ControlsOptionsListService {
selectedOptions?.join(','),
JSON.stringify(filters),
JSON.stringify(query),
runPastTimeout,
dataViewTitle,
searchString,
fieldName,
Expand Down Expand Up @@ -83,6 +88,7 @@ class OptionsListService implements ControlsOptionsListService {
filters: esFilters,
fieldName: field.name,
fieldSpec: field.toSpec?.(),
textFieldName: (field as OptionsListField).textFieldName,
};
};

Expand Down
17 changes: 1 addition & 16 deletions src/plugins/controls/public/services/options_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,7 @@
* Side Public License, v 1.
*/

import { Filter, Query } from '@kbn/es-query';

import { TimeRange } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { OptionsListRequestBody, OptionsListResponse } from '../control_types/options_list/types';

export type OptionsListRequest = Omit<
OptionsListRequestBody,
'filters' | 'fieldName' | 'fieldSpec'
> & {
timeRange?: TimeRange;
field: DataViewField;
dataView: DataView;
filters?: Filter[];
query?: Query;
};
import { OptionsListRequest, OptionsListResponse } from '../control_types/options_list/types';

export interface ControlsOptionsListService {
runOptionsListRequest: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
*/

import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { OptionsListResponse } from '../../control_types/options_list/types';
import { ControlsOptionsListService, OptionsListRequest } from '../options_list';

import { ControlsOptionsListService } from '../options_list';
import { OptionsListRequest, OptionsListResponse } from '../../control_types/options_list/types';

export type OptionsListServiceFactory = PluginServiceFactory<ControlsOptionsListService>;

Expand Down
Loading

0 comments on commit 11ae98d

Please sign in to comment.