[Analyst Experience Components] Dashboard & Control Group APIs (#150121)

Aligns the Portable Dashboard renderer and the Control Group renderer to a new API structure using `useImperativeHandle` rather than the overcomplicated and mostly unused wrapper provider system.
This commit is contained in:
Devon Thomson 2023-04-19 15:34:39 -04:00 committed by GitHub
parent befd429f4f
commit ffc349225e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
146 changed files with 2772 additions and 2854 deletions

View file

@ -9,11 +9,8 @@
import React from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { LazyControlGroupRenderer } from '@kbn/controls-plugin/public';
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
export const AddButtonExample = ({ dataViewId }: { dataViewId: string }) => {
return (

View file

@ -6,36 +6,18 @@
* Side Public License, v 1.
*/
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import {
LazyControlGroupRenderer,
ControlGroupContainer,
useControlGroupContainerContext,
ControlStyle,
} from '@kbn/controls-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { EuiButtonGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
import { EuiButtonGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ControlGroupRenderer, ControlStyle, ControlGroupAPI } from '@kbn/controls-plugin/public';
import { AwaitingControlGroupAPI } from '@kbn/controls-plugin/public/control_group';
export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
const ControlGroupReduxWrapper = useMemo(() => {
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);
const [controlGroupAPI, setControlGroupApi] = useState<AwaitingControlGroupAPI>(null);
const Buttons = ({ api }: { api: ControlGroupAPI }) => {
const controlStyle = api.select((state) => state.explicitInput.controlStyle);
return (
<EuiButtonGroup
legend="Text style"
@ -52,9 +34,7 @@ export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
},
]}
idSelected={controlStyle}
onChange={(id, value) => {
dispatch(setControlStyle(value));
}}
onChange={(id, value) => api.dispatch.setControlStyle(value)}
type="single"
/>
);
@ -70,16 +50,10 @@ export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
</EuiText>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true}>
{ControlGroupReduxWrapper && (
<ControlGroupReduxWrapper>
<ButtonControls />
</ControlGroupReduxWrapper>
)}
{controlGroupAPI && <Buttons api={controlGroupAPI} />}
<ControlGroupRenderer
onLoadComplete={async (newControlGroup) => {
setControlGroup(newControlGroup);
}}
ref={setControlGroupApi}
getCreationOptions={async (initialInput, builder) => {
await builder.addDataControlFromField(initialInput, {
dataViewId,

View file

@ -23,17 +23,14 @@ import {
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
import {
LazyControlGroupRenderer,
ControlGroupContainer,
ControlGroupInput,
type ControlGroupInput,
ControlGroupRenderer,
AwaitingControlGroupAPI,
ACTION_EDIT_CONTROL,
ACTION_DELETE_CONTROL,
} from '@kbn/controls-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { ControlInputTransform } from '@kbn/controls-plugin/common/types';
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
const INPUT_KEY = 'kbnControls:saveExample:input';
const WITH_CUSTOM_PLACEHOLDER = 'Custom Placeholder';
@ -41,7 +38,7 @@ const WITH_CUSTOM_PLACEHOLDER = 'Custom Placeholder';
export const EditExample = () => {
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>(null);
const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = useState<{
[id: string]: boolean;
}>({});
@ -54,20 +51,21 @@ export const EditExample = () => {
},
};
if (controlGroup) {
if (controlGroupAPI) {
const disabledActions: string[] = Object.keys(
pickBy(newToggleIconIdToSelectedMapIcon, (value) => value)
);
controlGroup.updateInput({ disabledActions });
controlGroupAPI.updateInput({ disabledActions });
}
setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon);
}
async function onSave() {
setIsSaving(true);
if (!controlGroupAPI) return;
localStorage.setItem(INPUT_KEY, JSON.stringify(controlGroup!.getInput()));
setIsSaving(true);
localStorage.setItem(INPUT_KEY, JSON.stringify(controlGroupAPI.getInput()));
// simulated async save await
await new Promise((resolve) => setTimeout(resolve, 1000));
@ -133,9 +131,9 @@ export const EditExample = () => {
<EuiButtonEmpty
color="primary"
iconType="plusInCircle"
isDisabled={controlGroup === undefined}
isDisabled={controlGroupAPI === undefined}
onClick={() => {
controlGroup!.openAddDataControlFlyout(controlInputTransform);
controlGroupAPI!.openAddDataControlFlyout(controlInputTransform);
}}
>
Add control
@ -171,7 +169,7 @@ export const EditExample = () => {
<EuiButton
fill
color="primary"
isDisabled={controlGroup === undefined || isSaving}
isDisabled={controlGroupAPI === undefined || isSaving}
onClick={onSave}
isLoading={isSaving}
>
@ -186,6 +184,7 @@ export const EditExample = () => {
</>
) : null}
<ControlGroupRenderer
ref={setControlGroupAPI}
getCreationOptions={async (initialInput, builder) => {
const persistedInput = await onLoad();
return {
@ -196,9 +195,6 @@ export const EditExample = () => {
},
};
}}
onLoadComplete={async (newControlGroup) => {
setControlGroup(newControlGroup);
}}
/>
</EuiPanel>
</>

View file

@ -22,12 +22,9 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { LazyControlGroupRenderer, ControlGroupContainer } from '@kbn/controls-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { PLUGIN_ID } from './constants';
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
interface Props {
data: DataPublicPluginStart;
dataView: DataView;
@ -36,7 +33,7 @@ interface Props {
export const SearchExample = ({ data, dataView, navigation }: Props) => {
const [controlFilters, setControlFilters] = useState<Filter[]>([]);
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>();
const [hits, setHits] = useState(0);
const [filters, setFilters] = useState<Filter[]>([]);
const [isSearching, setIsSearching] = useState(false);
@ -47,16 +44,16 @@ export const SearchExample = ({ data, dataView, navigation }: Props) => {
const [timeRange, setTimeRange] = useState<TimeRange>({ from: 'now-7d', to: 'now' });
useEffect(() => {
if (!controlGroup) {
if (!controlGroupAPI) {
return;
}
const subscription = controlGroup.onFiltersPublished$.subscribe((newFilters) => {
const subscription = controlGroupAPI.onFiltersPublished$.subscribe((newFilters) => {
setControlFilters([...newFilters]);
});
return () => {
subscription.unsubscribe();
};
}, [controlGroup]);
}, [controlGroupAPI]);
useEffect(() => {
const abortController = new AbortController();
@ -155,10 +152,8 @@ export const SearchExample = ({ data, dataView, navigation }: Props) => {
},
};
}}
onLoadComplete={async (newControlGroup) => {
setControlGroup(newControlGroup);
}}
query={query}
ref={setControlGroupAPI}
timeRange={timeRange}
/>
<EuiCallOut title="Search results">

View file

@ -18,10 +18,9 @@
"@kbn/data-plugin",
"@kbn/controls-plugin",
"@kbn/navigation-plugin",
"@kbn/presentation-util-plugin",
"@kbn/shared-ux-page-kibana-template",
"@kbn/embeddable-plugin",
"@kbn/data-views-plugin",
"@kbn/es-query"
"@kbn/es-query",
]
}

View file

@ -20,7 +20,6 @@
"unifiedSearch",
"developerExamples",
"embeddableExamples"
],
"requiredBundles": ["presentationUtil"]
]
}
}

View file

@ -6,19 +6,37 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
import { FILTER_DEBUGGER_EMBEDDABLE } from '@kbn/embeddable-examples-plugin/public';
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
// add a filter debugger panel as soon as the dashboard becomes available
useEffect(() => {
if (!dashboard) return;
(async () => {
const embeddable = await dashboard.addNewEmbeddable(FILTER_DEBUGGER_EMBEDDABLE, {});
const prevPanelState = dashboard.getExplicitInput().panels[embeddable.id];
// resize the new panel so that it fills up the entire width of the dashboard
dashboard.updateInput({
panels: {
[embeddable.id]: {
...prevPanelState,
gridData: { i: embeddable.id, x: 0, y: 0, w: 48, h: 12 },
},
},
});
})();
}, [dashboard]);
return (
<>
<EuiTitle>
@ -29,10 +47,10 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
</EuiText>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true}>
<DashboardContainerRenderer
<DashboardRenderer
getCreationOptions={async () => {
const builder = controlGroupInputBuilder;
const controlGroupInput = {};
const controlGroupInput = getDefaultControlGroupInput();
await builder.addDataControlFromField(controlGroupInput, {
dataViewId: dataView.id ?? '',
title: 'Destintion country',
@ -57,22 +75,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
},
};
}}
onDashboardContainerLoaded={(container) => {
const addFilterEmbeddable = async () => {
const embeddable = await container.addNewEmbeddable(FILTER_DEBUGGER_EMBEDDABLE, {});
const prevPanelState = container.getExplicitInput().panels[embeddable.id];
// resize the new panel so that it fills up the entire width of the dashboard
container.updateInput({
panels: {
[embeddable.id]: {
...prevPanelState,
gridData: { i: embeddable.id, x: 0, y: 0, w: 48, h: 12 },
},
},
});
};
addFilterEmbeddable();
}}
ref={setDashboard}
/>
</EuiPanel>
</>

View file

@ -6,9 +6,8 @@
* Side Public License, v 1.
*/
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { DashboardContainer, LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
import {
EuiButtonGroup,
EuiFlexGroup,
@ -18,35 +17,19 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import {
AwaitingDashboardAPI,
DashboardAPI,
DashboardRenderer,
} from '@kbn/dashboard-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { useDashboardContainerContext } from '@kbn/dashboard-plugin/public';
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
export const DualReduxExample = () => {
const [firstDashboardContainer, setFirstDashboardContainer] = useState<
DashboardContainer | undefined
>();
const [secondDashboardContainer, setSecondDashboardContainer] = useState<
DashboardContainer | undefined
>();
const [firstDashboardContainer, setFirstDashboardContainer] = useState<AwaitingDashboardAPI>();
const [secondDashboardContainer, setSecondDashboardContainer] = useState<AwaitingDashboardAPI>();
const FirstDashboardReduxWrapper = useMemo(() => {
if (firstDashboardContainer) return firstDashboardContainer.getReduxEmbeddableTools().Wrapper;
}, [firstDashboardContainer]);
const SecondDashboardReduxWrapper = useMemo(() => {
if (secondDashboardContainer) return secondDashboardContainer.getReduxEmbeddableTools().Wrapper;
}, [secondDashboardContainer]);
const ButtonControls = () => {
const {
useEmbeddableDispatch,
useEmbeddableSelector: select,
actions: { setViewMode },
} = useDashboardContainerContext();
const dispatch = useEmbeddableDispatch();
const viewMode = select((state) => state.explicitInput.viewMode);
const ButtonControls = ({ dashboard }: { dashboard: DashboardAPI }) => {
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
return (
<EuiButtonGroup
@ -64,9 +47,7 @@ export const DualReduxExample = () => {
},
]}
idSelected={viewMode}
onChange={(id, value) => {
dispatch(setViewMode(value));
}}
onChange={(id, value) => dashboard.dispatch.setViewMode(value)}
type="single"
/>
);
@ -91,34 +72,18 @@ export const DualReduxExample = () => {
<h3>Dashboard #1</h3>
</EuiTitle>
<EuiSpacer size="m" />
{FirstDashboardReduxWrapper && (
<FirstDashboardReduxWrapper>
<ButtonControls />
</FirstDashboardReduxWrapper>
)}
{firstDashboardContainer && <ButtonControls dashboard={firstDashboardContainer} />}
<EuiSpacer size="m" />
<DashboardContainerRenderer
onDashboardContainerLoaded={(container) => {
setFirstDashboardContainer(container);
}}
/>
<DashboardRenderer ref={setFirstDashboardContainer} />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>Dashboard #2</h3>
</EuiTitle>
<EuiSpacer size="m" />
{SecondDashboardReduxWrapper && (
<SecondDashboardReduxWrapper>
<ButtonControls />
</SecondDashboardReduxWrapper>
)}
{secondDashboardContainer && <ButtonControls dashboard={secondDashboardContainer} />}
<EuiSpacer size="m" />
<DashboardContainerRenderer
onDashboardContainerLoaded={(container) => {
setSecondDashboardContainer(container);
}}
/>
<DashboardRenderer ref={setSecondDashboardContainer} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -8,7 +8,7 @@
import React, { useMemo, useState } from 'react';
import { DashboardContainer, LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
import {
EuiButton,
EuiFlexGroup,
@ -23,19 +23,17 @@ import {
VisualizeInput,
VisualizeOutput,
} from '@kbn/visualizations-plugin/public/embeddable/visualize_embeddable';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
const INPUT_KEY = 'portableDashboard:saveExample:input';
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer); // make this so we don't have two loading states - loading in the dashboard plugin instead
export const DynamicByReferenceExample = () => {
const [isSaving, setIsSaving] = useState(false);
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer | undefined>();
const [dashboard, setdashboard] = useState<AwaitingDashboardAPI>();
const onSave = async () => {
if (!dashboard) return;
setIsSaving(true);
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboardContainer!.getInput()));
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboard.getInput()));
// simulated async save await
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsSaving(false);
@ -56,44 +54,37 @@ export const DynamicByReferenceExample = () => {
const resetPersistableInput = () => {
localStorage.removeItem(INPUT_KEY);
if (dashboardContainer) {
const children = dashboardContainer.getChildIds();
if (dashboard) {
const children = dashboard.getChildIds();
children.map((childId) => {
dashboardContainer.removeEmbeddable(childId);
dashboard.removeEmbeddable(childId);
});
}
};
const addByReference = () => {
if (dashboardContainer) {
dashboardContainer.addFromLibrary();
}
};
const addByValue = async () => {
if (dashboardContainer) {
dashboardContainer.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
'visualization',
{
title: 'Sample Markdown Vis',
savedVis: {
type: 'markdown',
title: '',
data: { aggs: [], searchSource: {} },
params: {
fontSize: 12,
openLinksInNewTab: false,
markdown: '### By Value Visualization\nThis is a sample by value panel.',
},
if (!dashboard) return;
dashboard.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
'visualization',
{
title: 'Sample Markdown Vis',
savedVis: {
type: 'markdown',
title: '',
data: { aggs: [], searchSource: {} },
params: {
fontSize: 12,
openLinksInNewTab: false,
markdown: '### By Value Visualization\nThis is a sample by value panel.',
},
}
);
}
},
}
);
};
const disableButtons = useMemo(() => {
return dashboardContainer === undefined || isSaving;
}, [dashboardContainer, isSaving]);
return !dashboard || isSaving;
}, [dashboard, isSaving]);
return (
<>
@ -114,7 +105,7 @@ export const DynamicByReferenceExample = () => {
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton onClick={addByReference} isDisabled={disableButtons}>
<EuiButton onClick={() => dashboard?.addFromLibrary()} isDisabled={disableButtons}>
Add visualization from library
</EuiButton>
</EuiFlexItem>
@ -141,7 +132,7 @@ export const DynamicByReferenceExample = () => {
</EuiFlexGroup>
<EuiSpacer size="m" />
<DashboardContainerRenderer
<DashboardRenderer
getCreationOptions={async () => {
const persistedInput = getPersistableInput();
return {
@ -151,9 +142,7 @@ export const DynamicByReferenceExample = () => {
},
};
}}
onDashboardContainerLoaded={(container) => {
setDashboardContainer(container);
}}
ref={setdashboard}
/>
</EuiPanel>
</>

View file

@ -11,15 +11,9 @@ import { css } from '@emotion/react';
import { buildPhraseFilter, Filter } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
LazyDashboardContainerRenderer,
DashboardCreationOptions,
} from '@kbn/dashboard-plugin/public';
import { DashboardRenderer, DashboardCreationOptions } from '@kbn/dashboard-plugin/public';
import { EuiCode, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
export const StaticByReferenceExample = ({
dashboardId,
@ -50,7 +44,7 @@ export const StaticByReferenceExample = ({
overflow-y: auto;
`}
>
<DashboardContainerRenderer
<DashboardRenderer
savedObjectId={dashboardId}
getCreationOptions={async () => {
const field = dataView.getFieldByName('machine.os.keyword');
@ -61,13 +55,10 @@ export const StaticByReferenceExample = ({
if (field) {
filter = buildPhraseFilter(field, 'win xp', dataView);
filter.meta.negate = true;
creationOptions = { ...creationOptions, overrideInput: { filters: [filter] } };
creationOptions = { ...creationOptions, initialInput: { filters: [filter] } };
}
return creationOptions; // if can't find the field, then just return no special creation options
}}
onDashboardContainerLoaded={(container) => {
return; // this example is static, so don't need to do anything with the dashboard container
}}
/>
</EuiPanel>
</>

View file

@ -11,13 +11,10 @@ import React from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common';
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { DashboardRenderer } from '@kbn/dashboard-plugin/public';
import panelsJson from './static_by_value_example_panels.json';
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
export const StaticByValueExample = () => {
return (
<>
@ -29,7 +26,7 @@ export const StaticByValueExample = () => {
</EuiText>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true}>
<DashboardContainerRenderer
<DashboardRenderer
getCreationOptions={async () => {
return {
initialInput: {
@ -39,9 +36,6 @@ export const StaticByValueExample = () => {
},
};
}}
onDashboardContainerLoaded={(container) => {
return; // this example is static, so don't need to do anything with the dashboard container
}}
/>
</EuiPanel>
</>

View file

@ -20,11 +20,9 @@
"@kbn/embeddable-plugin",
"@kbn/data-views-plugin",
"@kbn/visualizations-plugin",
"@kbn/presentation-util-plugin",
"@kbn/developer-examples-plugin",
"@kbn/embeddable-examples-plugin",
"@kbn/shared-ux-page-kibana-template",
"@kbn/shared-ux-utility",
"@kbn/controls-plugin",
"@kbn/shared-ux-router"
]