[Controls] Field first control creation (#131461)

* Field first *creation*

* Field first *editing*

* Add support for custom control options

* Add i18n

* Make field picker accept predicate again + clean up imports

* Fix functional tests

* Attempt 1 at case sensitivity

* Works both ways

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Clean up code

* Use React useMemo to calculate field registry

* Fix functional tests

* Fix default state + control settings label

* Fix functional tests

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Hannah Mudge 2022-05-19 10:12:00 -06:00 committed by GitHub
parent e2827350e9
commit 8de3401dff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 480 additions and 637 deletions

View file

@ -17,7 +17,6 @@ export const OPTIONS_LIST_CONTROL = 'optionsListControl';
export interface OptionsListEmbeddableInput extends DataControlInput {
selectedOptions?: string[];
runPastTimeout?: boolean;
textFieldName?: string;
singleSelect?: boolean;
loading?: boolean;
}

View file

@ -30,5 +30,7 @@ export type ControlInput = EmbeddableInput & {
export type DataControlInput = ControlInput & {
fieldName: string;
parentFieldName?: string;
childFieldName?: string;
dataViewId: string;
};

View file

@ -44,6 +44,14 @@ 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',
@ -56,6 +64,10 @@ export const ControlGroupStrings = {
i18n.translate('controls.controlGroup.manageControl.widthInputTitle', {
defaultMessage: 'Minimum width',
}),
getControlSettingsTitle: () =>
i18n.translate('controls.controlGroup.manageControl.controlSettingsTitle', {
defaultMessage: 'Additional settings',
}),
getSaveChangesTitle: () =>
i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', {
defaultMessage: 'Save and close',
@ -64,6 +76,14 @@ 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',

View file

@ -14,7 +14,9 @@
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import {
EuiFlyoutHeader,
EuiButtonGroup,
@ -29,32 +31,35 @@ import {
EuiForm,
EuiButtonEmpty,
EuiSpacer,
EuiKeyPadMenu,
EuiKeyPadMenuItem,
EuiIcon,
EuiToolTip,
EuiSwitch,
EuiTextColor,
} from '@elastic/eui';
import { DataViewListItem, DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { IFieldSubTypeMulti } from '@kbn/es-query';
import {
LazyDataViewPicker,
LazyFieldPicker,
withSuspense,
} from '@kbn/presentation-util-plugin/public';
import { EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
import { ControlGroupStrings } from '../control_group_strings';
import {
ControlEmbeddable,
ControlInput,
ControlWidth,
DataControlFieldRegistry,
DataControlInput,
IEditableControlFactory,
} from '../../types';
import { CONTROL_WIDTH_OPTIONS } from './editor_constants';
import { pluginServices } from '../../services';
interface EditControlProps {
embeddable?: ControlEmbeddable;
embeddable?: ControlEmbeddable<DataControlInput>;
isCreate: boolean;
title?: string;
width: ControlWidth;
onSave: (type?: string) => void;
grow: boolean;
onSave: (type: string) => void;
onCancel: () => void;
removeControl?: () => void;
updateGrow?: (grow: boolean) => void;
@ -62,9 +67,18 @@ interface EditControlProps {
updateWidth: (newWidth: ControlWidth) => void;
getRelevantDataViewId?: () => string | undefined;
setLastUsedDataViewId?: (newDataViewId: string) => void;
onTypeEditorChange: (partial: Partial<ControlInput>) => void;
onTypeEditorChange: (partial: Partial<DataControlInput>) => void;
}
interface ControlEditorState {
dataViewListItems: DataViewListItem[];
selectedDataView?: DataView;
selectedField?: DataViewField;
}
const FieldPicker = withSuspense(LazyFieldPicker, null);
const DataViewPicker = withSuspense(LazyDataViewPicker, null);
export const ControlEditor = ({
embeddable,
isCreate,
@ -81,81 +95,104 @@ export const ControlEditor = ({
getRelevantDataViewId,
setLastUsedDataViewId,
}: EditControlProps) => {
const { dataViews } = pluginServices.getHooks();
const { getIdsWithTitle, getDefaultId, get } = dataViews.useService();
const { controls } = pluginServices.getServices();
const { getControlTypes, getControlFactory } = controls;
const [state, setState] = useState<ControlEditorState>({
dataViewListItems: [],
});
const [selectedType, setSelectedType] = useState(
!isCreate && embeddable ? embeddable.type : getControlTypes()[0]
);
const [defaultTitle, setDefaultTitle] = useState<string>();
const [currentTitle, setCurrentTitle] = useState(title);
const [currentWidth, setCurrentWidth] = useState(width);
const [currentGrow, setCurrentGrow] = useState(grow);
const [controlEditorValid, setControlEditorValid] = useState(false);
const [selectedField, setSelectedField] = useState<string | undefined>(
embeddable
? (embeddable.getInput() as DataControlInput).fieldName // CLEAN THIS ONCE OTHER PR GETS IN
: undefined
embeddable ? embeddable.getInput().fieldName : undefined
);
const getControlTypeEditor = (type: string) => {
const factory = getControlFactory(type);
const ControlTypeEditor = (factory as IEditableControlFactory).controlEditorComponent;
return ControlTypeEditor ? (
<ControlTypeEditor
getRelevantDataViewId={getRelevantDataViewId}
setLastUsedDataViewId={setLastUsedDataViewId}
onChange={onTypeEditorChange}
setValidState={setControlEditorValid}
initialInput={embeddable?.getInput()}
selectedField={selectedField}
setSelectedField={setSelectedField}
setDefaultTitle={(newDefaultTitle) => {
if (!currentTitle || currentTitle === defaultTitle) {
setCurrentTitle(newDefaultTitle);
updateTitle(newDefaultTitle);
}
setDefaultTitle(newDefaultTitle);
}}
/>
) : null;
const doubleLinkFields = (dataView: DataView) => {
// double link the parent-child relationship specifically for case-sensitivity support for options lists
const fieldRegistry: DataControlFieldRegistry = {};
for (const field of dataView.fields.getAll()) {
if (!fieldRegistry[field.name]) {
fieldRegistry[field.name] = { field, compatibleControlTypes: [] };
}
const parentFieldName = (field.subType as IFieldSubTypeMulti)?.multi?.parent;
if (parentFieldName) {
fieldRegistry[field.name].parentFieldName = parentFieldName;
const parentField = dataView.getFieldByName(parentFieldName);
if (!fieldRegistry[parentFieldName] && parentField) {
fieldRegistry[parentFieldName] = { field: parentField, compatibleControlTypes: [] };
}
fieldRegistry[parentFieldName].childFieldName = field.name;
}
}
return fieldRegistry;
};
const getTypeButtons = () => {
return getControlTypes().map((type) => {
const factory = getControlFactory(type);
const icon = (factory as EmbeddableFactoryDefinition).getIconType?.();
const tooltip = (factory as EmbeddableFactoryDefinition).getDescription?.();
const menuPadItem = (
<EuiKeyPadMenuItem
id={`createControlButton_${type}`}
data-test-subj={`create-${type}-control`}
label={(factory as EmbeddableFactoryDefinition).getDisplayName()}
isSelected={selectedType === type}
onClick={() => {
setSelectedType(type);
if (!isCreate)
setSelectedField(
embeddable && type === embeddable.type
? (embeddable.getInput() as DataControlInput).fieldName
: undefined
);
}}
>
<EuiIcon type={!icon || icon === 'empty' ? 'controlsHorizontal' : icon} size="l" />
</EuiKeyPadMenuItem>
);
const fieldRegistry = useMemo(() => {
if (!state.selectedDataView) return;
const newFieldRegistry: DataControlFieldRegistry = doubleLinkFields(state.selectedDataView);
return tooltip ? (
<EuiToolTip content={tooltip} position="top">
{menuPadItem}
</EuiToolTip>
) : (
menuPadItem
);
const controlFactories = getControlTypes().map(
(controlType) => getControlFactory(controlType) as IEditableControlFactory
);
state.selectedDataView.fields.map((dataViewField) => {
for (const factory of controlFactories) {
if (factory.isFieldCompatible) {
factory.isFieldCompatible(newFieldRegistry[dataViewField.name]);
}
}
if (newFieldRegistry[dataViewField.name]?.compatibleControlTypes.length === 0) {
delete newFieldRegistry[dataViewField.name];
}
});
};
return newFieldRegistry;
}, [state.selectedDataView, getControlFactory, getControlTypes]);
useMount(() => {
let mounted = true;
if (selectedField) setDefaultTitle(selectedField);
(async () => {
const dataViewListItems = await getIdsWithTitle();
const initialId =
embeddable?.getInput().dataViewId ?? getRelevantDataViewId?.() ?? (await getDefaultId());
let dataView: DataView | undefined;
if (initialId) {
onTypeEditorChange({ dataViewId: initialId });
dataView = await get(initialId);
}
if (!mounted) return;
setState((s) => ({
...s,
selectedDataView: dataView,
dataViewListItems,
}));
})();
return () => {
mounted = false;
};
});
useEffect(
() => setControlEditorValid(Boolean(selectedField) && Boolean(state.selectedDataView)),
[selectedField, setControlEditorValid, state.selectedDataView]
);
const { selectedDataView: dataView } = state;
const controlType =
selectedField && fieldRegistry && fieldRegistry[selectedField].compatibleControlTypes[0];
const factory = controlType && getControlFactory(controlType);
const CustomSettings =
factory && (factory as IEditableControlFactory).controlEditorOptionsComponent;
return (
<>
<EuiFlyoutHeader hasBorder>
@ -169,64 +206,124 @@ export const ControlEditor = ({
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj="control-editor-flyout">
<EuiForm>
<EuiFormRow label={ControlGroupStrings.manageControl.getControlTypeTitle()}>
<EuiKeyPadMenu>{getTypeButtons()}</EuiKeyPadMenu>
<EuiFormRow label={ControlGroupStrings.manageControl.getDataViewTitle()}>
<DataViewPicker
dataViews={state.dataViewListItems}
selectedDataViewId={dataView?.id}
onChangeDataViewId={(dataViewId) => {
setLastUsedDataViewId?.(dataViewId);
if (dataViewId === dataView?.id) return;
onTypeEditorChange({ dataViewId });
setSelectedField(undefined);
get(dataViewId).then((newDataView) => {
setState((s) => ({ ...s, selectedDataView: newDataView }));
});
}}
trigger={{
label:
state.selectedDataView?.title ??
ControlGroupStrings.manageControl.getSelectDataViewMessage(),
}}
/>
</EuiFormRow>
{selectedType && (
<EuiFormRow label={ControlGroupStrings.manageControl.getFieldTitle()}>
<FieldPicker
filterPredicate={(field: DataViewField) => {
return Boolean(fieldRegistry?.[field.name]);
}}
selectedFieldName={selectedField}
dataView={dataView}
onSelectField={(field) => {
onTypeEditorChange({
fieldName: field.name,
parentFieldName: fieldRegistry?.[field.name].parentFieldName,
childFieldName: fieldRegistry?.[field.name].childFieldName,
});
const newDefaultTitle = field.displayName ?? field.name;
setDefaultTitle(newDefaultTitle);
setSelectedField(field.name);
if (!currentTitle || currentTitle === defaultTitle) {
setCurrentTitle(newDefaultTitle);
updateTitle(newDefaultTitle);
}
}}
/>
</EuiFormRow>
<EuiFormRow label={ControlGroupStrings.manageControl.getControlTypeTitle()}>
{factory ? (
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type={factory.getIconType()} />
</EuiFlexItem>
<EuiFlexItem data-test-subj="control-editor-type">
{factory.getDisplayName()}
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiTextColor color="subdued" data-test-subj="control-editor-type">
{ControlGroupStrings.manageControl.getSelectFieldMessage()}
</EuiTextColor>
)}
</EuiFormRow>
<EuiFormRow label={ControlGroupStrings.manageControl.getTitleInputTitle()}>
<EuiFieldText
data-test-subj="control-editor-title-input"
placeholder={defaultTitle}
value={currentTitle}
onChange={(e) => {
updateTitle(e.target.value || defaultTitle);
setCurrentTitle(e.target.value);
}}
/>
</EuiFormRow>
<EuiFormRow label={ControlGroupStrings.manageControl.getWidthInputTitle()}>
<EuiButtonGroup
color="primary"
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
options={CONTROL_WIDTH_OPTIONS}
idSelected={currentWidth}
onChange={(newWidth: string) => {
setCurrentWidth(newWidth as ControlWidth);
updateWidth(newWidth as ControlWidth);
}}
/>
</EuiFormRow>
{updateGrow ? (
<EuiFormRow>
<EuiSwitch
label={ControlGroupStrings.manageControl.getGrowSwitchTitle()}
color="primary"
checked={currentGrow}
onChange={() => {
setCurrentGrow(!currentGrow);
updateGrow(!currentGrow);
}}
data-test-subj="control-editor-grow-switch"
/>
</EuiFormRow>
) : null}
{CustomSettings && (factory as IEditableControlFactory).controlEditorOptionsComponent && (
<EuiFormRow label={ControlGroupStrings.manageControl.getControlSettingsTitle()}>
<CustomSettings onChange={onTypeEditorChange} initialInput={embeddable?.getInput()} />
</EuiFormRow>
)}
{removeControl && (
<>
{getControlTypeEditor(selectedType)}
<EuiFormRow label={ControlGroupStrings.manageControl.getTitleInputTitle()}>
<EuiFieldText
data-test-subj="control-editor-title-input"
placeholder={defaultTitle}
value={currentTitle}
onChange={(e) => {
updateTitle(e.target.value || defaultTitle);
setCurrentTitle(e.target.value);
}}
/>
</EuiFormRow>
<EuiFormRow label={ControlGroupStrings.manageControl.getWidthInputTitle()}>
<EuiButtonGroup
color="primary"
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
options={CONTROL_WIDTH_OPTIONS}
idSelected={currentWidth}
onChange={(newWidth: string) => {
setCurrentWidth(newWidth as ControlWidth);
updateWidth(newWidth as ControlWidth);
}}
/>
</EuiFormRow>
{updateGrow ? (
<EuiFormRow>
<EuiSwitch
label={ControlGroupStrings.manageControl.getGrowSwitchTitle()}
color="primary"
checked={currentGrow}
onChange={() => {
setCurrentGrow(!currentGrow);
updateGrow(!currentGrow);
}}
data-test-subj="control-editor-grow-switch"
/>
</EuiFormRow>
) : null}
<EuiSpacer size="l" />
{removeControl && (
<EuiButtonEmpty
aria-label={`delete-${title}`}
iconType="trash"
flush="left"
color="danger"
onClick={() => {
onCancel();
removeControl();
}}
>
{ControlGroupStrings.management.getDeleteButtonTitle()}
</EuiButtonEmpty>
)}
<EuiButtonEmpty
aria-label={`delete-${title}`}
iconType="trash"
flush="left"
color="danger"
onClick={() => {
onCancel();
removeControl();
}}
>
{ControlGroupStrings.management.getDeleteButtonTitle()}
</EuiButtonEmpty>
</>
)}
</EuiForm>
@ -250,7 +347,7 @@ export const ControlEditor = ({
iconType="check"
color="primary"
disabled={!controlEditorValid}
onClick={() => onSave(selectedType)}
onClick={() => onSave(controlType)}
>
{ControlGroupStrings.manageControl.getSaveChangesTitle()}
</EuiButton>

View file

@ -14,7 +14,7 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { pluginServices } from '../../services';
import { ControlEditor } from './control_editor';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlWidth, ControlInput, IEditableControlFactory } from '../../types';
import { ControlWidth, ControlInput, IEditableControlFactory, DataControlInput } from '../../types';
import {
DEFAULT_CONTROL_WIDTH,
DEFAULT_CONTROL_GROW,
@ -59,7 +59,7 @@ export const CreateControlButton = ({
const PresentationUtilProvider = pluginServices.getContextProvider();
const initialInputPromise = new Promise<CreateControlResult>((resolve, reject) => {
let inputToReturn: Partial<ControlInput> = {};
let inputToReturn: Partial<DataControlInput> = {};
const onCancel = (ref: OverlayRef) => {
if (Object.keys(inputToReturn).length === 0) {
@ -80,6 +80,21 @@ export const CreateControlButton = ({
});
};
const onSave = (ref: OverlayRef, type?: string) => {
if (!type) {
reject();
ref.close();
return;
}
const factory = getControlFactory(type) as IEditableControlFactory;
if (factory.presaveTransformFunction) {
inputToReturn = factory.presaveTransformFunction(inputToReturn);
}
resolve({ type, controlInput: inputToReturn });
ref.close();
};
const flyoutInstance = openFlyout(
toMountPoint(
<PresentationUtilProvider>
@ -92,14 +107,7 @@ export const CreateControlButton = ({
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
updateWidth={updateDefaultWidth}
updateGrow={updateDefaultGrow}
onSave={(type: string) => {
const factory = getControlFactory(type) as IEditableControlFactory;
if (factory.presaveTransformFunction) {
inputToReturn = factory.presaveTransformFunction(inputToReturn);
}
resolve({ type, controlInput: inputToReturn });
flyoutInstance.close();
}}
onSave={(type) => onSave(flyoutInstance, type)}
onCancel={() => onCancel(flyoutInstance)}
onTypeEditorChange={(partialInput) =>
(inputToReturn = { ...inputToReturn, ...partialInput })

View file

@ -11,14 +11,19 @@ import { EuiButtonIcon } from '@elastic/eui';
import React, { useEffect, useRef } from 'react';
import { OverlayRef } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { EmbeddableFactoryNotFoundError } from '@kbn/embeddable-plugin/public';
import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public';
import { ControlGroupInput } from '../types';
import { ControlEditor } from './control_editor';
import { pluginServices } from '../../services';
import { forwardAllContext } from './forward_all_context';
import { ControlGroupStrings } from '../control_group_strings';
import { IEditableControlFactory, ControlInput } from '../../types';
import {
IEditableControlFactory,
ControlInput,
DataControlInput,
ControlEmbeddable,
} from '../../types';
import { controlGroupReducers } from '../state/control_group_reducers';
import { ControlGroupContainer, setFlyoutRef } from '../embeddable/control_group_container';
@ -56,15 +61,19 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) =>
}, [panels, embeddableId]);
const editControl = async () => {
const panel = panels[embeddableId];
let factory = getControlFactory(panel.type);
if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
const embeddable = await untilEmbeddableLoaded(embeddableId);
const controlGroup = embeddable.getRoot() as ControlGroupContainer;
const PresentationUtilProvider = pluginServices.getContextProvider();
const embeddable = (await untilEmbeddableLoaded(
embeddableId
)) as ControlEmbeddable<DataControlInput>;
const initialInputPromise = new Promise<EditControlResult>((resolve, reject) => {
let inputToReturn: Partial<ControlInput> = {};
const panel = panels[embeddableId];
let factory = getControlFactory(panel.type);
if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
const controlGroup = embeddable.getRoot() as ControlGroupContainer;
let inputToReturn: Partial<DataControlInput> = {};
let removed = false;
const onCancel = (ref: OverlayRef) => {
@ -94,7 +103,13 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) =>
});
};
const onSave = (type: string, ref: OverlayRef) => {
const onSave = (ref: OverlayRef, type?: string) => {
if (!type) {
reject();
ref.close();
return;
}
// if the control now has a new type, need to replace the old factory with
// one of the correct new type
if (latestPanelState.current.type !== type) {
@ -110,44 +125,47 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) =>
};
const flyoutInstance = openFlyout(
forwardAllContext(
<ControlEditor
isCreate={false}
width={panel.width}
grow={panel.grow}
embeddable={embeddable}
title={embeddable.getTitle()}
onCancel={() => onCancel(flyoutInstance)}
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)}
updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, embeddableId }))}
updateGrow={(grow) => dispatch(setControlGrow({ grow, embeddableId }))}
onTypeEditorChange={(partialInput) => {
inputToReturn = { ...inputToReturn, ...partialInput };
}}
onSave={(type) => onSave(type, flyoutInstance)}
removeControl={() => {
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
title: ControlGroupStrings.management.deleteControls.getDeleteTitle(),
buttonColor: 'danger',
}).then((confirmed) => {
if (confirmed) {
removeEmbeddable(embeddableId);
removed = true;
flyoutInstance.close();
}
});
}}
/>,
reduxContainerContext
toMountPoint(
<PresentationUtilProvider>
<ControlEditor
isCreate={false}
width={panel.width}
grow={panel.grow}
embeddable={embeddable}
title={embeddable.getTitle()}
onCancel={() => onCancel(flyoutInstance)}
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)}
updateWidth={(newWidth) =>
dispatch(setControlWidth({ width: newWidth, embeddableId }))
}
updateGrow={(grow) => dispatch(setControlGrow({ grow, embeddableId }))}
onTypeEditorChange={(partialInput) => {
inputToReturn = { ...inputToReturn, ...partialInput };
}}
onSave={(type) => onSave(flyoutInstance, type)}
removeControl={() => {
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
title: ControlGroupStrings.management.deleteControls.getDeleteTitle(),
buttonColor: 'danger',
}).then((confirmed) => {
if (confirmed) {
removeEmbeddable(embeddableId);
removed = true;
flyoutInstance.close();
}
});
}}
/>
</PresentationUtilProvider>
),
{
outsideClickCloses: false,
onClose: (flyout) => {
setFlyoutRef(undefined);
onCancel(flyout);
setFlyoutRef(undefined);
},
}
);

View file

@ -1,182 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import useMount from 'react-use/lib/useMount';
import React, { useEffect, useState } from 'react';
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 { OptionsListStrings } from './options_list_strings';
import { OptionsListEmbeddableInput, OptionsListField } from './types';
interface OptionsListEditorState {
singleSelect?: boolean;
runPastTimeout?: boolean;
dataViewListItems: DataViewListItem[];
fieldsMap?: { [key: string]: OptionsListField };
dataView?: DataView;
}
const FieldPicker = withSuspense(LazyFieldPicker, null);
const DataViewPicker = withSuspense(LazyDataViewPicker, null);
export const OptionsListEditor = ({
onChange,
initialInput,
setValidState,
setDefaultTitle,
getRelevantDataViewId,
setLastUsedDataViewId,
selectedField,
setSelectedField,
}: ControlEditorProps<OptionsListEmbeddableInput>) => {
// Controls Services Context
const { dataViews } = pluginServices.getHooks();
const { getIdsWithTitle, getDefaultId, get } = dataViews.useService();
const [state, setState] = useState<OptionsListEditorState>({
singleSelect: initialInput?.singleSelect,
runPastTimeout: initialInput?.runPastTimeout,
dataViewListItems: [],
});
useMount(() => {
let mounted = true;
if (selectedField) setDefaultTitle(selectedField);
(async () => {
const dataViewListItems = await getIdsWithTitle();
const initialId =
initialInput?.dataViewId ?? getRelevantDataViewId?.() ?? (await getDefaultId());
let dataView: DataView | undefined;
if (initialId) {
onChange({ dataViewId: initialId });
dataView = await get(initialId);
}
if (!mounted) return;
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(selectedField) && Boolean(state.dataView)),
[selectedField, setValidState, state.dataView]
);
const { dataView } = state;
return (
<>
<EuiFormRow label={OptionsListStrings.editor.getDataViewTitle()}>
<DataViewPicker
dataViews={state.dataViewListItems}
selectedDataViewId={dataView?.id}
onChangeDataViewId={(dataViewId) => {
setLastUsedDataViewId?.(dataViewId);
if (dataViewId === dataView?.id) return;
onChange({ dataViewId });
setSelectedField(undefined);
get(dataViewId).then((newDataView) => {
setState((s) => ({ ...s, dataView: newDataView }));
});
}}
trigger={{
label: state.dataView?.title ?? OptionsListStrings.editor.getNoDataViewTitle(),
}}
/>
</EuiFormRow>
<EuiFormRow label={OptionsListStrings.editor.getFieldTitle()}>
<FieldPicker
filterPredicate={(field) => Boolean(state.fieldsMap?.[field.name])}
selectedFieldName={selectedField}
dataView={dataView}
onSelectField={(field) => {
setDefaultTitle(field.displayName ?? field.name);
const textFieldName = state.fieldsMap?.[field.name].textFieldName;
onChange({
fieldName: field.name,
textFieldName,
});
setSelectedField(field.name);
}}
/>
</EuiFormRow>
<EuiFormRow>
<EuiSwitch
label={OptionsListStrings.editor.getAllowMultiselectTitle()}
checked={!state.singleSelect}
onChange={() => {
onChange({ singleSelect: !state.singleSelect });
setState((s) => ({ ...s, singleSelect: !s.singleSelect }));
}}
/>
</EuiFormRow>
<EuiFormRow>
<EuiSwitch
label={OptionsListStrings.editor.getRunPastTimeoutTitle()}
checked={Boolean(state.runPastTimeout)}
onChange={() => {
onChange({ runPastTimeout: !state.runPastTimeout });
setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout }));
}}
/>
</EuiFormRow>
</>
);
};

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { OptionsListEmbeddableInput } from './types';
import { OptionsListStrings } from './options_list_strings';
import { ControlEditorProps } from '../..';
interface OptionsListEditorState {
singleSelect?: boolean;
runPastTimeout?: boolean;
}
export const OptionsListEditorOptions = ({
initialInput,
onChange,
}: ControlEditorProps<OptionsListEmbeddableInput>) => {
const [state, setState] = useState<OptionsListEditorState>({
singleSelect: initialInput?.singleSelect,
runPastTimeout: initialInput?.runPastTimeout,
});
return (
<>
<EuiFormRow>
<EuiSwitch
label={OptionsListStrings.editor.getAllowMultiselectTitle()}
checked={!state.singleSelect}
onChange={() => {
onChange({ singleSelect: !state.singleSelect });
setState((s) => ({ ...s, singleSelect: !s.singleSelect }));
}}
/>
</EuiFormRow>
<EuiFormRow>
<EuiSwitch
label={OptionsListStrings.editor.getRunPastTimeoutTitle()}
checked={Boolean(state.runPastTimeout)}
onChange={() => {
onChange({ runPastTimeout: !state.runPastTimeout });
setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout }));
}}
/>
</EuiFormRow>
</>
);
};

View file

@ -179,7 +179,8 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
dataView: DataView;
field: OptionsListField;
}> => {
const { dataViewId, fieldName, textFieldName } = this.getInput();
const { dataViewId, fieldName, parentFieldName, childFieldName } = this.getInput();
if (!this.dataView || this.dataView.id !== dataViewId) {
this.dataView = await this.dataViewsService.get(dataViewId);
if (this.dataView === undefined) {
@ -192,6 +193,16 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
if (!this.field || this.field.name !== fieldName) {
const originalField = this.dataView.getFieldByName(fieldName);
const childField =
(childFieldName && this.dataView.getFieldByName(childFieldName)) || undefined;
const parentField =
(parentFieldName && this.dataView.getFieldByName(parentFieldName)) || undefined;
const textFieldName = childField?.esTypes?.includes('text')
? childField.name
: parentField?.esTypes?.includes('text')
? parentField.name
: undefined;
(originalField as OptionsListField).textFieldName = textFieldName;
this.field = originalField;
@ -235,7 +246,6 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
},
this.abortController.signal
);
if (!selectedOptions || isEmpty(invalidSelections) || ignoreParentSettings?.ignoreValidations) {
this.updateComponentState({
availableOptions: suggestions,

View file

@ -9,8 +9,8 @@
import deepEqual from 'fast-deep-equal';
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import { OptionsListEditor } from './options_list_editor';
import { ControlEmbeddable, IEditableControlFactory } from '../../types';
import { OptionsListEditorOptions } from './options_list_editor_options';
import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types';
import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from './types';
import {
createOptionsListExtract,
@ -46,7 +46,16 @@ export class OptionsListEmbeddableFactory
return newInput;
};
public controlEditorComponent = OptionsListEditor;
public isFieldCompatible = (dataControlField: DataControlField) => {
if (
(dataControlField.field.aggregatable && dataControlField.field.type === 'string') ||
dataControlField.field.type === 'boolean'
) {
dataControlField.compatibleControlTypes.push(this.type);
}
};
public controlEditorOptionsComponent = OptionsListEditorOptions;
public isEditable = () => Promise.resolve(false);

View file

@ -1,111 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import useMount from 'react-use/lib/useMount';
import React, { useEffect, useState } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { DataViewListItem, DataView } from '@kbn/data-views-plugin/common';
import {
LazyDataViewPicker,
LazyFieldPicker,
withSuspense,
} from '@kbn/presentation-util-plugin/public';
import { pluginServices } from '../../services';
import { ControlEditorProps } from '../../types';
import { RangeSliderEmbeddableInput } from './types';
import { RangeSliderStrings } from './range_slider_strings';
interface RangeSliderEditorState {
dataViewListItems: DataViewListItem[];
dataView?: DataView;
}
const FieldPicker = withSuspense(LazyFieldPicker, null);
const DataViewPicker = withSuspense(LazyDataViewPicker, null);
export const RangeSliderEditor = ({
onChange,
initialInput,
setValidState,
setDefaultTitle,
getRelevantDataViewId,
setLastUsedDataViewId,
selectedField,
setSelectedField,
}: ControlEditorProps<RangeSliderEmbeddableInput>) => {
// Controls Services Context
const { dataViews } = pluginServices.getHooks();
const { getIdsWithTitle, getDefaultId, get } = dataViews.useService();
const [state, setState] = useState<RangeSliderEditorState>({
dataViewListItems: [],
});
useMount(() => {
let mounted = true;
if (selectedField) setDefaultTitle(selectedField);
(async () => {
const dataViewListItems = await getIdsWithTitle();
const initialId =
initialInput?.dataViewId ?? getRelevantDataViewId?.() ?? (await getDefaultId());
let dataView: DataView | undefined;
if (initialId) {
onChange({ dataViewId: initialId });
dataView = await get(initialId);
}
if (!mounted) return;
setState((s) => ({ ...s, dataView, dataViewListItems }));
})();
return () => {
mounted = false;
};
});
useEffect(
() => setValidState(Boolean(selectedField) && Boolean(state.dataView)),
[selectedField, setValidState, state.dataView]
);
const { dataView } = state;
return (
<>
<EuiFormRow label={RangeSliderStrings.editor.getDataViewTitle()}>
<DataViewPicker
dataViews={state.dataViewListItems}
selectedDataViewId={dataView?.id}
onChangeDataViewId={(dataViewId) => {
setLastUsedDataViewId?.(dataViewId);
if (dataViewId === dataView?.id) return;
onChange({ dataViewId });
setSelectedField(undefined);
get(dataViewId).then((newDataView) => {
setState((s) => ({ ...s, dataView: newDataView }));
});
}}
trigger={{
label: state.dataView?.title ?? RangeSliderStrings.editor.getNoDataViewTitle(),
}}
/>
</EuiFormRow>
<EuiFormRow label={RangeSliderStrings.editor.getFieldTitle()}>
<FieldPicker
filterPredicate={(field) => field.aggregatable && field.type === 'number'}
selectedFieldName={selectedField}
dataView={dataView}
onSelectField={(field) => {
setDefaultTitle(field.displayName ?? field.name);
onChange({ fieldName: field.name });
setSelectedField(field.name);
}}
/>
</EuiFormRow>
</>
);
};

View file

@ -9,8 +9,7 @@
import deepEqual from 'fast-deep-equal';
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import { RangeSliderEditor } from './range_slider_editor';
import { ControlEmbeddable, IEditableControlFactory } from '../../types';
import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types';
import { RangeSliderEmbeddableInput, RANGE_SLIDER_CONTROL } from './types';
import {
createRangeSliderExtract,
@ -46,7 +45,11 @@ export class RangeSliderEmbeddableFactory
return newInput;
};
public controlEditorComponent = RangeSliderEditor;
public isFieldCompatible = (dataControlField: DataControlField) => {
if (dataControlField.field.aggregatable && dataControlField.field.type === 'number') {
dataControlField.compatibleControlTypes.push(this.type);
}
};
public isEditable = () => Promise.resolve(false);

View file

@ -1,110 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import useMount from 'react-use/lib/useMount';
import React, { useEffect, useState } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { DataViewListItem, DataView } from '@kbn/data-views-plugin/common';
import {
LazyDataViewPicker,
LazyFieldPicker,
withSuspense,
} from '@kbn/presentation-util-plugin/public';
import { pluginServices } from '../../services';
import { ControlEditorProps } from '../../types';
import { TimeSliderStrings } from './time_slider_strings';
interface TimeSliderEditorState {
dataViewListItems: DataViewListItem[];
dataView?: DataView;
}
const FieldPicker = withSuspense(LazyFieldPicker, null);
const DataViewPicker = withSuspense(LazyDataViewPicker, null);
export const TimeSliderEditor = ({
onChange,
initialInput,
setValidState,
setDefaultTitle,
getRelevantDataViewId,
setLastUsedDataViewId,
selectedField,
setSelectedField,
}: ControlEditorProps<any>) => {
// Controls Services Context
const { dataViews } = pluginServices.getHooks();
const { getIdsWithTitle, getDefaultId, get } = dataViews.useService();
const [state, setState] = useState<TimeSliderEditorState>({
dataViewListItems: [],
});
useMount(() => {
let mounted = true;
if (selectedField) setDefaultTitle(selectedField);
(async () => {
const dataViewListItems = await getIdsWithTitle();
const initialId =
initialInput?.dataViewId ?? getRelevantDataViewId?.() ?? (await getDefaultId());
let dataView: DataView | undefined;
if (initialId) {
onChange({ dataViewId: initialId });
dataView = await get(initialId);
}
if (!mounted) return;
setState((s) => ({ ...s, dataView, dataViewListItems }));
})();
return () => {
mounted = false;
};
});
useEffect(
() => setValidState(Boolean(selectedField) && Boolean(state.dataView)),
[selectedField, setValidState, state.dataView]
);
const { dataView } = state;
return (
<>
<EuiFormRow label={TimeSliderStrings.editor.getDataViewTitle()}>
<DataViewPicker
dataViews={state.dataViewListItems}
selectedDataViewId={dataView?.id}
onChangeDataViewId={(dataViewId) => {
setLastUsedDataViewId?.(dataViewId);
if (dataViewId === dataView?.id) return;
onChange({ dataViewId });
setSelectedField(undefined);
get(dataViewId).then((newDataView) => {
setState((s) => ({ ...s, dataView: newDataView }));
});
}}
trigger={{
label: state.dataView?.title ?? TimeSliderStrings.editor.getNoDataViewTitle(),
}}
/>
</EuiFormRow>
<EuiFormRow label={TimeSliderStrings.editor.getFieldTitle()}>
<FieldPicker
filterPredicate={(field) => field.type === 'date'}
selectedFieldName={selectedField}
dataView={dataView}
onSelectField={(field) => {
setDefaultTitle(field.displayName ?? field.name);
onChange({ fieldName: field.name });
setSelectedField(field.name);
}}
/>
</EuiFormRow>
</>
);
};

View file

@ -10,12 +10,11 @@ import deepEqual from 'fast-deep-equal';
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import { TIME_SLIDER_CONTROL } from '../..';
import { ControlEmbeddable, IEditableControlFactory } from '../../types';
import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types';
import {
createOptionsListExtract,
createOptionsListInject,
} from '../../../common/control_types/options_list/options_list_persistable_state';
import { TimeSliderEditor } from './time_slider_editor';
import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types';
import { TimeSliderStrings } from './time_slider_strings';
@ -48,7 +47,11 @@ export class TimesliderEmbeddableFactory
return newInput;
};
public controlEditorComponent = TimeSliderEditor;
public isFieldCompatible = (dataControlField: DataControlField) => {
if (dataControlField.field.type === 'date') {
dataControlField.compatibleControlTypes.push(this.type);
}
};
public isEditable = () => Promise.resolve(false);

View file

@ -61,10 +61,11 @@ export class ControlsPlugin
factoryDef: IEditableControlFactory<I>,
factory: EmbeddableFactory
) {
(factory as IEditableControlFactory<I>).controlEditorComponent =
factoryDef.controlEditorComponent;
(factory as IEditableControlFactory<I>).controlEditorOptionsComponent =
factoryDef.controlEditorOptionsComponent ?? undefined;
(factory as IEditableControlFactory<I>).presaveTransformFunction =
factoryDef.presaveTransformFunction;
(factory as IEditableControlFactory<I>).isFieldCompatible = factoryDef.isFieldCompatible;
}
public setup(

View file

@ -16,7 +16,7 @@ import {
IEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DataView, DataViewField, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { ControlInput } from '../common/types';
import { ControlsService } from './services/controls';
@ -28,7 +28,11 @@ export interface CommonControlOutput {
export type ControlOutput = EmbeddableOutput & CommonControlOutput;
export type ControlFactory = EmbeddableFactory<ControlInput, ControlOutput, ControlEmbeddable>;
export type ControlFactory<T extends ControlInput = ControlInput> = EmbeddableFactory<
ControlInput,
ControlOutput,
ControlEmbeddable
>;
export type ControlEmbeddable<
TControlEmbeddableInput extends ControlInput = ControlInput,
@ -39,21 +43,28 @@ export type ControlEmbeddable<
* Control embeddable editor types
*/
export interface IEditableControlFactory<T extends ControlInput = ControlInput> {
controlEditorComponent?: (props: ControlEditorProps<T>) => JSX.Element;
controlEditorOptionsComponent?: (props: ControlEditorProps<T>) => JSX.Element;
presaveTransformFunction?: (
newState: Partial<T>,
embeddable?: ControlEmbeddable<T>
) => Partial<T>;
isFieldCompatible?: (dataControlField: DataControlField) => void; // reducer
}
export interface ControlEditorProps<T extends ControlInput = ControlInput> {
initialInput?: Partial<T>;
getRelevantDataViewId?: () => string | undefined;
setLastUsedDataViewId?: (newId: string) => void;
onChange: (partial: Partial<T>) => void;
setValidState: (valid: boolean) => void;
setDefaultTitle: (defaultTitle: string) => void;
selectedField: string | undefined;
setSelectedField: (newField: string | undefined) => void;
}
export interface DataControlField {
field: DataViewField;
parentFieldName?: string;
childFieldName?: string;
compatibleControlTypes: string[];
}
export interface DataControlFieldRegistry {
[fieldName: string]: DataControlField;
}
/**

View file

@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('apply new default width and grow', async () => {
it('defaults to medium width and grow enabled', async () => {
await dashboardControls.openCreateControlFlyout(OPTIONS_LIST_CONTROL);
await dashboardControls.openCreateControlFlyout();
const mediumWidthButton = await testSubjects.find('control-editor-width-medium');
expect(await mediumWidthButton.elementHasClass('euiButtonGroupButton-isSelected')).to.be(
true
@ -70,7 +70,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await secondControl.elementHasClass('controlFrameWrapper--small')).to.be(true);
expect(await secondControl.elementHasClass('euiFlexItem--flexGrowZero')).to.be(true);
await dashboardControls.openCreateControlFlyout(OPTIONS_LIST_CONTROL);
await dashboardControls.openCreateControlFlyout();
const smallWidthButton = await testSubjects.find('control-editor-width-small');
expect(await smallWidthButton.elementHasClass('euiButtonGroupButton-isSelected')).to.be(
true

View file

@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await saveButton.isEnabled()).to.be(true);
await dashboardControls.controlsEditorSetDataView('animals-*');
expect(await saveButton.isEnabled()).to.be(false);
await dashboardControls.controlsEditorSetfield('animal.keyword');
await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
await dashboardControls.controlEditorSave();
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
@ -129,7 +129,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlsEditorSetfield('animal.keyword');
await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
await dashboardControls.controlEditorSave();
const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);

View file

@ -121,7 +121,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await saveButton.isEnabled()).to.be(true);
await dashboardControls.controlsEditorSetDataView('kibana_sample_data_flights');
expect(await saveButton.isEnabled()).to.be(false);
await dashboardControls.controlsEditorSetfield('dayOfWeek');
await dashboardControls.controlsEditorSetfield('dayOfWeek', RANGE_SLIDER_CONTROL);
await dashboardControls.controlEditorSave();
await dashboardControls.rangeSliderWaitForLoading();
validateRange('placeholder', firstId, '0', '6');
@ -164,7 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('editing field clears selections', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlsEditorSetfield('FlightDelayMin');
await dashboardControls.controlsEditorSetfield('FlightDelayMin', RANGE_SLIDER_CONTROL);
await dashboardControls.controlEditorSave();
await dashboardControls.rangeSliderWaitForLoading();

View file

@ -6,8 +6,6 @@
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import {
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
@ -28,24 +26,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'header',
]);
const changeFieldType = async (newField: string) => {
const saveButton = await testSubjects.find('control-editor-save');
expect(await saveButton.isEnabled()).to.be(false);
await dashboardControls.controlsEditorSetfield(newField);
expect(await saveButton.isEnabled()).to.be(true);
const changeFieldType = async (controlId: string, newField: string, expectedType?: string) => {
await dashboardControls.editExistingControl(controlId);
await dashboardControls.controlsEditorSetfield(newField, expectedType);
await dashboardControls.controlEditorSave();
};
const replaceWithOptionsList = async (controlId: string) => {
await dashboardControls.controlEditorSetType(OPTIONS_LIST_CONTROL);
await changeFieldType('sound.keyword');
await changeFieldType(controlId, 'sound.keyword', OPTIONS_LIST_CONTROL);
await testSubjects.waitForEnabled(`optionsList-control-${controlId}`);
await dashboardControls.verifyControlType(controlId, 'optionsList-control');
};
const replaceWithRangeSlider = async (controlId: string) => {
await dashboardControls.controlEditorSetType(RANGE_SLIDER_CONTROL);
await changeFieldType('weightLbs');
await changeFieldType(controlId, 'weightLbs', RANGE_SLIDER_CONTROL);
await retry.try(async () => {
await dashboardControls.rangeSliderWaitForLoading();
await dashboardControls.verifyControlType(controlId, 'range-slider-control');
@ -53,8 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};
const replaceWithTimeSlider = async (controlId: string) => {
await dashboardControls.controlEditorSetType(TIME_SLIDER_CONTROL);
await changeFieldType('@timestamp');
await changeFieldType(controlId, '@timestamp', TIME_SLIDER_CONTROL);
await testSubjects.waitForDeleted('timeSlider-loading-spinner');
await dashboardControls.verifyControlType(controlId, 'timeSlider');
};
@ -78,7 +71,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
fieldName: 'sound.keyword',
});
controlId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(controlId);
});
it('with range slider', async () => {
@ -102,7 +94,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
await dashboardControls.rangeSliderWaitForLoading();
controlId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(controlId);
});
it('with options list', async () => {
@ -124,7 +115,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
await testSubjects.waitForDeleted('timeSlider-loading-spinner');
controlId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(controlId);
});
it('with options list', async () => {

View file

@ -7,12 +7,22 @@
*/
import expect from '@kbn/expect';
import { OPTIONS_LIST_CONTROL, ControlWidth } from '@kbn/controls-plugin/common';
import {
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
ControlWidth,
} from '@kbn/controls-plugin/common';
import { ControlGroupChainingSystem } from '@kbn/controls-plugin/common/control_group/types';
import { WebElementWrapper } from '../services/lib/web_element_wrapper';
import { FtrService } from '../ftr_provider_context';
const CONTROL_DISPLAY_NAMES: { [key: string]: string } = {
default: 'Please select a field',
[OPTIONS_LIST_CONTROL]: 'Options list',
[RANGE_SLIDER_CONTROL]: 'Range slider',
};
export class DashboardPageControls extends FtrService {
private readonly log = this.ctx.getService('log');
private readonly find = this.ctx.getService('find');
@ -78,14 +88,14 @@ export class DashboardPageControls extends FtrService {
}
}
public async openCreateControlFlyout(type: string) {
this.log.debug(`Opening flyout for ${type} control`);
public async openCreateControlFlyout() {
this.log.debug(`Opening flyout for creating a control`);
await this.testSubjects.click('dashboard-controls-menu-button');
await this.testSubjects.click('controls-create-button');
await this.retry.try(async () => {
await this.testSubjects.existOrFail('control-editor-flyout');
});
await this.controlEditorSetType(type);
await this.controlEditorVerifyType('default');
}
/* -----------------------------------------------------------
@ -238,10 +248,12 @@ export class DashboardPageControls extends FtrService {
grow?: boolean;
}) {
this.log.debug(`Creating ${controlType} control ${title ?? fieldName}`);
await this.openCreateControlFlyout(controlType);
await this.openCreateControlFlyout();
if (dataViewTitle) await this.controlsEditorSetDataView(dataViewTitle);
if (fieldName) await this.controlsEditorSetfield(fieldName);
if (fieldName) await this.controlsEditorSetfield(fieldName, controlType);
if (title) await this.controlEditorSetTitle(title);
if (width) await this.controlEditorSetWidth(width);
if (grow !== undefined) await this.controlEditorSetGrow(grow);
@ -377,6 +389,9 @@ export class DashboardPageControls extends FtrService {
public async controlEditorSave() {
this.log.debug(`Saving changes in control editor`);
await this.testSubjects.click(`control-editor-save`);
await this.retry.waitFor('flyout to close', async () => {
return !(await this.testSubjects.exists('control-editor-flyout'));
});
}
public async controlEditorCancel(confirm?: boolean) {
@ -396,7 +411,11 @@ export class DashboardPageControls extends FtrService {
await this.testSubjects.click(`data-view-picker-${dataViewTitle}`);
}
public async controlsEditorSetfield(fieldName: string, shouldSearch: boolean = false) {
public async controlsEditorSetfield(
fieldName: string,
expectedType?: string,
shouldSearch: boolean = false
) {
this.log.debug(`Setting control field to ${fieldName}`);
if (shouldSearch) {
await this.testSubjects.setValue('field-search-input', fieldName);
@ -405,17 +424,19 @@ export class DashboardPageControls extends FtrService {
await this.testSubjects.existOrFail(`field-picker-select-${fieldName}`);
});
await this.testSubjects.click(`field-picker-select-${fieldName}`);
if (expectedType) await this.controlEditorVerifyType(expectedType);
}
public async controlEditorSetType(type: string) {
this.log.debug(`Setting control type to ${type}`);
await this.testSubjects.click(`create-${type}-control`);
public async controlEditorVerifyType(type: string) {
this.log.debug(`Verifying that the control editor picked the type ${type}`);
const autoSelectedType = await this.testSubjects.getVisibleText('control-editor-type');
expect(autoSelectedType).to.equal(CONTROL_DISPLAY_NAMES[type]);
}
// Options List editor functions
public async optionsListEditorGetCurrentDataView(openAndCloseFlyout?: boolean) {
if (openAndCloseFlyout) {
await this.openCreateControlFlyout(OPTIONS_LIST_CONTROL);
await this.openCreateControlFlyout();
}
const dataViewName = (await this.testSubjects.find('open-data-view-picker')).getVisibleText();
if (openAndCloseFlyout) {