Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Controls] Allow wildcard searching in options list #158427

Merged
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0678560
Fix animation of ignore timeout switch
Heenawter May 24, 2023
94c718c
Add toggle for wildcard search
Heenawter May 24, 2023
a09faaa
Add search string highlight
Heenawter May 24, 2023
8bd7650
Prevent highlighting for exists
Heenawter May 24, 2023
d3eb86e
Refetch available options when search technique changes
Heenawter May 25, 2023
837eabc
Clean up
Heenawter May 25, 2023
434fe84
Redesign control editor flyout
Heenawter May 25, 2023
4eb520b
Fix strings
Heenawter May 26, 2023
e402117
Fix title bug
Heenawter May 26, 2023
2468414
Clean up flyout logic
Heenawter May 26, 2023
f7b6226
Remove scroll lock
Heenawter May 26, 2023
e4671ec
Clean up
Heenawter May 29, 2023
0f36492
Only show search settings when allowExpensive is `true`
Heenawter May 29, 2023
57b44f6
Switch to string for search technique instead of boolean
Heenawter May 29, 2023
bac225c
Further cleanup onSave/cancel logic
Heenawter May 29, 2023
312f5b4
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine May 29, 2023
f3b0d0a
Fix add control flyout
Heenawter May 29, 2023
8291f6f
Fix default grow+width
Heenawter May 29, 2023
ccaad0e
Fix creation with custom grow/width
Heenawter May 30, 2023
cc96abf
Remove `.only`
Heenawter May 30, 2023
4326c1c
Add title tests
Heenawter May 30, 2023
95624b1
Fix tests
Heenawter May 30, 2023
c38465f
Add editor tests
Heenawter May 31, 2023
db67256
Add functional tests
Heenawter Jun 1, 2023
b7d4f23
Move selection logic to editor jest tests
Heenawter Jun 1, 2023
45860ab
Clean up title logic
Heenawter Jun 1, 2023
b51ee83
Final clean up
Heenawter Jun 1, 2023
fb8cd7a
Add queries tests
Heenawter Jun 1, 2023
a415401
Merge branch 'main' into options-list-wildcard-query_2023-05-24
Heenawter Jun 1, 2023
c692463
Clean up unnecessary import
Heenawter Jun 1, 2023
e282238
Switch to different escape method for wildcard query
Heenawter Jun 2, 2023
8ae0b3b
Add empty string checks to query builders
Heenawter Jun 2, 2023
79297e7
Change copy
Heenawter Jun 2, 2023
4f76348
Fix failing tests
Heenawter Jun 2, 2023
f4d92a9
Add custom placeholder text
Heenawter Jun 2, 2023
6e3247f
Fix failing snapshot test
Heenawter Jun 2, 2023
cf5b2e0
Remove unused translations
Heenawter Jun 2, 2023
1a6cde8
Merge branch 'main' into options-list-wildcard-query_2023-05-24
Heenawter Jun 5, 2023
7f0aeab
Fix applying of `grow`
Heenawter Jun 5, 2023
6601b7f
Replace `'prefix'` with constant
Heenawter Jun 5, 2023
1da2197
Unskip test
Heenawter Jun 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -40,6 +40,7 @@ export const ControlPanelDiffSystems: {
hideExclude: hideExcludeA,
selectedOptions: selectedA,
singleSelect: singleSelectA,
searchTechnique: searchTechniqueA,
existsSelected: existsSelectedA,
runPastTimeout: runPastTimeoutA,
...inputA
Expand All @@ -52,6 +53,7 @@ export const ControlPanelDiffSystems: {
hideExclude: hideExcludeB,
selectedOptions: selectedB,
singleSelect: singleSelectB,
searchTechnique: searchTechniqueB,
existsSelected: existsSelectedB,
runPastTimeout: runPastTimeoutB,
...inputB
Expand All @@ -65,6 +67,7 @@ export const ControlPanelDiffSystems: {
Boolean(singleSelectA) === Boolean(singleSelectB) &&
Boolean(existsSelectedA) === Boolean(existsSelectedB) &&
Boolean(runPastTimeoutA) === Boolean(runPastTimeoutB) &&
isEqual(searchTechniqueA ?? 'prefix', searchTechniqueB ?? 'prefix') &&
Heenawter marked this conversation as resolved.
Show resolved Hide resolved
deepEqual(sortA ?? OPTIONS_LIST_DEFAULT_SORT, sortB ?? OPTIONS_LIST_DEFAULT_SORT) &&
isEqual(selectedA ?? [], selectedB ?? []) &&
deepEqual(inputA, inputB)
Expand Down
17 changes: 16 additions & 1 deletion src/plugins/controls/common/control_group/mocks.tsx
Expand Up @@ -6,8 +6,10 @@
* Side Public License, v 1.
*/

import { getDefaultControlGroupInput } from '..';
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import { ControlGroupInput } from './types';
import { getDefaultControlGroupInput } from '..';
import { ControlGroupContainerFactory } from '../../public';

export const mockControlGroupInput = (partial?: Partial<ControlGroupInput>): ControlGroupInput => ({
id: 'mocked_control_group',
Expand Down Expand Up @@ -45,3 +47,16 @@ export const mockControlGroupInput = (partial?: Partial<ControlGroupInput>): Con
},
...(partial ?? {}),
});

export const mockControlGroupContainer = async (explicitInput?: Partial<ControlGroupInput>) => {
const controlGroupFactoryStub = new ControlGroupContainerFactory(
{} as unknown as EmbeddablePersistableStateService
);
const controlGroupContainer = await controlGroupFactoryStub.create({
id: 'mocked-control-group',
...getDefaultControlGroupInput(),
...explicitInput,
});

return controlGroupContainer;
};
7 changes: 5 additions & 2 deletions src/plugins/controls/common/options_list/mocks.tsx
Expand Up @@ -7,7 +7,7 @@
*/

import { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public';
import { OptionsListComponentState, OptionsListReduxState } from '../../public/options_list/types';
import { OptionsListComponentState } from '../../public/options_list/types';
import { ControlFactory, ControlOutput } from '../../public/types';
import { OptionsListEmbeddableInput } from './types';

Expand Down Expand Up @@ -44,7 +44,10 @@ const mockOptionsListOutput = {
loading: false,
} as ControlOutput;

export const mockOptionsListEmbeddable = async (partialState?: Partial<OptionsListReduxState>) => {
export const mockOptionsListEmbeddable = async (partialState?: {
explicitInput?: Partial<OptionsListEmbeddableInput>;
componentState?: Partial<OptionsListComponentState>;
}) => {
const optionsListFactoryStub = new OptionsListEmbeddableFactory();
const optionsListControlFactory = optionsListFactoryStub as unknown as ControlFactory;
optionsListControlFactory.getDefaultInput = () => ({});
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/controls/common/options_list/types.ts
Expand Up @@ -14,7 +14,10 @@ import type { DataControlInput } from '../types';

export const OPTIONS_LIST_CONTROL = 'optionsListControl';

export type OptionsListSearchTechnique = 'prefix' | 'wildcard';

export interface OptionsListEmbeddableInput extends DataControlInput {
searchTechnique?: OptionsListSearchTechnique;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally, I had this as a boolean wildcardQuery property that would be true only if the user selected the "contains" search technique; however, after further thought about extensibility and preventing migrations, I think it makes more sense to store this as a string value.

After all, right now we only support two search types (both of which only work for "string" type fields) - "prefix" and "contains" searching. But what about when we add support for number fields to the options list control - perhaps search techniques like "less than" and "equal to" might make more sense? Or right now, we have essentially a version of "contains" for IP fields - but maybe this doesn't always make sense, and we want to introduce other searching techniques for IP fields in the future? Having the search technique stored as a string from the very beginning rather than a boolean value allows for these changes without having to worry about a potential migration :)

sort?: OptionsListSortingType;
selectedOptions?: string[];
existsSelected?: boolean;
Expand All @@ -23,9 +26,9 @@ export interface OptionsListEmbeddableInput extends DataControlInput {
hideActionBar?: boolean;
hideExclude?: boolean;
hideExists?: boolean;
placeholder?: string;
hideSort?: boolean;
exclude?: boolean;
placeholder?: string;
}

export type OptionsListSuggestions = Array<{ value: string; docCount?: number }>;
Expand Down Expand Up @@ -61,6 +64,7 @@ export type OptionsListRequest = Omit<
OptionsListRequestBody,
'filters' | 'fieldName' | 'fieldSpec' | 'textFieldName'
> & {
searchTechnique?: OptionsListSearchTechnique;
allowExpensiveQueries: boolean;
timeRange?: TimeRange;
runPastTimeout?: boolean;
Expand All @@ -75,6 +79,7 @@ export type OptionsListRequest = Omit<
*/
export interface OptionsListRequestBody {
runtimeFieldMap?: Record<string, RuntimeFieldSpec>;
searchTechnique?: OptionsListSearchTechnique;
allowExpensiveQueries: boolean;
sort?: OptionsListSortingType;
filters?: Array<{ bool: BoolQuery }>;
Expand Down
Expand Up @@ -115,8 +115,6 @@ export class EditControlAction implements Action<EditControlActionContext> {
flyout.close();
},
ownFocus: true,
// @ts-ignore - TODO: Remove this once https://github.com/elastic/eui/pull/6645 lands in Kibana
focusTrapProps: { scrollLock: true },
}
);
setFlyoutRef(flyoutInstance);
Expand Down
Expand Up @@ -7,11 +7,16 @@
*/

import { isEqual } from 'lodash';
import React, { useState } from 'react';
import React from 'react';

import { EmbeddableFactoryNotFoundError } from '@kbn/embeddable-plugin/public';

import { DataControlInput, ControlEmbeddable, IEditableControlFactory } from '../../types';
import {
DataControlInput,
ControlEmbeddable,
IEditableControlFactory,
DataControlEditorChanges,
} from '../../types';
import { pluginServices } from '../../services';
import { ControlGroupStrings } from '../control_group_strings';
import { useControlGroupContainer } from '../embeddable/control_group_container';
Expand All @@ -38,18 +43,14 @@ export const EditControlFlyout = ({
const panels = controlGroup.select((state) => state.explicitInput.panels);
const panel = panels[embeddable.id];

const [currentGrow, setCurrentGrow] = useState(panel.grow);
const [currentWidth, setCurrentWidth] = useState(panel.width);
const [inputToReturn, setInputToReturn] = useState<Partial<DataControlInput>>({});

const onCancel = () => {
const onCancel = (changes: DataControlEditorChanges) => {
if (
isEqual(panel.explicitInput, {
...panel.explicitInput,
...inputToReturn,
...changes.input,
}) &&
currentGrow === panel.grow &&
currentWidth === panel.width
changes.grow === panel.grow &&
changes.width === panel.width
) {
closeFlyout();
return;
Expand All @@ -66,22 +67,28 @@ export const EditControlFlyout = ({
});
};

const onSave = async (type?: string) => {
const onSave = async (changes: DataControlEditorChanges, type?: string) => {
if (!type) {
closeFlyout();
return;
}

const factory = getControlFactory(type) as IEditableControlFactory;
if (!factory) throw new EmbeddableFactoryNotFoundError(type);
let inputToReturn = changes.input;
if (factory.presaveTransformFunction) {
setInputToReturn(factory.presaveTransformFunction(inputToReturn, embeddable));
inputToReturn = factory.presaveTransformFunction(inputToReturn, embeddable);
}

if (currentWidth !== panel.width)
controlGroup.dispatch.setControlWidth({ width: currentWidth, embeddableId: embeddable.id });
if (currentGrow !== panel.grow)
controlGroup.dispatch.setControlGrow({ grow: currentGrow, embeddableId: embeddable.id });
if (changes.width && changes.width !== panel.width)
controlGroup.dispatch.setControlWidth({
width: changes.width,
embeddableId: embeddable.id,
});
if (changes.grow && changes.grow !== panel.grow)
controlGroup.dispatch.setControlGrow({
grow: changes.grow,
embeddableId: embeddable.id,
});

closeFlyout();
await controlGroup.replaceEmbeddable(embeddable.id, inputToReturn, type);
Expand All @@ -93,16 +100,9 @@ export const EditControlFlyout = ({
width={panel.width}
grow={panel.grow}
embeddable={embeddable}
title={embeddable.getTitle()}
onCancel={() => onCancel()}
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
onCancel={onCancel}
setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)}
updateWidth={(newWidth) => setCurrentWidth(newWidth)}
updateGrow={(newGrow) => setCurrentGrow(newGrow)}
onTypeEditorChange={(partialInput) => {
setInputToReturn({ ...inputToReturn, ...partialInput });
}}
onSave={(type) => onSave(type)}
onSave={onSave}
removeControl={() => {
closeFlyout();
removeControl();
Expand Down
103 changes: 67 additions & 36 deletions src/plugins/controls/public/control_group/control_group_strings.ts
Expand Up @@ -41,30 +41,73 @@ export const ControlGroupStrings = {
i18n.translate('controls.controlGroup.manageControl.editFlyoutTitle', {
defaultMessage: 'Edit control',
}),
getDataViewTitle: () =>
i18n.translate('controls.controlGroup.manageControl.dataViewTitle', {
defaultMessage: 'Data view',
}),
getFieldTitle: () =>
i18n.translate('controls.controlGroup.manageControl.fielditle', {
defaultMessage: 'Field',
}),
getTitleInputTitle: () =>
i18n.translate('controls.controlGroup.manageControl.titleInputTitle', {
defaultMessage: 'Label',
}),
getControlTypeTitle: () =>
i18n.translate('controls.controlGroup.manageControl.controlTypesTitle', {
defaultMessage: 'Control type',
}),
getWidthInputTitle: () =>
i18n.translate('controls.controlGroup.manageControl.widthInputTitle', {
defaultMessage: 'Minimum width',
}),
getControlSettingsTitle: () =>
i18n.translate('controls.controlGroup.manageControl.controlSettingsTitle', {
defaultMessage: 'Additional settings',
}),
dataSource: {
getFormGroupTitle: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.formGroupTitle', {
defaultMessage: 'Data source',
}),
getFormGroupDescription: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.formGroupDescription', {
defaultMessage: 'Select the data view and field that you want to create a control for.',
}),
getSelectDataViewMessage: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.selectDataViewMessage', {
defaultMessage: 'Please select a data view',
}),
getDataViewTitle: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.dataViewTitle', {
defaultMessage: 'Data view',
}),
getSelectFieldMessage: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.selectFieldMessage', {
defaultMessage: 'Please select a field',
}),
getFieldTitle: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.fieldTitle', {
defaultMessage: 'Field',
}),
getControlTypeTitle: () =>
i18n.translate('controls.controlGroup.manageControl.dataSource.controlTypesTitle', {
defaultMessage: 'Control type',
}),
},
displaySettings: {
getFormGroupTitle: () =>
i18n.translate('controls.controlGroup.manageControl.displaySettings.formGroupTitle', {
defaultMessage: 'Display settings',
}),
getFormGroupDescription: () =>
i18n.translate('controls.controlGroup.manageControl.displaySettings.formGroupDescription', {
defaultMessage: 'Change how the control will appear on your dashboard.',
}),
getTitleInputTitle: () =>
i18n.translate('controls.controlGroup.manageControl.displaySettings.titleInputTitle', {
defaultMessage: 'Label',
}),
getWidthInputTitle: () =>
i18n.translate('controls.controlGroup.manageControl.displaySettings.widthInputTitle', {
defaultMessage: 'Minimum width',
}),
getGrowSwitchTitle: () =>
i18n.translate('controls.controlGroup.manageControl.displaySettings.growSwitchTitle', {
defaultMessage: 'Expand width to fit available space',
}),
},
controlTypeSettings: {
getFormGroupTitle: (type: string) =>
i18n.translate('controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle', {
defaultMessage: '{controlType} settings',
values: { controlType: type },
}),
getFormGroupDescription: (type: string) =>
i18n.translate(
'controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription',
{
defaultMessage: 'Custom settings for your {controlType} control.',
values: { controlType: type.toLocaleLowerCase() },
}
),
},
Heenawter marked this conversation as resolved.
Show resolved Hide resolved
getSaveChangesTitle: () =>
i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', {
defaultMessage: 'Save and close',
Expand All @@ -73,18 +116,6 @@ export const ControlGroupStrings = {
i18n.translate('controls.controlGroup.manageControl.cancelTitle', {
defaultMessage: 'Cancel',
}),
getSelectFieldMessage: () =>
i18n.translate('controls.controlGroup.manageControl.selectFieldMessage', {
defaultMessage: 'Please select a field',
}),
getSelectDataViewMessage: () =>
i18n.translate('controls.controlGroup.manageControl.selectDataViewMessage', {
defaultMessage: 'Please select a data view',
}),
getGrowSwitchTitle: () =>
i18n.translate('controls.controlGroup.manageControl.growSwitchTitle', {
defaultMessage: 'Expand width to fit available space',
}),
},
management: {
getAddControlTitle: () =>
Expand Down