mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
e2827350e9
commit
8de3401dff
21 changed files with 480 additions and 637 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -30,5 +30,7 @@ export type ControlInput = EmbeddableInput & {
|
|||
|
||||
export type DataControlInput = ControlInput & {
|
||||
fieldName: string;
|
||||
parentFieldName?: string;
|
||||
childFieldName?: string;
|
||||
dataViewId: string;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue