mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[controls] complete control input builder API (#146764)
ControlGroupRenderer API changes * Added parameter `initialInput: Partial<ControlGroupInput>,` to getCreationOptions method signature so consumers don't need to call `getDefaultControlGroupInput` * Rename prop onEmbeddableLoad -> onLoadComplete * Rename prop getCreationOptions -> getInitialInput controlGroupInputBuilder API changes * Added `addOptionsListControl` method that allows users to pass selectedOptions * Added `addRangeSliderControl` * Added `addTimeSliderControl` Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Devon Thomson <devon.thomson@elastic.co>
This commit is contained in:
parent
f95414f76f
commit
15ed59d6f0
7 changed files with 209 additions and 88 deletions
|
@ -9,23 +9,23 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { AppMountParameters } from '@kbn/core/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { ControlsExampleStartDeps } from './plugin';
|
||||
import { BasicReduxExample } from './basic_redux_example';
|
||||
|
||||
interface Props {
|
||||
dataView: DataView;
|
||||
}
|
||||
|
||||
const ControlsExamples = ({ dataView }: Props) => {
|
||||
const ControlsExamples = ({ dataViewId }: { dataViewId?: string }) => {
|
||||
const examples = dataViewId ? (
|
||||
<>
|
||||
<BasicReduxExample dataViewId={dataViewId} />
|
||||
</>
|
||||
) : (
|
||||
<div>{'Please install e-commerce sample data to run controls examples.'}</div>
|
||||
);
|
||||
return (
|
||||
<KibanaPageTemplate>
|
||||
<KibanaPageTemplate.Header pageTitle="Controls as a Building Block" />
|
||||
<KibanaPageTemplate.Section>
|
||||
<BasicReduxExample dataView={dataView} />
|
||||
</KibanaPageTemplate.Section>
|
||||
<KibanaPageTemplate.Section>{examples}</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
};
|
||||
|
@ -35,8 +35,7 @@ export const renderApp = async (
|
|||
{ element }: AppMountParameters
|
||||
) => {
|
||||
const dataViews = await data.dataViews.find('kibana_sample_data_ecommerce');
|
||||
if (dataViews.length > 0) {
|
||||
ReactDOM.render(<ControlsExamples dataView={dataViews[0]} />, element);
|
||||
}
|
||||
const dataViewId = dataViews.length > 0 ? dataViews[0].id : undefined;
|
||||
ReactDOM.render(<ControlsExamples dataViewId={dataViewId} />, element);
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
|
|
|
@ -11,12 +11,10 @@ import React, { useMemo, useState } from 'react';
|
|||
import {
|
||||
LazyControlGroupRenderer,
|
||||
ControlGroupContainer,
|
||||
ControlGroupInput,
|
||||
useControlGroupContainerContext,
|
||||
ControlStyle,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
EuiFlexGroup,
|
||||
|
@ -26,27 +24,24 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
|
||||
interface Props {
|
||||
dataView: DataView;
|
||||
}
|
||||
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
|
||||
|
||||
export const BasicReduxExample = ({ dataView }: Props) => {
|
||||
const [myControlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
const [currentControlStyle, setCurrentControlStyle] = useState<ControlStyle>('oneLine');
|
||||
export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
|
||||
const ControlGroupReduxWrapper = useMemo(() => {
|
||||
if (myControlGroup) return myControlGroup.getReduxEmbeddableTools().Wrapper;
|
||||
}, [myControlGroup]);
|
||||
if (controlGroup) return controlGroup.getReduxEmbeddableTools().Wrapper;
|
||||
}, [controlGroup]);
|
||||
|
||||
const ButtonControls = () => {
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setControlStyle },
|
||||
} = useControlGroupContainerContext();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -71,9 +66,8 @@ export const BasicReduxExample = ({ dataView }: Props) => {
|
|||
value: 'twoLine' as ControlStyle,
|
||||
},
|
||||
]}
|
||||
idSelected={currentControlStyle}
|
||||
idSelected={controlStyle}
|
||||
onChange={(id, value) => {
|
||||
setCurrentControlStyle(value);
|
||||
dispatch(setControlStyle(value));
|
||||
}}
|
||||
type="single"
|
||||
|
@ -105,20 +99,17 @@ export const BasicReduxExample = ({ dataView }: Props) => {
|
|||
)}
|
||||
|
||||
<ControlGroupRenderer
|
||||
onEmbeddableLoad={async (controlGroup) => {
|
||||
setControlGroup(controlGroup);
|
||||
onLoadComplete={async (newControlGroup) => {
|
||||
setControlGroup(newControlGroup);
|
||||
}}
|
||||
getCreationOptions={async (controlGroupInputBuilder) => {
|
||||
const initialInput: Partial<ControlGroupInput> = {
|
||||
...getDefaultControlGroupInput(),
|
||||
defaultControlWidth: 'small',
|
||||
};
|
||||
await controlGroupInputBuilder.addDataControlFromField(initialInput, {
|
||||
dataViewId: dataView.id ?? 'kibana_sample_data_ecommerce',
|
||||
getInitialInput={async (initialInput, builder) => {
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
dataViewId,
|
||||
fieldName: 'customer_first_name.keyword',
|
||||
width: 'small',
|
||||
});
|
||||
await controlGroupInputBuilder.addDataControlFromField(initialInput, {
|
||||
dataViewId: dataView.id ?? 'kibana_sample_data_ecommerce',
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
dataViewId,
|
||||
fieldName: 'customer_last_name.keyword',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
import { ControlPanelState } from '../../common';
|
||||
import {
|
||||
DEFAULT_CONTROL_GROW,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
} from '../../common/control_group/control_group_constants';
|
||||
import { RangeValue } from '../../common/range_slider/types';
|
||||
import {
|
||||
ControlInput,
|
||||
ControlWidth,
|
||||
DataControlInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
RANGE_SLIDER_CONTROL,
|
||||
TIME_SLIDER_CONTROL,
|
||||
} from '..';
|
||||
import { ControlGroupInput } from './types';
|
||||
import { getCompatibleControlType, getNextPanelOrder } from './embeddable/control_group_helpers';
|
||||
|
||||
export interface AddDataControlProps {
|
||||
controlId?: string;
|
||||
dataViewId: string;
|
||||
fieldName: string;
|
||||
grow?: boolean;
|
||||
title?: string;
|
||||
width?: ControlWidth;
|
||||
}
|
||||
|
||||
export type AddOptionsListControlProps = AddDataControlProps & {
|
||||
selectedOptions?: string[];
|
||||
};
|
||||
|
||||
export type AddRangeSliderControlProps = AddDataControlProps & {
|
||||
value?: RangeValue;
|
||||
};
|
||||
|
||||
export const controlGroupInputBuilder = {
|
||||
addDataControlFromField: async (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
controlProps: AddDataControlProps
|
||||
) => {
|
||||
const { controlId, dataViewId, fieldName, title } = controlProps;
|
||||
const panelId = controlId ? controlId : uuid.v4();
|
||||
initialInput.panels = {
|
||||
...initialInput.panels,
|
||||
[panelId]: {
|
||||
order: getNextPanelOrder(initialInput),
|
||||
type: await getCompatibleControlType({ dataViewId, fieldName }),
|
||||
grow: getGrow(initialInput, controlProps),
|
||||
width: getWidth(initialInput, controlProps),
|
||||
explicitInput: {
|
||||
id: panelId,
|
||||
dataViewId,
|
||||
fieldName,
|
||||
title: title ?? fieldName,
|
||||
},
|
||||
} as ControlPanelState<DataControlInput>,
|
||||
};
|
||||
},
|
||||
addOptionsListControl: (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
controlProps: AddOptionsListControlProps
|
||||
) => {
|
||||
const { controlId, dataViewId, fieldName, selectedOptions, title } = controlProps;
|
||||
const panelId = controlId ? controlId : uuid.v4();
|
||||
initialInput.panels = {
|
||||
...initialInput.panels,
|
||||
[panelId]: {
|
||||
order: getNextPanelOrder(initialInput),
|
||||
type: OPTIONS_LIST_CONTROL,
|
||||
grow: getGrow(initialInput, controlProps),
|
||||
width: getWidth(initialInput, controlProps),
|
||||
explicitInput: {
|
||||
id: panelId,
|
||||
dataViewId,
|
||||
fieldName,
|
||||
selectedOptions,
|
||||
title: title ?? fieldName,
|
||||
},
|
||||
} as ControlPanelState<DataControlInput>,
|
||||
};
|
||||
},
|
||||
addRangeSliderControl: (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
controlProps: AddRangeSliderControlProps
|
||||
) => {
|
||||
const { controlId, dataViewId, fieldName, title, value } = controlProps;
|
||||
const panelId = controlId ? controlId : uuid.v4();
|
||||
initialInput.panels = {
|
||||
...initialInput.panels,
|
||||
[panelId]: {
|
||||
order: getNextPanelOrder(initialInput),
|
||||
type: RANGE_SLIDER_CONTROL,
|
||||
grow: getGrow(initialInput, controlProps),
|
||||
width: getWidth(initialInput, controlProps),
|
||||
explicitInput: {
|
||||
id: panelId,
|
||||
dataViewId,
|
||||
fieldName,
|
||||
title: title ?? fieldName,
|
||||
value: value ? value : ['', ''],
|
||||
},
|
||||
} as ControlPanelState<DataControlInput>,
|
||||
};
|
||||
},
|
||||
addTimeSliderControl: (initialInput: Partial<ControlGroupInput>) => {
|
||||
const panelId = uuid.v4();
|
||||
initialInput.panels = {
|
||||
...initialInput.panels,
|
||||
[panelId]: {
|
||||
order: getNextPanelOrder(initialInput),
|
||||
type: TIME_SLIDER_CONTROL,
|
||||
grow: true,
|
||||
width: 'large',
|
||||
explicitInput: {
|
||||
id: panelId,
|
||||
title: 'timeslider',
|
||||
},
|
||||
} as ControlPanelState<ControlInput>,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function getGrow(initialInput: Partial<ControlGroupInput>, controlProps: AddDataControlProps) {
|
||||
if (typeof controlProps.grow === 'boolean') {
|
||||
return controlProps.grow;
|
||||
}
|
||||
|
||||
return typeof initialInput.defaultControlGrow === 'boolean'
|
||||
? initialInput.defaultControlGrow
|
||||
: DEFAULT_CONTROL_GROW;
|
||||
}
|
||||
|
||||
function getWidth(initialInput: Partial<ControlGroupInput>, controlProps: AddDataControlProps) {
|
||||
if (controlProps.width) {
|
||||
return controlProps.width;
|
||||
}
|
||||
|
||||
return initialInput.defaultControlWidth
|
||||
? initialInput.defaultControlWidth
|
||||
: DEFAULT_CONTROL_WIDTH;
|
||||
}
|
|
@ -14,7 +14,7 @@ import { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
|||
import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { pluginServices } from '../services';
|
||||
import { ControlPanelState, getDefaultControlGroupInput } from '../../common';
|
||||
import { getDefaultControlGroupInput } from '../../common';
|
||||
import {
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
|
@ -22,60 +22,29 @@ import {
|
|||
CONTROL_GROUP_TYPE,
|
||||
} from './types';
|
||||
import { ControlGroupContainer } from './embeddable/control_group_container';
|
||||
import { DataControlInput } from '../types';
|
||||
import { getCompatibleControlType, getNextPanelOrder } from './embeddable/control_group_helpers';
|
||||
import { controlGroupReducers } from './state/control_group_reducers';
|
||||
|
||||
const ControlGroupInputBuilder = {
|
||||
addDataControlFromField: async (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
newPanelInput: {
|
||||
title?: string;
|
||||
panelId?: string;
|
||||
fieldName: string;
|
||||
dataViewId: string;
|
||||
} & Partial<ControlPanelState>
|
||||
) => {
|
||||
const { defaultControlGrow, defaultControlWidth } = getDefaultControlGroupInput();
|
||||
const controlGrow = initialInput.defaultControlGrow ?? defaultControlGrow;
|
||||
const controlWidth = initialInput.defaultControlWidth ?? defaultControlWidth;
|
||||
|
||||
const { panelId, dataViewId, fieldName, title, grow, width } = newPanelInput;
|
||||
const newPanelId = panelId || uuid.v4();
|
||||
const nextOrder = getNextPanelOrder(initialInput);
|
||||
const controlType = await getCompatibleControlType({ dataViewId, fieldName });
|
||||
|
||||
initialInput.panels = {
|
||||
...initialInput.panels,
|
||||
[newPanelId]: {
|
||||
order: nextOrder,
|
||||
type: controlType,
|
||||
grow: grow ?? controlGrow,
|
||||
width: width ?? controlWidth,
|
||||
explicitInput: { id: newPanelId, dataViewId, fieldName, title: title ?? fieldName },
|
||||
} as ControlPanelState<DataControlInput>,
|
||||
};
|
||||
},
|
||||
};
|
||||
import { controlGroupInputBuilder } from './control_group_input_builder';
|
||||
|
||||
export interface ControlGroupRendererProps {
|
||||
onEmbeddableLoad: (controlGroupContainer: ControlGroupContainer) => void;
|
||||
getCreationOptions: (
|
||||
builder: typeof ControlGroupInputBuilder
|
||||
onLoadComplete?: (controlGroup: ControlGroupContainer) => void;
|
||||
getInitialInput: (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
builder: typeof controlGroupInputBuilder
|
||||
) => Promise<Partial<ControlGroupInput>>;
|
||||
}
|
||||
|
||||
export const ControlGroupRenderer = ({
|
||||
onEmbeddableLoad,
|
||||
getCreationOptions,
|
||||
onLoadComplete,
|
||||
getInitialInput,
|
||||
}: ControlGroupRendererProps) => {
|
||||
const controlsRoot = useRef(null);
|
||||
const [controlGroupContainer, setControlGroupContainer] = useState<ControlGroupContainer>();
|
||||
const controlGroupRef = useRef(null);
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
const id = useMemo(() => uuid.v4(), []);
|
||||
/**
|
||||
* Use Lifecycles to load initial control group container
|
||||
*/
|
||||
useLifecycles(
|
||||
// onMount
|
||||
() => {
|
||||
const { embeddable } = pluginServices.getServices();
|
||||
(async () => {
|
||||
|
@ -84,25 +53,28 @@ export const ControlGroupRenderer = ({
|
|||
ControlGroupOutput,
|
||||
IEmbeddable<ControlGroupInput, ControlGroupOutput>
|
||||
>(CONTROL_GROUP_TYPE);
|
||||
const container = (await factory?.create({
|
||||
const newControlGroup = (await factory?.create({
|
||||
id,
|
||||
...getDefaultControlGroupInput(),
|
||||
...(await getCreationOptions(ControlGroupInputBuilder)),
|
||||
...(await getInitialInput(getDefaultControlGroupInput(), controlGroupInputBuilder)),
|
||||
})) as ControlGroupContainer;
|
||||
|
||||
if (controlsRoot.current) {
|
||||
container.render(controlsRoot.current);
|
||||
if (controlGroupRef.current) {
|
||||
newControlGroup.render(controlGroupRef.current);
|
||||
}
|
||||
setControlGroup(newControlGroup);
|
||||
if (onLoadComplete) {
|
||||
onLoadComplete(newControlGroup);
|
||||
}
|
||||
setControlGroupContainer(container);
|
||||
onEmbeddableLoad(container);
|
||||
})();
|
||||
},
|
||||
// onUnmount
|
||||
() => {
|
||||
controlGroupContainer?.destroy();
|
||||
controlGroup?.destroy();
|
||||
}
|
||||
);
|
||||
|
||||
return <div ref={controlsRoot} />;
|
||||
return <div ref={controlGroupRef} />;
|
||||
};
|
||||
|
||||
export const useControlGroupContainerContext = () =>
|
||||
|
|
|
@ -14,6 +14,12 @@ export type { ControlGroupInput, ControlGroupOutput } from './types';
|
|||
export { CONTROL_GROUP_TYPE } from './types';
|
||||
export { ControlGroupContainerFactory } from './embeddable/control_group_container_factory';
|
||||
|
||||
export {
|
||||
type AddDataControlProps,
|
||||
type AddOptionsListControlProps,
|
||||
controlGroupInputBuilder,
|
||||
} from './control_group_input_builder';
|
||||
|
||||
export {
|
||||
type ControlGroupRendererProps,
|
||||
useControlGroupContainerContext,
|
||||
|
|
|
@ -22,6 +22,7 @@ export type {
|
|||
ControlStyle,
|
||||
ParentIgnoreSettings,
|
||||
ControlInput,
|
||||
DataControlInput,
|
||||
} from '../common/types';
|
||||
|
||||
export {
|
||||
|
@ -32,9 +33,12 @@ export {
|
|||
} from '../common';
|
||||
|
||||
export {
|
||||
type AddDataControlProps,
|
||||
type AddOptionsListControlProps,
|
||||
type ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
type ControlGroupInput,
|
||||
controlGroupInputBuilder,
|
||||
type ControlGroupOutput,
|
||||
} from './control_group';
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ export const ControlsContent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<LazyControlsRenderer
|
||||
getCreationOptions={async ({ addDataControlFromField }) => ({
|
||||
getInitialInput={async () => ({
|
||||
id: dataViewId,
|
||||
type: CONTROL_GROUP_TYPE,
|
||||
timeRange,
|
||||
|
@ -72,7 +72,7 @@ export const ControlsContent: React.FC<Props> = ({
|
|||
defaultControlWidth: 'small',
|
||||
panels: controlPanel,
|
||||
})}
|
||||
onEmbeddableLoad={(newControlGroup) => {
|
||||
onLoadComplete={(newControlGroup) => {
|
||||
setControlGroup(newControlGroup);
|
||||
newControlGroup.onFiltersPublished$.subscribe((newFilters) => {
|
||||
setPanelFilters([...newFilters]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue