[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:
Nathan Reese 2022-12-01 16:38:43 -07:00 committed by GitHub
parent f95414f76f
commit 15ed59d6f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 209 additions and 88 deletions

View file

@ -9,23 +9,23 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import type { DataView } from '@kbn/data-views-plugin/public';
import { AppMountParameters } from '@kbn/core/public'; import { AppMountParameters } from '@kbn/core/public';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { ControlsExampleStartDeps } from './plugin'; import { ControlsExampleStartDeps } from './plugin';
import { BasicReduxExample } from './basic_redux_example'; import { BasicReduxExample } from './basic_redux_example';
interface Props { const ControlsExamples = ({ dataViewId }: { dataViewId?: string }) => {
dataView: DataView; const examples = dataViewId ? (
} <>
<BasicReduxExample dataViewId={dataViewId} />
const ControlsExamples = ({ dataView }: Props) => { </>
) : (
<div>{'Please install e-commerce sample data to run controls examples.'}</div>
);
return ( return (
<KibanaPageTemplate> <KibanaPageTemplate>
<KibanaPageTemplate.Header pageTitle="Controls as a Building Block" /> <KibanaPageTemplate.Header pageTitle="Controls as a Building Block" />
<KibanaPageTemplate.Section> <KibanaPageTemplate.Section>{examples}</KibanaPageTemplate.Section>
<BasicReduxExample dataView={dataView} />
</KibanaPageTemplate.Section>
</KibanaPageTemplate> </KibanaPageTemplate>
); );
}; };
@ -35,8 +35,7 @@ export const renderApp = async (
{ element }: AppMountParameters { element }: AppMountParameters
) => { ) => {
const dataViews = await data.dataViews.find('kibana_sample_data_ecommerce'); const dataViews = await data.dataViews.find('kibana_sample_data_ecommerce');
if (dataViews.length > 0) { const dataViewId = dataViews.length > 0 ? dataViews[0].id : undefined;
ReactDOM.render(<ControlsExamples dataView={dataViews[0]} />, element); ReactDOM.render(<ControlsExamples dataViewId={dataViewId} />, element);
}
return () => ReactDOM.unmountComponentAtNode(element); return () => ReactDOM.unmountComponentAtNode(element);
}; };

View file

