mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[Controls] Redux Toolkit and Embeddable Redux Wrapper (#114371)
Use new redux wrapper for control group management and upgrade all control group methods to use redux wrapper. Get order of controls from embeddable input, set up preconfigured story. Co-authored-by: andreadelrio <delrio.andre@gmail.com>
This commit is contained in:
parent
e5576d688d
commit
f8cbbbb99f
39 changed files with 1308 additions and 625 deletions
|
@ -91,6 +91,9 @@
|
|||
"yarn": "^1.21.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^3.1.1",
|
||||
"@dnd-kit/sortable": "^4.0.0",
|
||||
"@dnd-kit/utilities": "^2.0.0",
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@dnd-kit/core": "^3.1.1",
|
||||
"@dnd-kit/sortable": "^4.0.0",
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { InputControlFactory } from '../types';
|
||||
import { ControlsService } from '../controls_service';
|
||||
import { InputControlFactory } from '../../../services/controls';
|
||||
import { flightFields, getEuiSelectableOptions } from './flights';
|
||||
import { OptionsListEmbeddableFactory } from '../control_types/options_list';
|
||||
|
||||
|
|
|
@ -10,9 +10,14 @@ import React, { useEffect, useMemo } from 'react';
|
|||
import uuid from 'uuid';
|
||||
|
||||
import { decorators } from './decorators';
|
||||
import { providers } from '../../../services/storybook';
|
||||
import { getControlsServiceStub } from './controls_service_stub';
|
||||
import { ControlGroupContainerFactory } from '../control_group/control_group_container_factory';
|
||||
import { pluginServices, registry } from '../../../services/storybook';
|
||||
import { populateStorybookControlFactories } from './storybook_control_factories';
|
||||
import { ControlGroupContainerFactory } from '../control_group/embeddable/control_group_container_factory';
|
||||
import { ControlsPanels } from '../control_group/types';
|
||||
import {
|
||||
OptionsListEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
} from '../control_types/options_list/options_list_embeddable';
|
||||
|
||||
export default {
|
||||
title: 'Controls',
|
||||
|
@ -20,17 +25,15 @@ export default {
|
|||
decorators,
|
||||
};
|
||||
|
||||
const ControlGroupStoryComponent = () => {
|
||||
const EmptyControlGroupStoryComponent = ({ panels }: { panels?: ControlsPanels }) => {
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
|
||||
providers.overlays.start({});
|
||||
const overlays = providers.overlays.getService();
|
||||
|
||||
const controlsServiceStub = getControlsServiceStub();
|
||||
pluginServices.setRegistry(registry.start({}));
|
||||
populateStorybookControlFactories(pluginServices.getServices().controls);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const factory = new ControlGroupContainerFactory(controlsServiceStub, overlays);
|
||||
const factory = new ControlGroupContainerFactory();
|
||||
const controlGroupContainerEmbeddable = await factory.create({
|
||||
inheritParentState: {
|
||||
useQuery: false,
|
||||
|
@ -38,16 +41,57 @@ const ControlGroupStoryComponent = () => {
|
|||
useTimerange: false,
|
||||
},
|
||||
controlStyle: 'oneLine',
|
||||
panels: panels ?? {},
|
||||
id: uuid.v4(),
|
||||
panels: {},
|
||||
});
|
||||
if (controlGroupContainerEmbeddable && embeddableRoot.current) {
|
||||
controlGroupContainerEmbeddable.render(embeddableRoot.current);
|
||||
}
|
||||
})();
|
||||
}, [embeddableRoot, controlsServiceStub, overlays]);
|
||||
}, [embeddableRoot, panels]);
|
||||
|
||||
return <div ref={embeddableRoot} />;
|
||||
};
|
||||
|
||||
export const ControlGroupStory = () => <ControlGroupStoryComponent />;
|
||||
export const EmptyControlGroupStory = () => <EmptyControlGroupStoryComponent />;
|
||||
export const ConfiguredControlGroupStory = () => (
|
||||
<EmptyControlGroupStoryComponent
|
||||
panels={{
|
||||
optionsList1: {
|
||||
type: OPTIONS_LIST_CONTROL,
|
||||
order: 1,
|
||||
width: 'auto',
|
||||
explicitInput: {
|
||||
title: 'Origin City',
|
||||
id: 'optionsList1',
|
||||
indexPattern: 'demo data flights',
|
||||
field: 'OriginCityName',
|
||||
defaultSelections: ['Toronto'],
|
||||
} as OptionsListEmbeddableInput,
|
||||
},
|
||||
optionsList2: {
|
||||
type: OPTIONS_LIST_CONTROL,
|
||||
order: 2,
|
||||
width: 'auto',
|
||||
explicitInput: {
|
||||
title: 'Destination City',
|
||||
id: 'optionsList2',
|
||||
indexPattern: 'demo data flights',
|
||||
field: 'DestCityName',
|
||||
defaultSelections: ['London'],
|
||||
} as OptionsListEmbeddableInput,
|
||||
},
|
||||
optionsList3: {
|
||||
type: OPTIONS_LIST_CONTROL,
|
||||
order: 3,
|
||||
width: 'auto',
|
||||
explicitInput: {
|
||||
title: 'Carrier',
|
||||
id: 'optionsList3',
|
||||
indexPattern: 'demo data flights',
|
||||
field: 'Carrier',
|
||||
} as OptionsListEmbeddableInput,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { flightFields, getEuiSelectableOptions } from './flights';
|
||||
import { OptionsListEmbeddableFactory } from '../control_types/options_list';
|
||||
import { InputControlFactory, PresentationControlsService } from '../../../services/controls';
|
||||
|
||||
export const populateStorybookControlFactories = (
|
||||
controlsServiceStub: PresentationControlsService
|
||||
) => {
|
||||
const optionsListFactoryStub = new OptionsListEmbeddableFactory(
|
||||
({ field, search }) =>
|
||||
new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)),
|
||||
() => Promise.resolve(['demo data flights']),
|
||||
() => Promise.resolve(flightFields)
|
||||
);
|
||||
|
||||
// cast to unknown because the stub cannot use the embeddable start contract to transform the EmbeddableFactoryDefinition into an EmbeddableFactory
|
||||
const optionsListControlFactory = optionsListFactoryStub as unknown as InputControlFactory;
|
||||
optionsListControlFactory.getDefaultInput = () => ({});
|
||||
controlsServiceStub.registerInputControlType(optionsListControlFactory);
|
||||
};
|
|
@ -1,22 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ControlFrameStrings = {
|
||||
floatingActions: {
|
||||
getEditButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.editTitle', {
|
||||
defaultMessage: 'Manage control',
|
||||
}),
|
||||
getRemoveButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.removeTitle', {
|
||||
defaultMessage: 'Remove control',
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -15,32 +15,28 @@ import {
|
|||
EuiFormRow,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { ControlGroupContainer } from '../control_group/control_group_container';
|
||||
import { useChildEmbeddable } from '../hooks/use_child_embeddable';
|
||||
import { ControlStyle } from '../types';
|
||||
import { ControlFrameStrings } from './control_frame_strings';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { EditControlButton } from '../editor/edit_control';
|
||||
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
|
||||
import { useReduxContainerContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
|
||||
export interface ControlFrameProps {
|
||||
container: ControlGroupContainer;
|
||||
customPrepend?: JSX.Element;
|
||||
controlStyle: ControlStyle;
|
||||
enableActions?: boolean;
|
||||
onRemove?: () => void;
|
||||
embeddableId: string;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
export const ControlFrame = ({
|
||||
customPrepend,
|
||||
enableActions,
|
||||
embeddableId,
|
||||
controlStyle,
|
||||
container,
|
||||
onRemove,
|
||||
onEdit,
|
||||
}: ControlFrameProps) => {
|
||||
export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: ControlFrameProps) => {
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
const embeddable = useChildEmbeddable({ container, embeddableId });
|
||||
const {
|
||||
useEmbeddableSelector,
|
||||
containerActions: { untilEmbeddableLoaded, removeEmbeddable },
|
||||
} = useReduxContainerContext<ControlGroupInput>();
|
||||
const { controlStyle } = useEmbeddableSelector((state) => state);
|
||||
|
||||
const embeddable = useChildEmbeddable({ untilEmbeddableLoaded, embeddableId });
|
||||
|
||||
const [title, setTitle] = useState<string>();
|
||||
|
||||
|
@ -61,18 +57,13 @@ export const ControlFrame = ({
|
|||
'controlFrame--floatingActions-oneLine': !usingTwoLineLayout,
|
||||
})}
|
||||
>
|
||||
<EuiToolTip content={ControlFrameStrings.floatingActions.getEditButtonTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlFrameStrings.floatingActions.getEditButtonTitle()}
|
||||
iconType="pencil"
|
||||
onClick={onEdit}
|
||||
color="text"
|
||||
/>
|
||||
<EuiToolTip content={ControlGroupStrings.floatingActions.getEditButtonTitle()}>
|
||||
<EditControlButton embeddableId={embeddableId} />
|
||||
</EuiToolTip>
|
||||
<EuiToolTip content={ControlFrameStrings.floatingActions.getRemoveButtonTitle()}>
|
||||
<EuiToolTip content={ControlGroupStrings.floatingActions.getRemoveButtonTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlFrameStrings.floatingActions.getRemoveButtonTitle()}
|
||||
onClick={onRemove}
|
||||
aria-label={ControlGroupStrings.floatingActions.getRemoveButtonTitle()}
|
||||
onClick={() => removeEmbeddable(embeddableId)}
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
/>
|
|
@ -9,7 +9,7 @@
|
|||
import '../control_group.scss';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
arrayMove,
|
||||
|
@ -29,46 +29,51 @@ import {
|
|||
LayoutMeasuringStrategy,
|
||||
} from '@dnd-kit/core';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { pluginServices } from '../../../../services';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlGroupContainer } from '../control_group_container';
|
||||
import { CreateControlButton } from '../editor/create_control';
|
||||
import { EditControlGroup } from '../editor/edit_control_group';
|
||||
import { forwardAllContext } from '../editor/forward_all_context';
|
||||
import { ControlClone, SortableControl } from './control_group_sortable_item';
|
||||
import { OPTIONS_LIST_CONTROL } from '../../control_types/options_list/options_list_embeddable';
|
||||
import { useReduxContainerContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
|
||||
interface ControlGroupProps {
|
||||
controlGroupContainer: ControlGroupContainer;
|
||||
}
|
||||
export const ControlGroup = () => {
|
||||
// Presentation Services Context
|
||||
const { overlays } = pluginServices.getHooks();
|
||||
const { openFlyout } = overlays.useService();
|
||||
|
||||
export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => {
|
||||
const [controlIds, setControlIds] = useState<string[]>([]);
|
||||
// Redux embeddable container Context
|
||||
const reduxContainerContext = useReduxContainerContext<
|
||||
ControlGroupInput,
|
||||
typeof controlGroupReducers
|
||||
>();
|
||||
const {
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
actions: { setControlOrders },
|
||||
} = reduxContainerContext;
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
|
||||
// sync controlIds every time input panels change
|
||||
useEffect(() => {
|
||||
const subscription = controlGroupContainer.getInput$().subscribe(() => {
|
||||
setControlIds((currentIds) => {
|
||||
// sync control Ids with panels from container input.
|
||||
const { panels } = controlGroupContainer.getInput();
|
||||
const newIds: string[] = [];
|
||||
const allIds = [...currentIds, ...Object.keys(panels)];
|
||||
allIds.forEach((id) => {
|
||||
const currentIndex = currentIds.indexOf(id);
|
||||
if (!panels[id] && currentIndex !== -1) {
|
||||
currentIds.splice(currentIndex, 1);
|
||||
}
|
||||
if (currentIndex === -1 && Boolean(panels[id])) {
|
||||
newIds.push(id);
|
||||
}
|
||||
});
|
||||
return [...currentIds, ...newIds];
|
||||
});
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [controlGroupContainer]);
|
||||
// current state
|
||||
const { panels } = useEmbeddableSelector((state) => state);
|
||||
|
||||
const idsInOrder = useMemo(
|
||||
() =>
|
||||
Object.values(panels)
|
||||
.sort((a, b) => (a.order > b.order ? 1 : -1))
|
||||
.reduce((acc, panel) => {
|
||||
acc.push(panel.explicitInput.id);
|
||||
return acc;
|
||||
}, [] as string[]),
|
||||
[panels]
|
||||
);
|
||||
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
|
||||
const draggingIndex = useMemo(
|
||||
() => (draggingId ? controlIds.indexOf(draggingId) : -1),
|
||||
[controlIds, draggingId]
|
||||
() => (draggingId ? idsInOrder.indexOf(draggingId) : -1),
|
||||
[idsInOrder, draggingId]
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
|
@ -78,10 +83,10 @@ export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => {
|
|||
|
||||
const onDragEnd = ({ over }: DragEndEvent) => {
|
||||
if (over) {
|
||||
const overIndex = controlIds.indexOf(over.id);
|
||||
const overIndex = idsInOrder.indexOf(over.id);
|
||||
if (draggingIndex !== overIndex) {
|
||||
const newIndex = overIndex;
|
||||
setControlIds((currentControlIds) => arrayMove(currentControlIds, draggingIndex, newIndex));
|
||||
dispatch(setControlOrders({ ids: arrayMove([...idsInOrder], draggingIndex, newIndex) }));
|
||||
}
|
||||
}
|
||||
setDraggingId(null);
|
||||
|
@ -100,36 +105,26 @@ export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => {
|
|||
strategy: LayoutMeasuringStrategy.Always,
|
||||
}}
|
||||
>
|
||||
<SortableContext items={controlIds} strategy={rectSortingStrategy}>
|
||||
<SortableContext items={idsInOrder} strategy={rectSortingStrategy}>
|
||||
<EuiFlexGroup
|
||||
className={classNames('controlGroup', { 'controlGroup-isDragging': draggingId })}
|
||||
alignItems="center"
|
||||
gutterSize={'m'}
|
||||
wrap={true}
|
||||
>
|
||||
{controlIds.map((controlId, index) => (
|
||||
{idsInOrder.map(
|
||||
(controlId, index) =>
|
||||
panels[controlId] && (
|
||||
<SortableControl
|
||||
onEdit={() => controlGroupContainer.editControl(controlId)}
|
||||
onRemove={() => controlGroupContainer.removeEmbeddable(controlId)}
|
||||
dragInfo={{ index, draggingIndex }}
|
||||
container={controlGroupContainer}
|
||||
controlStyle={controlGroupContainer.getInput().controlStyle}
|
||||
embeddableId={controlId}
|
||||
width={controlGroupContainer.getInput().panels[controlId].width}
|
||||
key={controlId}
|
||||
/>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{draggingId ? (
|
||||
<ControlClone
|
||||
width={controlGroupContainer.getInput().panels[draggingId].width}
|
||||
embeddableId={draggingId}
|
||||
container={controlGroupContainer}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
<DragOverlay>{draggingId ? <ControlClone draggingId={draggingId} /> : null}</DragOverlay>
|
||||
</DndContext>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -141,19 +136,15 @@ export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => {
|
|||
iconType="gear"
|
||||
color="text"
|
||||
data-test-subj="inputControlsSortingButton"
|
||||
onClick={controlGroupContainer.editControlGroup}
|
||||
onClick={() =>
|
||||
openFlyout(forwardAllContext(<EditControlGroup />, reduxContainerContext))
|
||||
}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip content={ControlGroupStrings.management.getAddControlTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlGroupStrings.management.getManageButtonTitle()}
|
||||
iconType="plus"
|
||||
color="text"
|
||||
data-test-subj="inputControlsSortingButton"
|
||||
onClick={() => controlGroupContainer.createNewControl(OPTIONS_LIST_CONTROL)} // use popover when there are multiple types of control
|
||||
/>
|
||||
<CreateControlButton />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -12,10 +12,9 @@ import { useSortable } from '@dnd-kit/sortable';
|
|||
import { CSS } from '@dnd-kit/utilities';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ControlWidth } from '../../types';
|
||||
import { ControlGroupContainer } from '../control_group_container';
|
||||
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
|
||||
import { ControlFrame, ControlFrameProps } from '../../control_frame/control_frame_component';
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { ControlFrame, ControlFrameProps } from './control_frame_component';
|
||||
import { useReduxContainerContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
|
||||
interface DragInfo {
|
||||
isOver?: boolean;
|
||||
|
@ -26,7 +25,6 @@ interface DragInfo {
|
|||
|
||||
export type SortableControlProps = ControlFrameProps & {
|
||||
dragInfo: DragInfo;
|
||||
width: ControlWidth;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -60,22 +58,12 @@ export const SortableControl = (frameProps: SortableControlProps) => {
|
|||
const SortableControlInner = forwardRef<
|
||||
HTMLButtonElement,
|
||||
SortableControlProps & { style: HTMLAttributes<HTMLButtonElement>['style'] }
|
||||
>(
|
||||
(
|
||||
{
|
||||
embeddableId,
|
||||
controlStyle,
|
||||
container,
|
||||
dragInfo,
|
||||
onRemove,
|
||||
onEdit,
|
||||
style,
|
||||
width,
|
||||
...dragHandleProps
|
||||
},
|
||||
dragHandleRef
|
||||
) => {
|
||||
>(({ embeddableId, dragInfo, style, ...dragHandleProps }, dragHandleRef) => {
|
||||
const { isOver, isDragging, draggingIndex, index } = dragInfo;
|
||||
const { useEmbeddableSelector } = useReduxContainerContext<ControlGroupInput>();
|
||||
const { panels } = useEmbeddableSelector((state) => state);
|
||||
|
||||
const width = panels[embeddableId].width;
|
||||
|
||||
const dragHandle = (
|
||||
<button ref={dragHandleRef} {...dragHandleProps} className="controlFrame--dragHandle">
|
||||
|
@ -98,53 +86,39 @@ const SortableControlInner = forwardRef<
|
|||
>
|
||||
<ControlFrame
|
||||
enableActions={draggingIndex === -1}
|
||||
controlStyle={controlStyle}
|
||||
embeddableId={embeddableId}
|
||||
customPrepend={dragHandle}
|
||||
container={container}
|
||||
onRemove={onRemove}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* A simplified clone version of the control which is dragged. This version only shows
|
||||
* the title, because individual controls can be any size, and dragging a wide item
|
||||
* can be quite cumbersome.
|
||||
*/
|
||||
export const ControlClone = ({
|
||||
embeddableId,
|
||||
container,
|
||||
width,
|
||||
}: {
|
||||
embeddableId: string;
|
||||
container: ControlGroupContainer;
|
||||
width: ControlWidth;
|
||||
}) => {
|
||||
const embeddable = useChildEmbeddable({ embeddableId, container });
|
||||
const layout = container.getInput().controlStyle;
|
||||
export const ControlClone = ({ draggingId }: { draggingId: string }) => {
|
||||
const { useEmbeddableSelector } = useReduxContainerContext<ControlGroupInput>();
|
||||
const { panels, controlStyle } = useEmbeddableSelector((state) => state);
|
||||
|
||||
const width = panels[draggingId].width;
|
||||
const title = panels[draggingId].explicitInput.title;
|
||||
return (
|
||||
<EuiFlexItem
|
||||
className={classNames('controlFrame--cloneWrapper', {
|
||||
'controlFrame--cloneWrapper-small': width === 'small',
|
||||
'controlFrame--cloneWrapper-medium': width === 'medium',
|
||||
'controlFrame--cloneWrapper-large': width === 'large',
|
||||
'controlFrame--cloneWrapper-twoLine': layout === 'twoLine',
|
||||
'controlFrame--cloneWrapper-twoLine': controlStyle === 'twoLine',
|
||||
})}
|
||||
>
|
||||
{layout === 'twoLine' ? (
|
||||
<EuiFormLabel>{embeddable?.getInput().title}</EuiFormLabel>
|
||||
) : undefined}
|
||||
{controlStyle === 'twoLine' ? <EuiFormLabel>{title}</EuiFormLabel> : undefined}
|
||||
<EuiFlexGroup gutterSize="none" className={'controlFrame--draggable'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="grabHorizontal" className="controlFrame--dragHandle" />
|
||||
</EuiFlexItem>
|
||||
{container.getInput().controlStyle === 'oneLine' ? (
|
||||
<EuiFlexItem>{embeddable?.getInput().title}</EuiFlexItem>
|
||||
) : undefined}
|
||||
{controlStyle === 'oneLine' ? <EuiFlexItem>{title}</EuiFlexItem> : undefined}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -1,224 +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 React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import {
|
||||
Container,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
} from '../../../../../embeddable/public';
|
||||
import {
|
||||
InputControlEmbeddable,
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
IEditableControlFactory,
|
||||
ControlWidth,
|
||||
} from '../types';
|
||||
import { ControlsService } from '../controls_service';
|
||||
import { ControlGroupInput, ControlPanelState } from './types';
|
||||
import { ManageControlComponent } from './editor/manage_control';
|
||||
import { toMountPoint } from '../../../../../kibana_react/public';
|
||||
import { ControlGroup } from './component/control_group_component';
|
||||
import { PresentationOverlaysService } from '../../../services/overlays';
|
||||
import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_WIDTH } from './control_group_constants';
|
||||
import { ManageControlGroup } from './editor/manage_control_group_component';
|
||||
import { OverlayRef } from '../../../../../../core/public';
|
||||
import { ControlGroupStrings } from './control_group_strings';
|
||||
|
||||
export class ControlGroupContainer extends Container<InputControlInput, ControlGroupInput> {
|
||||
public readonly type = CONTROL_GROUP_TYPE;
|
||||
|
||||
private nextControlWidth: ControlWidth = DEFAULT_CONTROL_WIDTH;
|
||||
|
||||
constructor(
|
||||
initialInput: ControlGroupInput,
|
||||
private readonly controlsService: ControlsService,
|
||||
private readonly overlays: PresentationOverlaysService,
|
||||
parent?: Container
|
||||
) {
|
||||
super(initialInput, { embeddableLoaded: {} }, controlsService.getControlFactory, parent);
|
||||
this.overlays = overlays;
|
||||
this.controlsService = controlsService;
|
||||
}
|
||||
|
||||
protected createNewPanelState<TEmbeddableInput extends InputControlInput = InputControlInput>(
|
||||
factory: EmbeddableFactory<InputControlInput, InputControlOutput, InputControlEmbeddable>,
|
||||
partial: Partial<TEmbeddableInput> = {}
|
||||
): ControlPanelState<TEmbeddableInput> {
|
||||
const panelState = super.createNewPanelState(factory, partial);
|
||||
return {
|
||||
order: 1,
|
||||
width: this.nextControlWidth,
|
||||
...panelState,
|
||||
} as ControlPanelState<TEmbeddableInput>;
|
||||
}
|
||||
|
||||
protected getInheritedInput(id: string): InputControlInput {
|
||||
const { filters, query, timeRange, inheritParentState } = this.getInput();
|
||||
return {
|
||||
filters: inheritParentState.useFilters ? filters : undefined,
|
||||
query: inheritParentState.useQuery ? query : undefined,
|
||||
timeRange: inheritParentState.useTimerange ? timeRange : undefined,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
public createNewControl = async (type: string) => {
|
||||
const factory = this.controlsService.getControlFactory(type);
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(type);
|
||||
|
||||
const initialInputPromise = new Promise<Omit<InputControlInput, 'id'>>((resolve, reject) => {
|
||||
let inputToReturn: Partial<InputControlInput> = {};
|
||||
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
this.overlays
|
||||
.openConfirm(ControlGroupStrings.management.discardNewControl.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.discardNewControl.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.discardNewControl.getCancel(),
|
||||
title: ControlGroupStrings.management.discardNewControl.getTitle(),
|
||||
buttonColor: 'danger',
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
reject();
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutInstance = this.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<ManageControlComponent
|
||||
width={this.nextControlWidth}
|
||||
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
|
||||
updateWidth={(newWidth) => (this.nextControlWidth = newWidth)}
|
||||
controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
|
||||
onChange: (partialInput) => {
|
||||
inputToReturn = { ...inputToReturn, ...partialInput };
|
||||
},
|
||||
})}
|
||||
onSave={() => {
|
||||
resolve(inputToReturn);
|
||||
flyoutInstance.close();
|
||||
}}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
}
|
||||
);
|
||||
});
|
||||
initialInputPromise.then(
|
||||
async (explicitInput) => {
|
||||
await this.addNewEmbeddable(type, explicitInput);
|
||||
},
|
||||
() => {} // swallow promise rejection because it can be part of normal flow
|
||||
);
|
||||
};
|
||||
|
||||
public editControl = async (embeddableId: string) => {
|
||||
const panel = this.getInput().panels[embeddableId];
|
||||
const factory = this.getFactory(panel.type);
|
||||
const embeddable = await this.untilEmbeddableLoaded(embeddableId);
|
||||
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
|
||||
|
||||
const initialExplicitInput = cloneDeep(panel.explicitInput);
|
||||
const initialWidth = panel.width;
|
||||
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
this.overlays
|
||||
.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) {
|
||||
embeddable.updateInput(initialExplicitInput);
|
||||
this.updateInput({
|
||||
panels: {
|
||||
...this.getInput().panels,
|
||||
[embeddableId]: { ...this.getInput().panels[embeddableId], width: initialWidth },
|
||||
},
|
||||
});
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutInstance = this.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<ManageControlComponent
|
||||
width={panel.width}
|
||||
title={embeddable.getTitle()}
|
||||
removeControl={() => this.removeEmbeddable(embeddableId)}
|
||||
updateTitle={(newTitle) => embeddable.updateInput({ title: newTitle })}
|
||||
controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
|
||||
onChange: (partialInput) => embeddable.updateInput(partialInput),
|
||||
initialInput: embeddable.getInput(),
|
||||
})}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
onSave={() => flyoutInstance.close()}
|
||||
updateWidth={(newWidth) =>
|
||||
this.updateInput({
|
||||
panels: {
|
||||
...this.getInput().panels,
|
||||
[embeddableId]: { ...this.getInput().panels[embeddableId], width: newWidth },
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
public editControlGroup = () => {
|
||||
const flyoutInstance = this.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<ManageControlGroup
|
||||
controlStyle={this.getInput().controlStyle}
|
||||
setControlStyle={(newStyle) => this.updateInput({ controlStyle: newStyle })}
|
||||
deleteAllEmbeddables={() => {
|
||||
this.overlays
|
||||
.openConfirm(ControlGroupStrings.management.deleteAllControls.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.deleteAllControls.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.deleteAllControls.getCancel(),
|
||||
title: ControlGroupStrings.management.deleteAllControls.getTitle(),
|
||||
buttonColor: 'danger',
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
Object.keys(this.getInput().panels).forEach((id) => this.removeEmbeddable(id));
|
||||
flyoutInstance.close();
|
||||
}
|
||||
});
|
||||
}}
|
||||
setAllPanelWidths={(newWidth) => {
|
||||
const newPanels = cloneDeep(this.getInput().panels);
|
||||
Object.values(newPanels).forEach((panel) => (panel.width = newWidth));
|
||||
this.updateInput({ panels: { ...newPanels, ...newPanels } });
|
||||
}}
|
||||
panels={this.getInput().panels}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
public render(dom: HTMLElement) {
|
||||
ReactDOM.render(<ControlGroup controlGroupContainer={this} />, dom);
|
||||
}
|
||||
}
|
|
@ -48,13 +48,9 @@ export const ControlGroupStrings = {
|
|||
i18n.translate('presentationUtil.inputControls.controlGroup.management.flyoutTitle', {
|
||||
defaultMessage: 'Manage controls',
|
||||
}),
|
||||
getDesignTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.designTitle', {
|
||||
defaultMessage: 'Design',
|
||||
}),
|
||||
getWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.widthTitle', {
|
||||
defaultMessage: 'Width',
|
||||
getDefaultWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.defaultWidthTitle', {
|
||||
defaultMessage: 'Default width',
|
||||
}),
|
||||
getLayoutTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layoutTitle', {
|
||||
|
@ -64,23 +60,20 @@ export const ControlGroupStrings = {
|
|||
i18n.translate('presentationUtil.inputControls.controlGroup.management.delete', {
|
||||
defaultMessage: 'Delete control',
|
||||
}),
|
||||
getSetAllWidthsToDefaultTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.setAllWidths', {
|
||||
defaultMessage: 'Set all widths to default',
|
||||
}),
|
||||
getDeleteAllButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll', {
|
||||
defaultMessage: 'Delete all',
|
||||
}),
|
||||
controlWidth: {
|
||||
getChangeAllControlWidthsTitle: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.layout.changeAllControlWidths',
|
||||
{
|
||||
defaultMessage: 'Set width for all controls',
|
||||
}
|
||||
),
|
||||
getWidthSwitchLegend: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.layout.controlWidthLegend',
|
||||
{
|
||||
defaultMessage: 'Change individual control width',
|
||||
defaultMessage: 'Change control width',
|
||||
}
|
||||
),
|
||||
getAutoWidthTitle: () =>
|
||||
|
@ -117,21 +110,31 @@ export const ControlGroupStrings = {
|
|||
defaultMessage: 'Two line layout',
|
||||
}),
|
||||
},
|
||||
deleteAllControls: {
|
||||
getTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.title', {
|
||||
defaultMessage: 'Delete all?',
|
||||
}),
|
||||
deleteControls: {
|
||||
getDeleteAllTitle: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.delete.deleteAllTitle',
|
||||
{
|
||||
defaultMessage: 'Delete all controls?',
|
||||
}
|
||||
),
|
||||
getDeleteTitle: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.delete.deleteTitle',
|
||||
{
|
||||
defaultMessage: 'Delete control?',
|
||||
}
|
||||
),
|
||||
getSubtitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.sub', {
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.delete.sub', {
|
||||
defaultMessage: 'Controls are not recoverable once removed.',
|
||||
}),
|
||||
getConfirm: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.confirm', {
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.delete.confirm', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
getCancel: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.cancel', {
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.delete.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
},
|
||||
|
@ -143,7 +146,7 @@ export const ControlGroupStrings = {
|
|||
getSubtitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.sub', {
|
||||
defaultMessage:
|
||||
'Discard changes to this control? Controls are not recoverable once removed.',
|
||||
'Discard changes to this control? Changes are not recoverable once discardsd.',
|
||||
}),
|
||||
getConfirm: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.confirm', {
|
||||
|
@ -161,7 +164,7 @@ export const ControlGroupStrings = {
|
|||
}),
|
||||
getSubtitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.sub', {
|
||||
defaultMessage: 'Discard new control? Controls are not recoverable once removed.',
|
||||
defaultMessage: 'Discard new control? Controls are not recoverable once discarded.',
|
||||
}),
|
||||
getConfirm: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.confirm', {
|
||||
|
@ -173,4 +176,14 @@ export const ControlGroupStrings = {
|
|||
}),
|
||||
},
|
||||
},
|
||||
floatingActions: {
|
||||
getEditButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.editTitle', {
|
||||
defaultMessage: 'Manage control',
|
||||
}),
|
||||
getRemoveButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.removeTitle', {
|
||||
defaultMessage: 'Remove control',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -46,7 +46,7 @@ interface ManageControlProps {
|
|||
updateWidth: (newWidth: ControlWidth) => void;
|
||||
}
|
||||
|
||||
export const ManageControlComponent = ({
|
||||
export const ControlEditor = ({
|
||||
controlEditorComponent,
|
||||
removeControl,
|
||||
updateTitle,
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiButtonIconColor,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, ReactElement } from 'react';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { ControlEditor } from './control_editor';
|
||||
import { pluginServices } from '../../../../services';
|
||||
import { forwardAllContext } from './forward_all_context';
|
||||
import { OverlayRef } from '../../../../../../../core/public';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { InputControlInput } from '../../../../services/controls';
|
||||
import { DEFAULT_CONTROL_WIDTH } from '../control_group_constants';
|
||||
import { ControlWidth, IEditableControlFactory } from '../../types';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { EmbeddableFactoryNotFoundError } from '../../../../../../embeddable/public';
|
||||
import { useReduxContainerContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
|
||||
export const CreateControlButton = () => {
|
||||
// Presentation Services Context
|
||||
const { overlays, controls } = pluginServices.getHooks();
|
||||
const { getInputControlTypes, getControlFactory } = controls.useService();
|
||||
const { openFlyout, openConfirm } = overlays.useService();
|
||||
|
||||
// Redux embeddable container Context
|
||||
const reduxContainerContext = useReduxContainerContext<
|
||||
ControlGroupInput,
|
||||
typeof controlGroupReducers
|
||||
>();
|
||||
const {
|
||||
containerActions: { addNewEmbeddable },
|
||||
actions: { setDefaultControlWidth },
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
} = reduxContainerContext;
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
|
||||
// current state
|
||||
const { defaultControlWidth } = useEmbeddableSelector((state) => state);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const createNewControl = async (type: string) => {
|
||||
const factory = getControlFactory(type);
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(type);
|
||||
|
||||
const initialInputPromise = new Promise<Omit<InputControlInput, 'id'>>((resolve, reject) => {
|
||||
let inputToReturn: Partial<InputControlInput> = {};
|
||||
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
if (Object.keys(inputToReturn).length === 0) {
|
||||
reject();
|
||||
ref.close();
|
||||
return;
|
||||
}
|
||||
openConfirm(ControlGroupStrings.management.discardNewControl.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.discardNewControl.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.discardNewControl.getCancel(),
|
||||
title: ControlGroupStrings.management.discardNewControl.getTitle(),
|
||||
buttonColor: 'danger',
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
reject();
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutInstance = openFlyout(
|
||||
forwardAllContext(
|
||||
<ControlEditor
|
||||
width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
|
||||
updateWidth={(newWidth) => dispatch(setDefaultControlWidth(newWidth as ControlWidth))}
|
||||
controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
|
||||
onChange: (partialInput) => {
|
||||
inputToReturn = { ...inputToReturn, ...partialInput };
|
||||
},
|
||||
})}
|
||||
onSave={() => {
|
||||
resolve(inputToReturn);
|
||||
flyoutInstance.close();
|
||||
}}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
/>,
|
||||
reduxContainerContext
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
}
|
||||
);
|
||||
});
|
||||
initialInputPromise.then(
|
||||
async (explicitInput) => {
|
||||
await addNewEmbeddable(type, explicitInput);
|
||||
},
|
||||
() => {} // swallow promise rejection because it can be part of normal flow
|
||||
);
|
||||
};
|
||||
|
||||
if (getInputControlTypes().length === 0) return null;
|
||||
|
||||
const commonButtonProps = {
|
||||
iconType: 'plus',
|
||||
color: 'text' as EuiButtonIconColor,
|
||||
'data-test-subj': 'inputControlsSortingButton',
|
||||
'aria-label': ControlGroupStrings.management.getManageButtonTitle(),
|
||||
};
|
||||
|
||||
if (getInputControlTypes().length > 1) {
|
||||
const items: ReactElement[] = [];
|
||||
getInputControlTypes().forEach((type) => {
|
||||
const factory = getControlFactory(type);
|
||||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key={type}
|
||||
icon={factory.getIconType?.()}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
createNewControl(type);
|
||||
}}
|
||||
>
|
||||
{factory.getDisplayName()}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
const button = <EuiButtonIcon {...commonButtonProps} onClick={() => setIsPopoverOpen(true)} />;
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
{...commonButtonProps}
|
||||
onClick={() => createNewControl(getInputControlTypes()[0])}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { isEqual } from 'lodash';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { ControlEditor } from './control_editor';
|
||||
import { IEditableControlFactory } from '../../types';
|
||||
import { pluginServices } from '../../../../services';
|
||||
import { forwardAllContext } from './forward_all_context';
|
||||
import { OverlayRef } from '../../../../../../../core/public';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { EmbeddableFactoryNotFoundError } from '../../../../../../embeddable/public';
|
||||
import { useReduxContainerContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
|
||||
export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => {
|
||||
// Presentation Services Context
|
||||
const { overlays, controls } = pluginServices.getHooks();
|
||||
const { getControlFactory } = controls.useService();
|
||||
const { openFlyout, openConfirm } = overlays.useService();
|
||||
|
||||
// Redux embeddable container Context
|
||||
const reduxContainerContext = useReduxContainerContext<
|
||||
ControlGroupInput,
|
||||
typeof controlGroupReducers
|
||||
>();
|
||||
const {
|
||||
containerActions: { untilEmbeddableLoaded, removeEmbeddable, updateInputForChild },
|
||||
actions: { setControlWidth },
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
} = reduxContainerContext;
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
|
||||
// current state
|
||||
const { panels } = useEmbeddableSelector((state) => state);
|
||||
|
||||
// keep up to date ref of latest panel state for comparison when closing editor.
|
||||
const latestPanelState = useRef(panels[embeddableId]);
|
||||
useEffect(() => {
|
||||
latestPanelState.current = panels[embeddableId];
|
||||
}, [panels, embeddableId]);
|
||||
|
||||
const editControl = async () => {
|
||||
const panel = panels[embeddableId];
|
||||
const factory = getControlFactory(panel.type);
|
||||
const embeddable = await untilEmbeddableLoaded(embeddableId);
|
||||
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
|
||||
|
||||
let removed = false;
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
if (
|
||||
removed ||
|
||||
(isEqual(latestPanelState.current.explicitInput, panel.explicitInput) &&
|
||||
isEqual(latestPanelState.current.width, panel.width))
|
||||
) {
|
||||
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) {
|
||||
updateInputForChild(embeddableId, panel.explicitInput);
|
||||
dispatch(setControlWidth({ width: panel.width, embeddableId }));
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutInstance = openFlyout(
|
||||
forwardAllContext(
|
||||
<ControlEditor
|
||||
width={panel.width}
|
||||
title={embeddable.getTitle()}
|
||||
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();
|
||||
}
|
||||
});
|
||||
}}
|
||||
updateTitle={(newTitle) => updateInputForChild(embeddableId, { title: newTitle })}
|
||||
controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
|
||||
onChange: (partialInput) => updateInputForChild(embeddableId, partialInput),
|
||||
initialInput: embeddable.getInput(),
|
||||
})}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
onSave={() => flyoutInstance.close()}
|
||||
updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, embeddableId }))}
|
||||
/>,
|
||||
reduxContainerContext
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlGroupStrings.floatingActions.getEditButtonTitle()}
|
||||
iconType="pencil"
|
||||
onClick={() => editControl()}
|
||||
color="text"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFormRow,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlyoutBody,
|
||||
EuiButtonGroup,
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutHeader,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
CONTROL_LAYOUT_OPTIONS,
|
||||
CONTROL_WIDTH_OPTIONS,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
} from '../control_group_constants';
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { pluginServices } from '../../../../services';
|
||||
import { ControlStyle, ControlWidth } from '../../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { useReduxContainerContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
|
||||
export const EditControlGroup = () => {
|
||||
const { overlays } = pluginServices.getHooks();
|
||||
const { openConfirm } = overlays.useService();
|
||||
|
||||
const {
|
||||
containerActions,
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
actions: { setControlStyle, setAllControlWidths, setDefaultControlWidth },
|
||||
} = useReduxContainerContext<ControlGroupInput, typeof controlGroupReducers>();
|
||||
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const { panels, controlStyle, defaultControlWidth } = useEmbeddableSelector((state) => state);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{ControlGroupStrings.management.getFlyoutTitle()}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()}
|
||||
options={CONTROL_LAYOUT_OPTIONS}
|
||||
idSelected={controlStyle}
|
||||
onChange={(newControlStyle) =>
|
||||
dispatch(setControlStyle(newControlStyle as ControlStyle))
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
|
||||
<EuiFlexGroup direction={'row'}>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
idSelected={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
onChange={(newWidth: string) =>
|
||||
dispatch(setDefaultControlWidth(newWidth as ControlWidth))
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
onClick={() =>
|
||||
dispatch(setAllControlWidths(defaultControlWidth ?? DEFAULT_CONTROL_WIDTH))
|
||||
}
|
||||
aria-label={'delete-all'}
|
||||
iconType="returnKey"
|
||||
size="s"
|
||||
>
|
||||
{ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
if (!containerActions?.removeEmbeddable) return;
|
||||
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
|
||||
title: ControlGroupStrings.management.deleteControls.getDeleteAllTitle(),
|
||||
buttonColor: 'danger',
|
||||
}).then((confirmed) => {
|
||||
if (confirmed)
|
||||
Object.keys(panels).forEach((panelId) =>
|
||||
containerActions.removeEmbeddable(panelId)
|
||||
);
|
||||
});
|
||||
}}
|
||||
aria-label={'delete-all'}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
flush="left"
|
||||
size="s"
|
||||
>
|
||||
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { Provider } from 'react-redux';
|
||||
import { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { pluginServices } from '../../../../services';
|
||||
import { toMountPoint } from '../../../../../../kibana_react/public';
|
||||
import { ReduxContainerContextServices } from '../../../redux_embeddables/types';
|
||||
import { ReduxEmbeddableContext } from '../../../redux_embeddables/redux_embeddable_context';
|
||||
import { getManagedEmbeddablesStore } from '../../../redux_embeddables/generic_embeddable_store';
|
||||
|
||||
/**
|
||||
* The overlays service creates its divs outside the flow of the component. This necessitates
|
||||
* passing all context from the component to the flyout.
|
||||
*/
|
||||
export const forwardAllContext = (
|
||||
component: ReactElement,
|
||||
reduxContainerContext: ReduxContainerContextServices<ControlGroupInput>
|
||||
) => {
|
||||
const PresentationUtilProvider = pluginServices.getContextProvider();
|
||||
return toMountPoint(
|
||||
<Provider store={getManagedEmbeddablesStore()}>
|
||||
<ReduxEmbeddableContext.Provider value={reduxContainerContext}>
|
||||
<PresentationUtilProvider>{component}</PresentationUtilProvider>
|
||||
</ReduxEmbeddableContext.Provider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
|
@ -1,113 +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, { useState } from 'react';
|
||||
import {
|
||||
EuiFlyoutHeader,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonGroup,
|
||||
EuiFlyoutBody,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ControlsPanels } from '../types';
|
||||
import { ControlStyle, ControlWidth } from '../../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from '../control_group_constants';
|
||||
|
||||
interface ManageControlGroupProps {
|
||||
panels: ControlsPanels;
|
||||
controlStyle: ControlStyle;
|
||||
deleteAllEmbeddables: () => void;
|
||||
setControlStyle: (style: ControlStyle) => void;
|
||||
setAllPanelWidths: (newWidth: ControlWidth) => void;
|
||||
}
|
||||
|
||||
export const ManageControlGroup = ({
|
||||
panels,
|
||||
controlStyle,
|
||||
setControlStyle,
|
||||
setAllPanelWidths,
|
||||
deleteAllEmbeddables,
|
||||
}: ManageControlGroupProps) => {
|
||||
const [currentControlStyle, setCurrentControlStyle] = useState<ControlStyle>(controlStyle);
|
||||
const [selectedWidth, setSelectedWidth] = useState<ControlWidth>();
|
||||
const [selectionDisplay, setSelectionDisplay] = useState(false);
|
||||
|
||||
useMount(() => {
|
||||
if (!panels || Object.keys(panels).length === 0) return;
|
||||
const firstWidth = panels[Object.keys(panels)[0]].width;
|
||||
if (Object.values(panels).every((panel) => panel.width === firstWidth)) {
|
||||
setSelectedWidth(firstWidth);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{ControlGroupStrings.management.getFlyoutTitle()}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()}
|
||||
options={CONTROL_LAYOUT_OPTIONS}
|
||||
idSelected={currentControlStyle}
|
||||
onChange={(newControlStyle) => {
|
||||
setControlStyle(newControlStyle as ControlStyle);
|
||||
setCurrentControlStyle(newControlStyle as ControlStyle);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label={ControlGroupStrings.management.getWidthTitle()}>
|
||||
<EuiSwitch
|
||||
label={ControlGroupStrings.management.controlWidth.getChangeAllControlWidthsTitle()}
|
||||
checked={selectionDisplay}
|
||||
onChange={() => setSelectionDisplay(!selectionDisplay)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{selectionDisplay ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
idSelected={selectedWidth ?? ''}
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
onChange={(newWidth: string) => {
|
||||
setAllPanelWidths(newWidth as ControlWidth);
|
||||
setSelectedWidth(newWidth as ControlWidth);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : undefined}
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
onClick={deleteAllEmbeddables}
|
||||
aria-label={'delete-all'}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
flush="left"
|
||||
size="s"
|
||||
>
|
||||
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
InputControlEmbeddable,
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
} from '../../../../services/controls';
|
||||
import { pluginServices } from '../../../../services';
|
||||
import { ControlGroupInput, ControlPanelState } from '../types';
|
||||
import { ControlGroup } from '../component/control_group_component';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { Container, EmbeddableFactory } from '../../../../../../embeddable/public';
|
||||
import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_WIDTH } from '../control_group_constants';
|
||||
import { ReduxEmbeddableWrapper } from '../../../redux_embeddables/redux_embeddable_wrapper';
|
||||
|
||||
export class ControlGroupContainer extends Container<InputControlInput, ControlGroupInput> {
|
||||
public readonly type = CONTROL_GROUP_TYPE;
|
||||
|
||||
constructor(initialInput: ControlGroupInput, parent?: Container) {
|
||||
super(
|
||||
initialInput,
|
||||
{ embeddableLoaded: {} },
|
||||
pluginServices.getServices().controls.getControlFactory,
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
||||
protected createNewPanelState<TEmbeddableInput extends InputControlInput = InputControlInput>(
|
||||
factory: EmbeddableFactory<InputControlInput, InputControlOutput, InputControlEmbeddable>,
|
||||
partial: Partial<TEmbeddableInput> = {}
|
||||
): ControlPanelState<TEmbeddableInput> {
|
||||
const panelState = super.createNewPanelState(factory, partial);
|
||||
const highestOrder = Object.values(this.getInput().panels).reduce((highestSoFar, panel) => {
|
||||
if (panel.order > highestSoFar) highestSoFar = panel.order;
|
||||
return highestSoFar;
|
||||
}, 0);
|
||||
return {
|
||||
order: highestOrder + 1,
|
||||
width: this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH,
|
||||
...panelState,
|
||||
} as ControlPanelState<TEmbeddableInput>;
|
||||
}
|
||||
|
||||
protected getInheritedInput(id: string): InputControlInput {
|
||||
const { filters, query, timeRange, inheritParentState } = this.getInput();
|
||||
return {
|
||||
filters: inheritParentState.useFilters ? filters : undefined,
|
||||
query: inheritParentState.useQuery ? query : undefined,
|
||||
timeRange: inheritParentState.useTimerange ? timeRange : undefined,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
public render(dom: HTMLElement) {
|
||||
const PresentationUtilProvider = pluginServices.getContextProvider();
|
||||
ReactDOM.render(
|
||||
<PresentationUtilProvider>
|
||||
<ReduxEmbeddableWrapper<ControlGroupInput>
|
||||
embeddable={this}
|
||||
reducers={controlGroupReducers}
|
||||
>
|
||||
<ControlGroup />
|
||||
</ReduxEmbeddableWrapper>
|
||||
</PresentationUtilProvider>,
|
||||
dom
|
||||
);
|
||||
}
|
||||
}
|
|
@ -20,13 +20,11 @@ import {
|
|||
EmbeddableFactory,
|
||||
EmbeddableFactoryDefinition,
|
||||
ErrorEmbeddable,
|
||||
} from '../../../../../embeddable/public';
|
||||
import { ControlGroupInput } from './types';
|
||||
import { ControlsService } from '../controls_service';
|
||||
import { ControlGroupStrings } from './control_group_strings';
|
||||
import { CONTROL_GROUP_TYPE } from './control_group_constants';
|
||||
} from '../../../../../../embeddable/public';
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { CONTROL_GROUP_TYPE } from '../control_group_constants';
|
||||
import { ControlGroupContainer } from './control_group_container';
|
||||
import { PresentationOverlaysService } from '../../../services/overlays';
|
||||
|
||||
export type DashboardContainerFactory = EmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
|
@ -38,13 +36,6 @@ export class ControlGroupContainerFactory
|
|||
{
|
||||
public readonly isContainerType = true;
|
||||
public readonly type = CONTROL_GROUP_TYPE;
|
||||
public readonly controlsService: ControlsService;
|
||||
private readonly overlays: PresentationOverlaysService;
|
||||
|
||||
constructor(controlsService: ControlsService, overlays: PresentationOverlaysService) {
|
||||
this.overlays = overlays;
|
||||
this.controlsService = controlsService;
|
||||
}
|
||||
|
||||
public isEditable = async () => false;
|
||||
|
||||
|
@ -67,6 +58,6 @@ export class ControlGroupContainerFactory
|
|||
initialInput: ControlGroupInput,
|
||||
parent?: Container
|
||||
): Promise<ControlGroupContainer | ErrorEmbeddable> => {
|
||||
return new ControlGroupContainer(initialInput, this.controlsService, this.overlays, parent);
|
||||
return new ControlGroupContainer(initialInput, parent);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { WritableDraft } from 'immer/dist/types/types-external';
|
||||
|
||||
import { ControlWidth } from '../../types';
|
||||
import { ControlGroupInput } from '../types';
|
||||
|
||||
export const controlGroupReducers = {
|
||||
setControlStyle: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<ControlGroupInput['controlStyle']>
|
||||
) => {
|
||||
state.controlStyle = action.payload;
|
||||
},
|
||||
setDefaultControlWidth: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<ControlGroupInput['defaultControlWidth']>
|
||||
) => {
|
||||
state.defaultControlWidth = action.payload;
|
||||
},
|
||||
setAllControlWidths: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<ControlWidth>
|
||||
) => {
|
||||
Object.keys(state.panels).forEach((panelId) => (state.panels[panelId].width = action.payload));
|
||||
},
|
||||
setControlWidth: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<{ width: ControlWidth; embeddableId: string }>
|
||||
) => {
|
||||
state.panels[action.payload.embeddableId].width = action.payload.width;
|
||||
},
|
||||
setControlOrders: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<{ ids: string[] }>
|
||||
) => {
|
||||
action.payload.ids.forEach((id, index) => {
|
||||
state.panels[id].order = index;
|
||||
});
|
||||
},
|
||||
};
|
|
@ -7,7 +7,8 @@
|
|||
*/
|
||||
|
||||
import { PanelState, EmbeddableInput } from '../../../../../embeddable/public';
|
||||
import { ControlStyle, ControlWidth, InputControlInput } from '../types';
|
||||
import { InputControlInput } from '../../../services/controls';
|
||||
import { ControlStyle, ControlWidth } from '../types';
|
||||
|
||||
export interface ControlGroupInput
|
||||
extends EmbeddableInput,
|
||||
|
@ -17,6 +18,7 @@ export interface ControlGroupInput
|
|||
useQuery: boolean;
|
||||
useTimerange: boolean;
|
||||
};
|
||||
defaultControlWidth?: ControlWidth;
|
||||
controlStyle: ControlStyle;
|
||||
panels: ControlsPanels;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { tap, debounceTime, map, distinctUntilChanged } from 'rxjs/operators';
|
|||
import { esFilters } from '../../../../../../data/public';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { Embeddable, IContainer } from '../../../../../../embeddable/public';
|
||||
import { InputControlInput, InputControlOutput } from '../../types';
|
||||
import { InputControlInput, InputControlOutput } from '../../../../services/controls';
|
||||
import { OptionsListComponent, OptionsListComponentState } from './options_list_component';
|
||||
|
||||
const toggleAvailableOptions = (
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
import { EmbeddableFactory } from '../../../../embeddable/public';
|
||||
import {
|
||||
ControlTypeRegistry,
|
||||
InputControlEmbeddable,
|
||||
ControlTypeRegistry,
|
||||
InputControlFactory,
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
} from './types';
|
||||
InputControlInput,
|
||||
} from '../../services/controls';
|
||||
|
||||
export class ControlsService {
|
||||
private controlsFactoriesMap: ControlTypeRegistry = {};
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { InputControlEmbeddable } from '../types';
|
||||
import { IContainer } from '../../../../../embeddable/public';
|
||||
import { InputControlEmbeddable } from '../../../services/controls';
|
||||
|
||||
export const useChildEmbeddable = ({
|
||||
container,
|
||||
untilEmbeddableLoaded,
|
||||
embeddableId,
|
||||
}: {
|
||||
container: IContainer;
|
||||
untilEmbeddableLoaded: (embeddableId: string) => Promise<InputControlEmbeddable>;
|
||||
embeddableId: string;
|
||||
}) => {
|
||||
const [embeddable, setEmbeddable] = useState<InputControlEmbeddable>();
|
||||
|
@ -21,14 +20,14 @@ export const useChildEmbeddable = ({
|
|||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
const newEmbeddable = await container.untilEmbeddableLoaded(embeddableId);
|
||||
const newEmbeddable = await untilEmbeddableLoaded(embeddableId);
|
||||
if (!mounted) return;
|
||||
setEmbeddable(newEmbeddable);
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [container, embeddableId]);
|
||||
}, [untilEmbeddableLoaded, embeddableId]);
|
||||
|
||||
return embeddable;
|
||||
};
|
||||
|
|
|
@ -6,47 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query, TimeRange } from '../../../../data/public';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IEmbeddable,
|
||||
} from '../../../../embeddable/public';
|
||||
import { InputControlInput } from '../../services/controls';
|
||||
|
||||
export type ControlWidth = 'auto' | 'small' | 'medium' | 'large';
|
||||
export type ControlStyle = 'twoLine' | 'oneLine';
|
||||
|
||||
/**
|
||||
* Control embeddable types
|
||||
*/
|
||||
export type InputControlFactory = EmbeddableFactory<
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
InputControlEmbeddable
|
||||
>;
|
||||
|
||||
export interface ControlTypeRegistry {
|
||||
[key: string]: InputControlFactory;
|
||||
}
|
||||
|
||||
export type InputControlInput = EmbeddableInput & {
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
twoLineLayout?: boolean;
|
||||
};
|
||||
|
||||
export type InputControlOutput = EmbeddableOutput & {
|
||||
filters?: Filter[];
|
||||
};
|
||||
|
||||
export type InputControlEmbeddable<
|
||||
TInputControlEmbeddableInput extends InputControlInput = InputControlInput,
|
||||
TInputControlEmbeddableOutput extends InputControlOutput = InputControlOutput
|
||||
> = IEmbeddable<TInputControlEmbeddableInput, TInputControlEmbeddableOutput>;
|
||||
|
||||
/**
|
||||
* Control embeddable editor types
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { configureStore, EnhancedStore } from '@reduxjs/toolkit';
|
||||
import { combineReducers, Reducer } from 'redux';
|
||||
|
||||
export interface InjectReducerProps<StateShape> {
|
||||
key: string;
|
||||
asyncReducer: Reducer<StateShape>;
|
||||
}
|
||||
|
||||
type ManagedEmbeddableReduxStore = EnhancedStore & {
|
||||
asyncReducers: { [key: string]: Reducer<unknown> };
|
||||
injectReducer: <StateShape>(props: InjectReducerProps<StateShape>) => void;
|
||||
};
|
||||
const embeddablesStore = configureStore({ reducer: {} as { [key: string]: Reducer } });
|
||||
|
||||
const managedEmbeddablesStore = embeddablesStore as ManagedEmbeddableReduxStore;
|
||||
managedEmbeddablesStore.asyncReducers = {};
|
||||
|
||||
managedEmbeddablesStore.injectReducer = <StateShape>({
|
||||
key,
|
||||
asyncReducer,
|
||||
}: InjectReducerProps<StateShape>) => {
|
||||
managedEmbeddablesStore.asyncReducers[key] = asyncReducer as Reducer<unknown>;
|
||||
managedEmbeddablesStore.replaceReducer(
|
||||
combineReducers({ ...managedEmbeddablesStore.asyncReducers })
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A managed Redux store which can be used with multiple embeddables at once. When a new embeddable is created at runtime,
|
||||
* all passed in reducers will be made into a slice, then combined into the store using combineReducers.
|
||||
*/
|
||||
export const getManagedEmbeddablesStore = () => managedEmbeddablesStore;
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { createContext, useContext } from 'react';
|
||||
|
||||
import {
|
||||
GenericEmbeddableReducers,
|
||||
ReduxContainerContextServices,
|
||||
ReduxEmbeddableContextServices,
|
||||
} from './types';
|
||||
import { ContainerInput, EmbeddableInput } from '../../../../embeddable/public';
|
||||
|
||||
/**
|
||||
* When creating the context, a generic EmbeddableInput as placeholder is used. This will later be cast to
|
||||
* the generic type passed in by the useReduxEmbeddableContext or useReduxContainerContext hooks
|
||||
**/
|
||||
export const ReduxEmbeddableContext = createContext<
|
||||
| ReduxEmbeddableContextServices<EmbeddableInput>
|
||||
| ReduxContainerContextServices<EmbeddableInput>
|
||||
| null
|
||||
>(null);
|
||||
|
||||
/**
|
||||
* A typed use context hook for embeddables that are not containers. it @returns an
|
||||
* ReduxEmbeddableContextServices object typed to the generic inputTypes and ReducerTypes you pass in.
|
||||
* Note that the reducer type is optional, but will be required to correctly infer the keys and payload
|
||||
* types of your reducers. use `typeof MyReducers` here to retain them.
|
||||
*/
|
||||
export const useReduxEmbeddableContext = <
|
||||
InputType extends EmbeddableInput = EmbeddableInput,
|
||||
ReducerType extends GenericEmbeddableReducers<InputType> = GenericEmbeddableReducers<InputType>
|
||||
>(): ReduxEmbeddableContextServices<InputType, ReducerType> => {
|
||||
const context = useContext<ReduxEmbeddableContextServices<InputType, ReducerType>>(
|
||||
ReduxEmbeddableContext as unknown as React.Context<
|
||||
ReduxEmbeddableContextServices<InputType, ReducerType>
|
||||
>
|
||||
);
|
||||
if (context == null) {
|
||||
throw new Error(
|
||||
'useReduxEmbeddableContext must be used inside the useReduxEmbeddableContextProvider.'
|
||||
);
|
||||
}
|
||||
|
||||
return context!;
|
||||
};
|
||||
|
||||
/**
|
||||
* A typed use context hook for embeddable containers. it @returns an
|
||||
* ReduxContainerContextServices object typed to the generic inputTypes and ReducerTypes you pass in.
|
||||
* Note that the reducer type is optional, but will be required to correctly infer the keys and payload
|
||||
* types of your reducers. use `typeof MyReducers` here to retain them. It also includes a containerActions
|
||||
* key which contains most of the commonly used container operations
|
||||
*/
|
||||
export const useReduxContainerContext = <
|
||||
InputType extends ContainerInput = ContainerInput,
|
||||
ReducerType extends GenericEmbeddableReducers<InputType> = GenericEmbeddableReducers<InputType>
|
||||
>(): ReduxContainerContextServices<InputType, ReducerType> => {
|
||||
const context = useContext<ReduxContainerContextServices<InputType, ReducerType>>(
|
||||
ReduxEmbeddableContext as unknown as React.Context<
|
||||
ReduxContainerContextServices<InputType, ReducerType>
|
||||
>
|
||||
);
|
||||
if (context == null) {
|
||||
throw new Error(
|
||||
'useReduxEmbeddableContext must be used inside the useReduxEmbeddableContextProvider.'
|
||||
);
|
||||
}
|
||||
return context!;
|
||||
};
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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, { PropsWithChildren, useEffect, useMemo, useRef } from 'react';
|
||||
import { Provider, TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import { Draft } from 'immer/dist/types/types-external';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SliceCaseReducers, PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
IEmbeddable,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IContainer,
|
||||
} from '../../../../embeddable/public';
|
||||
import { getManagedEmbeddablesStore } from './generic_embeddable_store';
|
||||
import {
|
||||
ReduxContainerContextServices,
|
||||
ReduxEmbeddableContextServices,
|
||||
ReduxEmbeddableWrapperProps,
|
||||
} from './types';
|
||||
import { ReduxEmbeddableContext, useReduxEmbeddableContext } from './redux_embeddable_context';
|
||||
|
||||
const getDefaultProps = <InputType extends EmbeddableInput = EmbeddableInput>(): Required<
|
||||
Pick<ReduxEmbeddableWrapperProps<InputType>, 'diffInput'>
|
||||
> => ({
|
||||
diffInput: (a, b) => {
|
||||
const differences: Partial<InputType> = {};
|
||||
const allKeys = [...Object.keys(a), ...Object.keys(b)] as Array<keyof InputType>;
|
||||
allKeys.forEach((key) => {
|
||||
if (!isEqual(a[key], b[key])) differences[key] = a[key];
|
||||
});
|
||||
return differences;
|
||||
},
|
||||
});
|
||||
|
||||
const embeddableIsContainer = (
|
||||
embeddable: IEmbeddable<EmbeddableInput, EmbeddableOutput>
|
||||
): embeddable is IContainer => embeddable.isContainer;
|
||||
|
||||
/**
|
||||
* Place this wrapper around the react component when rendering an embeddable to automatically set up
|
||||
* redux for use with the embeddable via the supplied reducers. Any child components can then use ReduxEmbeddableContext
|
||||
* or ReduxContainerContext to interface with the state of the embeddable.
|
||||
*/
|
||||
export const ReduxEmbeddableWrapper = <InputType extends EmbeddableInput = EmbeddableInput>(
|
||||
props: PropsWithChildren<ReduxEmbeddableWrapperProps<InputType>>
|
||||
) => {
|
||||
const { embeddable, reducers, diffInput } = useMemo(
|
||||
() => ({ ...getDefaultProps<InputType>(), ...props }),
|
||||
[props]
|
||||
);
|
||||
|
||||
const containerActions: ReduxContainerContextServices['containerActions'] | undefined =
|
||||
useMemo(() => {
|
||||
if (embeddableIsContainer(embeddable)) {
|
||||
return {
|
||||
untilEmbeddableLoaded: embeddable.untilEmbeddableLoaded.bind(embeddable),
|
||||
updateInputForChild: embeddable.updateInputForChild.bind(embeddable),
|
||||
removeEmbeddable: embeddable.removeEmbeddable.bind(embeddable),
|
||||
addNewEmbeddable: embeddable.addNewEmbeddable.bind(embeddable),
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [embeddable]);
|
||||
|
||||
const reduxEmbeddableContext: ReduxEmbeddableContextServices | ReduxContainerContextServices =
|
||||
useMemo(() => {
|
||||
const key = `${embeddable.type}_${embeddable.id}`;
|
||||
|
||||
// A generic reducer used to update redux state when the embeddable input changes
|
||||
const updateEmbeddableReduxState = (
|
||||
state: Draft<InputType>,
|
||||
action: PayloadAction<Partial<InputType>>
|
||||
) => {
|
||||
return { ...state, ...action.payload };
|
||||
};
|
||||
|
||||
const slice = createSlice<InputType, SliceCaseReducers<InputType>>({
|
||||
initialState: embeddable.getInput(),
|
||||
name: key,
|
||||
reducers: { ...reducers, updateEmbeddableReduxState },
|
||||
});
|
||||
const store = getManagedEmbeddablesStore();
|
||||
|
||||
store.injectReducer({
|
||||
key,
|
||||
asyncReducer: slice.reducer,
|
||||
});
|
||||
|
||||
const useEmbeddableSelector: TypedUseSelectorHook<InputType> = () =>
|
||||
useSelector((state: ReturnType<typeof store.getState>) => state[key]);
|
||||
|
||||
return {
|
||||
useEmbeddableDispatch: () => useDispatch<typeof store.dispatch>(),
|
||||
useEmbeddableSelector,
|
||||
actions: slice.actions as ReduxEmbeddableContextServices['actions'],
|
||||
containerActions,
|
||||
};
|
||||
}, [reducers, embeddable, containerActions]);
|
||||
|
||||
return (
|
||||
<Provider store={getManagedEmbeddablesStore()}>
|
||||
<ReduxEmbeddableContext.Provider value={reduxEmbeddableContext}>
|
||||
<ReduxEmbeddableSync diffInput={diffInput} embeddable={embeddable}>
|
||||
{props.children}
|
||||
</ReduxEmbeddableSync>
|
||||
</ReduxEmbeddableContext.Provider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
interface ReduxEmbeddableSyncProps<InputType extends EmbeddableInput = EmbeddableInput> {
|
||||
diffInput: (a: InputType, b: InputType) => Partial<InputType>;
|
||||
embeddable: IEmbeddable<InputType, EmbeddableOutput>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component uses the context from the embeddable wrapper to set up a generic two-way binding between the embeddable input and
|
||||
* the redux store. a custom diffInput function can be provided, this function should always prioritize input A over input B.
|
||||
*/
|
||||
const ReduxEmbeddableSync = <InputType extends EmbeddableInput = EmbeddableInput>({
|
||||
embeddable,
|
||||
diffInput,
|
||||
children,
|
||||
}: PropsWithChildren<ReduxEmbeddableSyncProps<InputType>>) => {
|
||||
const {
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
actions: { updateEmbeddableReduxState },
|
||||
} = useReduxEmbeddableContext<InputType>();
|
||||
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const currentState = useEmbeddableSelector((state) => state);
|
||||
const stateRef = useRef(currentState);
|
||||
|
||||
// When Embeddable Input changes, push differences to redux.
|
||||
useEffect(() => {
|
||||
embeddable.getInput$().subscribe(() => {
|
||||
const differences = diffInput(embeddable.getInput(), stateRef.current);
|
||||
if (differences && Object.keys(differences).length > 0) {
|
||||
dispatch(updateEmbeddableReduxState(differences));
|
||||
}
|
||||
});
|
||||
}, [diffInput, dispatch, embeddable, updateEmbeddableReduxState]);
|
||||
|
||||
// When redux state changes, push differences to Embeddable Input.
|
||||
useEffect(() => {
|
||||
stateRef.current = currentState;
|
||||
const differences = diffInput(currentState, embeddable.getInput());
|
||||
if (differences && Object.keys(differences).length > 0) {
|
||||
embeddable.updateInput(differences);
|
||||
}
|
||||
}, [currentState, diffInput, embeddable]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 {
|
||||
ActionCreatorWithPayload,
|
||||
AnyAction,
|
||||
CaseReducer,
|
||||
Dispatch,
|
||||
PayloadAction,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { TypedUseSelectorHook } from 'react-redux';
|
||||
import {
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IContainer,
|
||||
IEmbeddable,
|
||||
} from '../../../../embeddable/public';
|
||||
|
||||
export interface GenericEmbeddableReducers<InputType> {
|
||||
/**
|
||||
* PayloadAction of type any is strategic here because we want to allow payloads of any shape in generic reducers.
|
||||
* This type will be overridden to remove any and be type safe when returned by ReduxEmbeddableContextServices.
|
||||
*/
|
||||
[key: string]: CaseReducer<InputType, PayloadAction<any>>;
|
||||
}
|
||||
|
||||
export interface ReduxEmbeddableWrapperProps<InputType extends EmbeddableInput = EmbeddableInput> {
|
||||
embeddable: IEmbeddable<InputType, EmbeddableOutput>;
|
||||
reducers: GenericEmbeddableReducers<InputType>;
|
||||
diffInput?: (a: InputType, b: InputType) => Partial<InputType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This context allows components underneath the redux embeddable wrapper to get access to the actions, selector, dispatch, and containerActions.
|
||||
*/
|
||||
export interface ReduxEmbeddableContextServices<
|
||||
InputType extends EmbeddableInput = EmbeddableInput,
|
||||
ReducerType extends GenericEmbeddableReducers<InputType> = GenericEmbeddableReducers<InputType>
|
||||
> {
|
||||
actions: {
|
||||
[Property in keyof ReducerType]: ActionCreatorWithPayload<
|
||||
Parameters<ReducerType[Property]>[1]['payload']
|
||||
>;
|
||||
} & { updateEmbeddableReduxState: ActionCreatorWithPayload<Partial<InputType>> };
|
||||
useEmbeddableSelector: TypedUseSelectorHook<InputType>;
|
||||
useEmbeddableDispatch: () => Dispatch<AnyAction>;
|
||||
}
|
||||
|
||||
export type ReduxContainerContextServices<
|
||||
InputType extends EmbeddableInput = EmbeddableInput,
|
||||
ReducerType extends GenericEmbeddableReducers<InputType> = GenericEmbeddableReducers<InputType>
|
||||
> = ReduxEmbeddableContextServices<InputType, ReducerType> & {
|
||||
containerActions: Pick<
|
||||
IContainer,
|
||||
'untilEmbeddableLoaded' | 'removeEmbeddable' | 'addNewEmbeddable' | 'updateInputForChild'
|
||||
>;
|
||||
};
|
|
@ -17,6 +17,7 @@ const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart
|
|||
const startContract: PresentationUtilPluginStart = {
|
||||
ContextProvider: pluginServices.getContextProvider(),
|
||||
labsService: pluginServices.getServices().labs,
|
||||
controlsService: pluginServices.getServices().controls,
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
|
|
@ -39,6 +39,7 @@ export class PresentationUtilPlugin
|
|||
pluginServices.setRegistry(registry.start({ coreStart, startPlugins }));
|
||||
return {
|
||||
ContextProvider: pluginServices.getContextProvider(),
|
||||
controlsService: pluginServices.getServices().controls,
|
||||
labsService: pluginServices.getServices().labs,
|
||||
};
|
||||
}
|
||||
|
|
85
src/plugins/presentation_util/public/services/controls.ts
Normal file
85
src/plugins/presentation_util/public/services/controls.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { Filter } from '@kbn/es-query';
|
||||
import { Query, TimeRange } from '../../../data/public';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IEmbeddable,
|
||||
} from '../../../embeddable/public';
|
||||
|
||||
/**
|
||||
* Control embeddable types
|
||||
*/
|
||||
export type InputControlFactory = EmbeddableFactory<
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
InputControlEmbeddable
|
||||
>;
|
||||
|
||||
export type InputControlInput = EmbeddableInput & {
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
twoLineLayout?: boolean;
|
||||
};
|
||||
|
||||
export type InputControlOutput = EmbeddableOutput & {
|
||||
filters?: Filter[];
|
||||
};
|
||||
|
||||
export type InputControlEmbeddable<
|
||||
TInputControlEmbeddableInput extends InputControlInput = InputControlInput,
|
||||
TInputControlEmbeddableOutput extends InputControlOutput = InputControlOutput
|
||||
> = IEmbeddable<TInputControlEmbeddableInput, TInputControlEmbeddableOutput>;
|
||||
|
||||
export interface ControlTypeRegistry {
|
||||
[key: string]: InputControlFactory;
|
||||
}
|
||||
|
||||
export interface PresentationControlsService {
|
||||
registerInputControlType: (factory: InputControlFactory) => void;
|
||||
|
||||
getControlFactory: <
|
||||
I extends InputControlInput = InputControlInput,
|
||||
O extends InputControlOutput = InputControlOutput,
|
||||
E extends InputControlEmbeddable<I, O> = InputControlEmbeddable<I, O>
|
||||
>(
|
||||
type: string
|
||||
) => EmbeddableFactory<I, O, E>;
|
||||
|
||||
getInputControlTypes: () => string[];
|
||||
}
|
||||
|
||||
export const getCommonControlsService = () => {
|
||||
const controlsFactoriesMap: ControlTypeRegistry = {};
|
||||
|
||||
const registerInputControlType = (factory: InputControlFactory) => {
|
||||
controlsFactoriesMap[factory.type] = factory;
|
||||
};
|
||||
|
||||
const getControlFactory = <
|
||||
I extends InputControlInput = InputControlInput,
|
||||
O extends InputControlOutput = InputControlOutput,
|
||||
E extends InputControlEmbeddable<I, O> = InputControlEmbeddable<I, O>
|
||||
>(
|
||||
type: string
|
||||
) => {
|
||||
return controlsFactoriesMap[type] as EmbeddableFactory<I, O, E>;
|
||||
};
|
||||
|
||||
const getInputControlTypes = () => Object.keys(controlsFactoriesMap);
|
||||
|
||||
return {
|
||||
registerInputControlType,
|
||||
getControlFactory,
|
||||
getInputControlTypes,
|
||||
};
|
||||
};
|
|
@ -13,6 +13,7 @@ import { PresentationDashboardsService } from './dashboards';
|
|||
import { PresentationLabsService } from './labs';
|
||||
import { registry as stubRegistry } from './stub';
|
||||
import { PresentationOverlaysService } from './overlays';
|
||||
import { PresentationControlsService } from './controls';
|
||||
|
||||
export { PresentationCapabilitiesService } from './capabilities';
|
||||
export { PresentationDashboardsService } from './dashboards';
|
||||
|
@ -21,6 +22,7 @@ export interface PresentationUtilServices {
|
|||
dashboards: PresentationDashboardsService;
|
||||
capabilities: PresentationCapabilitiesService;
|
||||
overlays: PresentationOverlaysService;
|
||||
controls: PresentationControlsService;
|
||||
labs: PresentationLabsService;
|
||||
}
|
||||
|
||||
|
@ -31,5 +33,6 @@ export const getStubPluginServices = (): PresentationUtilPluginStart => {
|
|||
return {
|
||||
ContextProvider: pluginServices.getContextProvider(),
|
||||
labsService: pluginServices.getServices().labs,
|
||||
controlsService: pluginServices.getServices().controls,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,3 +5,9 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginServiceFactory } from '../create';
|
||||
import { getCommonControlsService, PresentationControlsService } from '../controls';
|
||||
|
||||
export type ControlsServiceFactory = PluginServiceFactory<PresentationControlsService>;
|
||||
export const controlsServiceFactory = () => getCommonControlsService();
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '../create';
|
||||
import { PresentationUtilPluginStartDeps } from '../../types';
|
||||
import { PresentationUtilServices } from '..';
|
||||
import { controlsServiceFactory } from './controls';
|
||||
|
||||
export { capabilitiesServiceFactory } from './capabilities';
|
||||
export { dashboardsServiceFactory } from './dashboards';
|
||||
|
@ -32,6 +33,7 @@ export const providers: PluginServiceProviders<
|
|||
labs: new PluginServiceProvider(labsServiceFactory),
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
overlays: new PluginServiceProvider(overlaysServiceFactory),
|
||||
controls: new PluginServiceProvider(controlsServiceFactory),
|
||||
};
|
||||
|
||||
export const registry = new PluginServiceRegistry<
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { PluginServiceFactory } from '../create';
|
||||
import { getCommonControlsService, PresentationControlsService } from '../controls';
|
||||
|
||||
export type ControlsServiceFactory = PluginServiceFactory<PresentationControlsService>;
|
||||
export const controlsServiceFactory = () => getCommonControlsService();
|
|
@ -6,12 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create';
|
||||
import {
|
||||
PluginServices,
|
||||
PluginServiceProviders,
|
||||
PluginServiceProvider,
|
||||
PluginServiceRegistry,
|
||||
} from '../create';
|
||||
import { dashboardsServiceFactory } from '../stub/dashboards';
|
||||
import { labsServiceFactory } from './labs';
|
||||
import { capabilitiesServiceFactory } from './capabilities';
|
||||
import { PresentationUtilServices } from '..';
|
||||
import { overlaysServiceFactory } from './overlays';
|
||||
import { controlsServiceFactory } from './controls';
|
||||
|
||||
export { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
|
||||
export { PresentationUtilServices } from '..';
|
||||
|
@ -27,7 +33,10 @@ export const providers: PluginServiceProviders<PresentationUtilServices, Storybo
|
|||
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
overlays: new PluginServiceProvider(overlaysServiceFactory),
|
||||
controls: new PluginServiceProvider(controlsServiceFactory),
|
||||
labs: new PluginServiceProvider(labsServiceFactory),
|
||||
};
|
||||
|
||||
export const pluginServices = new PluginServices<PresentationUtilServices>();
|
||||
|
||||
export const registry = new PluginServiceRegistry<PresentationUtilServices>(providers);
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { PluginServiceFactory } from '../create';
|
||||
import { getCommonControlsService, PresentationControlsService } from '../controls';
|
||||
|
||||
export type ControlsServiceFactory = PluginServiceFactory<PresentationControlsService>;
|
||||
export const controlsServiceFactory = () => getCommonControlsService();
|
|
@ -12,7 +12,7 @@ import { labsServiceFactory } from './labs';
|
|||
import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
|
||||
import { PresentationUtilServices } from '..';
|
||||
import { overlaysServiceFactory } from './overlays';
|
||||
|
||||
import { controlsServiceFactory } from './controls';
|
||||
export { dashboardsServiceFactory } from './dashboards';
|
||||
export { capabilitiesServiceFactory } from './capabilities';
|
||||
|
||||
|
@ -20,6 +20,7 @@ export const providers: PluginServiceProviders<PresentationUtilServices> = {
|
|||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
|
||||
overlays: new PluginServiceProvider(overlaysServiceFactory),
|
||||
controls: new PluginServiceProvider(controlsServiceFactory),
|
||||
labs: new PluginServiceProvider(labsServiceFactory),
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PresentationControlsService } from './services/controls';
|
||||
import { PresentationLabsService } from './services/labs';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
@ -14,6 +15,7 @@ export interface PresentationUtilPluginSetup {}
|
|||
export interface PresentationUtilPluginStart {
|
||||
ContextProvider: React.FC;
|
||||
labsService: PresentationLabsService;
|
||||
controlsService: PresentationControlsService;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue