diff --git a/src/plugins/controls/public/control_group/component/control_frame_component.tsx b/src/plugins/controls/public/control_group/component/control_frame_component.tsx index 36879bea110d8..dabe351376b7f 100644 --- a/src/plugins/controls/public/control_group/component/control_frame_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_frame_component.tsx @@ -28,9 +28,15 @@ export interface ControlFrameProps { customPrepend?: JSX.Element; enableActions?: boolean; embeddableId: string; + embeddableType: string; } -export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: ControlFrameProps) => { +export const ControlFrame = ({ + customPrepend, + enableActions, + embeddableId, + embeddableType, +}: ControlFrameProps) => { const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []); const { useEmbeddableSelector, @@ -42,7 +48,7 @@ export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: Con const { overlays } = pluginServices.getHooks(); const { openConfirm } = overlays.useService(); - const embeddable = useChildEmbeddable({ untilEmbeddableLoaded, embeddableId }); + const embeddable = useChildEmbeddable({ untilEmbeddableLoaded, embeddableId, embeddableType }); const [title, setTitle] = useState(); diff --git a/src/plugins/controls/public/control_group/component/control_group_component.tsx b/src/plugins/controls/public/control_group/component/control_group_component.tsx index 3abee52002db1..72dc49b2f9fbb 100644 --- a/src/plugins/controls/public/control_group/component/control_group_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_component.tsx @@ -144,6 +144,7 @@ export const ControlGroup = () => { isEditable={isEditable} dragInfo={{ index, draggingIndex }} embeddableId={controlId} + embeddableType={panels[controlId].type} key={controlId} /> ) diff --git a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx index 2741752b4df88..bdf1851a0daa1 100644 --- a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx @@ -60,44 +60,50 @@ export const SortableControl = (frameProps: SortableControlProps) => { const SortableControlInner = forwardRef< HTMLButtonElement, SortableControlProps & { style: HTMLAttributes['style'] } ->(({ embeddableId, dragInfo, style, isEditable, ...dragHandleProps }, dragHandleRef) => { - const { isOver, isDragging, draggingIndex, index } = dragInfo; - const { useEmbeddableSelector } = useReduxContainerContext(); - const { panels } = useEmbeddableSelector((state) => state); +>( + ( + { embeddableId, embeddableType, dragInfo, style, isEditable, ...dragHandleProps }, + dragHandleRef + ) => { + const { isOver, isDragging, draggingIndex, index } = dragInfo; + const { useEmbeddableSelector } = useReduxContainerContext(); + const { panels } = useEmbeddableSelector((state) => state); - const width = panels[embeddableId].width; + const width = panels[embeddableId].width; - const dragHandle = ( - - ); + const dragHandle = ( + + ); - return ( - (draggingIndex ?? -1), - })} - style={style} - > - - - ); -}); + return ( + (draggingIndex ?? -1), + })} + style={style} + > + + + ); + } +); /** * A simplified clone version of the control which is dragged. This version only shows diff --git a/src/plugins/controls/public/control_group/editor/control_editor.tsx b/src/plugins/controls/public/control_group/editor/control_editor.tsx index 3cd5b92e503c1..eb7eff4abb42a 100644 --- a/src/plugins/controls/public/control_group/editor/control_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_editor.tsx @@ -41,6 +41,7 @@ import { ControlEmbeddable, ControlInput, ControlWidth, + DataControlInput, IEditableControlFactory, } from '../../types'; import { CONTROL_WIDTH_OPTIONS } from './editor_constants'; @@ -85,6 +86,11 @@ export const ControlEditor = ({ const [currentTitle, setCurrentTitle] = useState(title); const [currentWidth, setCurrentWidth] = useState(width); const [controlEditorValid, setControlEditorValid] = useState(false); + const [selectedField, setSelectedField] = useState( + embeddable + ? (embeddable.getInput() as DataControlInput).fieldName // CLEAN THIS ONCE OTHER PR GETS IN + : undefined + ); const getControlTypeEditor = (type: string) => { const factory = getControlFactory(type); @@ -96,6 +102,8 @@ export const ControlEditor = ({ onChange={onTypeEditorChange} setValidState={setControlEditorValid} initialInput={embeddable?.getInput()} + selectedField={selectedField} + setSelectedField={setSelectedField} setDefaultTitle={(newDefaultTitle) => { if (!currentTitle || currentTitle === defaultTitle) { setCurrentTitle(newDefaultTitle); @@ -107,8 +115,8 @@ export const ControlEditor = ({ ) : null; }; - const getTypeButtons = (controlTypes: string[]) => { - return controlTypes.map((type) => { + const getTypeButtons = () => { + return getControlTypes().map((type) => { const factory = getControlFactory(type); const icon = (factory as EmbeddableFactoryDefinition).getIconType?.(); const tooltip = (factory as EmbeddableFactoryDefinition).getDescription?.(); @@ -120,6 +128,12 @@ export const ControlEditor = ({ isSelected={selectedType === type} onClick={() => { setSelectedType(type); + if (!isCreate) + setSelectedField( + embeddable && type === embeddable.type + ? (embeddable.getInput() as DataControlInput).fieldName + : undefined + ); }} > @@ -150,9 +164,7 @@ export const ControlEditor = ({ - - {isCreate ? getTypeButtons(getControlTypes()) : getTypeButtons([selectedType])} - + {getTypeButtons()} {selectedType && ( <> diff --git a/src/plugins/controls/public/control_group/editor/edit_control.tsx b/src/plugins/controls/public/control_group/editor/edit_control.tsx index 11a2e705a13f3..6866148ac7e9d 100644 --- a/src/plugins/controls/public/control_group/editor/edit_control.tsx +++ b/src/plugins/controls/public/control_group/editor/edit_control.tsx @@ -22,6 +22,11 @@ import { IEditableControlFactory, ControlInput } from '../../types'; import { controlGroupReducers } from '../state/control_group_reducers'; import { ControlGroupContainer, setFlyoutRef } from '../embeddable/control_group_container'; +interface EditControlResult { + type: string; + controlInput: Omit; +} + export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => { // Controls Services Context const { overlays, controls } = pluginServices.getHooks(); @@ -34,7 +39,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => typeof controlGroupReducers >(); const { - containerActions: { untilEmbeddableLoaded, removeEmbeddable, updateInputForChild }, + containerActions: { untilEmbeddableLoaded, removeEmbeddable, replaceEmbeddable }, actions: { setControlWidth }, useEmbeddableSelector, useEmbeddableDispatch, @@ -52,88 +57,107 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => const editControl = async () => { const panel = panels[embeddableId]; - const factory = getControlFactory(panel.type); + let factory = getControlFactory(panel.type); + if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type); + const embeddable = await untilEmbeddableLoaded(embeddableId); const controlGroup = embeddable.getRoot() as ControlGroupContainer; - let inputToReturn: Partial = {}; + const initialInputPromise = new Promise((resolve, reject) => { + let inputToReturn: Partial = {}; - if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type); + let removed = false; + const onCancel = (ref: OverlayRef) => { + if ( + removed || + (isEqual(latestPanelState.current.explicitInput, { + ...panel.explicitInput, + ...inputToReturn, + }) && + isEqual(latestPanelState.current.width, panel.width)) + ) { + reject(); + ref.close(); + return; + } + openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), { + confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(), + cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(), + title: ControlGroupStrings.management.discardChanges.getTitle(), + buttonColor: 'danger', + }).then((confirmed) => { + if (confirmed) { + dispatch(setControlWidth({ width: panel.width, embeddableId })); + reject(); + ref.close(); + } + }); + }; - let removed = false; - const onCancel = (ref: OverlayRef) => { - if ( - removed || - (isEqual(latestPanelState.current.explicitInput, { - ...panel.explicitInput, - ...inputToReturn, - }) && - isEqual(latestPanelState.current.width, panel.width)) - ) { + const onSave = (type: string, ref: OverlayRef) => { + // 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) { + factory = getControlFactory(type); + if (!factory) throw new EmbeddableFactoryNotFoundError(type); + } + const editableFactory = factory as IEditableControlFactory; + if (editableFactory.presaveTransformFunction) { + inputToReturn = editableFactory.presaveTransformFunction(inputToReturn, embeddable); + } + resolve({ type, controlInput: inputToReturn }); ref.close(); - return; - } - openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), { - confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(), - cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(), - title: ControlGroupStrings.management.discardChanges.getTitle(), - buttonColor: 'danger', - }).then((confirmed) => { - if (confirmed) { - dispatch(setControlWidth({ width: panel.width, embeddableId })); - ref.close(); + }; + + const flyoutInstance = openFlyout( + forwardAllContext( + onCancel(flyoutInstance)} + updateTitle={(newTitle) => (inputToReturn.title = newTitle)} + setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)} + updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, 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 + ), + { + outsideClickCloses: false, + onClose: (flyout) => { + setFlyoutRef(undefined); + onCancel(flyout); + }, } - }); - }; + ); + setFlyoutRef(flyoutInstance); + }); - const flyoutInstance = openFlyout( - forwardAllContext( - onCancel(flyoutInstance)} - updateTitle={(newTitle) => (inputToReturn.title = newTitle)} - setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)} - updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, embeddableId }))} - onTypeEditorChange={(partialInput) => - (inputToReturn = { ...inputToReturn, ...partialInput }) - } - onSave={() => { - const editableFactory = factory as IEditableControlFactory; - if (editableFactory.presaveTransformFunction) { - inputToReturn = editableFactory.presaveTransformFunction(inputToReturn, embeddable); - } - updateInputForChild(embeddableId, inputToReturn); - flyoutInstance.close(); - }} - 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 - ), - { - outsideClickCloses: false, - onClose: (flyout) => { - setFlyoutRef(undefined); - onCancel(flyout); - }, - } + initialInputPromise.then( + async (promise) => { + await replaceEmbeddable(embeddable.id, promise.controlInput, promise.type); + }, + () => {} // swallow promise rejection because it can be part of normal flow ); - setFlyoutRef(flyoutInstance); }; return ( diff --git a/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx b/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx index 19ad5fc3dce67..b6d5a0877d7ce 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx +++ b/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx @@ -28,7 +28,6 @@ interface OptionsListEditorState { dataViewListItems: DataViewListItem[]; fieldsMap?: { [key: string]: OptionsListField }; dataView?: DataView; - fieldName?: string; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -41,13 +40,14 @@ export const OptionsListEditor = ({ setDefaultTitle, getRelevantDataViewId, setLastUsedDataViewId, + selectedField, + setSelectedField, }: ControlEditorProps) => { // Controls Services Context const { dataViews } = pluginServices.getHooks(); const { getIdsWithTitle, getDefaultId, get } = dataViews.useService(); const [state, setState] = useState({ - fieldName: initialInput?.fieldName, singleSelect: initialInput?.singleSelect, runPastTimeout: initialInput?.runPastTimeout, dataViewListItems: [], @@ -55,7 +55,7 @@ export const OptionsListEditor = ({ useMount(() => { let mounted = true; - if (state.fieldName) setDefaultTitle(state.fieldName); + if (selectedField) setDefaultTitle(selectedField); (async () => { const dataViewListItems = await getIdsWithTitle(); const initialId = @@ -115,11 +115,11 @@ export const OptionsListEditor = ({ }, [state.dataView]); useEffect( - () => setValidState(Boolean(state.fieldName) && Boolean(state.dataView)), - [state.fieldName, setValidState, state.dataView] + () => setValidState(Boolean(selectedField) && Boolean(state.dataView)), + [selectedField, setValidState, state.dataView] ); - const { dataView, fieldName } = state; + const { dataView } = state; return ( <> @@ -131,7 +131,7 @@ export const OptionsListEditor = ({ if (dataViewId === dataView?.id) return; onChange({ dataViewId }); - setState((s) => ({ ...s, fieldName: undefined })); + setSelectedField(undefined); get(dataViewId).then((newDataView) => { setState((s) => ({ ...s, dataView: newDataView })); }); @@ -144,7 +144,7 @@ export const OptionsListEditor = ({ Boolean(state.fieldsMap?.[field.name])} - selectedFieldName={fieldName} + selectedFieldName={selectedField} dataView={dataView} onSelectField={(field) => { setDefaultTitle(field.displayName ?? field.name); @@ -153,7 +153,7 @@ export const OptionsListEditor = ({ fieldName: field.name, textFieldName, }); - setState((s) => ({ ...s, fieldName: field.name })); + setSelectedField(field.name); }} /> diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx index fa0c2c7d3cc45..13f688c5dd318 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx @@ -24,7 +24,6 @@ import { RangeSliderStrings } from './range_slider_strings'; interface RangeSliderEditorState { dataViewListItems: DataViewListItem[]; dataView?: DataView; - fieldName?: string; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -37,19 +36,20 @@ export const RangeSliderEditor = ({ setDefaultTitle, getRelevantDataViewId, setLastUsedDataViewId, + selectedField, + setSelectedField, }: ControlEditorProps) => { // Controls Services Context const { dataViews } = pluginServices.getHooks(); const { getIdsWithTitle, getDefaultId, get } = dataViews.useService(); const [state, setState] = useState({ - fieldName: initialInput?.fieldName, dataViewListItems: [], }); useMount(() => { let mounted = true; - if (state.fieldName) setDefaultTitle(state.fieldName); + if (selectedField) setDefaultTitle(selectedField); (async () => { const dataViewListItems = await getIdsWithTitle(); const initialId = @@ -68,11 +68,11 @@ export const RangeSliderEditor = ({ }); useEffect( - () => setValidState(Boolean(state.fieldName) && Boolean(state.dataView)), - [state.fieldName, setValidState, state.dataView] + () => setValidState(Boolean(selectedField) && Boolean(state.dataView)), + [selectedField, setValidState, state.dataView] ); - const { dataView, fieldName } = state; + const { dataView } = state; return ( <> @@ -84,7 +84,7 @@ export const RangeSliderEditor = ({ if (dataViewId === dataView?.id) return; onChange({ dataViewId }); - setState((s) => ({ ...s, fieldName: undefined })); + setSelectedField(undefined); get(dataViewId).then((newDataView) => { setState((s) => ({ ...s, dataView: newDataView })); }); @@ -97,12 +97,12 @@ export const RangeSliderEditor = ({ field.aggregatable && field.type === 'number'} - selectedFieldName={fieldName} + selectedFieldName={selectedField} dataView={dataView} onSelectField={(field) => { setDefaultTitle(field.displayName ?? field.name); onChange({ fieldName: field.name }); - setState((s) => ({ ...s, fieldName: field.name })); + setSelectedField(field.name); }} /> diff --git a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx index 7ae7871497045..90ea07dc276bd 100644 --- a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx +++ b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx @@ -16,7 +16,7 @@ export default { description: '', }; -const TimeSliderWrapper: FC> = (props) => { +const TimeSliderWrapper: FC> = (props) => { const [value, setValue] = useState(props.value); const onChange = useCallback( (newValue: [number | null, number | null]) => { @@ -31,7 +31,13 @@ const TimeSliderWrapper: FC> = ( return (
- +
); diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx index 89efce270d14c..1bb2f90b44121 100644 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx @@ -70,6 +70,7 @@ export function getInterval(min: number, max: number, steps = 6): number { } export interface TimeSliderProps { + id: string; range?: [number | undefined, number | undefined]; value: [number | null, number | null]; onChange: (range: [number | null, number | null]) => void; @@ -167,10 +168,15 @@ export const TimeSlider: FC = (props) => { } const button = ( -