@ -11,12 +11,10 @@ import React, { useMemo, useState } from 'react';
import { import {
LazyControlGroupRenderer, LazyControlGroupRenderer,
ControlGroupContainer, ControlGroupContainer,
ControlGroupInput,
useControlGroupContainerContext, useControlGroupContainerContext,
ControlStyle, ControlStyle,
} from '@kbn/controls-plugin/public'; } from '@kbn/controls-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public'; import { withSuspense } from '@kbn/presentation-util-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { import {
EuiButtonGroup, EuiButtonGroup,
EuiFlexGroup, EuiFlexGroup,
@ -26,27 +24,24 @@ import {
EuiText, EuiText,
EuiTitle, EuiTitle,
} from '@elastic/eui'; } from '@elastic/eui';
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
interface Props {
dataView: DataView;
}
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer); const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
export const BasicReduxExample = ({ dataView }: Props) => { export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
const [myControlGroup, setControlGroup] = useState<ControlGroupContainer>(); const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
const [currentControlStyle, setCurrentControlStyle] = useState<ControlStyle>('oneLine');
const ControlGroupReduxWrapper = useMemo(() => { const ControlGroupReduxWrapper = useMemo(() => {
if (myControlGroup) return myControlGroup.getReduxEmbeddableTools().Wrapper; if (controlGroup) return controlGroup.getReduxEmbeddableTools().Wrapper;
}, [myControlGroup]); }, [controlGroup]);
const ButtonControls = () => { const ButtonControls = () => {
const { const {
useEmbeddableDispatch, useEmbeddableDispatch,
useEmbeddableSelector: select,
actions: { setControlStyle }, actions: { setControlStyle },
} = useControlGroupContainerContext(); } = useControlGroupContainerContext();
const dispatch = useEmbeddableDispatch(); const dispatch = useEmbeddableDispatch();
const controlStyle = select((state) => state.explicitInput.controlStyle);
return ( return (
<> <>
@ -71,9 +66,8 @@ export const BasicReduxExample = ({ dataView }: Props) => {
value: 'twoLine' as ControlStyle, value: 'twoLine' as ControlStyle,
}, },
]} ]}
idSelected={currentControlStyle} idSelected={controlStyle}
onChange={(id, value) => { onChange={(id, value) => {
setCurrentControlStyle(value);
dispatch(setControlStyle(value)); dispatch(setControlStyle(value));
}} }}
type="single" type="single"
@ -105,20 +99,17 @@ export const BasicReduxExample = ({ dataView }: Props) => {
)} )}
<ControlGroupRenderer <ControlGroupRenderer
onEmbeddableLoad={async (controlGroup) => { onLoadComplete={async (newControlGroup) => {
setControlGroup(controlGroup); setControlGroup(newControlGroup);
}} }}
getCreationOptions={async (controlGroupInputBuilder) => { getInitialInput={async (initialInput, builder) => {
const initialInput: Partial<ControlGroupInput> = { await builder.addDataControlFromField(initialInput, {
...getDefaultControlGroupInput(), dataViewId,
defaultControlWidth: 'small',
};
await controlGroupInputBuilder.addDataControlFromField(initialInput, {
dataViewId: dataView.id ?? 'kibana_sample_data_ecommerce',
fieldName: 'customer_first_name.keyword', fieldName: 'customer_first_name.keyword',
width: 'small',
}); });
await controlGroupInputBuilder.addDataControlFromField(initialInput, { await builder.addDataControlFromField(initialInput, {
dataViewId: dataView.id ?? 'kibana_sample_data_ecommerce', dataViewId,
fieldName: 'customer_last_name.keyword', fieldName: 'customer_last_name.keyword',
width: 'medium', width: 'medium',
grow: false, grow: false,

View file

@ -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;
}

View file

@ -14,7 +14,7 @@ import { IEmbeddable } from '@kbn/embeddable-plugin/public';
import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public'; import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public';
import { pluginServices } from '../services'; import { pluginServices } from '../services';
import { ControlPanelState, getDefaultControlGroupInput } from '../../common'; import { getDefaultControlGroupInput } from '../../common';
import { import {
ControlGroupInput, ControlGroupInput,
ControlGroupOutput, ControlGroupOutput,
@ -22,60 +22,29 @@ import {
CONTROL_GROUP_TYPE, CONTROL_GROUP_TYPE,
} from './types'; } from './types';
import { ControlGroupContainer } from './embeddable/control_group_container'; 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'; import { controlGroupReducers } from './state/control_group_reducers';
import { controlGroupInputBuilder } from './control_group_input_builder';
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>,
};
},
};
export interface ControlGroupRendererProps { export interface ControlGroupRendererProps {
onEmbeddableLoad: (controlGroupContainer: ControlGroupContainer) => void; onLoadComplete?: (controlGroup: ControlGroupContainer) => void;
getCreationOptions: ( getInitialInput: (
builder: typeof ControlGroupInputBuilder initialInput: Partial<ControlGroupInput>,
builder: typeof controlGroupInputBuilder
) => Promise<Partial<ControlGroupInput>>; ) => Promise<Partial<ControlGroupInput>>;
} }
export const ControlGroupRenderer = ({ export const ControlGroupRenderer = ({
onEmbeddableLoad, onLoadComplete,
getCreationOptions, getInitialInput,
}: ControlGroupRendererProps) => { }: ControlGroupRendererProps) => {
const controlsRoot = useRef(null); const controlGroupRef = useRef(null);
const [controlGroupContainer, setControlGroupContainer] = useState<ControlGroupContainer>(); const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
const id = useMemo(() => uuid.v4(), []); const id = useMemo(() => uuid.v4(), []);
/** /**
* Use Lifecycles to load initial control group container * Use Lifecycles to load initial control group container
*/ */
useLifecycles( useLifecycles(
// onMount
() => { () => {
const { embeddable } = pluginServices.getServices(); const { embeddable } = pluginServices.getServices();
(async () => { (async () => {
@ -84,25 +53,28 @@ export const ControlGroupRenderer = ({
ControlGroupOutput, ControlGroupOutput,
IEmbeddable<ControlGroupInput, ControlGroupOutput> IEmbeddable<ControlGroupInput, ControlGroupOutput>
>(CONTROL_GROUP_TYPE); >(CONTROL_GROUP_TYPE);
const container = (await factory?.create({ const newControlGroup = (await factory?.create({
id, id,
...getDefaultControlGroupInput(), ...getDefaultControlGroupInput(),
...(await getCreationOptions(ControlGroupInputBuilder)), ...(await getInitialInput(getDefaultControlGroupInput(), controlGroupInputBuilder)),
})) as ControlGroupContainer; })) as ControlGroupContainer;
if (controlsRoot.current) { if (controlGroupRef.current) {
container.render(controlsRoot.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 = () => export const useControlGroupContainerContext = () =>

View file

@ -14,6 +14,12 @@ export type { ControlGroupInput, ControlGroupOutput } from './types';
export { CONTROL_GROUP_TYPE } from './types'; export { CONTROL_GROUP_TYPE } from './types';
export { ControlGroupContainerFactory } from './embeddable/control_group_container_factory'; export { ControlGroupContainerFactory } from './embeddable/control_group_container_factory';
export {
type AddDataControlProps,
type AddOptionsListControlProps,
controlGroupInputBuilder,
} from './control_group_input_builder';
export { export {
type ControlGroupRendererProps, type ControlGroupRendererProps,
useControlGroupContainerContext, useControlGroupContainerContext,

View file

@ -22,6 +22,7 @@ export type {
ControlStyle, ControlStyle,
ParentIgnoreSettings, ParentIgnoreSettings,
ControlInput, ControlInput,
DataControlInput,
} from '../common/types'; } from '../common/types';
export { export {
@ -32,9 +33,12 @@ export {
} from '../common'; } from '../common';
export { export {
type AddDataControlProps,
type AddOptionsListControlProps,
type ControlGroupContainer, type ControlGroupContainer,
ControlGroupContainerFactory, ControlGroupContainerFactory,
type ControlGroupInput, type ControlGroupInput,
controlGroupInputBuilder,
type ControlGroupOutput, type ControlGroupOutput,
} from './control_group'; } from './control_group';

View file

@ -59,7 +59,7 @@ export const ControlsContent: React.FC<Props> = ({
return ( return (
<LazyControlsRenderer <LazyControlsRenderer
getCreationOptions={async ({ addDataControlFromField }) => ({ getInitialInput={async () => ({
id: dataViewId, id: dataViewId,
type: CONTROL_GROUP_TYPE, type: CONTROL_GROUP_TYPE,
timeRange, timeRange,
@ -72,7 +72,7 @@ export const ControlsContent: React.FC<Props> = ({
defaultControlWidth: 'small', defaultControlWidth: 'small',
panels: controlPanel, panels: controlPanel,
})} })}
onEmbeddableLoad={(newControlGroup) => { onLoadComplete={(newControlGroup) => {
setControlGroup(newControlGroup); setControlGroup(newControlGroup);
newControlGroup.onFiltersPublished$.subscribe((newFilters) => { newControlGroup.onFiltersPublished$.subscribe((newFilters) => {
setPanelFilters([...newFilters]); setPanelFilters([...newFilters]);