[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 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);
};

View file

@ -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,

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 { 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 = () =>

View file

@ -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,

View file

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

View file

@ -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]);