mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
befd429f4f
commit
ffc349225e
146 changed files with 2772 additions and 2854 deletions
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
"unifiedSearch",
|
||||
"developerExamples",
|
||||
"embeddableExamples"
|
||||
],
|
||||
"requiredBundles": ["presentationUtil"]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -6,19 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public/redux_embeddables/create_redux_embeddable_tools';
|
||||
|
||||
import { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../../public/options_list/types';
|
||||
import {
|
||||
getDefaultComponentState,
|
||||
optionsListReducers,
|
||||
} from '../../public/options_list/options_list_reducers';
|
||||
import { ControlFactory, ControlOutput } from '../../public/types';
|
||||
import { OptionsListEmbeddableInput } from './types';
|
||||
|
||||
import * as optionsListStateModule from '../../public/options_list/options_list_reducers';
|
||||
|
||||
const mockOptionsListComponentState = {
|
||||
...getDefaultComponentState(),
|
||||
searchString: { value: '', valid: true },
|
||||
field: undefined,
|
||||
totalCardinality: 0,
|
||||
availableOptions: {
|
||||
|
@ -29,6 +25,8 @@ const mockOptionsListComponentState = {
|
|||
moo: { doc_count: 5 },
|
||||
},
|
||||
invalidSelections: [],
|
||||
allowExpensiveQueries: true,
|
||||
popoverOpen: false,
|
||||
validSelections: [],
|
||||
} as OptionsListComponentState;
|
||||
|
||||
|
@ -46,26 +44,24 @@ const mockOptionsListOutput = {
|
|||
loading: false,
|
||||
} as ControlOutput;
|
||||
|
||||
export const mockOptionsListReduxEmbeddableTools = async (
|
||||
partialState?: Partial<OptionsListReduxState>
|
||||
) => {
|
||||
export const mockOptionsListEmbeddable = async (partialState?: Partial<OptionsListReduxState>) => {
|
||||
const optionsListFactoryStub = new OptionsListEmbeddableFactory();
|
||||
const optionsListControlFactory = optionsListFactoryStub as unknown as ControlFactory;
|
||||
optionsListControlFactory.getDefaultInput = () => ({});
|
||||
|
||||
// initial component state can be provided by overriding the defaults.
|
||||
const initialComponentState = {
|
||||
...mockOptionsListComponentState,
|
||||
...partialState?.componentState,
|
||||
};
|
||||
jest
|
||||
.spyOn(optionsListStateModule, 'getDefaultComponentState')
|
||||
.mockImplementation(() => initialComponentState);
|
||||
|
||||
const mockEmbeddable = (await optionsListControlFactory.create({
|
||||
...mockOptionsListEmbeddableInput,
|
||||
...partialState?.explicitInput,
|
||||
})) as OptionsListEmbeddable;
|
||||
mockEmbeddable.getOutput = jest.fn().mockReturnValue(mockOptionsListOutput);
|
||||
|
||||
const mockReduxEmbeddableTools = createReduxEmbeddableTools<OptionsListReduxState>({
|
||||
embeddable: mockEmbeddable,
|
||||
reducers: optionsListReducers,
|
||||
initialComponentState: {
|
||||
...mockOptionsListComponentState,
|
||||
...partialState?.componentState,
|
||||
},
|
||||
});
|
||||
|
||||
return mockReduxEmbeddableTools;
|
||||
return mockEmbeddable;
|
||||
};
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
lazyLoadReduxEmbeddablePackage,
|
||||
ReduxEmbeddablePackage,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { ControlOutput } from '../../types';
|
||||
|
@ -17,17 +13,15 @@ import { ControlGroupInput } from '../types';
|
|||
import { pluginServices } from '../../services';
|
||||
import { DeleteControlAction } from './delete_control_action';
|
||||
import { OptionsListEmbeddableInput } from '../../options_list';
|
||||
import { controlGroupInputBuilder } from '../control_group_input_builder';
|
||||
import { controlGroupInputBuilder } from '../external_api/control_group_input_builder';
|
||||
import { ControlGroupContainer } from '../embeddable/control_group_container';
|
||||
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';
|
||||
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
|
||||
let container: ControlGroupContainer;
|
||||
let embeddable: OptionsListEmbeddable;
|
||||
let reduxEmbeddablePackage: ReduxEmbeddablePackage;
|
||||
|
||||
beforeAll(async () => {
|
||||
reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
||||
|
||||
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
||||
controlGroupInputBuilder.addOptionsListControl(controlGroupInput, {
|
||||
dataViewId: 'test-data-view',
|
||||
|
@ -36,7 +30,7 @@ beforeAll(async () => {
|
|||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
||||
container = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||
await container.untilInitialized();
|
||||
|
||||
embeddable = container.getChild(container.getChildIds()[0]);
|
||||
|
@ -53,7 +47,7 @@ test('Action is incompatible with Error Embeddables', async () => {
|
|||
test('Execute throws an error when called with an embeddable not in a parent', async () => {
|
||||
const deleteControlAction = new DeleteControlAction();
|
||||
const optionsListEmbeddable = new OptionsListEmbeddable(
|
||||
reduxEmbeddablePackage,
|
||||
mockedReduxEmbeddablePackage,
|
||||
{} as OptionsListEmbeddableInput,
|
||||
{} as ControlOutput
|
||||
);
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
lazyLoadReduxEmbeddablePackage,
|
||||
ReduxEmbeddablePackage,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { ControlOutput } from '../../types';
|
||||
|
@ -21,16 +17,11 @@ import { TimeSliderEmbeddableFactory } from '../../time_slider';
|
|||
import { OptionsListEmbeddableFactory, OptionsListEmbeddableInput } from '../../options_list';
|
||||
import { ControlGroupContainer } from '../embeddable/control_group_container';
|
||||
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';
|
||||
|
||||
let reduxEmbeddablePackage: ReduxEmbeddablePackage;
|
||||
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
|
||||
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
||||
const deleteControlAction = new DeleteControlAction();
|
||||
|
||||
beforeAll(async () => {
|
||||
reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
||||
});
|
||||
|
||||
test('Action is incompatible with Error Embeddables', async () => {
|
||||
const editControlAction = new EditControlAction(deleteControlAction);
|
||||
const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' });
|
||||
|
@ -44,7 +35,7 @@ test('Action is incompatible with embeddables that are not editable', async () =
|
|||
pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory;
|
||||
|
||||
const editControlAction = new EditControlAction(deleteControlAction);
|
||||
const emptyContainer = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
||||
const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||
await emptyContainer.untilInitialized();
|
||||
await emptyContainer.addTimeSliderControl();
|
||||
|
||||
|
@ -62,7 +53,7 @@ test('Action is compatible with embeddables that are editable', async () => {
|
|||
pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory;
|
||||
|
||||
const editControlAction = new EditControlAction(deleteControlAction);
|
||||
const emptyContainer = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
||||
const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||
await emptyContainer.untilInitialized();
|
||||
await emptyContainer.addOptionsListControl({
|
||||
dataViewId: 'test-data-view',
|
||||
|
@ -82,7 +73,7 @@ test('Action is compatible with embeddables that are editable', async () => {
|
|||
test('Execute throws an error when called with an embeddable not in a parent', async () => {
|
||||
const editControlAction = new EditControlAction(deleteControlAction);
|
||||
const optionsListEmbeddable = new OptionsListEmbeddable(
|
||||
reduxEmbeddablePackage,
|
||||
mockedReduxEmbeddablePackage,
|
||||
{} as OptionsListEmbeddableInput,
|
||||
{} as ControlOutput
|
||||
);
|
||||
|
@ -95,7 +86,7 @@ test('Execute should open a flyout', async () => {
|
|||
const spyOn = jest.fn().mockResolvedValue(undefined);
|
||||
pluginServices.getServices().overlays.openFlyout = spyOn;
|
||||
|
||||
const emptyContainer = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
||||
const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||
await emptyContainer.untilInitialized();
|
||||
await emptyContainer.addOptionsListControl({
|
||||
dataViewId: 'test-data-view',
|
||||
|
|
|
@ -19,7 +19,7 @@ import { DeleteControlAction } from './delete_control_action';
|
|||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ACTION_EDIT_CONTROL, ControlGroupContainer } from '..';
|
||||
import { ControlEmbeddable, DataControlInput } from '../../types';
|
||||
import { setFlyoutRef } from '../embeddable/control_group_container';
|
||||
import { ControlGroupContainerContext, setFlyoutRef } from '../embeddable/control_group_container';
|
||||
import { isControlGroup } from '../embeddable/control_group_helpers';
|
||||
|
||||
export interface EditControlActionContext {
|
||||
|
@ -91,11 +91,10 @@ export class EditControlAction implements Action<EditControlActionContext> {
|
|||
throw new IncompatibleActionError();
|
||||
}
|
||||
const controlGroup = embeddable.parent as ControlGroupContainer;
|
||||
const ReduxWrapper = controlGroup.getReduxEmbeddableTools().Wrapper;
|
||||
|
||||
const flyoutInstance = this.openFlyout(
|
||||
toMountPoint(
|
||||
<ReduxWrapper>
|
||||
<ControlGroupContainerContext.Provider value={controlGroup}>
|
||||
<EditControlFlyout
|
||||
embeddable={embeddable}
|
||||
removeControl={() => this.deleteControlAction.execute({ embeddable })}
|
||||
|
@ -104,7 +103,7 @@ export class EditControlAction implements Action<EditControlActionContext> {
|
|||
flyoutInstance.close();
|
||||
}}
|
||||
/>
|
||||
</ReduxWrapper>,
|
||||
</ControlGroupContainerContext.Provider>,
|
||||
|
||||
{ theme$: this.theme$ }
|
||||
),
|
||||
|
|
|
@ -14,7 +14,7 @@ import { EmbeddableFactoryNotFoundError } from '@kbn/embeddable-plugin/public';
|
|||
import { DataControlInput, ControlEmbeddable, IEditableControlFactory } from '../../types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { useControlGroupContainerContext } from '../control_group_renderer';
|
||||
import { useControlGroupContainer } from '../embeddable/control_group_container';
|
||||
import { ControlEditor } from '../editor/control_editor';
|
||||
|
||||
export const EditControlFlyout = ({
|
||||
|
@ -32,17 +32,10 @@ export const EditControlFlyout = ({
|
|||
controls: { getControlFactory },
|
||||
} = pluginServices.getServices();
|
||||
// Redux embeddable container Context
|
||||
const reduxContext = useControlGroupContainerContext();
|
||||
const {
|
||||
embeddableInstance: controlGroup,
|
||||
actions: { setControlWidth, setControlGrow },
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
} = reduxContext;
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const controlGroup = useControlGroupContainer();
|
||||
|
||||
// current state
|
||||
const panels = useEmbeddableSelector((state) => state.explicitInput.panels);
|
||||
const panels = controlGroup.select((state) => state.explicitInput.panels);
|
||||
const panel = panels[embeddable.id];
|
||||
|
||||
const [currentGrow, setCurrentGrow] = useState(panel.grow);
|
||||
|
@ -86,9 +79,9 @@ export const EditControlFlyout = ({
|
|||
}
|
||||
|
||||
if (currentWidth !== panel.width)
|
||||
dispatch(setControlWidth({ width: currentWidth, embeddableId: embeddable.id }));
|
||||
controlGroup.dispatch.setControlWidth({ width: currentWidth, embeddableId: embeddable.id });
|
||||
if (currentGrow !== panel.grow)
|
||||
dispatch(setControlGrow({ grow: currentGrow, embeddableId: embeddable.id }));
|
||||
controlGroup.dispatch.setControlGrow({ grow: currentGrow, embeddableId: embeddable.id });
|
||||
|
||||
closeFlyout();
|
||||
await controlGroup.replaceEmbeddable(embeddable.id, inputToReturn, type);
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFormControlLayout,
|
||||
|
@ -17,15 +18,16 @@ import {
|
|||
EuiPopover,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Markdown } from '@kbn/kibana-react-plugin/public';
|
||||
import { useReduxEmbeddableContext, FloatingActions } from '@kbn/presentation-util-plugin/public';
|
||||
import { ControlGroupReduxState } from '../types';
|
||||
import { FloatingActions } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
controlGroupSelector,
|
||||
useControlGroupContainer,
|
||||
} from '../embeddable/control_group_container';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { ControlGroupContainer } from '..';
|
||||
|
||||
interface ControlFrameErrorProps {
|
||||
error: Error;
|
||||
|
@ -82,16 +84,11 @@ export const ControlFrame = ({
|
|||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
const [fatalError, setFatalError] = useState<Error>();
|
||||
|
||||
const { useEmbeddableSelector: select, embeddableInstance: controlGroup } =
|
||||
useReduxEmbeddableContext<
|
||||
ControlGroupReduxState,
|
||||
typeof controlGroupReducers,
|
||||
ControlGroupContainer
|
||||
>();
|
||||
const controlGroup = useControlGroupContainer();
|
||||
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
||||
const disabledActions = select((state) => state.explicitInput.disabledActions);
|
||||
const controlStyle = controlGroupSelector((state) => state.explicitInput.controlStyle);
|
||||
const viewMode = controlGroupSelector((state) => state.explicitInput.viewMode);
|
||||
const disabledActions = controlGroupSelector((state) => state.explicitInput.disabledActions);
|
||||
|
||||
const embeddable = useChildEmbeddable({
|
||||
untilEmbeddableLoaded: controlGroup.untilEmbeddableLoaded.bind(controlGroup),
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
|
||||
import '../control_group.scss';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
|
@ -28,28 +25,28 @@ import {
|
|||
useSensors,
|
||||
LayoutMeasuringStrategy,
|
||||
} from '@dnd-kit/core';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { ControlClone, SortableControl } from './control_group_sortable_item';
|
||||
import { useControlGroupContainerContext } from '../control_group_renderer';
|
||||
|
||||
import { ControlGroupReduxState } from '../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlClone, SortableControl } from './control_group_sortable_item';
|
||||
import { useControlGroupContainer } from '../embeddable/control_group_container';
|
||||
|
||||
const contextSelect = useSelector as TypedUseSelectorHook<ControlGroupReduxState>;
|
||||
|
||||
export const ControlGroup = () => {
|
||||
// Redux embeddable container Context
|
||||
const reduxContext = useControlGroupContainerContext();
|
||||
const {
|
||||
embeddableInstance: controlGroup,
|
||||
actions: { setControlOrders },
|
||||
useEmbeddableSelector: select,
|
||||
useEmbeddableDispatch,
|
||||
} = reduxContext;
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const controlGroup = useControlGroupContainer();
|
||||
|
||||
// current state
|
||||
const panels = select((state) => state.explicitInput.panels);
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
||||
const showAddButton = select((state) => state.componentState.showAddButton);
|
||||
const panels = contextSelect((state) => state.explicitInput.panels);
|
||||
const viewMode = contextSelect((state) => state.explicitInput.viewMode);
|
||||
const controlStyle = contextSelect((state) => state.explicitInput.controlStyle);
|
||||
const showAddButton = contextSelect((state) => state.componentState.showAddButton);
|
||||
|
||||
const isEditable = viewMode === ViewMode.EDIT;
|
||||
|
||||
|
@ -80,7 +77,9 @@ export const ControlGroup = () => {
|
|||
const overIndex = idsInOrder.indexOf(over.id);
|
||||
if (draggingIndex !== overIndex) {
|
||||
const newIndex = overIndex;
|
||||
dispatch(setControlOrders({ ids: arrayMove([...idsInOrder], draggingIndex, newIndex) }));
|
||||
controlGroup.dispatch.setControlOrders({
|
||||
ids: arrayMove([...idsInOrder], draggingIndex, newIndex),
|
||||
});
|
||||
}
|
||||
}
|
||||
setDraggingId(null);
|
||||
|
|
|
@ -6,16 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFlexItem, EuiFormLabel, EuiIcon, EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { forwardRef, HTMLAttributes } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import classNames from 'classnames';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import React, { forwardRef, HTMLAttributes } from 'react';
|
||||
import { EuiFlexItem, EuiFormLabel, EuiIcon, EuiFlexGroup } from '@elastic/eui';
|
||||
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { ControlFrame, ControlFrameProps } from './control_frame_component';
|
||||
import { ControlGroupReduxState } from '../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlFrame, ControlFrameProps } from './control_frame_component';
|
||||
import { controlGroupSelector } from '../embeddable/control_group_container';
|
||||
|
||||
interface DragInfo {
|
||||
isOver?: boolean;
|
||||
|
@ -70,8 +69,7 @@ const SortableControlInner = forwardRef<
|
|||
dragHandleRef
|
||||
) => {
|
||||
const { isOver, isDragging, draggingIndex, index } = dragInfo;
|
||||
const { useEmbeddableSelector } = useReduxEmbeddableContext<ControlGroupReduxState>();
|
||||
const panels = useEmbeddableSelector((state) => state.explicitInput.panels);
|
||||
const panels = controlGroupSelector((state) => state.explicitInput.panels);
|
||||
|
||||
const grow = panels[embeddableId].grow;
|
||||
const width = panels[embeddableId].width;
|
||||
|
@ -122,9 +120,8 @@ const SortableControlInner = forwardRef<
|
|||
* can be quite cumbersome.
|
||||
*/
|
||||
export const ControlClone = ({ draggingId }: { draggingId: string }) => {
|
||||
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<ControlGroupReduxState>();
|
||||
const panels = select((state) => state.explicitInput.panels);
|
||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
||||
const panels = controlGroupSelector((state) => state.explicitInput.panels);
|
||||
const controlStyle = controlGroupSelector((state) => state.explicitInput.controlStyle);
|
||||
|
||||
const width = panels[draggingId].width;
|
||||
const title = panels[draggingId].explicitInput.title;
|
||||
|
|
|
@ -1,128 +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 { v4 as uuidv4 } from 'uuid';
|
||||
import { isEqual } from 'lodash';
|
||||
import useLifecycles from 'react-use/lib/useLifecycles';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { compareFilters } from '@kbn/es-query';
|
||||
import type { Filter, TimeRange, Query } from '@kbn/es-query';
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
ControlGroupCreationOptions,
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupReduxState,
|
||||
CONTROL_GROUP_TYPE,
|
||||
} from './types';
|
||||
import { pluginServices } from '../services';
|
||||
import { getDefaultControlGroupInput } from '../../common';
|
||||
import { controlGroupReducers } from './state/control_group_reducers';
|
||||
import { controlGroupInputBuilder } from './control_group_input_builder';
|
||||
import { ControlGroupContainer } from './embeddable/control_group_container';
|
||||
import { ControlGroupContainerFactory } from './embeddable/control_group_container_factory';
|
||||
|
||||
export interface ControlGroupRendererProps {
|
||||
filters?: Filter[];
|
||||
getCreationOptions: (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
builder: typeof controlGroupInputBuilder
|
||||
) => Promise<ControlGroupCreationOptions>;
|
||||
onLoadComplete?: (controlGroup: ControlGroupContainer) => void;
|
||||
timeRange?: TimeRange;
|
||||
query?: Query;
|
||||
}
|
||||
|
||||
export const ControlGroupRenderer = ({
|
||||
onLoadComplete,
|
||||
getCreationOptions,
|
||||
filters,
|
||||
timeRange,
|
||||
query,
|
||||
}: ControlGroupRendererProps) => {
|
||||
const controlGroupRef = useRef(null);
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
/**
|
||||
* Use Lifecycles to load initial control group container
|
||||
*/
|
||||
useLifecycles(
|
||||
// onMount
|
||||
() => {
|
||||
const { embeddable } = pluginServices.getServices();
|
||||
(async () => {
|
||||
const factory = embeddable.getEmbeddableFactory(CONTROL_GROUP_TYPE) as EmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
> & {
|
||||
create: ControlGroupContainerFactory['create'];
|
||||
};
|
||||
const { initialInput, settings } = await getCreationOptions(
|
||||
getDefaultControlGroupInput(),
|
||||
controlGroupInputBuilder
|
||||
);
|
||||
const newControlGroup = (await factory?.create(
|
||||
{
|
||||
id,
|
||||
...getDefaultControlGroupInput(),
|
||||
...initialInput,
|
||||
},
|
||||
undefined,
|
||||
settings
|
||||
)) as ControlGroupContainer;
|
||||
|
||||
if (controlGroupRef.current) {
|
||||
newControlGroup.render(controlGroupRef.current);
|
||||
}
|
||||
setControlGroup(newControlGroup);
|
||||
if (onLoadComplete) {
|
||||
onLoadComplete(newControlGroup);
|
||||
}
|
||||
})();
|
||||
},
|
||||
// onUnmount
|
||||
() => {
|
||||
controlGroup?.destroy();
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!controlGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(timeRange && !isEqual(controlGroup.getInput().timeRange, timeRange)) ||
|
||||
!compareFilters(controlGroup.getInput().filters ?? [], filters ?? []) ||
|
||||
!isEqual(controlGroup.getInput().query, query)
|
||||
) {
|
||||
controlGroup.updateInput({
|
||||
timeRange,
|
||||
query,
|
||||
filters,
|
||||
});
|
||||
}
|
||||
}, [query, filters, controlGroup, timeRange]);
|
||||
|
||||
return <div ref={controlGroupRef} />;
|
||||
};
|
||||
|
||||
export const useControlGroupContainerContext = () =>
|
||||
useReduxEmbeddableContext<
|
||||
ControlGroupReduxState,
|
||||
typeof controlGroupReducers,
|
||||
ControlGroupContainer
|
||||
>();
|
||||
|
||||
// required for dynamic import using React.lazy()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ControlGroupRenderer;
|
|
@ -53,7 +53,8 @@ import {
|
|||
import { CONTROL_WIDTH_OPTIONS } from './editor_constants';
|
||||
import { pluginServices } from '../../services';
|
||||
import { getDataControlFieldRegistry } from './data_control_editor_tools';
|
||||
import { useControlGroupContainerContext } from '../control_group_renderer';
|
||||
import { useControlGroupContainer } from '../embeddable/control_group_container';
|
||||
|
||||
interface EditControlProps {
|
||||
embeddable?: ControlEmbeddable<DataControlInput>;
|
||||
isCreate: boolean;
|
||||
|
@ -95,9 +96,11 @@ export const ControlEditor = ({
|
|||
controls: { getControlFactory },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const { useEmbeddableSelector: select } = useControlGroupContainerContext();
|
||||
const editorConfig = select((state) => state.componentState.editorConfig);
|
||||
const customFilterPredicate = select((state) => state.componentState.fieldFilterPredicate);
|
||||
const controlGroup = useControlGroupContainer();
|
||||
const editorConfig = controlGroup.select((state) => state.componentState.editorConfig);
|
||||
const customFilterPredicate = controlGroup.select(
|
||||
(state) => state.componentState.fieldFilterPredicate
|
||||
);
|
||||
|
||||
const [defaultTitle, setDefaultTitle] = useState<string>();
|
||||
const [currentTitle, setCurrentTitle] = useState(title ?? '');
|
||||
|
|
|
@ -8,22 +8,27 @@
|
|||
|
||||
import React from 'react';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { ControlInputTransform } from '../../../common/types';
|
||||
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
ControlGroupContainerContext,
|
||||
setFlyoutRef,
|
||||
} from '../embeddable/control_group_container';
|
||||
import type {
|
||||
AddDataControlProps,
|
||||
AddOptionsListControlProps,
|
||||
AddRangeSliderControlProps,
|
||||
} from '../control_group_input_builder';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlGroupContainer, setFlyoutRef } from '../embeddable/control_group_container';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlEditor } from './control_editor';
|
||||
import { DataControlInput, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '../..';
|
||||
import { IEditableControlFactory } from '../../types';
|
||||
} from '../external_api/control_group_input_builder';
|
||||
import {
|
||||
DEFAULT_CONTROL_GROW,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
} from '../../../common/control_group/control_group_constants';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlEditor } from './control_editor';
|
||||
import { IEditableControlFactory } from '../../types';
|
||||
import { ControlInputTransform } from '../../../common/types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { DataControlInput, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '../..';
|
||||
|
||||
export function openAddDataControlFlyout(
|
||||
this: ControlGroupContainer,
|
||||
|
@ -34,8 +39,6 @@ export function openAddDataControlFlyout(
|
|||
controls: { getControlFactory },
|
||||
theme: { theme$ },
|
||||
} = pluginServices.getServices();
|
||||
const ControlsServicesProvider = pluginServices.getContextProvider();
|
||||
const ReduxWrapper = this.getReduxEmbeddableTools().Wrapper;
|
||||
|
||||
let controlInput: Partial<DataControlInput> = {};
|
||||
const onCancel = () => {
|
||||
|
@ -58,51 +61,49 @@ export function openAddDataControlFlyout(
|
|||
|
||||
const flyoutInstance = openFlyout(
|
||||
toMountPoint(
|
||||
<ControlsServicesProvider>
|
||||
<ReduxWrapper>
|
||||
<ControlEditor
|
||||
setLastUsedDataViewId={(newId) => this.setLastUsedDataViewId(newId)}
|
||||
getRelevantDataViewId={this.getMostRelevantDataViewId}
|
||||
isCreate={true}
|
||||
width={this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
grow={this.getInput().defaultControlGrow ?? DEFAULT_CONTROL_GROW}
|
||||
updateTitle={(newTitle) => (controlInput.title = newTitle)}
|
||||
updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
|
||||
updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })}
|
||||
onSave={(type) => {
|
||||
this.closeAllFlyouts();
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const factory = getControlFactory(type) as IEditableControlFactory;
|
||||
if (factory.presaveTransformFunction) {
|
||||
controlInput = factory.presaveTransformFunction(controlInput);
|
||||
}
|
||||
|
||||
if (controlInputTransform) {
|
||||
controlInput = controlInputTransform({ ...controlInput }, type);
|
||||
}
|
||||
|
||||
if (type === OPTIONS_LIST_CONTROL) {
|
||||
this.addOptionsListControl(controlInput as AddOptionsListControlProps);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === RANGE_SLIDER_CONTROL) {
|
||||
this.addRangeSliderControl(controlInput as AddRangeSliderControlProps);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addDataControlFromField(controlInput as AddDataControlProps);
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
onTypeEditorChange={(partialInput) =>
|
||||
(controlInput = { ...controlInput, ...partialInput })
|
||||
<ControlGroupContainerContext.Provider value={this}>
|
||||
<ControlEditor
|
||||
setLastUsedDataViewId={(newId) => this.setLastUsedDataViewId(newId)}
|
||||
getRelevantDataViewId={this.getMostRelevantDataViewId}
|
||||
isCreate={true}
|
||||
width={this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
grow={this.getInput().defaultControlGrow ?? DEFAULT_CONTROL_GROW}
|
||||
updateTitle={(newTitle) => (controlInput.title = newTitle)}
|
||||
updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
|
||||
updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })}
|
||||
onSave={(type) => {
|
||||
this.closeAllFlyouts();
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
/>
|
||||
</ReduxWrapper>
|
||||
</ControlsServicesProvider>,
|
||||
|
||||
const factory = getControlFactory(type) as IEditableControlFactory;
|
||||
if (factory.presaveTransformFunction) {
|
||||
controlInput = factory.presaveTransformFunction(controlInput);
|
||||
}
|
||||
|
||||
if (controlInputTransform) {
|
||||
controlInput = controlInputTransform({ ...controlInput }, type);
|
||||
}
|
||||
|
||||
if (type === OPTIONS_LIST_CONTROL) {
|
||||
this.addOptionsListControl(controlInput as AddOptionsListControlProps);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === RANGE_SLIDER_CONTROL) {
|
||||
this.addRangeSliderControl(controlInput as AddRangeSliderControlProps);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addDataControlFromField(controlInput as AddDataControlProps);
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
onTypeEditorChange={(partialInput) =>
|
||||
(controlInput = { ...controlInput, ...partialInput })
|
||||
}
|
||||
/>
|
||||
</ControlGroupContainerContext.Provider>,
|
||||
{ theme$ }
|
||||
),
|
||||
{
|
||||
|
|
|
@ -13,14 +13,17 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
|||
import { pluginServices } from '../../services';
|
||||
import { ControlGroupEditor } from './control_group_editor';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlGroupContainer, setFlyoutRef } from '../embeddable/control_group_container';
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
ControlGroupContainerContext,
|
||||
setFlyoutRef,
|
||||
} from '../embeddable/control_group_container';
|
||||
|
||||
export function openEditControlGroupFlyout(this: ControlGroupContainer) {
|
||||
const {
|
||||
overlays: { openFlyout, openConfirm },
|
||||
theme: { theme$ },
|
||||
} = pluginServices.getServices();
|
||||
const ReduxWrapper = this.getReduxEmbeddableTools().Wrapper;
|
||||
|
||||
const onDeleteAll = (ref: OverlayRef) => {
|
||||
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
|
||||
|
@ -37,7 +40,7 @@ export function openEditControlGroupFlyout(this: ControlGroupContainer) {
|
|||
|
||||
const flyoutInstance = openFlyout(
|
||||
toMountPoint(
|
||||
<ReduxWrapper>
|
||||
<ControlGroupContainerContext.Provider value={this}>
|
||||
<ControlGroupEditor
|
||||
initialInput={this.getInput()}
|
||||
updateInput={(changes) => this.updateInput(changes)}
|
||||
|
@ -45,7 +48,7 @@ export function openEditControlGroupFlyout(this: ControlGroupContainer) {
|
|||
onDeleteAll={() => onDeleteAll(flyoutInstance)}
|
||||
onClose={() => flyoutInstance.close()}
|
||||
/>
|
||||
</ReduxWrapper>,
|
||||
</ControlGroupContainerContext.Provider>,
|
||||
{ theme$ }
|
||||
),
|
||||
{
|
||||
|
|
|
@ -35,7 +35,7 @@ interface OnChildChangedProps {
|
|||
interface ChainingSystem {
|
||||
getContainerSettings: (
|
||||
initialInput: ControlGroupInput
|
||||
) => EmbeddableContainerSettings<ControlGroupInput> | undefined;
|
||||
) => EmbeddableContainerSettings | undefined;
|
||||
getPrecedingFilters: (
|
||||
props: GetPrecedingFiltersProps
|
||||
) => { filters: Filter[]; timeslice?: [number, number] } | undefined;
|
||||
|
|
|
@ -5,18 +5,19 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { skip, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import React from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, Filter, uniqFilters } from '@kbn/es-query';
|
||||
import { Provider, TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { BehaviorSubject, merge, Subject, Subscription } from 'rxjs';
|
||||
import _ from 'lodash';
|
||||
import { skip, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, Filter, uniqFilters } from '@kbn/es-query';
|
||||
|
||||
import { ReduxEmbeddablePackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||
import { OverlayRef } from '@kbn/core/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { Container, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
|
@ -31,23 +32,21 @@ import {
|
|||
ControlGroupChainingSystems,
|
||||
controlOrdersAreEqual,
|
||||
} from './control_group_chaining_system';
|
||||
import { pluginServices } from '../../services';
|
||||
import { openAddDataControlFlyout } from '../editor/open_add_data_control_flyout';
|
||||
import { ControlGroup } from '../component/control_group_component';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types';
|
||||
import { getNextPanelOrder } from './control_group_helpers';
|
||||
import type {
|
||||
AddDataControlProps,
|
||||
AddOptionsListControlProps,
|
||||
AddRangeSliderControlProps,
|
||||
} from '../control_group_input_builder';
|
||||
import {
|
||||
type AddDataControlProps,
|
||||
type AddOptionsListControlProps,
|
||||
type AddRangeSliderControlProps,
|
||||
getDataControlPanelState,
|
||||
getOptionsListPanelState,
|
||||
getRangeSliderPanelState,
|
||||
getTimeSliderPanelState,
|
||||
} from '../control_group_input_builder';
|
||||
} from '../external_api/control_group_input_builder';
|
||||
import { pluginServices } from '../../services';
|
||||
import { getNextPanelOrder } from './control_group_helpers';
|
||||
import { ControlGroup } from '../component/control_group_component';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types';
|
||||
import { openAddDataControlFlyout } from '../editor/open_add_data_control_flyout';
|
||||
import { openEditControlGroupFlyout } from '../editor/open_edit_control_group_flyout';
|
||||
|
||||
let flyoutRef: OverlayRef | undefined;
|
||||
|
@ -55,6 +54,21 @@ export const setFlyoutRef = (newRef: OverlayRef | undefined) => {
|
|||
flyoutRef = newRef;
|
||||
};
|
||||
|
||||
export const ControlGroupContainerContext = createContext<ControlGroupContainer | null>(null);
|
||||
export const controlGroupSelector = useSelector as TypedUseSelectorHook<ControlGroupReduxState>;
|
||||
export const useControlGroupContainer = (): ControlGroupContainer => {
|
||||
const controlGroup = useContext<ControlGroupContainer | null>(ControlGroupContainerContext);
|
||||
if (controlGroup == null) {
|
||||
throw new Error('useControlGroupContainer must be used inside ControlGroupContainerContext.');
|
||||
}
|
||||
return controlGroup!;
|
||||
};
|
||||
|
||||
type ControlGroupReduxEmbeddableTools = ReduxEmbeddableTools<
|
||||
ControlGroupReduxState,
|
||||
typeof controlGroupReducers
|
||||
>;
|
||||
|
||||
export class ControlGroupContainer extends Container<
|
||||
ControlInput,
|
||||
ControlGroupInput,
|
||||
|
@ -71,63 +85,21 @@ export class ControlGroupContainer extends Container<
|
|||
private relevantDataViewId?: string;
|
||||
private lastUsedDataViewId?: string;
|
||||
|
||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
||||
ControlGroupReduxState,
|
||||
typeof controlGroupReducers
|
||||
>;
|
||||
// state management
|
||||
public select: ControlGroupReduxEmbeddableTools['select'];
|
||||
public getState: ControlGroupReduxEmbeddableTools['getState'];
|
||||
public dispatch: ControlGroupReduxEmbeddableTools['dispatch'];
|
||||
public onStateChange: ControlGroupReduxEmbeddableTools['onStateChange'];
|
||||
|
||||
private store: ControlGroupReduxEmbeddableTools['store'];
|
||||
|
||||
private cleanupStateTools: () => void;
|
||||
|
||||
public onFiltersPublished$: Subject<Filter[]>;
|
||||
public onControlRemoved$: Subject<string>;
|
||||
|
||||
public setLastUsedDataViewId = (lastUsedDataViewId: string) => {
|
||||
this.lastUsedDataViewId = lastUsedDataViewId;
|
||||
};
|
||||
|
||||
public setRelevantDataViewId = (newRelevantDataViewId: string) => {
|
||||
this.relevantDataViewId = newRelevantDataViewId;
|
||||
};
|
||||
|
||||
public getMostRelevantDataViewId = () => {
|
||||
const staticDataViewId =
|
||||
this.getReduxEmbeddableTools().getState().componentState.staticDataViewId;
|
||||
return staticDataViewId ?? this.lastUsedDataViewId ?? this.relevantDataViewId;
|
||||
};
|
||||
|
||||
public getReduxEmbeddableTools = () => {
|
||||
return this.reduxEmbeddableTools;
|
||||
};
|
||||
|
||||
public closeAllFlyouts() {
|
||||
flyoutRef?.close();
|
||||
flyoutRef = undefined;
|
||||
}
|
||||
|
||||
public async addDataControlFromField(controlProps: AddDataControlProps) {
|
||||
const panelState = await getDataControlPanelState(this.getInput(), controlProps);
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public addOptionsListControl(controlProps: AddOptionsListControlProps) {
|
||||
const panelState = getOptionsListPanelState(this.getInput(), controlProps);
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public addRangeSliderControl(controlProps: AddRangeSliderControlProps) {
|
||||
const panelState = getRangeSliderPanelState(this.getInput(), controlProps);
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public addTimeSliderControl() {
|
||||
const panelState = getTimeSliderPanelState(this.getInput());
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public openAddDataControlFlyout = openAddDataControlFlyout;
|
||||
|
||||
public openEditControlGroupFlyout = openEditControlGroupFlyout;
|
||||
|
||||
constructor(
|
||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
initialInput: ControlGroupInput,
|
||||
parent?: Container,
|
||||
settings?: ControlGroupSettings
|
||||
|
@ -145,7 +117,7 @@ export class ControlGroupContainer extends Container<
|
|||
this.onControlRemoved$ = new Subject<string>();
|
||||
|
||||
// build redux embeddable tools
|
||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
||||
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||
ControlGroupReduxState,
|
||||
typeof controlGroupReducers
|
||||
>({
|
||||
|
@ -154,6 +126,14 @@ export class ControlGroupContainer extends Container<
|
|||
initialComponentState: settings,
|
||||
});
|
||||
|
||||
this.select = reduxEmbeddableTools.select;
|
||||
this.getState = reduxEmbeddableTools.getState;
|
||||
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||
|
||||
this.store = reduxEmbeddableTools.store;
|
||||
|
||||
// when all children are ready setup subscriptions
|
||||
this.untilAllChildrenReady().then(() => {
|
||||
this.recalculateDataViews();
|
||||
|
@ -204,6 +184,47 @@ export class ControlGroupContainer extends Container<
|
|||
);
|
||||
};
|
||||
|
||||
public setLastUsedDataViewId = (lastUsedDataViewId: string) => {
|
||||
this.lastUsedDataViewId = lastUsedDataViewId;
|
||||
};
|
||||
|
||||
public setRelevantDataViewId = (newRelevantDataViewId: string) => {
|
||||
this.relevantDataViewId = newRelevantDataViewId;
|
||||
};
|
||||
|
||||
public getMostRelevantDataViewId = () => {
|
||||
return this.lastUsedDataViewId ?? this.relevantDataViewId;
|
||||
};
|
||||
|
||||
public closeAllFlyouts() {
|
||||
flyoutRef?.close();
|
||||
flyoutRef = undefined;
|
||||
}
|
||||
|
||||
public async addDataControlFromField(controlProps: AddDataControlProps) {
|
||||
const panelState = await getDataControlPanelState(this.getInput(), controlProps);
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public addOptionsListControl(controlProps: AddOptionsListControlProps) {
|
||||
const panelState = getOptionsListPanelState(this.getInput(), controlProps);
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public addRangeSliderControl(controlProps: AddRangeSliderControlProps) {
|
||||
const panelState = getRangeSliderPanelState(this.getInput(), controlProps);
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public addTimeSliderControl() {
|
||||
const panelState = getTimeSliderPanelState(this.getInput());
|
||||
return this.createAndSaveEmbeddable(panelState.type, panelState);
|
||||
}
|
||||
|
||||
public openAddDataControlFlyout = openAddDataControlFlyout;
|
||||
|
||||
public openEditControlGroupFlyout = openEditControlGroupFlyout;
|
||||
|
||||
public getPanelCount = () => {
|
||||
return Object.keys(this.getInput().panels).length;
|
||||
};
|
||||
|
@ -225,7 +246,7 @@ export class ControlGroupContainer extends Container<
|
|||
// if filters are different, publish them
|
||||
if (
|
||||
!compareFilters(this.output.filters ?? [], allFilters ?? [], COMPARE_ALL_OPTIONS) ||
|
||||
!_.isEqual(this.output.timeslice, timeslice)
|
||||
!isEqual(this.output.timeslice, timeslice)
|
||||
) {
|
||||
this.updateOutput({ filters: uniqFilters(allFilters), timeslice });
|
||||
this.onFiltersPublished$.next(allFilters);
|
||||
|
@ -337,15 +358,13 @@ export class ControlGroupContainer extends Container<
|
|||
ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
this.domNode = dom;
|
||||
const ControlsServicesProvider = pluginServices.getContextProvider();
|
||||
const { Wrapper: ControlGroupReduxWrapper } = this.reduxEmbeddableTools;
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||
<ControlsServicesProvider>
|
||||
<ControlGroupReduxWrapper>
|
||||
<Provider store={this.store}>
|
||||
<ControlGroupContainerContext.Provider value={this}>
|
||||
<ControlGroup />
|
||||
</ControlGroupReduxWrapper>
|
||||
</ControlsServicesProvider>
|
||||
</ControlGroupContainerContext.Provider>
|
||||
</Provider>
|
||||
</KibanaThemeProvider>,
|
||||
dom
|
||||
);
|
||||
|
@ -355,7 +374,7 @@ export class ControlGroupContainer extends Container<
|
|||
super.destroy();
|
||||
this.closeAllFlyouts();
|
||||
this.subscriptions.unsubscribe();
|
||||
this.reduxEmbeddableTools.cleanup();
|
||||
this.cleanupStateTools();
|
||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Container, EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
|
||||
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||
|
||||
import { ControlGroupInput, ControlGroupSettings, CONTROL_GROUP_TYPE } from '../types';
|
||||
|
@ -54,7 +54,7 @@ export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition
|
|||
parent?: Container,
|
||||
settings?: ControlGroupSettings
|
||||
) => {
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||
const { ControlGroupContainer } = await import('./control_group_container');
|
||||
return new ControlGroupContainer(reduxEmbeddablePackage, initialInput, parent, settings);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { ControlGroupContainer } from '..';
|
||||
|
||||
// TODO lock down ControlGroupAPI
|
||||
export type ControlGroupAPI = ControlGroupContainer;
|
||||
export type AwaitingControlGroupAPI = ControlGroupAPI | null;
|
||||
|
||||
export const buildApiFromControlGroupContainer = (container?: ControlGroupContainer) =>
|
||||
container ?? null;
|
|
@ -8,22 +8,22 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ControlPanelState, OptionsListEmbeddableInput } from '../../common';
|
||||
|
||||
import {
|
||||
ControlPanelState,
|
||||
ControlWidth,
|
||||
OptionsListEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
TIME_SLIDER_CONTROL,
|
||||
} 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';
|
||||
} from '../../../common/control_group/control_group_constants';
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { ControlInput, DataControlInput } from '../../types';
|
||||
import { RangeValue, RANGE_SLIDER_CONTROL } from '../../../common/range_slider/types';
|
||||
import { getCompatibleControlType, getNextPanelOrder } from '../embeddable/control_group_helpers';
|
||||
|
||||
export interface AddDataControlProps {
|
||||
controlId?: string;
|
||||
|
@ -40,6 +40,8 @@ export type AddRangeSliderControlProps = AddDataControlProps & {
|
|||
value?: RangeValue;
|
||||
};
|
||||
|
||||
export type ControlGroupInputBuilder = typeof controlGroupInputBuilder;
|
||||
|
||||
export const controlGroupInputBuilder = {
|
||||
addDataControlFromField: async (
|
||||
initialInput: Partial<ControlGroupInput>,
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
ControlGroupRenderer,
|
||||
CONTROL_GROUP_TYPE,
|
||||
} from '..';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
describe('control group renderer', () => {
|
||||
let mockControlGroupFactory: ControlGroupContainerFactory;
|
||||
let mockControlGroupContainer: ControlGroupContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
mockControlGroupContainer = {
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
updateInput: jest.fn(),
|
||||
getInput: jest.fn().mockReturnValue({}),
|
||||
} as unknown as ControlGroupContainer;
|
||||
mockControlGroupFactory = {
|
||||
create: jest.fn().mockReturnValue(mockControlGroupContainer),
|
||||
} as unknown as ControlGroupContainerFactory;
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockControlGroupFactory);
|
||||
});
|
||||
|
||||
test('calls create method on the Control Group embeddable factory with returned initial input', async () => {
|
||||
await act(async () => {
|
||||
mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { controlStyle: 'twoLine' } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(pluginServices.getServices().embeddable.getEmbeddableFactory).toHaveBeenCalledWith(
|
||||
CONTROL_GROUP_TYPE
|
||||
);
|
||||
expect(mockControlGroupFactory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ controlStyle: 'twoLine' }),
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test('destroys control group container on unmount', async () => {
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(<ControlGroupRenderer />);
|
||||
});
|
||||
wrapper!.unmount();
|
||||
expect(mockControlGroupContainer.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('filter changes are dispatched to control group if they are different', async () => {
|
||||
const initialFilters: Filter[] = [
|
||||
{ meta: { alias: 'test', disabled: false, negate: false, index: 'test' } },
|
||||
];
|
||||
const updatedFilters: Filter[] = [
|
||||
{ meta: { alias: 'test', disabled: false, negate: true, index: 'test' } },
|
||||
];
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { filters: initialFilters } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ filters: updatedFilters });
|
||||
});
|
||||
expect(mockControlGroupContainer.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filters: updatedFilters })
|
||||
);
|
||||
});
|
||||
|
||||
test('query changes are dispatched to control group if they are different', async () => {
|
||||
const initialQuery = { language: 'kql', query: 'query' };
|
||||
const updatedQuery = { language: 'kql', query: 'super query' };
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { query: initialQuery } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ query: updatedQuery });
|
||||
});
|
||||
expect(mockControlGroupContainer.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ query: updatedQuery })
|
||||
);
|
||||
});
|
||||
|
||||
test('time range changes are dispatched to control group if they are different', async () => {
|
||||
const initialTime = { from: new Date().toISOString(), to: new Date().toISOString() };
|
||||
const updatedTime = { from: new Date().toISOString() + 10, to: new Date().toISOString() + 20 };
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { timeRange: initialTime } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ timeRange: updatedTime });
|
||||
});
|
||||
expect(mockControlGroupContainer.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeRange: updatedTime })
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { compareFilters } from '@kbn/es-query';
|
||||
import type { Filter, TimeRange, Query } from '@kbn/es-query';
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
ControlGroupInput,
|
||||
CONTROL_GROUP_TYPE,
|
||||
ControlGroupOutput,
|
||||
ControlGroupCreationOptions,
|
||||
} from '../types';
|
||||
import {
|
||||
ControlGroupAPI,
|
||||
AwaitingControlGroupAPI,
|
||||
buildApiFromControlGroupContainer,
|
||||
} from './control_group_api';
|
||||
import { getDefaultControlGroupInput } from '../../../common';
|
||||
import { controlGroupInputBuilder, ControlGroupInputBuilder } from './control_group_input_builder';
|
||||
import { ControlGroupContainer } from '../embeddable/control_group_container';
|
||||
import { ControlGroupContainerFactory } from '../embeddable/control_group_container_factory';
|
||||
|
||||
export interface ControlGroupRendererProps {
|
||||
filters?: Filter[];
|
||||
getCreationOptions?: (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
builder: ControlGroupInputBuilder
|
||||
) => Promise<ControlGroupCreationOptions>;
|
||||
timeRange?: TimeRange;
|
||||
query?: Query;
|
||||
}
|
||||
|
||||
export const ControlGroupRenderer = forwardRef<AwaitingControlGroupAPI, ControlGroupRendererProps>(
|
||||
({ getCreationOptions, filters, timeRange, query }, ref) => {
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => buildApiFromControlGroupContainer(controlGroup) as ControlGroupAPI,
|
||||
[controlGroup]
|
||||
);
|
||||
|
||||
const controlGroupDomRef = useRef(null);
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
|
||||
// onMount
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
let destroyControlGroup: () => void;
|
||||
|
||||
(async () => {
|
||||
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
|
||||
const { pluginServices } = await import('../../services/plugin_services');
|
||||
const { embeddable } = pluginServices.getServices();
|
||||
|
||||
const factory = embeddable.getEmbeddableFactory(CONTROL_GROUP_TYPE) as EmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
> & {
|
||||
create: ControlGroupContainerFactory['create'];
|
||||
};
|
||||
const { initialInput, settings } =
|
||||
(await getCreationOptions?.(getDefaultControlGroupInput(), controlGroupInputBuilder)) ??
|
||||
{};
|
||||
const newControlGroup = (await factory?.create(
|
||||
{
|
||||
id,
|
||||
...getDefaultControlGroupInput(),
|
||||
...initialInput,
|
||||
},
|
||||
undefined,
|
||||
settings
|
||||
)) as ControlGroupContainer;
|
||||
|
||||
if (canceled) {
|
||||
newControlGroup.destroy();
|
||||
controlGroup?.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (controlGroupDomRef.current) {
|
||||
newControlGroup.render(controlGroupDomRef.current);
|
||||
}
|
||||
setControlGroup(newControlGroup);
|
||||
destroyControlGroup = () => newControlGroup.destroy();
|
||||
})();
|
||||
return () => {
|
||||
canceled = true;
|
||||
destroyControlGroup?.();
|
||||
};
|
||||
// exhaustive deps disabled because we want the control group to be created only on first render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!controlGroup) return;
|
||||
if (
|
||||
(timeRange && !isEqual(controlGroup.getInput().timeRange, timeRange)) ||
|
||||
!compareFilters(controlGroup.getInput().filters ?? [], filters ?? []) ||
|
||||
!isEqual(controlGroup.getInput().query, query)
|
||||
) {
|
||||
controlGroup.updateInput({
|
||||
timeRange,
|
||||
query,
|
||||
filters,
|
||||
});
|
||||
}
|
||||
}, [query, filters, controlGroup, timeRange]);
|
||||
|
||||
return <div ref={controlGroupDomRef} />;
|
||||
}
|
||||
);
|
|
@ -6,8 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export type { ControlGroupContainer } from './embeddable/control_group_container';
|
||||
export type { ControlGroupInput, ControlGroupOutput } from './types';
|
||||
|
||||
|
@ -19,12 +17,14 @@ export { ACTION_EDIT_CONTROL, ACTION_DELETE_CONTROL } from './actions';
|
|||
export {
|
||||
type AddDataControlProps,
|
||||
type AddOptionsListControlProps,
|
||||
type ControlGroupInputBuilder,
|
||||
type AddRangeSliderControlProps,
|
||||
controlGroupInputBuilder,
|
||||
} from './control_group_input_builder';
|
||||
} from './external_api/control_group_input_builder';
|
||||
|
||||
export type { ControlGroupAPI, AwaitingControlGroupAPI } from './external_api/control_group_api';
|
||||
|
||||
export {
|
||||
type ControlGroupRendererProps,
|
||||
useControlGroupContainerContext,
|
||||
} from './control_group_renderer';
|
||||
export const LazyControlGroupRenderer = React.lazy(() => import('./control_group_renderer'));
|
||||
ControlGroupRenderer,
|
||||
} from './external_api/control_group_renderer';
|
||||
|
|
|
@ -19,6 +19,12 @@ export const controlGroupReducers = {
|
|||
) => {
|
||||
state.explicitInput.controlStyle = action.payload;
|
||||
},
|
||||
setChainingSystem: (
|
||||
state: WritableDraft<ControlGroupReduxState>,
|
||||
action: PayloadAction<ControlGroupInput['chainingSystem']>
|
||||
) => {
|
||||
state.explicitInput.chainingSystem = action.payload;
|
||||
},
|
||||
setDefaultControlWidth: (
|
||||
state: WritableDraft<ControlGroupReduxState>,
|
||||
action: PayloadAction<ControlGroupInput['defaultControlWidth']>
|
||||
|
|
|
@ -39,8 +39,11 @@ export {
|
|||
type ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
type ControlGroupInput,
|
||||
controlGroupInputBuilder,
|
||||
type ControlGroupInputBuilder,
|
||||
type ControlGroupAPI,
|
||||
type AwaitingControlGroupAPI,
|
||||
type ControlGroupOutput,
|
||||
controlGroupInputBuilder,
|
||||
} from './control_group';
|
||||
|
||||
export {
|
||||
|
@ -56,11 +59,10 @@ export {
|
|||
} from './range_slider';
|
||||
|
||||
export {
|
||||
LazyControlGroupRenderer,
|
||||
useControlGroupContainerContext,
|
||||
type ControlGroupRendererProps,
|
||||
ACTION_DELETE_CONTROL,
|
||||
ACTION_EDIT_CONTROL,
|
||||
ACTION_DELETE_CONTROL,
|
||||
ControlGroupRenderer,
|
||||
type ControlGroupRendererProps,
|
||||
} from './control_group';
|
||||
|
||||
export function plugin() {
|
||||
|
|
|
@ -11,9 +11,10 @@ import React from 'react';
|
|||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
import { OptionsListEmbeddableContext } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
||||
import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks';
|
||||
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||
import { OptionsListControl } from './options_list_control';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
|
@ -30,16 +31,16 @@ describe('Options list control', () => {
|
|||
}
|
||||
|
||||
async function mountComponent(options?: Partial<MountOptions>) {
|
||||
const mockReduxEmbeddableTools = await mockOptionsListReduxEmbeddableTools({
|
||||
const optionsListEmbeddable = await mockOptionsListEmbeddable({
|
||||
componentState: options?.componentState ?? {},
|
||||
explicitInput: options?.explicitInput ?? {},
|
||||
output: options?.output ?? {},
|
||||
} as Partial<OptionsListReduxState>);
|
||||
|
||||
return mountWithIntl(
|
||||
<mockReduxEmbeddableTools.Wrapper>
|
||||
<OptionsListEmbeddableContext.Provider value={optionsListEmbeddable}>
|
||||
<OptionsListControl {...defaultProps} />
|
||||
</mockReduxEmbeddableTools.Wrapper>
|
||||
</OptionsListEmbeddableContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,12 +12,11 @@ import { debounce, isEmpty } from 'lodash';
|
|||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
|
||||
import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { OptionsListPopover } from './options_list_popover';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
|
||||
import './options_list.scss';
|
||||
|
||||
|
@ -29,38 +28,29 @@ export const OptionsListControl = ({
|
|||
loadMoreSubject: Subject<number>;
|
||||
}) => {
|
||||
const resizeRef = useRef(null);
|
||||
const optionsList = useOptionsList();
|
||||
const dimensions = useResizeObserver(resizeRef.current);
|
||||
|
||||
// Redux embeddable Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
actions: { replaceSelection, setSearchString, setPopoverOpen },
|
||||
useEmbeddableSelector: select,
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const isPopoverOpen = optionsList.select((state) => state.componentState.popoverOpen);
|
||||
const validSelections = optionsList.select((state) => state.componentState.validSelections);
|
||||
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
||||
const validSelections = select((state) => state.componentState.validSelections);
|
||||
const isPopoverOpen = select((state) => state.componentState.popoverOpen);
|
||||
const id = optionsList.select((state) => state.explicitInput.id);
|
||||
const exclude = optionsList.select((state) => state.explicitInput.exclude);
|
||||
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||
const placeholder = optionsList.select((state) => state.explicitInput.placeholder);
|
||||
const controlStyle = optionsList.select((state) => state.explicitInput.controlStyle);
|
||||
const singleSelect = optionsList.select((state) => state.explicitInput.singleSelect);
|
||||
const existsSelected = optionsList.select((state) => state.explicitInput.existsSelected);
|
||||
const selectedOptions = optionsList.select((state) => state.explicitInput.selectedOptions);
|
||||
|
||||
const selectedOptions = select((state) => state.explicitInput.selectedOptions);
|
||||
const existsSelected = select((state) => state.explicitInput.existsSelected);
|
||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
||||
const singleSelect = select((state) => state.explicitInput.singleSelect);
|
||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
||||
const exclude = select((state) => state.explicitInput.exclude);
|
||||
const id = select((state) => state.explicitInput.id);
|
||||
|
||||
const placeholder = select((state) => state.explicitInput.placeholder);
|
||||
|
||||
const loading = select((state) => state.output.loading);
|
||||
const loading = optionsList.select((state) => state.output.loading);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(setPopoverOpen(false)); // on unmount, close the popover
|
||||
optionsList.dispatch.setPopoverOpen(false); // on unmount, close the popover
|
||||
};
|
||||
}, [dispatch, setPopoverOpen]);
|
||||
}, [optionsList]);
|
||||
|
||||
// debounce loading state so loading doesn't flash when user types
|
||||
const [debouncedLoading, setDebouncedLoading] = useState(true);
|
||||
|
@ -76,16 +66,16 @@ export const OptionsListControl = ({
|
|||
// remove all other selections if this control is single select
|
||||
useEffect(() => {
|
||||
if (singleSelect && selectedOptions && selectedOptions?.length > 1) {
|
||||
dispatch(replaceSelection(selectedOptions[0]));
|
||||
optionsList.dispatch.replaceSelection(selectedOptions[0]);
|
||||
}
|
||||
}, [selectedOptions, singleSelect, dispatch, replaceSelection]);
|
||||
}, [selectedOptions, singleSelect, optionsList.dispatch]);
|
||||
|
||||
const updateSearchString = useCallback(
|
||||
(newSearchString: string) => {
|
||||
typeaheadSubject.next(newSearchString);
|
||||
dispatch(setSearchString(newSearchString));
|
||||
optionsList.dispatch.setSearchString(newSearchString);
|
||||
},
|
||||
[typeaheadSubject, dispatch, setSearchString]
|
||||
[typeaheadSubject, optionsList.dispatch]
|
||||
);
|
||||
|
||||
const loadMoreSuggestions = useCallback(
|
||||
|
@ -141,7 +131,7 @@ export const OptionsListControl = ({
|
|||
'optionsList--filterBtnPlaceholder': !hasSelections,
|
||||
})}
|
||||
data-test-subj={`optionsList-control-${id}`}
|
||||
onClick={() => dispatch(setPopoverOpen(!isPopoverOpen))}
|
||||
onClick={() => optionsList.dispatch.setPopoverOpen(!isPopoverOpen)}
|
||||
isSelected={isPopoverOpen}
|
||||
numActiveFilters={validSelectionsCount}
|
||||
hasActiveFilters={Boolean(validSelectionsCount)}
|
||||
|
@ -168,7 +158,7 @@ export const OptionsListControl = ({
|
|||
anchorPosition="downCenter"
|
||||
initialFocus={'[data-test-subj=optionsList-control-search-input]'}
|
||||
className="optionsList__popoverOverride"
|
||||
closePopover={() => dispatch(setPopoverOpen(false))}
|
||||
closePopover={() => optionsList.dispatch.setPopoverOpen(false)}
|
||||
anchorClassName="optionsList__anchorOverride"
|
||||
aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)}
|
||||
>
|
||||
|
|
|
@ -13,10 +13,12 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||
import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks';
|
||||
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||
import { OptionsListEmbeddableContext } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover';
|
||||
import { pluginServices } from '../../services';
|
||||
|
||||
describe('Options list popover', () => {
|
||||
const defaultProps = {
|
||||
|
@ -35,16 +37,16 @@ describe('Options list popover', () => {
|
|||
|
||||
async function mountComponent(options?: Partial<MountOptions>) {
|
||||
const compProps = { ...defaultProps, ...(options?.popoverProps ?? {}) };
|
||||
const mockReduxEmbeddableTools = await mockOptionsListReduxEmbeddableTools({
|
||||
const optionsListEmbeddable = await mockOptionsListEmbeddable({
|
||||
componentState: options?.componentState ?? {},
|
||||
explicitInput: options?.explicitInput ?? {},
|
||||
output: options?.output ?? {},
|
||||
} as Partial<OptionsListReduxState>);
|
||||
|
||||
return mountWithIntl(
|
||||
<mockReduxEmbeddableTools.Wrapper>
|
||||
<OptionsListEmbeddableContext.Provider value={optionsListEmbeddable}>
|
||||
<OptionsListPopover {...compProps} />
|
||||
</mockReduxEmbeddableTools.Wrapper>
|
||||
</OptionsListEmbeddableContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -290,6 +292,9 @@ describe('Options list popover', () => {
|
|||
});
|
||||
|
||||
test('ensure warning icon shows up when testAllowExpensiveQueries = false', async () => {
|
||||
pluginServices.getServices().optionsList.getAllowExpensiveQueries = jest.fn(() =>
|
||||
Promise.resolve(false)
|
||||
);
|
||||
const popover = await mountComponent({
|
||||
componentState: {
|
||||
field: { name: 'Test keyword field', type: 'keyword' } as FieldSpec,
|
||||
|
|
|
@ -9,16 +9,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { OptionsListReduxState } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { OptionsListPopoverTitle } from './options_list_popover_title';
|
||||
import { OptionsListPopoverFooter } from './options_list_popover_footer';
|
||||
import { OptionsListPopoverActionBar } from './options_list_popover_action_bar';
|
||||
import { OptionsListPopoverSuggestions } from './options_list_popover_suggestions';
|
||||
import { OptionsListPopoverInvalidSelections } from './options_list_popover_invalid_selections';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
|
||||
export interface OptionsListPopoverProps {
|
||||
width: number;
|
||||
|
@ -33,21 +30,16 @@ export const OptionsListPopover = ({
|
|||
updateSearchString,
|
||||
loadMoreSuggestions,
|
||||
}: OptionsListPopoverProps) => {
|
||||
// Redux embeddable container Context
|
||||
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<
|
||||
OptionsListReduxState,
|
||||
typeof optionsListReducers
|
||||
>();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
||||
const availableOptions = select((state) => state.componentState.availableOptions);
|
||||
const field = select((state) => state.componentState.field);
|
||||
const field = optionsList.select((state) => state.componentState.field);
|
||||
const availableOptions = optionsList.select((state) => state.componentState.availableOptions);
|
||||
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||
|
||||
const hideActionBar = select((state) => state.explicitInput.hideActionBar);
|
||||
const hideExclude = select((state) => state.explicitInput.hideExclude);
|
||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
||||
const id = select((state) => state.explicitInput.id);
|
||||
const id = optionsList.select((state) => state.explicitInput.id);
|
||||
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||
const hideExclude = optionsList.select((state) => state.explicitInput.hideExclude);
|
||||
const hideActionBar = optionsList.select((state) => state.explicitInput.hideActionBar);
|
||||
|
||||
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
||||
|
||||
|
|
|
@ -17,11 +17,9 @@ import {
|
|||
EuiToolTip,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { OptionsListReduxState } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListPopoverSortingButton } from './options_list_popover_sorting_button';
|
||||
|
||||
interface OptionsListPopoverProps {
|
||||
|
@ -35,20 +33,18 @@ export const OptionsListPopoverActionBar = ({
|
|||
updateSearchString,
|
||||
setShowOnlySelected,
|
||||
}: OptionsListPopoverProps) => {
|
||||
// Redux embeddable container Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { clearSelections },
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const allowExpensiveQueries = select((state) => state.componentState.allowExpensiveQueries);
|
||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
||||
const totalCardinality = select((state) => state.componentState.totalCardinality) ?? 0;
|
||||
const searchString = select((state) => state.componentState.searchString);
|
||||
const hideSort = select((state) => state.explicitInput.hideSort);
|
||||
const totalCardinality =
|
||||
optionsList.select((state) => state.componentState.totalCardinality) ?? 0;
|
||||
const searchString = optionsList.select((state) => state.componentState.searchString);
|
||||
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||
|
||||
const allowExpensiveQueries = optionsList.select(
|
||||
(state) => state.componentState.allowExpensiveQueries
|
||||
);
|
||||
|
||||
const hideSort = optionsList.select((state) => state.explicitInput.hideSort);
|
||||
|
||||
return (
|
||||
<div className="optionsList__actions">
|
||||
|
@ -142,7 +138,7 @@ export const OptionsListPopoverActionBar = ({
|
|||
size="xs"
|
||||
color="danger"
|
||||
iconType="eraser"
|
||||
onClick={() => dispatch(clearSelections({}))}
|
||||
onClick={() => optionsList.dispatch.clearSelections({})}
|
||||
data-test-subj="optionsList-control-clear-all-selections"
|
||||
aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()}
|
||||
/>
|
||||
|
|
|
@ -7,19 +7,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
useEuiBackgroundColor,
|
||||
useEuiPaddingSize,
|
||||
EuiPopoverFooter,
|
||||
EuiButtonGroup,
|
||||
EuiProgress,
|
||||
EuiButtonGroup,
|
||||
EuiPopoverFooter,
|
||||
useEuiPaddingSize,
|
||||
useEuiBackgroundColor,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { OptionsListReduxState } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
|
||||
const aggregationToggleButtons = [
|
||||
{
|
||||
|
@ -33,16 +32,9 @@ const aggregationToggleButtons = [
|
|||
];
|
||||
|
||||
export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean }) => {
|
||||
// Redux embeddable container Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setExclude },
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const exclude = select((state) => state.explicitInput.exclude);
|
||||
const exclude = optionsList.select((state) => state.explicitInput.exclude);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -71,7 +63,7 @@ export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean })
|
|||
options={aggregationToggleButtons}
|
||||
idSelected={exclude ? 'optionsList__excludeResults' : 'optionsList__includeResults'}
|
||||
onChange={(optionId) =>
|
||||
dispatch(setExclude(optionId === 'optionsList__excludeResults'))
|
||||
optionsList.dispatch.setExclude(optionId === 'optionsList__excludeResults')
|
||||
}
|
||||
buttonSize="compressed"
|
||||
data-test-subj="optionsList__includeExcludeButtonGroup"
|
||||
|
|
|
@ -15,24 +15,15 @@ import {
|
|||
EuiTitle,
|
||||
EuiScreenReaderOnly,
|
||||
} from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { OptionsListReduxState } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
|
||||
export const OptionsListPopoverInvalidSelections = () => {
|
||||
// Redux embeddable container Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { deselectOption },
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
||||
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||
|
||||
const [selectableOptions, setSelectableOptions] = useState<EuiSelectableOption[]>([]); // will be set in following useEffect
|
||||
useEffect(() => {
|
||||
|
@ -80,7 +71,7 @@ export const OptionsListPopoverInvalidSelections = () => {
|
|||
listProps={{ onFocusBadge: false, isVirtualized: false }}
|
||||
onChange={(newSuggestions, _, changedOption) => {
|
||||
setSelectableOptions(newSuggestions);
|
||||
dispatch(deselectOption(changedOption.label));
|
||||
optionsList.dispatch.deselectOption(changedOption.label);
|
||||
}}
|
||||
>
|
||||
{(list) => list}
|
||||
|
|
|
@ -21,16 +21,14 @@ import {
|
|||
Direction,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
getCompatibleSortingTypes,
|
||||
OPTIONS_LIST_DEFAULT_SORT,
|
||||
OptionsListSortBy,
|
||||
} from '../../../common/options_list/suggestions_sorting';
|
||||
import { OptionsListReduxState } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
|
||||
interface OptionsListSortingPopoverProps {
|
||||
showOnlySelected: boolean;
|
||||
|
@ -42,17 +40,10 @@ type SortByItem = EuiSelectableOption & {
|
|||
export const OptionsListPopoverSortingButton = ({
|
||||
showOnlySelected,
|
||||
}: OptionsListSortingPopoverProps) => {
|
||||
// Redux embeddable container Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setSort },
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const field = select((state) => state.componentState.field);
|
||||
const sort = select((state) => state.explicitInput.sort ?? OPTIONS_LIST_DEFAULT_SORT);
|
||||
const field = optionsList.select((state) => state.componentState.field);
|
||||
const sort = optionsList.select((state) => state.explicitInput.sort ?? OPTIONS_LIST_DEFAULT_SORT);
|
||||
|
||||
const [isSortingPopoverOpen, setIsSortingPopoverOpen] = useState(false);
|
||||
|
||||
|
@ -87,7 +78,7 @@ export const OptionsListPopoverSortingButton = ({
|
|||
setSortByOptions(updatedOptions);
|
||||
const selectedOption = updatedOptions.find(({ checked }) => checked === 'on');
|
||||
if (selectedOption) {
|
||||
dispatch(setSort({ by: selectedOption.data.sortBy }));
|
||||
optionsList.dispatch.setSort({ by: selectedOption.data.sortBy });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -135,7 +126,9 @@ export const OptionsListPopoverSortingButton = ({
|
|||
options={sortOrderOptions}
|
||||
idSelected={sort.direction}
|
||||
legend={OptionsListStrings.editorAndPopover.getSortDirectionLegend()}
|
||||
onChange={(value) => dispatch(setSort({ direction: value as Direction }))}
|
||||
onChange={(value) =>
|
||||
optionsList.dispatch.setSort({ direction: value as Direction })
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -10,12 +10,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiSelectable } from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
|
||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListPopoverEmptyMessage } from './options_list_popover_empty_message';
|
||||
import { OptionsListPopoverSuggestionBadge } from './options_list_popover_suggestion_badge';
|
||||
|
||||
|
@ -28,27 +27,21 @@ export const OptionsListPopoverSuggestions = ({
|
|||
showOnlySelected,
|
||||
loadMoreSuggestions,
|
||||
}: OptionsListPopoverSuggestionsProps) => {
|
||||
// Redux embeddable container Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { replaceSelection, deselectOption, selectOption, selectExists },
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
||||
const availableOptions = select((state) => state.componentState.availableOptions);
|
||||
const totalCardinality = select((state) => state.componentState.totalCardinality);
|
||||
const searchString = select((state) => state.componentState.searchString);
|
||||
const searchString = optionsList.select((state) => state.componentState.searchString);
|
||||
const availableOptions = optionsList.select((state) => state.componentState.availableOptions);
|
||||
const totalCardinality = optionsList.select((state) => state.componentState.totalCardinality);
|
||||
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||
|
||||
const selectedOptions = select((state) => state.explicitInput.selectedOptions);
|
||||
const existsSelected = select((state) => state.explicitInput.existsSelected);
|
||||
const singleSelect = select((state) => state.explicitInput.singleSelect);
|
||||
const hideExists = select((state) => state.explicitInput.hideExists);
|
||||
const isLoading = select((state) => state.output.loading) ?? false;
|
||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
||||
const sort = select((state) => state.explicitInput.sort);
|
||||
const sort = optionsList.select((state) => state.explicitInput.sort);
|
||||
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||
const hideExists = optionsList.select((state) => state.explicitInput.hideExists);
|
||||
const singleSelect = optionsList.select((state) => state.explicitInput.singleSelect);
|
||||
const existsSelected = optionsList.select((state) => state.explicitInput.existsSelected);
|
||||
const selectedOptions = optionsList.select((state) => state.explicitInput.selectedOptions);
|
||||
|
||||
const isLoading = optionsList.select((state) => state.output.loading) ?? false;
|
||||
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -173,13 +166,13 @@ export const OptionsListPopoverSuggestions = ({
|
|||
setSelectableOptions(newSuggestions);
|
||||
// the order of these checks matters, so be careful if rearranging them
|
||||
if (key === 'exists-option') {
|
||||
dispatch(selectExists(!Boolean(existsSelected)));
|
||||
optionsList.dispatch.selectExists(!Boolean(existsSelected));
|
||||
} else if (showOnlySelected || selectedOptionsSet.has(key)) {
|
||||
dispatch(deselectOption(key));
|
||||
optionsList.dispatch.deselectOption(key);
|
||||
} else if (singleSelect) {
|
||||
dispatch(replaceSelection(key));
|
||||
optionsList.dispatch.replaceSelection(key);
|
||||
} else {
|
||||
dispatch(selectOption(key));
|
||||
optionsList.dispatch.selectOption(key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -9,22 +9,17 @@
|
|||
import React from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiIconTip } from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { OptionsListReduxState } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
|
||||
export const OptionsListPopoverTitle = () => {
|
||||
// Redux embeddable container Context
|
||||
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<
|
||||
OptionsListReduxState,
|
||||
typeof optionsListReducers
|
||||
>();
|
||||
const optionsList = useOptionsList();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const allowExpensiveQueries = select((state) => state.componentState.allowExpensiveQueries);
|
||||
const title = select((state) => state.explicitInput.title);
|
||||
const allowExpensiveQueries = optionsList.select(
|
||||
(state) => state.componentState.allowExpensiveQueries
|
||||
);
|
||||
const title = optionsList.select((state) => state.explicitInput.title);
|
||||
|
||||
return (
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
|
|
|
@ -6,15 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { batch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { isEmpty, isEqual } from 'lodash';
|
||||
import { merge, Subject, Subscription } from 'rxjs';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { debounceTime, map, distinctUntilChanged, skip } from 'rxjs/operators';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
Filter,
|
||||
compareFilters,
|
||||
|
@ -23,23 +22,24 @@ import {
|
|||
COMPARE_ALL_OPTIONS,
|
||||
buildExistsFilter,
|
||||
} from '@kbn/es-query';
|
||||
import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataView, FieldSpec } from '@kbn/data-views-plugin/public';
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { MIN_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
||||
import { pluginServices } from '../../services';
|
||||
import {
|
||||
ControlInput,
|
||||
ControlOutput,
|
||||
OptionsListEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
OptionsListEmbeddableInput,
|
||||
} from '../..';
|
||||
import { getDefaultComponentState, optionsListReducers } from '../options_list_reducers';
|
||||
import { pluginServices } from '../../services';
|
||||
import { MIN_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
||||
import { OptionsListControl } from '../components/options_list_control';
|
||||
import { ControlsDataViewsService } from '../../services/data_views/types';
|
||||
import { ControlsOptionsListService } from '../../services/options_list/types';
|
||||
import { getDefaultComponentState, optionsListReducers } from '../options_list_reducers';
|
||||
|
||||
const diffDataFetchProps = (
|
||||
last?: OptionsListDataFetchProps,
|
||||
|
@ -62,6 +62,20 @@ interface OptionsListDataFetchProps {
|
|||
filters?: ControlInput['filters'];
|
||||
}
|
||||
|
||||
export const OptionsListEmbeddableContext = createContext<OptionsListEmbeddable | null>(null);
|
||||
export const useOptionsList = (): OptionsListEmbeddable => {
|
||||
const optionsList = useContext<OptionsListEmbeddable | null>(OptionsListEmbeddableContext);
|
||||
if (optionsList == null) {
|
||||
throw new Error('useOptionsList must be used inside OptionsListEmbeddableContext.');
|
||||
}
|
||||
return optionsList!;
|
||||
};
|
||||
|
||||
type OptionsListReduxEmbeddableTools = ReduxEmbeddableTools<
|
||||
OptionsListReduxState,
|
||||
typeof optionsListReducers
|
||||
>;
|
||||
|
||||
export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput, ControlOutput> {
|
||||
public readonly type = OPTIONS_LIST_CONTROL;
|
||||
public deferEmbeddableLoad = true;
|
||||
|
@ -80,13 +94,16 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
private dataView?: DataView;
|
||||
private field?: FieldSpec;
|
||||
|
||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
||||
OptionsListReduxState,
|
||||
typeof optionsListReducers
|
||||
>;
|
||||
// state management
|
||||
public select: OptionsListReduxEmbeddableTools['select'];
|
||||
public getState: OptionsListReduxEmbeddableTools['getState'];
|
||||
public dispatch: OptionsListReduxEmbeddableTools['dispatch'];
|
||||
public onStateChange: OptionsListReduxEmbeddableTools['onStateChange'];
|
||||
|
||||
private cleanupStateTools: () => void;
|
||||
|
||||
constructor(
|
||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
input: OptionsListEmbeddableInput,
|
||||
output: ControlOutput,
|
||||
parent?: IContainer
|
||||
|
@ -101,7 +118,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
this.loadMoreSubject = new Subject<number>();
|
||||
|
||||
// build redux embeddable tools
|
||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
||||
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||
OptionsListReduxState,
|
||||
typeof optionsListReducers
|
||||
>({
|
||||
|
@ -110,6 +127,12 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
initialComponentState: getDefaultComponentState(),
|
||||
});
|
||||
|
||||
this.select = reduxEmbeddableTools.select;
|
||||
this.getState = reduxEmbeddableTools.getState;
|
||||
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
|
@ -117,11 +140,9 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
const { selectedOptions: initialSelectedOptions } = this.getInput();
|
||||
if (!initialSelectedOptions) this.setInitializationFinished();
|
||||
|
||||
const {
|
||||
actions: { setAllowExpensiveQueries },
|
||||
dispatch,
|
||||
} = this.reduxEmbeddableTools;
|
||||
dispatch(setAllowExpensiveQueries(await this.optionsListService.getAllowExpensiveQueries()));
|
||||
this.dispatch.setAllowExpensiveQueries(
|
||||
await this.optionsListService.getAllowExpensiveQueries()
|
||||
);
|
||||
|
||||
this.runOptionsListQuery().then(async () => {
|
||||
if (initialSelectedOptions) {
|
||||
|
@ -183,19 +204,10 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
)
|
||||
)
|
||||
.subscribe(async ({ selectedOptions: newSelectedOptions }) => {
|
||||
const {
|
||||
actions: {
|
||||
clearValidAndInvalidSelections,
|
||||
setValidAndInvalidSelections,
|
||||
publishFilters,
|
||||
},
|
||||
dispatch,
|
||||
} = this.reduxEmbeddableTools;
|
||||
|
||||
if (!newSelectedOptions || isEmpty(newSelectedOptions)) {
|
||||
dispatch(clearValidAndInvalidSelections({}));
|
||||
this.dispatch.clearValidAndInvalidSelections({});
|
||||
} else {
|
||||
const { invalidSelections } = this.reduxEmbeddableTools.getState().componentState ?? {};
|
||||
const { invalidSelections } = this.getState().componentState ?? {};
|
||||
const newValidSelections: string[] = [];
|
||||
const newInvalidSelections: string[] = [];
|
||||
for (const selectedOption of newSelectedOptions) {
|
||||
|
@ -205,15 +217,13 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
}
|
||||
newValidSelections.push(selectedOption);
|
||||
}
|
||||
dispatch(
|
||||
setValidAndInvalidSelections({
|
||||
validSelections: newValidSelections,
|
||||
invalidSelections: newInvalidSelections,
|
||||
})
|
||||
);
|
||||
this.dispatch.setValidAndInvalidSelections({
|
||||
validSelections: newValidSelections,
|
||||
invalidSelections: newInvalidSelections,
|
||||
});
|
||||
}
|
||||
const newFilters = await this.buildFilter();
|
||||
dispatch(publishFilters(newFilters));
|
||||
this.dispatch.publishFilters(newFilters);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -222,15 +232,9 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
dataView?: DataView;
|
||||
field?: FieldSpec;
|
||||
}> => {
|
||||
const {
|
||||
dispatch,
|
||||
getState,
|
||||
actions: { setField, setDataViewId },
|
||||
} = this.reduxEmbeddableTools;
|
||||
|
||||
const {
|
||||
explicitInput: { dataViewId, fieldName },
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
|
||||
if (!this.dataView || this.dataView.id !== dataViewId) {
|
||||
try {
|
||||
|
@ -246,7 +250,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
this.onFatalError(e);
|
||||
}
|
||||
|
||||
dispatch(setDataViewId(this.dataView?.id));
|
||||
this.dispatch.setDataViewId(this.dataView?.id);
|
||||
}
|
||||
|
||||
if (this.dataView && (!this.field || this.field.name !== fieldName)) {
|
||||
|
@ -265,31 +269,26 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
} catch (e) {
|
||||
this.onFatalError(e);
|
||||
}
|
||||
dispatch(setField(this.field));
|
||||
this.dispatch.setField(this.field);
|
||||
}
|
||||
|
||||
return { dataView: this.dataView, field: this.field! };
|
||||
};
|
||||
|
||||
private runOptionsListQuery = async (size: number = MIN_OPTIONS_LIST_REQUEST_SIZE) => {
|
||||
const {
|
||||
dispatch,
|
||||
getState,
|
||||
actions: { setLoading, publishFilters, setSearchString, updateQueryResults },
|
||||
} = this.reduxEmbeddableTools;
|
||||
const previousFieldName = this.field?.name;
|
||||
const { dataView, field } = await this.getCurrentDataViewAndField();
|
||||
if (!dataView || !field) return;
|
||||
|
||||
if (previousFieldName && field.name !== previousFieldName) {
|
||||
dispatch(setSearchString(''));
|
||||
this.dispatch.setSearchString('');
|
||||
}
|
||||
|
||||
const {
|
||||
componentState: { searchString, allowExpensiveQueries },
|
||||
explicitInput: { selectedOptions, runPastTimeout, existsSelected, sort },
|
||||
} = getState();
|
||||
dispatch(setLoading(true));
|
||||
} = this.getState();
|
||||
this.dispatch.setLoading(true);
|
||||
if (searchString.valid) {
|
||||
// need to get filters, query, ignoreParentSettings, and timeRange from input for inheritance
|
||||
const {
|
||||
|
@ -342,14 +341,12 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
isEmpty(invalidSelections) ||
|
||||
ignoreParentSettings?.ignoreValidations
|
||||
) {
|
||||
dispatch(
|
||||
updateQueryResults({
|
||||
availableOptions: suggestions,
|
||||
invalidSelections: undefined,
|
||||
validSelections: selectedOptions,
|
||||
totalCardinality,
|
||||
})
|
||||
);
|
||||
this.dispatch.updateQueryResults({
|
||||
availableOptions: suggestions,
|
||||
invalidSelections: undefined,
|
||||
validSelections: selectedOptions,
|
||||
totalCardinality,
|
||||
});
|
||||
} else {
|
||||
const valid: string[] = [];
|
||||
const invalid: string[] = [];
|
||||
|
@ -357,38 +354,33 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
if (invalidSelections?.includes(selectedOption)) invalid.push(selectedOption);
|
||||
else valid.push(selectedOption);
|
||||
}
|
||||
dispatch(
|
||||
updateQueryResults({
|
||||
availableOptions: suggestions,
|
||||
invalidSelections: invalid,
|
||||
validSelections: valid,
|
||||
totalCardinality,
|
||||
})
|
||||
);
|
||||
this.dispatch.updateQueryResults({
|
||||
availableOptions: suggestions,
|
||||
invalidSelections: invalid,
|
||||
validSelections: valid,
|
||||
totalCardinality,
|
||||
});
|
||||
}
|
||||
|
||||
// publish filter
|
||||
const newFilters = await this.buildFilter();
|
||||
batch(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(publishFilters(newFilters));
|
||||
this.dispatch.setLoading(false);
|
||||
this.dispatch.publishFilters(newFilters);
|
||||
});
|
||||
} else {
|
||||
batch(() => {
|
||||
dispatch(
|
||||
updateQueryResults({
|
||||
availableOptions: {},
|
||||
})
|
||||
);
|
||||
dispatch(setLoading(false));
|
||||
this.dispatch.updateQueryResults({
|
||||
availableOptions: {},
|
||||
});
|
||||
this.dispatch.setLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private buildFilter = async () => {
|
||||
const { getState } = this.reduxEmbeddableTools;
|
||||
const { validSelections } = getState().componentState ?? {};
|
||||
const { existsSelected } = getState().explicitInput ?? {};
|
||||
const { validSelections } = this.getState().componentState ?? {};
|
||||
const { existsSelected } = this.getState().explicitInput ?? {};
|
||||
const { exclude } = this.getInput();
|
||||
|
||||
if ((!validSelections || isEmpty(validSelections)) && !existsSelected) {
|
||||
|
@ -421,22 +413,18 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
};
|
||||
|
||||
public onFatalError = (e: Error) => {
|
||||
const {
|
||||
dispatch,
|
||||
actions: { setPopoverOpen, setLoading },
|
||||
} = this.reduxEmbeddableTools;
|
||||
batch(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(setPopoverOpen(false));
|
||||
this.dispatch.setLoading(false);
|
||||
this.dispatch.setPopoverOpen(false);
|
||||
});
|
||||
super.onFatalError(e);
|
||||
};
|
||||
|
||||
public destroy = () => {
|
||||
super.destroy();
|
||||
this.cleanupStateTools();
|
||||
this.abortController?.abort();
|
||||
this.subscriptions.unsubscribe();
|
||||
this.reduxEmbeddableTools.cleanup();
|
||||
if (this.node) ReactDOM.unmountComponentAtNode(this.node);
|
||||
};
|
||||
|
||||
|
@ -444,16 +432,15 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
const { Wrapper: OptionsListReduxWrapper } = this.reduxEmbeddableTools;
|
||||
this.node = node;
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||
<OptionsListReduxWrapper>
|
||||
<OptionsListEmbeddableContext.Provider value={this}>
|
||||
<OptionsListControl
|
||||
typeaheadSubject={this.typeaheadSubject}
|
||||
loadMoreSubject={this.loadMoreSubject}
|
||||
/>
|
||||
</OptionsListReduxWrapper>
|
||||
</OptionsListEmbeddableContext.Provider>
|
||||
</KibanaThemeProvider>,
|
||||
node
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ import deepEqual from 'fast-deep-equal';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
|
@ -33,7 +33,7 @@ export class OptionsListEmbeddableFactory
|
|||
constructor() {}
|
||||
|
||||
public async create(initialInput: OptionsListEmbeddableInput, parent?: IContainer) {
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||
const { OptionsListEmbeddable } = await import('./options_list_embeddable');
|
||||
return Promise.resolve(
|
||||
new OptionsListEmbeddable(reduxEmbeddablePackage, initialInput, {}, parent)
|
||||
|
|
|
@ -16,10 +16,8 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { rangeSliderReducers } from '../range_slider_reducers';
|
||||
import { RangeSliderReduxState } from '../types';
|
||||
import { useRangeSlider } from '../embeddable/range_slider_embeddable';
|
||||
import { RangeSliderPopover, EuiDualRangeRef } from './range_slider_popover';
|
||||
|
||||
import './range_slider.scss';
|
||||
|
@ -30,21 +28,14 @@ export const RangeSliderControl: FC = () => {
|
|||
const rangeRef = useRef<EuiDualRangeRef>(null);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
// Controls Services Context
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setSelectedRange },
|
||||
} = useReduxEmbeddableContext<RangeSliderReduxState, typeof rangeSliderReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const rangeSlider = useRangeSlider();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const min = select((state) => state.componentState.min);
|
||||
const max = select((state) => state.componentState.max);
|
||||
const isInvalid = select((state) => state.componentState.isInvalid);
|
||||
const id = select((state) => state.explicitInput.id);
|
||||
const value = select((state) => state.explicitInput.value) ?? ['', ''];
|
||||
const isLoading = select((state) => state.output.loading);
|
||||
const min = rangeSlider.select((state) => state.componentState.min);
|
||||
const max = rangeSlider.select((state) => state.componentState.max);
|
||||
const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid);
|
||||
const id = rangeSlider.select((state) => state.explicitInput.id);
|
||||
const value = rangeSlider.select((state) => state.explicitInput.value) ?? ['', ''];
|
||||
const isLoading = rangeSlider.select((state) => state.output.loading);
|
||||
|
||||
const hasAvailableRange = min !== '' && max !== '';
|
||||
|
||||
|
@ -76,12 +67,10 @@ export const RangeSliderControl: FC = () => {
|
|||
}`}
|
||||
value={hasLowerBoundSelection ? lowerBoundValue : ''}
|
||||
onChange={(event) => {
|
||||
dispatch(
|
||||
setSelectedRange([
|
||||
event.target.value,
|
||||
isNaN(upperBoundValue) ? '' : String(upperBoundValue),
|
||||
])
|
||||
);
|
||||
rangeSlider.dispatch.setSelectedRange([
|
||||
event.target.value,
|
||||
isNaN(upperBoundValue) ? '' : String(upperBoundValue),
|
||||
]);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
placeholder={`${hasAvailableRange ? roundedMin : ''}`}
|
||||
|
@ -103,12 +92,10 @@ export const RangeSliderControl: FC = () => {
|
|||
}`}
|
||||
value={hasUpperBoundSelection ? upperBoundValue : ''}
|
||||
onChange={(event) => {
|
||||
dispatch(
|
||||
setSelectedRange([
|
||||
isNaN(lowerBoundValue) ? '' : String(lowerBoundValue),
|
||||
event.target.value,
|
||||
])
|
||||
);
|
||||
rangeSlider.dispatch.setSelectedRange([
|
||||
isNaN(lowerBoundValue) ? '' : String(lowerBoundValue),
|
||||
event.target.value,
|
||||
]);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
placeholder={`${hasAvailableRange ? roundedMax : ''}`}
|
||||
|
|
|
@ -19,13 +19,11 @@ import {
|
|||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { RangeValue } from '../../../common/range_slider/types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { rangeSliderReducers } from '../range_slider_reducers';
|
||||
import { RangeSliderReduxState } from '../types';
|
||||
import { RangeSliderStrings } from './range_slider_strings';
|
||||
import { RangeValue } from '../../../common/range_slider/types';
|
||||
import { useRangeSlider } from '../embeddable/range_slider_embeddable';
|
||||
|
||||
// Unfortunately, wrapping EuiDualRange in `withEuiTheme` has created this annoying/verbose typing
|
||||
export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps<typeof EuiDualRange>;
|
||||
|
@ -37,23 +35,17 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
|||
const {
|
||||
dataViews: { get: getDataViewById },
|
||||
} = pluginServices.getServices();
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setSelectedRange },
|
||||
} = useReduxEmbeddableContext<RangeSliderReduxState, typeof rangeSliderReducers>();
|
||||
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const rangeSlider = useRangeSlider();
|
||||
|
||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||
const dataViewId = select((state) => state.output.dataViewId);
|
||||
const fieldSpec = select((state) => state.componentState.field);
|
||||
const id = select((state) => state.explicitInput.id);
|
||||
const isInvalid = select((state) => state.componentState.isInvalid);
|
||||
const max = select((state) => state.componentState.max);
|
||||
const min = select((state) => state.componentState.min);
|
||||
const title = select((state) => state.explicitInput.title);
|
||||
const value = select((state) => state.explicitInput.value) ?? ['', ''];
|
||||
const dataViewId = rangeSlider.select((state) => state.output.dataViewId);
|
||||
const fieldSpec = rangeSlider.select((state) => state.componentState.field);
|
||||
const id = rangeSlider.select((state) => state.explicitInput.id);
|
||||
const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid);
|
||||
const max = rangeSlider.select((state) => state.componentState.max);
|
||||
const min = rangeSlider.select((state) => state.componentState.min);
|
||||
const title = rangeSlider.select((state) => state.explicitInput.title);
|
||||
const value = rangeSlider.select((state) => state.explicitInput.value) ?? ['', ''];
|
||||
|
||||
const hasAvailableRange = min !== '' && max !== '';
|
||||
const hasLowerBoundSelection = value[0] !== '';
|
||||
|
@ -154,7 +146,7 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
|||
const updatedUpperBound =
|
||||
typeof newUpperBound === 'number' ? String(newUpperBound) : value[1];
|
||||
|
||||
dispatch(setSelectedRange([updatedLowerBound, updatedUpperBound]));
|
||||
rangeSlider.dispatch.setSelectedRange([updatedLowerBound, updatedUpperBound]);
|
||||
}}
|
||||
value={displayedValue}
|
||||
ticks={hasAvailableRange ? ticks : undefined}
|
||||
|
@ -179,7 +171,7 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
|||
iconType="eraser"
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
dispatch(setSelectedRange(['', '']));
|
||||
rangeSlider.dispatch.setSelectedRange(['', '']);
|
||||
}}
|
||||
aria-label={RangeSliderStrings.popover.getClearRangeButtonTitle()}
|
||||
data-test-subj="rangeSlider__clearRangeButton"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { batch } from 'react-redux';
|
||||
|
@ -15,8 +15,6 @@ import deepEqual from 'fast-deep-equal';
|
|||
import { Subscription, lastValueFrom } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, skip, map } from 'rxjs/operators';
|
||||
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
compareFilters,
|
||||
buildRangeFilter,
|
||||
|
@ -27,7 +25,9 @@ import {
|
|||
} from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
ControlInput,
|
||||
|
@ -36,11 +36,11 @@ import {
|
|||
RANGE_SLIDER_CONTROL,
|
||||
} from '../..';
|
||||
import { pluginServices } from '../../services';
|
||||
import { RangeSliderControl } from '../components/range_slider_control';
|
||||
import { getDefaultComponentState, rangeSliderReducers } from '../range_slider_reducers';
|
||||
import { RangeSliderReduxState } from '../types';
|
||||
import { ControlsDataService } from '../../services/data/types';
|
||||
import { RangeSliderControl } from '../components/range_slider_control';
|
||||
import { ControlsDataViewsService } from '../../services/data_views/types';
|
||||
import { getDefaultComponentState, rangeSliderReducers } from '../range_slider_reducers';
|
||||
|
||||
const diffDataFetchProps = (
|
||||
current?: RangeSliderDataFetchProps,
|
||||
|
@ -65,6 +65,20 @@ interface RangeSliderDataFetchProps {
|
|||
const fieldMissingError = (fieldName: string) =>
|
||||
new Error(`field ${fieldName} not found in index pattern`);
|
||||
|
||||
export const RangeSliderControlContext = createContext<RangeSliderEmbeddable | null>(null);
|
||||
export const useRangeSlider = (): RangeSliderEmbeddable => {
|
||||
const rangeSlider = useContext<RangeSliderEmbeddable | null>(RangeSliderControlContext);
|
||||
if (rangeSlider == null) {
|
||||
throw new Error('useRangeSlider must be used inside RangeSliderControlContext.');
|
||||
}
|
||||
return rangeSlider!;
|
||||
};
|
||||
|
||||
type RangeSliderReduxEmbeddableTools = ReduxEmbeddableTools<
|
||||
RangeSliderReduxState,
|
||||
typeof rangeSliderReducers
|
||||
>;
|
||||
|
||||
export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput, ControlOutput> {
|
||||
public readonly type = RANGE_SLIDER_CONTROL;
|
||||
public deferEmbeddableLoad = true;
|
||||
|
@ -80,13 +94,16 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
private dataView?: DataView;
|
||||
private field?: DataViewField;
|
||||
|
||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
||||
RangeSliderReduxState,
|
||||
typeof rangeSliderReducers
|
||||
>;
|
||||
// state management
|
||||
public select: RangeSliderReduxEmbeddableTools['select'];
|
||||
public getState: RangeSliderReduxEmbeddableTools['getState'];
|
||||
public dispatch: RangeSliderReduxEmbeddableTools['dispatch'];
|
||||
public onStateChange: RangeSliderReduxEmbeddableTools['onStateChange'];
|
||||
|
||||
private cleanupStateTools: () => void;
|
||||
|
||||
constructor(
|
||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
input: RangeSliderEmbeddableInput,
|
||||
output: ControlOutput,
|
||||
parent?: IContainer
|
||||
|
@ -96,7 +113,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
// Destructure controls services
|
||||
({ data: this.dataService, dataViews: this.dataViewsService } = pluginServices.getServices());
|
||||
|
||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
||||
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||
RangeSliderReduxState,
|
||||
typeof rangeSliderReducers
|
||||
>({
|
||||
|
@ -104,6 +121,11 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
reducers: rangeSliderReducers,
|
||||
initialComponentState: getDefaultComponentState(),
|
||||
});
|
||||
this.select = reduxEmbeddableTools.select;
|
||||
this.getState = reduxEmbeddableTools.getState;
|
||||
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
@ -157,14 +179,9 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
dataView?: DataView;
|
||||
field?: DataViewField;
|
||||
}> => {
|
||||
const {
|
||||
getState,
|
||||
dispatch,
|
||||
actions: { setField, setDataViewId },
|
||||
} = this.reduxEmbeddableTools;
|
||||
const {
|
||||
explicitInput: { dataViewId, fieldName },
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
|
||||
if (!this.dataView || this.dataView.id !== dataViewId) {
|
||||
try {
|
||||
|
@ -179,7 +196,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
);
|
||||
}
|
||||
|
||||
dispatch(setDataViewId(this.dataView.id));
|
||||
this.dispatch.setDataViewId(this.dataView.id);
|
||||
} catch (e) {
|
||||
this.onFatalError(e);
|
||||
}
|
||||
|
@ -198,19 +215,14 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
);
|
||||
}
|
||||
|
||||
dispatch(setField(this.field?.toSpec()));
|
||||
this.dispatch.setField(this.field?.toSpec());
|
||||
}
|
||||
|
||||
return { dataView: this.dataView, field: this.field! };
|
||||
};
|
||||
|
||||
private runRangeSliderQuery = async () => {
|
||||
const {
|
||||
dispatch,
|
||||
actions: { setLoading, publishFilters, setMinMax },
|
||||
} = this.reduxEmbeddableTools;
|
||||
|
||||
dispatch(setLoading(true));
|
||||
this.dispatch.setLoading(true);
|
||||
const { dataView, field } = await this.getCurrentDataViewAndField();
|
||||
if (!dataView || !field) return;
|
||||
|
||||
|
@ -226,8 +238,8 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
|
||||
if (!field) {
|
||||
batch(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(publishFilters([]));
|
||||
this.dispatch.setLoading(false);
|
||||
this.dispatch.publishFilters([]);
|
||||
});
|
||||
throw fieldMissingError(fieldName);
|
||||
}
|
||||
|
@ -258,12 +270,10 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
query,
|
||||
});
|
||||
|
||||
dispatch(
|
||||
setMinMax({
|
||||
min: `${min ?? ''}`,
|
||||
max: `${max ?? ''}`,
|
||||
})
|
||||
);
|
||||
this.dispatch.setMinMax({
|
||||
min: `${min ?? ''}`,
|
||||
max: `${max ?? ''}`,
|
||||
});
|
||||
|
||||
// build filter with new min/max
|
||||
await this.buildFilter();
|
||||
|
@ -323,11 +333,6 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
};
|
||||
|
||||
private buildFilter = async () => {
|
||||
const {
|
||||
dispatch,
|
||||
getState,
|
||||
actions: { setLoading, setIsInvalid, setDataViewId, publishFilters },
|
||||
} = this.reduxEmbeddableTools;
|
||||
const {
|
||||
componentState: { min: availableMin, max: availableMax },
|
||||
explicitInput: {
|
||||
|
@ -337,7 +342,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
ignoreParentSettings,
|
||||
value: [selectedMin, selectedMax] = ['', ''],
|
||||
},
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
|
||||
const hasData = !isEmpty(availableMin) && !isEmpty(availableMax);
|
||||
const hasLowerSelection = !isEmpty(selectedMin);
|
||||
|
@ -349,10 +354,10 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
|
||||
if (!hasData || !hasEitherSelection) {
|
||||
batch(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(setIsInvalid(!ignoreParentSettings?.ignoreValidations && hasEitherSelection));
|
||||
dispatch(setDataViewId(dataView.id));
|
||||
dispatch(publishFilters([]));
|
||||
this.dispatch.setLoading(false);
|
||||
this.dispatch.setIsInvalid(!ignoreParentSettings?.ignoreValidations && hasEitherSelection);
|
||||
this.dispatch.setDataViewId(dataView.id);
|
||||
this.dispatch.publishFilters([]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -404,20 +409,20 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
|
||||
if (!docCount) {
|
||||
batch(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(setIsInvalid(true));
|
||||
dispatch(setDataViewId(dataView.id));
|
||||
dispatch(publishFilters([]));
|
||||
this.dispatch.setLoading(false);
|
||||
this.dispatch.setIsInvalid(true);
|
||||
this.dispatch.setDataViewId(dataView.id);
|
||||
this.dispatch.publishFilters([]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(setIsInvalid(false));
|
||||
dispatch(setDataViewId(dataView.id));
|
||||
dispatch(publishFilters([rangeFilter]));
|
||||
this.dispatch.setLoading(false);
|
||||
this.dispatch.setIsInvalid(false);
|
||||
this.dispatch.setDataViewId(dataView.id);
|
||||
this.dispatch.publishFilters([rangeFilter]);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -427,23 +432,22 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
|||
|
||||
public destroy = () => {
|
||||
super.destroy();
|
||||
this.cleanupStateTools();
|
||||
this.subscriptions.unsubscribe();
|
||||
this.reduxEmbeddableTools.cleanup();
|
||||
};
|
||||
|
||||
public render = (node: HTMLElement) => {
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
const { Wrapper: RangeSliderReduxWrapper } = this.reduxEmbeddableTools;
|
||||
this.node = node;
|
||||
const ControlsServicesProvider = pluginServices.getContextProvider();
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||
<ControlsServicesProvider>
|
||||
<RangeSliderReduxWrapper>
|
||||
<RangeSliderControlContext.Provider value={this}>
|
||||
<RangeSliderControl />
|
||||
</RangeSliderReduxWrapper>
|
||||
</RangeSliderControlContext.Provider>
|
||||
</ControlsServicesProvider>
|
||||
</KibanaThemeProvider>,
|
||||
node
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
createRangeSliderExtract,
|
||||
|
@ -45,7 +45,7 @@ export class RangeSliderEmbeddableFactory
|
|||
public isEditable = () => Promise.resolve(true);
|
||||
|
||||
public async create(initialInput: RangeSliderEmbeddableInput, parent?: IContainer) {
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||
const { RangeSliderEmbeddable } = await import('./range_slider_embeddable');
|
||||
|
||||
return Promise.resolve(
|
||||
|
|
|
@ -8,14 +8,12 @@
|
|||
|
||||
import React, { FC, useRef } from 'react';
|
||||
import { EuiInputPopover } from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { timeSliderReducers } from '../time_slider_reducers';
|
||||
import { TimeSliderReduxState } from '../types';
|
||||
import { FROM_INDEX, TO_INDEX } from '../time_utils';
|
||||
import { EuiDualRangeRef } from './time_slider_sliding_window_range';
|
||||
import { getRoundedTimeRangeBounds } from '../time_slider_selectors';
|
||||
import { useTimeSlider } from '../embeddable/time_slider_embeddable';
|
||||
import { TimeSliderPopoverButton } from './time_slider_popover_button';
|
||||
import { TimeSliderPopoverContent } from './time_slider_popover_content';
|
||||
import { EuiDualRangeRef } from './time_slider_sliding_window_range';
|
||||
import { FROM_INDEX, TO_INDEX } from '../time_utils';
|
||||
import { getRoundedTimeRangeBounds } from '../time_slider_selectors';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
@ -25,25 +23,21 @@ interface Props {
|
|||
}
|
||||
|
||||
export const TimeSlider: FC<Props> = (props: Props) => {
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions,
|
||||
} = useReduxEmbeddableContext<TimeSliderReduxState, typeof timeSliderReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const stepSize = select((state) => {
|
||||
const timeSlider = useTimeSlider();
|
||||
|
||||
const stepSize = timeSlider.select((state) => {
|
||||
return state.componentState.stepSize;
|
||||
});
|
||||
const ticks = select((state) => {
|
||||
const ticks = timeSlider.select((state) => {
|
||||
return state.componentState.ticks;
|
||||
});
|
||||
const timeRangeBounds = select(getRoundedTimeRangeBounds);
|
||||
const timeRangeBounds = timeSlider.select(getRoundedTimeRangeBounds);
|
||||
const timeRangeMin = timeRangeBounds[FROM_INDEX];
|
||||
const timeRangeMax = timeRangeBounds[TO_INDEX];
|
||||
const value = select((state) => {
|
||||
const value = timeSlider.select((state) => {
|
||||
return state.componentState.value;
|
||||
});
|
||||
const isOpen = select((state) => {
|
||||
const isOpen = timeSlider.select((state) => {
|
||||
return state.componentState.isOpen;
|
||||
});
|
||||
|
||||
|
@ -64,7 +58,7 @@ export const TimeSlider: FC<Props> = (props: Props) => {
|
|||
input={
|
||||
<TimeSliderPopoverButton
|
||||
onClick={() => {
|
||||
dispatch(actions.setIsOpen({ isOpen: !isOpen }));
|
||||
timeSlider.dispatch.setIsOpen({ isOpen: !isOpen });
|
||||
}}
|
||||
formatDate={props.formatDate}
|
||||
from={from}
|
||||
|
@ -72,7 +66,7 @@ export const TimeSlider: FC<Props> = (props: Props) => {
|
|||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => dispatch(actions.setIsOpen({ isOpen: false }))}
|
||||
closePopover={() => timeSlider.dispatch.setIsOpen({ isOpen: false })}
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downCenter"
|
||||
disableFocusTrap
|
||||
|
|
|
@ -8,13 +8,12 @@
|
|||
|
||||
import React, { Ref } from 'react';
|
||||
import { EuiButtonIcon, EuiRangeTick, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { TimeSliderAnchoredRange } from './time_slider_anchored_range';
|
||||
import { EuiDualRangeRef, TimeSliderSlidingWindowRange } from './time_slider_sliding_window_range';
|
||||
import { timeSliderReducers } from '../time_slider_reducers';
|
||||
import { TimeSliderReduxState } from '../types';
|
||||
|
||||
import { getIsAnchored } from '../time_slider_selectors';
|
||||
import { TimeSliderStrings } from './time_slider_strings';
|
||||
import { useTimeSlider } from '../embeddable/time_slider_embeddable';
|
||||
import { TimeSliderAnchoredRange } from './time_slider_anchored_range';
|
||||
import { EuiDualRangeRef, TimeSliderSlidingWindowRange } from './time_slider_sliding_window_range';
|
||||
|
||||
interface Props {
|
||||
value: [number, number];
|
||||
|
@ -41,13 +40,8 @@ export function TimeSliderPopoverContent(props: Props) {
|
|||
};
|
||||
});
|
||||
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setIsAnchored },
|
||||
} = useReduxEmbeddableContext<TimeSliderReduxState, typeof timeSliderReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const isAnchored = select(getIsAnchored);
|
||||
const timeSlider = useTimeSlider();
|
||||
const isAnchored = timeSlider.select(getIsAnchored);
|
||||
const rangeInput = isAnchored ? (
|
||||
<TimeSliderAnchoredRange
|
||||
value={props.value}
|
||||
|
@ -88,7 +82,7 @@ export function TimeSliderPopoverContent(props: Props) {
|
|||
if (nextIsAnchored) {
|
||||
props.onChange([props.timeRangeMin, props.value[1]]);
|
||||
}
|
||||
dispatch(setIsAnchored({ isAnchored: nextIsAnchored }));
|
||||
timeSlider.dispatch.setIsAnchored({ isAnchored: nextIsAnchored });
|
||||
}}
|
||||
aria-label={anchorStartToggleButtonLabel}
|
||||
data-test-subj="timeSlider__anchorStartToggleButton"
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { first } from 'rxjs/operators';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { timeSliderReducers } from '../time_slider_reducers';
|
||||
import { TimeSliderReduxState } from '../types';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { useTimeSlider } from '../embeddable/time_slider_embeddable';
|
||||
|
||||
interface Props {
|
||||
onNext: () => void;
|
||||
|
@ -22,11 +20,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const TimeSliderPrepend: FC<Props> = (props: Props) => {
|
||||
const { useEmbeddableDispatch, actions } = useReduxEmbeddableContext<
|
||||
TimeSliderReduxState,
|
||||
typeof timeSliderReducers
|
||||
>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const timeSlider = useTimeSlider();
|
||||
|
||||
const [isPaused, setIsPaused] = useState(true);
|
||||
const [timeoutId, setTimeoutId] = useState<number | undefined>(undefined);
|
||||
|
@ -51,13 +45,13 @@ export const TimeSliderPrepend: FC<Props> = (props: Props) => {
|
|||
};
|
||||
|
||||
const onPlay = () => {
|
||||
dispatch(actions.setIsOpen({ isOpen: true }));
|
||||
timeSlider.dispatch.setIsOpen({ isOpen: true });
|
||||
setIsPaused(false);
|
||||
playNextFrame();
|
||||
};
|
||||
|
||||
const onPause = () => {
|
||||
dispatch(actions.setIsOpen({ isOpen: true }));
|
||||
timeSlider.dispatch.setIsOpen({ isOpen: true });
|
||||
setIsPaused(true);
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
|
|
|
@ -10,10 +10,10 @@ import _ from 'lodash';
|
|||
import { debounceTime, first, map } from 'rxjs/operators';
|
||||
import moment from 'moment-timezone';
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import React from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { TIME_SLIDER_CONTROL } from '../..';
|
||||
|
@ -37,6 +37,20 @@ import {
|
|||
} from '../time_utils';
|
||||
import { getIsAnchored, getRoundedTimeRangeBounds } from '../time_slider_selectors';
|
||||
|
||||
export const TimeSliderControlContext = createContext<TimeSliderControlEmbeddable | null>(null);
|
||||
export const useTimeSlider = (): TimeSliderControlEmbeddable => {
|
||||
const timeSlider = useContext<TimeSliderControlEmbeddable | null>(TimeSliderControlContext);
|
||||
if (timeSlider == null) {
|
||||
throw new Error('useTimeSlider must be used inside TimeSliderControlContext.');
|
||||
}
|
||||
return timeSlider!;
|
||||
};
|
||||
|
||||
type TimeSliderReduxEmbeddableTools = ReduxEmbeddableTools<
|
||||
TimeSliderReduxState,
|
||||
typeof timeSliderReducers
|
||||
>;
|
||||
|
||||
export class TimeSliderControlEmbeddable extends Embeddable<
|
||||
TimeSliderControlEmbeddableInput,
|
||||
ControlOutput
|
||||
|
@ -47,6 +61,14 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
private inputSubscription: Subscription;
|
||||
private node?: HTMLElement;
|
||||
|
||||
// state management
|
||||
public select: TimeSliderReduxEmbeddableTools['select'];
|
||||
public getState: TimeSliderReduxEmbeddableTools['getState'];
|
||||
public dispatch: TimeSliderReduxEmbeddableTools['dispatch'];
|
||||
public onStateChange: TimeSliderReduxEmbeddableTools['onStateChange'];
|
||||
|
||||
private cleanupStateTools: () => void;
|
||||
|
||||
private getTimezone: ControlsSettingsService['getTimezone'];
|
||||
private timefilter: ControlsDataService['timefilter'];
|
||||
private prevTimeRange: TimeRange | undefined;
|
||||
|
@ -56,13 +78,8 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
};
|
||||
private readonly waitForControlOutputConsumersToLoad$;
|
||||
|
||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
||||
TimeSliderReduxState,
|
||||
typeof timeSliderReducers
|
||||
>;
|
||||
|
||||
constructor(
|
||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
input: TimeSliderControlEmbeddableInput,
|
||||
output: ControlOutput,
|
||||
parent?: IContainer
|
||||
|
@ -85,7 +102,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
timeRangeBounds[TO_INDEX],
|
||||
this.getTimezone()
|
||||
);
|
||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
||||
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||
TimeSliderReduxState,
|
||||
typeof timeSliderReducers
|
||||
>({
|
||||
|
@ -99,6 +116,12 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
},
|
||||
});
|
||||
|
||||
this.select = reduxEmbeddableTools.select;
|
||||
this.getState = reduxEmbeddableTools.getState;
|
||||
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||
|
||||
this.inputSubscription = this.getInput$().subscribe(() => this.onInputChange());
|
||||
|
||||
this.waitForControlOutputConsumersToLoad$ =
|
||||
|
@ -125,7 +148,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
|
||||
public destroy = () => {
|
||||
super.destroy();
|
||||
this.reduxEmbeddableTools.cleanup();
|
||||
this.cleanupStateTools();
|
||||
if (this.inputSubscription) {
|
||||
this.inputSubscription.unsubscribe();
|
||||
}
|
||||
|
@ -136,7 +159,6 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
||||
this.prevTimesliceAsPercentage ?? {};
|
||||
|
||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
||||
if (
|
||||
timesliceStartAsPercentageOfTimeRange !== input.timesliceStartAsPercentageOfTimeRange ||
|
||||
timesliceEndAsPercentageOfTimeRange !== input.timesliceEndAsPercentageOfTimeRange
|
||||
|
@ -149,8 +171,8 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
) {
|
||||
// If no selections have been saved into the timeslider, then both `timesliceStartAsPercentageOfTimeRange`
|
||||
// and `timesliceEndAsPercentageOfTimeRange` will be undefined - so, need to reset component state to match
|
||||
dispatch(actions.publishValue({ value: undefined }));
|
||||
dispatch(actions.setValue({ value: undefined }));
|
||||
this.dispatch.publishValue({ value: undefined });
|
||||
this.dispatch.setValue({ value: undefined });
|
||||
} else {
|
||||
// Otherwise, need to call `syncWithTimeRange` so that the component state value can be calculated and set
|
||||
this.syncWithTimeRange();
|
||||
|
@ -158,31 +180,26 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
} else if (input.timeRange && !_.isEqual(input.timeRange, this.prevTimeRange)) {
|
||||
const nextBounds = this.timeRangeToBounds(input.timeRange);
|
||||
const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], this.getTimezone());
|
||||
dispatch(
|
||||
actions.setTimeRangeBounds({
|
||||
...getStepSize(ticks),
|
||||
ticks,
|
||||
timeRangeBounds: nextBounds,
|
||||
})
|
||||
);
|
||||
this.dispatch.setTimeRangeBounds({
|
||||
...getStepSize(ticks),
|
||||
ticks,
|
||||
timeRangeBounds: nextBounds,
|
||||
});
|
||||
this.syncWithTimeRange();
|
||||
}
|
||||
}
|
||||
|
||||
private syncWithTimeRange() {
|
||||
this.prevTimeRange = this.getInput().timeRange;
|
||||
const { actions, dispatch, getState } = this.reduxEmbeddableTools;
|
||||
const stepSize = getState().componentState.stepSize;
|
||||
const timesliceStartAsPercentageOfTimeRange =
|
||||
getState().explicitInput.timesliceStartAsPercentageOfTimeRange;
|
||||
const timesliceEndAsPercentageOfTimeRange =
|
||||
getState().explicitInput.timesliceEndAsPercentageOfTimeRange;
|
||||
const stepSize = this.getState().componentState.stepSize;
|
||||
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
||||
this.getState().explicitInput;
|
||||
|
||||
if (
|
||||
timesliceStartAsPercentageOfTimeRange !== undefined &&
|
||||
timesliceEndAsPercentageOfTimeRange !== undefined
|
||||
) {
|
||||
const timeRangeBounds = getState().componentState.timeRangeBounds;
|
||||
const timeRangeBounds = this.getState().componentState.timeRangeBounds;
|
||||
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
||||
const from = timeRangeBounds[FROM_INDEX] + timesliceStartAsPercentageOfTimeRange * timeRange;
|
||||
const to = timeRangeBounds[FROM_INDEX] + timesliceEndAsPercentageOfTimeRange * timeRange;
|
||||
|
@ -190,8 +207,8 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
roundDownToNextStepSizeFactor(from, stepSize),
|
||||
roundUpToNextStepSizeFactor(to, stepSize),
|
||||
] as [number, number];
|
||||
dispatch(actions.publishValue({ value }));
|
||||
dispatch(actions.setValue({ value }));
|
||||
this.dispatch.publishValue({ value });
|
||||
this.dispatch.setValue({ value });
|
||||
this.onRangeChange(value[TO_INDEX] - value[FROM_INDEX]);
|
||||
}
|
||||
}
|
||||
|
@ -208,16 +225,14 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
}
|
||||
|
||||
private debouncedPublishChange = _.debounce((value?: [number, number]) => {
|
||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
||||
dispatch(actions.publishValue({ value }));
|
||||
this.dispatch.publishValue({ value });
|
||||
}, 500);
|
||||
|
||||
private getTimeSliceAsPercentageOfTimeRange(value?: [number, number]) {
|
||||
const { getState } = this.reduxEmbeddableTools;
|
||||
let timesliceStartAsPercentageOfTimeRange: number | undefined;
|
||||
let timesliceEndAsPercentageOfTimeRange: number | undefined;
|
||||
if (value) {
|
||||
const timeRangeBounds = getState().componentState.timeRangeBounds;
|
||||
const timeRangeBounds = this.getState().componentState.timeRangeBounds;
|
||||
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
||||
timesliceStartAsPercentageOfTimeRange =
|
||||
(value[FROM_INDEX] - timeRangeBounds[FROM_INDEX]) / timeRange;
|
||||
|
@ -232,39 +247,29 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
}
|
||||
|
||||
private onTimesliceChange = (value?: [number, number]) => {
|
||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
||||
|
||||
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
||||
this.getTimeSliceAsPercentageOfTimeRange(value);
|
||||
dispatch(
|
||||
actions.setValueAsPercentageOfTimeRange({
|
||||
timesliceStartAsPercentageOfTimeRange,
|
||||
timesliceEndAsPercentageOfTimeRange,
|
||||
})
|
||||
);
|
||||
dispatch(actions.setValue({ value }));
|
||||
this.dispatch.setValueAsPercentageOfTimeRange({
|
||||
timesliceStartAsPercentageOfTimeRange,
|
||||
timesliceEndAsPercentageOfTimeRange,
|
||||
});
|
||||
this.dispatch.setValue({ value });
|
||||
this.debouncedPublishChange(value);
|
||||
};
|
||||
|
||||
private onRangeChange = (range?: number) => {
|
||||
const { actions, dispatch, getState } = this.reduxEmbeddableTools;
|
||||
const timeRangeBounds = getState().componentState.timeRangeBounds;
|
||||
const timeRangeBounds = this.getState().componentState.timeRangeBounds;
|
||||
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
||||
dispatch(
|
||||
actions.setRange({
|
||||
range: range !== undefined && range < timeRange ? range : undefined,
|
||||
})
|
||||
);
|
||||
this.dispatch.setRange({
|
||||
range: range !== undefined && range < timeRange ? range : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private onNext = () => {
|
||||
const { getState } = this.reduxEmbeddableTools;
|
||||
const value = getState().componentState.value;
|
||||
const range = getState().componentState.range;
|
||||
const ticks = getState().componentState.ticks;
|
||||
const isAnchored = getIsAnchored(getState());
|
||||
const { value, range, ticks } = this.getState().componentState;
|
||||
const isAnchored = getIsAnchored(this.getState());
|
||||
const tickRange = ticks[1].value - ticks[0].value;
|
||||
const timeRangeBounds = getRoundedTimeRangeBounds(getState());
|
||||
const timeRangeBounds = getRoundedTimeRangeBounds(this.getState());
|
||||
|
||||
if (isAnchored) {
|
||||
if (value === undefined || value[TO_INDEX] >= timeRangeBounds[TO_INDEX]) {
|
||||
|
@ -304,13 +309,10 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
};
|
||||
|
||||
private onPrevious = () => {
|
||||
const { getState } = this.reduxEmbeddableTools;
|
||||
const value = getState().componentState.value;
|
||||
const range = getState().componentState.range;
|
||||
const ticks = getState().componentState.ticks;
|
||||
const isAnchored = getIsAnchored(getState());
|
||||
const { value, range, ticks } = this.getState().componentState;
|
||||
const isAnchored = getIsAnchored(this.getState());
|
||||
const tickRange = ticks[1].value - ticks[0].value;
|
||||
const timeRangeBounds = getRoundedTimeRangeBounds(getState());
|
||||
const timeRangeBounds = getRoundedTimeRangeBounds(this.getState());
|
||||
|
||||
if (isAnchored) {
|
||||
const prevTick = value
|
||||
|
@ -347,10 +349,9 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
};
|
||||
|
||||
private formatDate = (epoch: number) => {
|
||||
const { getState } = this.reduxEmbeddableTools;
|
||||
return moment
|
||||
.tz(epoch, getMomentTimezone(this.getTimezone()))
|
||||
.format(getState().componentState.format);
|
||||
.format(this.getState().componentState.format);
|
||||
};
|
||||
|
||||
public render = (node: HTMLElement) => {
|
||||
|
@ -358,12 +359,9 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
this.node = node;
|
||||
|
||||
const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools;
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||
<TimeSliderControlReduxWrapper>
|
||||
<TimeSliderControlContext.Provider value={this}>
|
||||
<TimeSlider
|
||||
formatDate={this.formatDate}
|
||||
onChange={(value?: [number, number]) => {
|
||||
|
@ -372,22 +370,21 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
|||
this.onRangeChange(range);
|
||||
}}
|
||||
/>
|
||||
</TimeSliderControlReduxWrapper>
|
||||
</TimeSliderControlContext.Provider>
|
||||
</KibanaThemeProvider>,
|
||||
node
|
||||
);
|
||||
};
|
||||
|
||||
public renderPrepend() {
|
||||
const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools;
|
||||
return (
|
||||
<TimeSliderControlReduxWrapper>
|
||||
<TimeSliderControlContext.Provider value={this}>
|
||||
<TimeSliderPrepend
|
||||
onNext={this.onNext}
|
||||
onPrevious={this.onPrevious}
|
||||
waitForControlOutputConsumersToLoad$={this.waitForControlOutputConsumersToLoad$}
|
||||
/>
|
||||
</TimeSliderControlReduxWrapper>
|
||||
</TimeSliderControlContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import {
|
||||
createTimeSliderExtract,
|
||||
createTimeSliderInject,
|
||||
|
@ -24,7 +24,7 @@ export class TimeSliderEmbeddableFactory
|
|||
constructor() {}
|
||||
|
||||
public async create(initialInput: any, parent?: IContainer) {
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
||||
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||
const { TimeSliderControlEmbeddable } = await import('./time_slider_embeddable');
|
||||
|
||||
return Promise.resolve(
|
||||
|
|
|
@ -1,25 +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 type {
|
||||
DashboardContainerByReferenceInput,
|
||||
DashboardContainerByValueInput,
|
||||
DashboardContainerInput,
|
||||
} from './types';
|
||||
|
||||
export const dashboardContainerInputIsByValue = (
|
||||
containerInput: DashboardContainerInput
|
||||
): containerInput is DashboardContainerByValueInput => {
|
||||
if (
|
||||
(containerInput as DashboardContainerByValueInput).panels &&
|
||||
!(containerInput as DashboardContainerByReferenceInput).savedObjectId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -30,13 +30,9 @@ export interface DashboardPanelState<
|
|||
panelRefName?: string;
|
||||
}
|
||||
|
||||
export type DashboardContainerInput =
|
||||
| DashboardContainerByReferenceInput
|
||||
| DashboardContainerByValueInput;
|
||||
export type DashboardContainerByReferenceInput = SavedObjectEmbeddableInput;
|
||||
|
||||
export type DashboardContainerByReferenceInput = SavedObjectEmbeddableInput & { panels: never };
|
||||
|
||||
export interface DashboardContainerByValueInput extends EmbeddableInput {
|
||||
export interface DashboardContainerInput extends EmbeddableInput {
|
||||
// filter context to be passed to children
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
|
|
|
@ -17,7 +17,6 @@ export type {
|
|||
DashboardPanelMap,
|
||||
DashboardPanelState,
|
||||
DashboardContainerInput,
|
||||
DashboardContainerByValueInput,
|
||||
DashboardContainerByReferenceInput,
|
||||
} from './dashboard_container/types';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EmbeddableInput, EmbeddableStateWithType } from '@kbn/embeddable-plugin
|
|||
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
|
||||
import { SavedDashboardPanel } from './dashboard_saved_object/types';
|
||||
import { DashboardContainerByValueInput, DashboardPanelState } from './dashboard_container/types';
|
||||
import { DashboardContainerInput, DashboardPanelState } from './dashboard_container/types';
|
||||
|
||||
export interface DashboardOptions {
|
||||
hidePanelTitles: boolean;
|
||||
|
@ -32,7 +32,7 @@ export interface DashboardCapabilities {
|
|||
* For BWC reasons, dashboard state is stored with panels as an array instead of a map
|
||||
*/
|
||||
export type SharedDashboardState = Partial<
|
||||
Omit<DashboardContainerByValueInput, 'panels'> & { panels: SavedDashboardPanel[] }
|
||||
Omit<DashboardContainerInput, 'panels'> & { panels: SavedDashboardPanel[] }
|
||||
>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
|
||||
import { getSampleDashboardInput } from '../mocks';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { AddToLibraryAction } from './add_to_library_action';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
@ -48,8 +48,7 @@ Object.defineProperty(pluginServices.getServices().application, 'capabilities',
|
|||
beforeEach(async () => {
|
||||
pluginServices.getServices().application.capabilities = defaultCapabilities;
|
||||
|
||||
container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
container = buildMockDashboard();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -21,7 +21,7 @@ import { ErrorEmbeddable, IContainer, isErrorEmbeddable } from '@kbn/embeddable-
|
|||
import { DashboardPanelState } from '../../common';
|
||||
import { ClonePanelAction } from './clone_panel_action';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
||||
let container: DashboardContainer;
|
||||
|
@ -37,7 +37,12 @@ beforeEach(async () => {
|
|||
create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })),
|
||||
};
|
||||
|
||||
const input = getSampleDashboardInput({
|
||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockEmbeddableFactory);
|
||||
container = buildMockDashboard({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Kibanana', id: '123' },
|
||||
|
@ -45,13 +50,6 @@ beforeEach(async () => {
|
|||
}),
|
||||
},
|
||||
});
|
||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockEmbeddableFactory);
|
||||
container = new DashboardContainer(input);
|
||||
await container.untilInitialized();
|
||||
|
||||
const refOrValContactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { ExpandPanelAction } from './expand_panel_action';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
@ -30,7 +30,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
|||
.mockReturnValue(mockEmbeddableFactory);
|
||||
|
||||
beforeEach(async () => {
|
||||
const input = getSampleDashboardInput({
|
||||
container = buildMockDashboard({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
|
@ -39,9 +39,6 @@ beforeEach(async () => {
|
|||
},
|
||||
});
|
||||
|
||||
container = new DashboardContainer(input);
|
||||
await container.untilInitialized();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
|
|
@ -20,7 +20,7 @@ import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '@kbn/embeddable-
|
|||
|
||||
import { ExportCSVAction } from './export_csv_action';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
||||
describe('Export CSV action', () => {
|
||||
|
@ -45,7 +45,7 @@ describe('Export CSV action', () => {
|
|||
create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })),
|
||||
};
|
||||
|
||||
const input = getSampleDashboardInput({
|
||||
container = buildMockDashboard({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Kibanana', id: '123' },
|
||||
|
@ -53,8 +53,6 @@ describe('Export CSV action', () => {
|
|||
}),
|
||||
},
|
||||
});
|
||||
container = new DashboardContainer(input);
|
||||
await container.untilInitialized();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -17,9 +17,8 @@ import {
|
|||
import { type Query, type AggregateQuery, Filter } from '@kbn/es-query';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
|
||||
import { getSampleDashboardInput } from '../mocks';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
import { FiltersNotificationAction } from './filters_notification_action';
|
||||
|
||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||
|
@ -51,8 +50,7 @@ const getMockPhraseFilter = (key: string, value: string) => {
|
|||
};
|
||||
|
||||
const buildEmbeddable = async (input?: Partial<ContactCardEmbeddableInput>) => {
|
||||
const container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
const container = buildMockDashboard();
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { FilterableEmbeddable, isErrorEmbeddable, ViewMode } from '@kbn/embeddab
|
|||
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
import { getSampleDashboardInput } from '../mocks';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { EuiPopover } from '@elastic/eui';
|
||||
import {
|
||||
FiltersNotificationPopover,
|
||||
|
@ -40,8 +40,7 @@ describe('filters notification popover', () => {
|
|||
let defaultProps: FiltersNotificationProps;
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
container = buildMockDashboard();
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
|
|
@ -22,11 +22,11 @@ import {
|
|||
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
|
||||
import { getSampleDashboardInput } from '../mocks';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||
import { LibraryNotificationAction } from './library_notification_action';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
|
||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
|
@ -43,8 +43,7 @@ beforeEach(async () => {
|
|||
execute: jest.fn(),
|
||||
} as unknown as UnlinkFromLibraryAction;
|
||||
|
||||
container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
container = buildMockDashboard();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
LibraryNotificationPopover,
|
||||
LibraryNotificationProps,
|
||||
} from './library_notification_popover';
|
||||
import { getSampleDashboardInput } from '../mocks';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
||||
|
@ -38,8 +38,7 @@ describe('LibraryNotificationPopover', () => {
|
|||
let defaultProps: LibraryNotificationProps;
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
container = buildMockDashboard();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -17,7 +17,7 @@ import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
|||
|
||||
import { ReplacePanelAction } from './replace_panel_action';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||
|
@ -28,7 +28,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
|||
let container: DashboardContainer;
|
||||
let embeddable: ContactCardEmbeddable;
|
||||
beforeEach(async () => {
|
||||
const input = getSampleDashboardInput({
|
||||
container = buildMockDashboard({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
|
@ -36,8 +36,6 @@ beforeEach(async () => {
|
|||
}),
|
||||
},
|
||||
});
|
||||
container = new DashboardContainer(input);
|
||||
await container.untilInitialized();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
@ -54,7 +52,7 @@ beforeEach(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('Executes the replace panel action', async () => {
|
||||
test('Executes the replace panel action', () => {
|
||||
let SavedObjectFinder: any;
|
||||
const action = new ReplacePanelAction(SavedObjectFinder);
|
||||
action.execute({ embeddable });
|
||||
|
@ -82,13 +80,13 @@ test('Execute throws an error when called with an embeddable not in a parent', a
|
|||
await expect(check()).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
test('Returns title', async () => {
|
||||
test('Returns title', () => {
|
||||
let SavedObjectFinder: any;
|
||||
const action = new ReplacePanelAction(SavedObjectFinder);
|
||||
expect(action.getDisplayName({ embeddable })).toBeDefined();
|
||||
});
|
||||
|
||||
test('Returns an icon', async () => {
|
||||
test('Returns an icon', () => {
|
||||
let SavedObjectFinder: any;
|
||||
const action = new ReplacePanelAction(SavedObjectFinder);
|
||||
expect(action.getIconType({ embeddable })).toBeDefined();
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
|
||||
import { getSampleDashboardInput } from '../mocks';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { DashboardPanelState } from '../../common';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||
|
@ -38,8 +38,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
|||
.mockReturnValue(mockEmbeddableFactory);
|
||||
|
||||
beforeEach(async () => {
|
||||
container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
container = buildMockDashboard();
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
import { History } from 'history';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -28,14 +29,14 @@ import {
|
|||
removeSearchSessionIdFromURL,
|
||||
createSessionRestorationDataProvider,
|
||||
} from './url/search_sessions_integration';
|
||||
import { DashboardAPI, DashboardRenderer } from '..';
|
||||
import { DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
|
||||
import type { DashboardContainer } from '../dashboard_container';
|
||||
import { AwaitingDashboardAPI } from '../dashboard_container';
|
||||
import { type DashboardEmbedSettings, DashboardRedirect } from './types';
|
||||
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
||||
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
|
||||
import DashboardContainerRenderer from '../dashboard_container/dashboard_container_renderer';
|
||||
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
|
||||
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
|
||||
|
||||
|
@ -46,6 +47,16 @@ export interface DashboardAppProps {
|
|||
embedSettings?: DashboardEmbedSettings;
|
||||
}
|
||||
|
||||
export const DashboardAPIContext = createContext<AwaitingDashboardAPI>(null);
|
||||
|
||||
export const useDashboardAPI = (): DashboardAPI => {
|
||||
const api = useContext<AwaitingDashboardAPI>(DashboardAPIContext);
|
||||
if (api == null) {
|
||||
throw new Error('useDashboardAPI must be used inside DashboardAPIContext');
|
||||
}
|
||||
return api!;
|
||||
};
|
||||
|
||||
export function DashboardApp({
|
||||
savedDashboardId,
|
||||
embedSettings,
|
||||
|
@ -57,10 +68,7 @@ export function DashboardApp({
|
|||
useMount(() => {
|
||||
(async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
|
||||
});
|
||||
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [dashboardAPI, setDashboardAPI] = useState<AwaitingDashboardAPI>(null);
|
||||
|
||||
/**
|
||||
* Unpack & set up dashboard services
|
||||
|
@ -72,7 +80,9 @@ export function DashboardApp({
|
|||
notifications: { toasts },
|
||||
settings: { uiSettings },
|
||||
data: { search },
|
||||
customBranding,
|
||||
} = pluginServices.getServices();
|
||||
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
|
||||
|
||||
const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage(
|
||||
DASHBOARD_APP_ID,
|
||||
|
@ -140,7 +150,7 @@ export function DashboardApp({
|
|||
},
|
||||
|
||||
// Override all state with URL + Locator input
|
||||
overrideInput: {
|
||||
initialInput: {
|
||||
// State loaded from the dashboard app URL and from the locator overrides all other dashboard state.
|
||||
...initialUrlState,
|
||||
...stateFromLocator,
|
||||
|
@ -163,26 +173,17 @@ export function DashboardApp({
|
|||
getScreenshotContext,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Get the redux wrapper from the dashboard container. This is used to wrap the top nav so it can interact with the
|
||||
* dashboard's redux state.
|
||||
*/
|
||||
const DashboardReduxWrapper = useMemo(
|
||||
() => dashboardContainer?.getReduxEmbeddableTools().Wrapper,
|
||||
[dashboardContainer]
|
||||
);
|
||||
|
||||
/**
|
||||
* When the dashboard container is created, or re-created, start syncing dashboard state with the URL
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dashboardContainer) return;
|
||||
if (!dashboardAPI) return;
|
||||
const { stopWatchingAppStateInUrl } = startSyncingDashboardUrlState({
|
||||
kbnUrlStateStorage,
|
||||
dashboardContainer,
|
||||
dashboardAPI,
|
||||
});
|
||||
return () => stopWatchingAppStateInUrl();
|
||||
}, [dashboardContainer, kbnUrlStateStorage]);
|
||||
}, [dashboardAPI, kbnUrlStateStorage]);
|
||||
|
||||
return (
|
||||
<div className="dshAppWrapper">
|
||||
|
@ -191,18 +192,19 @@ export function DashboardApp({
|
|||
)}
|
||||
{!showNoDataPage && (
|
||||
<>
|
||||
{DashboardReduxWrapper && (
|
||||
<DashboardReduxWrapper>
|
||||
{dashboardAPI && (
|
||||
<DashboardAPIContext.Provider value={dashboardAPI}>
|
||||
<DashboardTopNav redirectTo={redirectTo} embedSettings={embedSettings} />
|
||||
</DashboardReduxWrapper>
|
||||
</DashboardAPIContext.Provider>
|
||||
)}
|
||||
|
||||
{getLegacyConflictWarning?.()}
|
||||
|
||||
<DashboardContainerRenderer
|
||||
<DashboardRenderer
|
||||
ref={setDashboardAPI}
|
||||
savedObjectId={savedDashboardId}
|
||||
showPlainSpinner={showPlainSpinner}
|
||||
getCreationOptions={getCreationOptions}
|
||||
onDashboardContainerLoaded={(container) => setDashboardContainer(container)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import { ScopedHistory } from '@kbn/core-application-browser';
|
||||
|
||||
import { ForwardedDashboardState } from './locator';
|
||||
import { convertSavedPanelsToPanelMap, DashboardContainerByValueInput } from '../../../common';
|
||||
import { convertSavedPanelsToPanelMap, DashboardContainerInput } from '../../../common';
|
||||
|
||||
export const loadDashboardHistoryLocationState = (
|
||||
getScopedHistory: () => ScopedHistory
|
||||
): Partial<DashboardContainerByValueInput> => {
|
||||
): Partial<DashboardContainerInput> => {
|
||||
const state = getScopedHistory().location.state as undefined | ForwardedDashboardState;
|
||||
|
||||
if (!state) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'
|
|||
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
|
||||
|
||||
import { DASHBOARD_APP_ID, SEARCH_SESSION_ID } from '../../dashboard_constants';
|
||||
import type { DashboardContainerByValueInput, SavedDashboardPanel } from '../../../common';
|
||||
import type { DashboardContainerInput, SavedDashboardPanel } from '../../../common';
|
||||
|
||||
/**
|
||||
* Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions).
|
||||
|
@ -36,7 +36,7 @@ export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR';
|
|||
|
||||
export type DashboardAppLocatorParams = Partial<
|
||||
Omit<
|
||||
DashboardContainerByValueInput,
|
||||
DashboardContainerInput,
|
||||
'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally'
|
||||
>
|
||||
> & {
|
||||
|
|
|
@ -6,25 +6,31 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { AddFromLibraryButton, Toolbar, ToolbarButton } from '@kbn/shared-ux-button-toolbar';
|
||||
import { IconButton, IconButtonGroup } from '@kbn/shared-ux-button-toolbar';
|
||||
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { IconType, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
|
||||
import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import React, { useCallback } from 'react';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { IconType, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
AddFromLibraryButton,
|
||||
IconButton,
|
||||
IconButtonGroup,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
} from '@kbn/shared-ux-button-toolbar';
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
|
||||
import {
|
||||
getCreateVisualizationButtonTitle,
|
||||
getQuickCreateButtonGroupLegend,
|
||||
} from '../_dashboard_app_strings';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { ControlsToolbarButton } from './controls_toolbar_button';
|
||||
import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
|
||||
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
|
||||
|
||||
export function DashboardEditingToolbar() {
|
||||
const {
|
||||
|
@ -36,7 +42,7 @@ export function DashboardEditingToolbar() {
|
|||
} = pluginServices.getServices();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
|
||||
const dashboard = useDashboardAPI();
|
||||
|
||||
const stateTransferService = getStateTransfer();
|
||||
|
||||
|
@ -101,10 +107,7 @@ export function DashboardEditingToolbar() {
|
|||
return;
|
||||
}
|
||||
|
||||
const newEmbeddable = await dashboardContainer.addNewEmbeddable(
|
||||
embeddableFactory.type,
|
||||
explicitInput
|
||||
);
|
||||
const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput);
|
||||
|
||||
if (newEmbeddable) {
|
||||
toasts.addSuccess({
|
||||
|
@ -113,7 +116,7 @@ export function DashboardEditingToolbar() {
|
|||
});
|
||||
}
|
||||
},
|
||||
[trackUiMetric, dashboardContainer, toasts]
|
||||
[trackUiMetric, dashboard, toasts]
|
||||
);
|
||||
|
||||
const getVisTypeQuickButton = (
|
||||
|
@ -170,12 +173,12 @@ export function DashboardEditingToolbar() {
|
|||
const extraButtons = [
|
||||
<EditorMenu createNewVisType={createNewVisType} createNewEmbeddable={createNewEmbeddable} />,
|
||||
<AddFromLibraryButton
|
||||
onClick={() => dashboardContainer.addFromLibrary()}
|
||||
onClick={() => dashboard.addFromLibrary()}
|
||||
data-test-subj="dashboardAddPanelButton"
|
||||
/>,
|
||||
];
|
||||
if (dashboardContainer.controlGroup) {
|
||||
extraButtons.push(<ControlsToolbarButton controlGroup={dashboardContainer.controlGroup} />);
|
||||
if (dashboard.controlGroup) {
|
||||
extraButtons.push(<ControlsToolbarButton controlGroup={dashboard.controlGroup} />);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -26,13 +26,13 @@ import {
|
|||
unsavedChangesBadgeStrings,
|
||||
} from '../_dashboard_app_strings';
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { useDashboardMenuItems } from './use_dashboard_menu_items';
|
||||
import { DashboardEmbedSettings, DashboardRedirect } from '../types';
|
||||
import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
|
||||
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
|
||||
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
||||
|
||||
import './_dashboard_top_nav.scss';
|
||||
export interface DashboardTopNavProps {
|
||||
|
@ -69,36 +69,26 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
} = pluginServices.getServices();
|
||||
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
||||
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
||||
/**
|
||||
* Unpack dashboard state from redux
|
||||
*/
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
actions: { setSavedQueryId },
|
||||
useEmbeddableSelector: select,
|
||||
embeddableInstance: dashboardContainer,
|
||||
} = useDashboardContainerContext();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
|
||||
const dashboard = useDashboardAPI();
|
||||
const PresentationUtilContextProvider = getPresentationUtilContextProvider();
|
||||
|
||||
const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges);
|
||||
const fullScreenMode = select((state) => state.componentState.fullScreenMode);
|
||||
const savedQueryId = select((state) => state.componentState.savedQueryId);
|
||||
const lastSavedId = select((state) => state.componentState.lastSavedId);
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
const query = select((state) => state.explicitInput.query);
|
||||
const title = select((state) => state.explicitInput.title);
|
||||
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
|
||||
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
|
||||
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
|
||||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const query = dashboard.select((state) => state.explicitInput.query);
|
||||
const title = dashboard.select((state) => state.explicitInput.title);
|
||||
|
||||
// store data views in state & subscribe to dashboard data view changes.
|
||||
const [allDataViews, setAllDataViews] = useState<DataView[]>(
|
||||
dashboardContainer.getAllDataViews()
|
||||
);
|
||||
const [allDataViews, setAllDataViews] = useState<DataView[]>(dashboard.getAllDataViews());
|
||||
useEffect(() => {
|
||||
const subscription = dashboardContainer.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||
setAllDataViews(dataViews)
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [dashboardContainer]);
|
||||
}, [dashboard]);
|
||||
|
||||
const dashboardTitle = useMemo(() => {
|
||||
return getDashboardTitle(title, viewMode, !lastSavedId);
|
||||
|
@ -212,7 +202,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
}, [embedSettings, filterManager, fullScreenMode, isChromeVisible, viewMode]);
|
||||
|
||||
UseUnmount(() => {
|
||||
dashboardContainer.clearOverlays();
|
||||
dashboard.clearOverlays();
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -264,12 +254,12 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
}
|
||||
onQuerySubmit={(_payload, isUpdate) => {
|
||||
if (isUpdate === false) {
|
||||
dashboardContainer.forceRefresh();
|
||||
dashboard.forceRefresh();
|
||||
}
|
||||
}}
|
||||
onSavedQueryIdChange={(newId: string | undefined) => {
|
||||
dispatch(setSavedQueryId(newId));
|
||||
}}
|
||||
onSavedQueryIdChange={(newId: string | undefined) =>
|
||||
dashboard.dispatch.setSavedQueryId(newId)
|
||||
}
|
||||
/>
|
||||
{viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? (
|
||||
<PresentationUtilContextProvider>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { Capabilities } from '@kbn/core/public';
|
||||
import { convertPanelMapToSavedPanels, DashboardContainerByValueInput } from '../../../../common';
|
||||
import { convertPanelMapToSavedPanels, DashboardContainerInput } from '../../../../common';
|
||||
|
||||
import { DashboardAppLocatorParams } from '../../..';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
|
@ -68,7 +68,7 @@ describe('ShowShareModal', () => {
|
|||
});
|
||||
|
||||
const getPropsAndShare = (
|
||||
unsavedState?: Partial<DashboardContainerByValueInput>
|
||||
unsavedState?: Partial<DashboardContainerInput>
|
||||
): ShowShareModalProps => {
|
||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||
.fn()
|
||||
|
@ -94,7 +94,7 @@ describe('ShowShareModal', () => {
|
|||
});
|
||||
|
||||
it('locatorParams unsaved state is properly propagated to locator', () => {
|
||||
const unsavedDashboardState: DashboardContainerByValueInput = {
|
||||
const unsavedDashboardState: DashboardContainerInput = {
|
||||
panels: {
|
||||
panel_1: {
|
||||
type: 'panel_type',
|
||||
|
@ -121,7 +121,7 @@ describe('ShowShareModal', () => {
|
|||
},
|
||||
],
|
||||
query: { query: 'bye', language: 'kuery' },
|
||||
} as unknown as DashboardContainerByValueInput;
|
||||
} as unknown as DashboardContainerInput;
|
||||
const showModalProps = getPropsAndShare(unsavedDashboardState);
|
||||
ShowShareModal(showModalProps);
|
||||
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -14,13 +14,13 @@ import { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
|||
|
||||
import { DashboardRedirect } from '../types';
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { topNavStrings } from '../_dashboard_app_strings';
|
||||
import { ShowShareModal } from './share/show_share_modal';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
||||
import { SaveDashboardReturn } from '../../services/dashboard_saved_object/types';
|
||||
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
||||
|
||||
export const useDashboardMenuItems = ({
|
||||
redirectTo,
|
||||
|
@ -46,18 +46,12 @@ export const useDashboardMenuItems = ({
|
|||
/**
|
||||
* Unpack dashboard state from redux
|
||||
*/
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
embeddableInstance: dashboardContainer,
|
||||
actions: { setViewMode, setFullScreenMode },
|
||||
} = useDashboardContainerContext();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const dashboard = useDashboardAPI();
|
||||
|
||||
const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges);
|
||||
const hasOverlays = select((state) => state.componentState.hasOverlays);
|
||||
const lastSavedId = select((state) => state.componentState.lastSavedId);
|
||||
const dashboardTitle = select((state) => state.explicitInput.title);
|
||||
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
|
||||
const hasOverlays = dashboard.select((state) => state.componentState.hasOverlays);
|
||||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||
|
||||
/**
|
||||
* Show the Dashboard app's share menu
|
||||
|
@ -95,41 +89,41 @@ export const useDashboardMenuItems = ({
|
|||
*/
|
||||
const quickSaveDashboard = useCallback(() => {
|
||||
setIsSaveInProgress(true);
|
||||
dashboardContainer
|
||||
dashboard
|
||||
.runQuickSave()
|
||||
.then(() => setTimeout(() => setIsSaveInProgress(false), CHANGE_CHECK_DEBOUNCE));
|
||||
}, [dashboardContainer]);
|
||||
}, [dashboard]);
|
||||
|
||||
/**
|
||||
* Show the dashboard's save modal
|
||||
*/
|
||||
const saveDashboardAs = useCallback(() => {
|
||||
dashboardContainer.runSaveAs().then((result) => maybeRedirect(result));
|
||||
}, [maybeRedirect, dashboardContainer]);
|
||||
dashboard.runSaveAs().then((result) => maybeRedirect(result));
|
||||
}, [maybeRedirect, dashboard]);
|
||||
|
||||
/**
|
||||
* Clone the dashboard
|
||||
*/
|
||||
const clone = useCallback(() => {
|
||||
dashboardContainer.runClone().then((result) => maybeRedirect(result));
|
||||
}, [maybeRedirect, dashboardContainer]);
|
||||
dashboard.runClone().then((result) => maybeRedirect(result));
|
||||
}, [maybeRedirect, dashboard]);
|
||||
|
||||
/**
|
||||
* Returns to view mode. If the dashboard has unsaved changes shows a warning and resets to last saved state.
|
||||
*/
|
||||
const returnToViewMode = useCallback(() => {
|
||||
dashboardContainer.clearOverlays();
|
||||
dashboard.clearOverlays();
|
||||
if (hasUnsavedChanges) {
|
||||
confirmDiscardUnsavedChanges(() => {
|
||||
batch(() => {
|
||||
dashboardContainer.resetToLastSavedState();
|
||||
dispatch(setViewMode(ViewMode.VIEW));
|
||||
dashboard.resetToLastSavedState();
|
||||
dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
dispatch(setViewMode(ViewMode.VIEW));
|
||||
}, [dashboardContainer, dispatch, hasUnsavedChanges, setViewMode]);
|
||||
dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||
}, [dashboard, hasUnsavedChanges]);
|
||||
|
||||
/**
|
||||
* Register all of the top nav configs that can be used by dashboard.
|
||||
|
@ -140,7 +134,7 @@ export const useDashboardMenuItems = ({
|
|||
...topNavStrings.fullScreen,
|
||||
id: 'full-screen',
|
||||
testId: 'dashboardFullScreenMode',
|
||||
run: () => dispatch(setFullScreenMode(true)),
|
||||
run: () => dashboard.dispatch.setFullScreenMode(true),
|
||||
} as TopNavMenuData,
|
||||
|
||||
labs: {
|
||||
|
@ -158,8 +152,8 @@ export const useDashboardMenuItems = ({
|
|||
testId: 'dashboardEditMode',
|
||||
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
|
||||
run: () => {
|
||||
dashboardContainer.clearOverlays();
|
||||
dispatch(setViewMode(ViewMode.EDIT));
|
||||
dashboard.dispatch.setViewMode(ViewMode.EDIT);
|
||||
dashboard.clearOverlays();
|
||||
},
|
||||
} as TopNavMenuData,
|
||||
|
||||
|
@ -206,7 +200,7 @@ export const useDashboardMenuItems = ({
|
|||
id: 'settings',
|
||||
testId: 'dashboardSettingsButton',
|
||||
disableButton: isSaveInProgress || hasOverlays,
|
||||
run: () => dashboardContainer.showSettings(),
|
||||
run: () => dashboard.showSettings(),
|
||||
} as TopNavMenuData,
|
||||
|
||||
clone: {
|
||||
|
@ -219,19 +213,16 @@ export const useDashboardMenuItems = ({
|
|||
};
|
||||
}, [
|
||||
quickSaveDashboard,
|
||||
dashboardContainer,
|
||||
hasUnsavedChanges,
|
||||
hasOverlays,
|
||||
setFullScreenMode,
|
||||
isSaveInProgress,
|
||||
returnToViewMode,
|
||||
saveDashboardAs,
|
||||
setIsLabsShown,
|
||||
hasOverlays,
|
||||
lastSavedId,
|
||||
setViewMode,
|
||||
isLabsShown,
|
||||
showShare,
|
||||
dispatch,
|
||||
dashboard,
|
||||
clone,
|
||||
]);
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ function getLocatorParams({
|
|||
const {
|
||||
componentState: { lastSavedId },
|
||||
explicitInput: { panels, query, viewMode },
|
||||
} = container.getReduxEmbeddableTools().getState();
|
||||
} = container.getState();
|
||||
|
||||
return {
|
||||
viewMode,
|
||||
|
|
|
@ -18,9 +18,9 @@ import {
|
|||
SavedDashboardPanel,
|
||||
SharedDashboardState,
|
||||
convertSavedPanelsToPanelMap,
|
||||
DashboardContainerByValueInput,
|
||||
DashboardContainerInput,
|
||||
} from '../../../common';
|
||||
import { DashboardContainer } from '../../dashboard_container';
|
||||
import { DashboardAPI } from '../../dashboard_container';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
|
||||
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
|
||||
|
@ -59,7 +59,7 @@ function getPanelsMap(appStateInUrl: SharedDashboardState): DashboardPanelMap |
|
|||
*/
|
||||
export const loadAndRemoveDashboardState = (
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage
|
||||
): Partial<DashboardContainerByValueInput> => {
|
||||
): Partial<DashboardContainerInput> => {
|
||||
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
|
||||
DASHBOARD_STATE_STORAGE_KEY
|
||||
);
|
||||
|
@ -72,7 +72,7 @@ export const loadAndRemoveDashboardState = (
|
|||
return hashQuery;
|
||||
});
|
||||
kbnUrlStateStorage.kbnUrlControls.update(nextUrl, true);
|
||||
const partialState: Partial<DashboardContainerByValueInput> = {
|
||||
const partialState: Partial<DashboardContainerInput> = {
|
||||
..._.omit(rawAppStateInUrl, ['panels', 'query']),
|
||||
...(panelsMap ? { panels: panelsMap } : {}),
|
||||
...(rawAppStateInUrl.query ? { query: migrateLegacyQuery(rawAppStateInUrl.query) } : {}),
|
||||
|
@ -83,10 +83,10 @@ export const loadAndRemoveDashboardState = (
|
|||
|
||||
export const startSyncingDashboardUrlState = ({
|
||||
kbnUrlStateStorage,
|
||||
dashboardContainer,
|
||||
dashboardAPI,
|
||||
}: {
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
dashboardContainer: DashboardContainer;
|
||||
dashboardAPI: DashboardAPI;
|
||||
}) => {
|
||||
const appStateSubscription = kbnUrlStateStorage
|
||||
.change$(DASHBOARD_STATE_STORAGE_KEY)
|
||||
|
@ -94,7 +94,7 @@ export const startSyncingDashboardUrlState = ({
|
|||
.subscribe(() => {
|
||||
const stateFromUrl = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||
if (Object.keys(stateFromUrl).length === 0) return;
|
||||
dashboardContainer.updateInput(stateFromUrl);
|
||||
dashboardAPI.updateInput(stateFromUrl);
|
||||
});
|
||||
|
||||
const stopWatchingAppStateInUrl = () => appStateSubscription.unsubscribe();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
import type { DashboardContainerByValueInput } from '../common';
|
||||
import type { DashboardContainerInput } from '../common';
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// URL Constants
|
||||
|
@ -69,7 +69,7 @@ export const CHANGE_CHECK_DEBOUNCE = 100;
|
|||
// ------------------------------------------------------------------
|
||||
// Default State
|
||||
// ------------------------------------------------------------------
|
||||
export const DEFAULT_DASHBOARD_INPUT: Omit<DashboardContainerByValueInput, 'id'> = {
|
||||
export const DEFAULT_DASHBOARD_INPUT: Omit<DashboardContainerInput, 'id'> = {
|
||||
viewMode: ViewMode.EDIT, // new dashboards start in edit mode.
|
||||
timeRestore: false,
|
||||
query: { query: '', language: 'kuery' },
|
||||
|
|
|
@ -6,17 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import React from 'react';
|
||||
|
||||
import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardGrid } from './dashboard_grid';
|
||||
import { DashboardContainer } from '../../embeddable/dashboard_container';
|
||||
import { getSampleDashboardInput } from '../../../mocks';
|
||||
import { buildMockDashboard } from '../../../mocks';
|
||||
import type { Props as DashboardGridItemProps } from './dashboard_grid_item';
|
||||
import { DashboardContainerContext } from '../../embeddable/dashboard_container';
|
||||
|
||||
jest.mock('./dashboard_grid_item', () => {
|
||||
return {
|
||||
|
@ -39,10 +37,8 @@ jest.mock('./dashboard_grid_item', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const DashboardServicesProvider = pluginServices.getContextProvider();
|
||||
|
||||
async function getDashboardContainer() {
|
||||
const initialInput = getSampleDashboardInput({
|
||||
const createAndMountDashboardGrid = () => {
|
||||
const dashboardContainer = buildMockDashboard({
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
|
@ -56,55 +52,29 @@ async function getDashboardContainer() {
|
|||
},
|
||||
},
|
||||
});
|
||||
const dashboardContainer = new DashboardContainer(initialInput);
|
||||
await dashboardContainer.untilInitialized();
|
||||
return dashboardContainer;
|
||||
}
|
||||
const component = mountWithIntl(
|
||||
<DashboardContainerContext.Provider value={dashboardContainer}>
|
||||
<DashboardGrid viewportWidth={1000} />
|
||||
</DashboardContainerContext.Provider>
|
||||
);
|
||||
return { dashboardContainer, component };
|
||||
};
|
||||
|
||||
test('renders DashboardGrid', async () => {
|
||||
const dashboardContainer = await getDashboardContainer();
|
||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DashboardServicesProvider>
|
||||
<DashboardReduxWrapper>
|
||||
<DashboardGrid viewportWidth={1000} />
|
||||
</DashboardReduxWrapper>
|
||||
</DashboardServicesProvider>
|
||||
);
|
||||
const { component } = createAndMountDashboardGrid();
|
||||
const panelElements = component.find('GridItem');
|
||||
expect(panelElements.length).toBe(2);
|
||||
});
|
||||
|
||||
test('renders DashboardGrid with no visualizations', async () => {
|
||||
const dashboardContainer = await getDashboardContainer();
|
||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DashboardServicesProvider>
|
||||
<DashboardReduxWrapper>
|
||||
<DashboardGrid viewportWidth={1000} />
|
||||
</DashboardReduxWrapper>
|
||||
</DashboardServicesProvider>
|
||||
);
|
||||
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
dashboardContainer.updateInput({ panels: {} });
|
||||
component.update();
|
||||
expect(component.find('GridItem').length).toBe(0);
|
||||
});
|
||||
|
||||
test('DashboardGrid removes panel when removed from container', async () => {
|
||||
const dashboardContainer = await getDashboardContainer();
|
||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DashboardServicesProvider>
|
||||
<DashboardReduxWrapper>
|
||||
<DashboardGrid viewportWidth={1000} />
|
||||
</DashboardReduxWrapper>
|
||||
</DashboardServicesProvider>
|
||||
);
|
||||
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
const originalPanels = dashboardContainer.getInput().panels;
|
||||
const filteredPanels = { ...originalPanels };
|
||||
delete filteredPanels['1'];
|
||||
|
@ -115,17 +85,7 @@ test('DashboardGrid removes panel when removed from container', async () => {
|
|||
});
|
||||
|
||||
test('DashboardGrid renders expanded panel', async () => {
|
||||
const dashboardContainer = await getDashboardContainer();
|
||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DashboardServicesProvider>
|
||||
<DashboardReduxWrapper>
|
||||
<DashboardGrid viewportWidth={1000} />
|
||||
</DashboardReduxWrapper>
|
||||
</DashboardServicesProvider>
|
||||
);
|
||||
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
dashboardContainer.setExpandedPanelId('1');
|
||||
component.update();
|
||||
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
|
||||
|
|
|
@ -19,23 +19,18 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
|
|||
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { DashboardGridItem } from './dashboard_grid_item';
|
||||
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
|
||||
import { useDashboardGridSettings } from './use_dashboard_grid_settings';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
import { useDashboardPerformanceTracker } from './use_dashboard_performance_tracker';
|
||||
import { getPanelLayoutsAreEqual } from '../../embeddable/integrations/diff_state/dashboard_diffing_utils';
|
||||
import { getPanelLayoutsAreEqual } from '../../state/diffing/dashboard_diffing_utils';
|
||||
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
|
||||
|
||||
export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
||||
const {
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setPanels },
|
||||
useEmbeddableDispatch,
|
||||
} = useDashboardContainerContext();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const panels = select((state) => state.explicitInput.panels);
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
const useMargins = select((state) => state.explicitInput.useMargins);
|
||||
const expandedPanelId = select((state) => state.componentState.expandedPanelId);
|
||||
const dashboard = useDashboardContainer();
|
||||
const panels = dashboard.select((state) => state.explicitInput.panels);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
|
||||
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
||||
|
||||
// turn off panel transform animations for the first 500ms so that the dashboard doesn't animate on its first render.
|
||||
const [animatePanelTransforms, setAnimatePanelTransforms] = useState(false);
|
||||
|
@ -93,10 +88,10 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
|||
{} as { [key: string]: DashboardPanelState }
|
||||
);
|
||||
if (!getPanelLayoutsAreEqual(panels, updatedPanels)) {
|
||||
dispatch(setPanels(updatedPanels));
|
||||
dashboard.dispatch.setPanels(updatedPanels);
|
||||
}
|
||||
},
|
||||
[dispatch, panels, setPanels]
|
||||
[dashboard, panels]
|
||||
);
|
||||
|
||||
const classes = classNames({
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
|
||||
type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'>;
|
||||
|
||||
|
@ -55,7 +55,7 @@ const Item = React.forwardRef<HTMLDivElement, Props>(
|
|||
const {
|
||||
embeddable: { EmbeddablePanel: PanelComponent },
|
||||
} = pluginServices.getServices();
|
||||
const { embeddableInstance: container } = useDashboardContainerContext();
|
||||
const container = useDashboardContainer();
|
||||
|
||||
const expandPanel = expandedPanelId !== undefined && expandedPanelId === id;
|
||||
const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id;
|
||||
|
@ -134,9 +134,9 @@ export const DashboardGridItem = React.forwardRef<HTMLDivElement, Props>((props,
|
|||
settings: { isProjectEnabledInLabs },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const { useEmbeddableSelector: select } = useDashboardContainerContext();
|
||||
const dashboard = useDashboardContainer();
|
||||
|
||||
const isPrintMode = select((state) => state.explicitInput.viewMode) === ViewMode.PRINT;
|
||||
const isPrintMode = dashboard.select((state) => state.explicitInput.viewMode) === ViewMode.PRINT;
|
||||
const isEnabled = !isPrintMode && isProjectEnabledInLabs('labs:dashboard:deferBelowFold');
|
||||
|
||||
return isEnabled ? <ObservedItem ref={ref} {...props} /> : <Item ref={ref} {...props} />;
|
||||
|
|
|
@ -12,14 +12,14 @@ import { useEuiTheme } from '@elastic/eui';
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
|
||||
export const useDashboardGridSettings = (panelsInOrder: string[]) => {
|
||||
const { useEmbeddableSelector: select } = useDashboardContainerContext();
|
||||
const dashboard = useDashboardContainer();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const panels = select((state) => state.explicitInput.panels);
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
const panels = dashboard.select((state) => state.explicitInput.panels);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
|
||||
const layouts = useMemo(() => {
|
||||
return {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useCallback, useRef } from 'react';
|
|||
|
||||
import { EmbeddablePhaseEvent } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
import { DashboardLoadedEventStatus, DashboardRenderPerformanceStats } from '../../types';
|
||||
|
||||
type DashboardRenderPerformanceTracker = DashboardRenderPerformanceStats & {
|
||||
|
@ -30,7 +30,7 @@ const getDefaultPerformanceTracker: () => DashboardRenderPerformanceTracker = ()
|
|||
});
|
||||
|
||||
export const useDashboardPerformanceTracker = ({ panelCount }: { panelCount: number }) => {
|
||||
const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
|
||||
const dashboard = useDashboardContainer();
|
||||
|
||||
// reset performance tracker on each render.
|
||||
const performanceRefs = useRef<DashboardRenderPerformanceTracker>(getDefaultPerformanceTracker());
|
||||
|
@ -52,11 +52,11 @@ export const useDashboardPerformanceTracker = ({ panelCount }: { panelCount: num
|
|||
performanceRefs.current.doneCount++;
|
||||
if (performanceRefs.current.doneCount === panelCount) {
|
||||
performanceRefs.current.panelsRenderDoneTime = performance.now();
|
||||
dashboardContainer.reportPerformanceMetrics(performanceRefs.current);
|
||||
dashboard.reportPerformanceMetrics(performanceRefs.current);
|
||||
}
|
||||
}
|
||||
},
|
||||
[dashboardContainer, panelCount]
|
||||
[dashboard, panelCount]
|
||||
);
|
||||
|
||||
return { onPanelStatusChange };
|
||||
|
|
|
@ -26,9 +26,9 @@ import {
|
|||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DashboardContainerByValueInput } from '../../../../common';
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
|
||||
interface DashboardSettingsProps {
|
||||
onClose: () => void;
|
||||
|
@ -42,23 +42,18 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
|
|||
dashboardSavedObject: { checkForDuplicateDashboardTitle },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setStateFromSettingsFlyout },
|
||||
embeddableInstance: dashboardContainer,
|
||||
} = useDashboardContainerContext();
|
||||
const dashboard = useDashboardContainer();
|
||||
|
||||
const [dashboardSettingsState, setDashboardSettingsState] = useState({
|
||||
...dashboardContainer.getInputAsValueType(),
|
||||
...dashboard.getInput(),
|
||||
});
|
||||
|
||||
const [isTitleDuplicate, setIsTitleDuplicate] = useState(false);
|
||||
const [isTitleDuplicateConfirmed, setIsTitleDuplicateConfirmed] = useState(false);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
const lastSavedId = select((state) => state.componentState.lastSavedId);
|
||||
const lastSavedTitle = select((state) => state.explicitInput.title);
|
||||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const lastSavedTitle = dashboard.select((state) => state.explicitInput.title);
|
||||
|
||||
const isMounted = useMountedState();
|
||||
|
||||
|
@ -83,24 +78,19 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
|
|||
setIsApplying(false);
|
||||
|
||||
if (validTitle) {
|
||||
dispatch(setStateFromSettingsFlyout({ lastSavedId, ...dashboardSettingsState }));
|
||||
dashboard.dispatch.setStateFromSettingsFlyout({ lastSavedId, ...dashboardSettingsState });
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const updateDashboardSetting = useCallback(
|
||||
(newSettings: Partial<DashboardContainerByValueInput>) => {
|
||||
setDashboardSettingsState((prevDashboardSettingsState) => {
|
||||
return {
|
||||
...prevDashboardSettingsState,
|
||||
...newSettings,
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const updateDashboardSetting = useCallback((newSettings: Partial<DashboardContainerInput>) => {
|
||||
setDashboardSettingsState((prevDashboardSettingsState) => {
|
||||
return {
|
||||
...prevDashboardSettingsState,
|
||||
...newSettings,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderDuplicateTitleCallout = () => {
|
||||
if (!isTitleDuplicate) {
|
||||
|
|
|
@ -17,8 +17,8 @@ import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
|
|||
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
|
||||
export const useDebouncedWidthObserver = (wait = 250) => {
|
||||
const [width, setWidth] = useState<number>(0);
|
||||
|
@ -38,26 +38,25 @@ export const DashboardViewportComponent = () => {
|
|||
} = pluginServices.getServices();
|
||||
const controlsRoot = useRef(null);
|
||||
|
||||
const { useEmbeddableSelector: select, embeddableInstance: dashboardContainer } =
|
||||
useDashboardContainerContext();
|
||||
const dashboard = useDashboardContainer();
|
||||
|
||||
/**
|
||||
* Render Control group
|
||||
*/
|
||||
const controlGroup = dashboardContainer.controlGroup;
|
||||
const controlGroup = dashboard.controlGroup;
|
||||
useEffect(() => {
|
||||
if (controlGroup && controlsRoot.current) controlGroup.render(controlsRoot.current);
|
||||
}, [controlGroup]);
|
||||
|
||||
const panelCount = Object.keys(select((state) => state.explicitInput.panels)).length;
|
||||
const panelCount = Object.keys(dashboard.select((state) => state.explicitInput.panels)).length;
|
||||
const controlCount = Object.keys(
|
||||
select((state) => state.explicitInput.controlGroupInput?.panels) ?? {}
|
||||
dashboard.select((state) => state.explicitInput.controlGroupInput?.panels) ?? {}
|
||||
).length;
|
||||
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
const dashboardTitle = select((state) => state.explicitInput.title);
|
||||
const description = select((state) => state.explicitInput.description);
|
||||
const expandedPanelId = select((state) => state.componentState.expandedPanelId);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||
const description = dashboard.select((state) => state.explicitInput.description);
|
||||
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
||||
const controlsEnabled = isProjectEnabledInLabs('labs:dashboard:dashboardControls');
|
||||
|
||||
const { ref: resizeRef, width: viewportWidth } = useDebouncedWidthObserver();
|
||||
|
@ -98,15 +97,12 @@ export const DashboardViewportComponent = () => {
|
|||
// because ExitFullScreenButton sets isFullscreenMode to false on unmount while rerendering.
|
||||
// This specifically fixed maximizing/minimizing panels without exiting fullscreen mode.
|
||||
const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setFullScreenMode },
|
||||
} = useDashboardContainerContext();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const dashboard = useDashboardContainer();
|
||||
|
||||
const isFullScreenMode = select((state) => state.componentState.fullScreenMode);
|
||||
const isEmbeddedExternally = select((state) => state.componentState.isEmbeddedExternally);
|
||||
const isFullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
|
||||
const isEmbeddedExternally = dashboard.select(
|
||||
(state) => state.componentState.isEmbeddedExternally
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -114,7 +110,7 @@ const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
|
|||
{isFullScreenMode && (
|
||||
<EuiPortal>
|
||||
<ExitFullScreenButton
|
||||
onExit={() => dispatch(setFullScreenMode(false))}
|
||||
onExit={() => dashboard.dispatch.setFullScreenMode(false)}
|
||||
toggleChrome={!isEmbeddedExternally}
|
||||
/>
|
||||
</EuiPortal>
|
||||
|
|
|
@ -1,19 +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 { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { dashboardContainerReducers } from './state/dashboard_container_reducers';
|
||||
import { DashboardReduxState } from './types';
|
||||
import { DashboardContainer } from '..';
|
||||
|
||||
export const useDashboardContainerContext = () =>
|
||||
useReduxEmbeddableContext<
|
||||
DashboardReduxState,
|
||||
typeof dashboardContainerReducers,
|
||||
DashboardContainer
|
||||
>();
|
|
@ -1,123 +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 './_dashboard_container.scss';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
DashboardContainerFactory,
|
||||
DashboardContainerFactoryDefinition,
|
||||
DashboardCreationOptions,
|
||||
} from './embeddable/dashboard_container_factory';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DEFAULT_DASHBOARD_INPUT } from '../dashboard_constants';
|
||||
import { DashboardContainer } from './embeddable/dashboard_container';
|
||||
|
||||
export interface DashboardContainerRendererProps {
|
||||
savedObjectId?: string;
|
||||
getCreationOptions?: () => Promise<DashboardCreationOptions>;
|
||||
onDashboardContainerLoaded?: (dashboardContainer: DashboardContainer) => void;
|
||||
}
|
||||
|
||||
export const DashboardContainerRenderer = ({
|
||||
savedObjectId,
|
||||
getCreationOptions,
|
||||
onDashboardContainerLoaded,
|
||||
}: DashboardContainerRendererProps) => {
|
||||
const {
|
||||
embeddable,
|
||||
screenshotMode: { isScreenshotMode },
|
||||
customBranding,
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const dashboardRoot = useRef(null);
|
||||
const [dashboardIdToBuild, setDashboardIdToBuild] = useState<string | undefined>(savedObjectId);
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
|
||||
|
||||
useEffect(() => {
|
||||
// check if dashboard container is expecting id change... if not, update dashboardIdToBuild to force it to rebuild the container.
|
||||
if (!dashboardContainer) return;
|
||||
if (!dashboardContainer.isExpectingIdChange()) setDashboardIdToBuild(savedObjectId);
|
||||
|
||||
// Disabling exhaustive deps because this useEffect should only be triggered when the savedObjectId changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [savedObjectId]);
|
||||
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
let destroyContainer: () => void;
|
||||
|
||||
(async () => {
|
||||
const creationOptions = await getCreationOptions?.();
|
||||
const dashboardFactory = embeddable.getEmbeddableFactory(
|
||||
DASHBOARD_CONTAINER_TYPE
|
||||
) as DashboardContainerFactory & { create: DashboardContainerFactoryDefinition['create'] };
|
||||
const container = (await dashboardFactory?.create(
|
||||
{
|
||||
id,
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
...creationOptions?.initialInput,
|
||||
savedObjectId: dashboardIdToBuild,
|
||||
},
|
||||
undefined,
|
||||
creationOptions
|
||||
)) as DashboardContainer;
|
||||
|
||||
await container.untilInitialized();
|
||||
if (canceled) {
|
||||
container.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
if (dashboardRoot.current) {
|
||||
container.render(dashboardRoot.current);
|
||||
}
|
||||
onDashboardContainerLoaded?.(container);
|
||||
setDashboardContainer(container);
|
||||
|
||||
destroyContainer = () => container.destroy();
|
||||
})();
|
||||
return () => {
|
||||
canceled = true;
|
||||
destroyContainer?.();
|
||||
};
|
||||
// Disabling exhaustive deps because embeddable should only be created when the dashboardIdToBuild changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardIdToBuild]);
|
||||
|
||||
const viewportClasses = classNames(
|
||||
'dashboardViewport',
|
||||
{ 'dashboardViewport--screenshotMode': isScreenshotMode() },
|
||||
{ 'dashboardViewport--loading': loading }
|
||||
);
|
||||
|
||||
const loadingSpinner = showPlainSpinner ? (
|
||||
<EuiLoadingSpinner size="xxl" />
|
||||
) : (
|
||||
<EuiLoadingElastic size="xxl" />
|
||||
);
|
||||
return (
|
||||
<div className={viewportClasses}>{loading ? loadingSpinner : <div ref={dashboardRoot} />}</div>
|
||||
);
|
||||
};
|
||||
|
||||
// required for dynamic import using React.lazy()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default DashboardContainerRenderer;
|
|
@ -17,7 +17,7 @@ import { DashboardSaveModal } from './overlays/save_modal';
|
|||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { showCloneModal } from './overlays/show_clone_modal';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardContainerByValueInput } from '../../../../common';
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { SaveDashboardReturn } from '../../../services/dashboard_saved_object/types';
|
||||
|
||||
export function runSaveAs(this: DashboardContainer) {
|
||||
|
@ -31,15 +31,10 @@ export function runSaveAs(this: DashboardContainer) {
|
|||
dashboardSavedObject: { checkForDuplicateDashboardTitle, saveDashboardStateToSavedObject },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
getState,
|
||||
dispatch,
|
||||
actions: { setStateFromSaveModal, setLastSavedInput },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
const {
|
||||
explicitInput: currentState,
|
||||
componentState: { lastSavedId },
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
|
||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
||||
const onSave = async ({
|
||||
|
@ -81,7 +76,7 @@ export function runSaveAs(this: DashboardContainer) {
|
|||
// do not save if title is duplicate and is unconfirmed
|
||||
return {};
|
||||
}
|
||||
const stateToSave: DashboardContainerByValueInput = {
|
||||
const stateToSave: DashboardContainerInput = {
|
||||
...currentState,
|
||||
...stateFromSaveModal,
|
||||
};
|
||||
|
@ -103,8 +98,8 @@ export function runSaveAs(this: DashboardContainer) {
|
|||
stateFromSaveModal.lastSavedId = saveResult.id;
|
||||
if (saveResult.id) {
|
||||
batch(() => {
|
||||
dispatch(setStateFromSaveModal(stateFromSaveModal));
|
||||
dispatch(setLastSavedInput(stateToSave));
|
||||
this.dispatch.setStateFromSaveModal(stateFromSaveModal);
|
||||
this.dispatch.setLastSavedInput(stateToSave);
|
||||
});
|
||||
}
|
||||
if (newCopyOnSave || !lastSavedId) this.expectIdChange();
|
||||
|
@ -136,22 +131,17 @@ export async function runQuickSave(this: DashboardContainer) {
|
|||
dashboardSavedObject: { saveDashboardStateToSavedObject },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
getState,
|
||||
dispatch,
|
||||
actions: { setLastSavedInput },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
const {
|
||||
explicitInput: currentState,
|
||||
componentState: { lastSavedId },
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
|
||||
const saveResult = await saveDashboardStateToSavedObject({
|
||||
lastSavedId,
|
||||
currentState,
|
||||
saveOptions: {},
|
||||
});
|
||||
dispatch(setLastSavedInput(currentState));
|
||||
this.dispatch.setLastSavedInput(currentState);
|
||||
|
||||
return saveResult;
|
||||
}
|
||||
|
@ -161,12 +151,7 @@ export async function runClone(this: DashboardContainer) {
|
|||
dashboardSavedObject: { saveDashboardStateToSavedObject, checkForDuplicateDashboardTitle },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
getState,
|
||||
dispatch,
|
||||
actions: { setTitle },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
const { explicitInput: currentState } = getState();
|
||||
const { explicitInput: currentState } = this.getState();
|
||||
|
||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
||||
const onClone = async (
|
||||
|
@ -191,7 +176,7 @@ export async function runClone(this: DashboardContainer) {
|
|||
currentState: { ...currentState, title: newTitle },
|
||||
});
|
||||
|
||||
dispatch(setTitle(newTitle));
|
||||
this.dispatch.setTitle(newTitle);
|
||||
resolve(saveResult);
|
||||
this.expectIdChange();
|
||||
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
||||
|
|
|
@ -10,9 +10,9 @@ import React from 'react';
|
|||
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { DashboardSettings } from '../../component/settings/settings_flyout';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardSettings } from '../../component/settings/settings_flyout';
|
||||
import { DashboardContainer, DashboardContainerContext } from '../dashboard_container';
|
||||
|
||||
export function showSettings(this: DashboardContainer) {
|
||||
const {
|
||||
|
@ -22,26 +22,20 @@ export function showSettings(this: DashboardContainer) {
|
|||
overlays,
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
dispatch,
|
||||
Wrapper: DashboardReduxWrapper,
|
||||
actions: { setHasOverlays },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
|
||||
// TODO Move this action into DashboardContainer.openOverlay
|
||||
dispatch(setHasOverlays(true));
|
||||
this.dispatch.setHasOverlays(true);
|
||||
|
||||
this.openOverlay(
|
||||
overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<DashboardReduxWrapper>
|
||||
<DashboardContainerContext.Provider value={this}>
|
||||
<DashboardSettings
|
||||
onClose={() => {
|
||||
dispatch(setHasOverlays(false));
|
||||
this.dispatch.setHasOverlays(false);
|
||||
this.clearOverlays();
|
||||
}}
|
||||
/>
|
||||
</DashboardReduxWrapper>,
|
||||
</DashboardContainerContext.Provider>,
|
||||
{ theme$ }
|
||||
),
|
||||
{
|
||||
|
@ -49,7 +43,7 @@ export function showSettings(this: DashboardContainer) {
|
|||
'data-test-subj': 'dashboardSettingsFlyout',
|
||||
onClose: (flyout) => {
|
||||
this.clearOverlays();
|
||||
dispatch(setHasOverlays(false));
|
||||
this.dispatch.setHasOverlays(false);
|
||||
flyout.close();
|
||||
},
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { mockControlGroupInput } from '@kbn/controls-plugin/common/mocks';
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public/control_group/embeddable/control_group_container';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { combineDashboardFiltersWithControlGroupFilters } from './dashboard_control_group_integration';
|
||||
|
||||
jest.mock('@kbn/controls-plugin/public/control_group/embeddable/control_group_container');
|
||||
|
@ -52,7 +52,7 @@ const testFilter3: Filter = {
|
|||
};
|
||||
|
||||
const mockControlGroupContainer = new ControlGroupContainer(
|
||||
{ getTools: () => {} } as unknown as ReduxEmbeddablePackage,
|
||||
{ getTools: () => {} } as unknown as ReduxToolsPackage,
|
||||
mockControlGroupInput()
|
||||
);
|
||||
|
|
@ -6,24 +6,21 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import _, { identity, pickBy } from 'lodash';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
|
||||
import { debounceTime, distinctUntilChanged, distinctUntilKeyChanged, skip } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
ControlGroupInput,
|
||||
CONTROL_GROUP_TYPE,
|
||||
getDefaultControlGroupInput,
|
||||
persistableControlGroupInputIsEqual,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { ControlGroupContainer, ControlGroupOutput } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
|
||||
import { DashboardContainer } from '../../dashboard_container';
|
||||
import { pluginServices } from '../../../../services/plugin_services';
|
||||
import { DashboardContainerByValueInput } from '../../../../../common';
|
||||
import { DashboardContainerInput } from '../../../../../common';
|
||||
|
||||
interface DiffChecks {
|
||||
[key: string]: (a?: unknown, b?: unknown) => boolean;
|
||||
|
@ -35,61 +32,16 @@ const distinctUntilDiffCheck = <T extends {}>(a: T, b: T, diffChecks: DiffChecks
|
|||
.includes(false);
|
||||
|
||||
type DashboardControlGroupCommonKeys = keyof Pick<
|
||||
DashboardContainerByValueInput | ControlGroupInput,
|
||||
DashboardContainerInput | ControlGroupInput,
|
||||
'filters' | 'lastReloadRequestTime' | 'timeRange' | 'query'
|
||||
>;
|
||||
|
||||
export async function startControlGroupIntegration(
|
||||
this: DashboardContainer,
|
||||
initialInput: DashboardContainerByValueInput
|
||||
): Promise<ControlGroupContainer | undefined> {
|
||||
const {
|
||||
embeddable: { getEmbeddableFactory },
|
||||
} = pluginServices.getServices();
|
||||
const controlsGroupFactory = getEmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
>(CONTROL_GROUP_TYPE);
|
||||
const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput;
|
||||
const controlGroup = await controlsGroupFactory?.create({
|
||||
id: `control_group_${id ?? 'new_dashboard'}`,
|
||||
...getDefaultControlGroupInput(),
|
||||
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
|
||||
timeRange,
|
||||
viewMode,
|
||||
filters,
|
||||
query,
|
||||
});
|
||||
if (!controlGroup || isErrorEmbeddable(controlGroup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.untilInitialized().then(() => {
|
||||
const stopSyncingControlGroup =
|
||||
startSyncingDashboardControlGroup.bind(this)()?.stopSyncingWithControlGroup;
|
||||
this.onDestroyControlGroup = () => {
|
||||
stopSyncingControlGroup?.();
|
||||
this.controlGroup?.destroy();
|
||||
};
|
||||
});
|
||||
await controlGroup.untilInitialized();
|
||||
return controlGroup;
|
||||
}
|
||||
|
||||
function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||
export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||
if (!this.controlGroup) return;
|
||||
const subscriptions = new Subscription();
|
||||
|
||||
const {
|
||||
actions: { setControlGroupState },
|
||||
dispatch,
|
||||
} = this.getReduxEmbeddableTools();
|
||||
|
||||
const isControlGroupInputEqual = () =>
|
||||
persistableControlGroupInputIsEqual(
|
||||
this.controlGroup!.getInput(),
|
||||
this.getInputAsValueType().controlGroupInput
|
||||
this.getInput().controlGroupInput
|
||||
);
|
||||
|
||||
// Because dashboard container stores control group state, certain control group changes need to be passed up dashboard container
|
||||
|
@ -99,7 +51,7 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
chainingSystem: deepEqual,
|
||||
ignoreParentSettings: deepEqual,
|
||||
};
|
||||
subscriptions.add(
|
||||
this.subscriptions.add(
|
||||
this.controlGroup
|
||||
.getInput$()
|
||||
.pipe(
|
||||
|
@ -111,9 +63,12 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
const { panels, controlStyle, chainingSystem, ignoreParentSettings } =
|
||||
this.controlGroup!.getInput();
|
||||
if (!isControlGroupInputEqual()) {
|
||||
dispatch(
|
||||
setControlGroupState({ panels, controlStyle, chainingSystem, ignoreParentSettings })
|
||||
);
|
||||
this.dispatch.setControlGroupState({
|
||||
panels,
|
||||
controlStyle,
|
||||
chainingSystem,
|
||||
ignoreParentSettings,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -129,23 +84,20 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
};
|
||||
|
||||
// pass down any pieces of input needed to refetch or force refetch data for the controls
|
||||
subscriptions.add(
|
||||
(this.getInput$() as Readonly<Observable<DashboardContainerByValueInput>>)
|
||||
this.subscriptions.add(
|
||||
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
distinctUntilDiffCheck<DashboardContainerByValueInput>(a, b, dashboardRefetchDiff)
|
||||
distinctUntilDiffCheck<DashboardContainerInput>(a, b, dashboardRefetchDiff)
|
||||
)
|
||||
)
|
||||
.subscribe(() => {
|
||||
const newInput: { [key: string]: unknown } = {};
|
||||
(Object.keys(dashboardRefetchDiff) as DashboardControlGroupCommonKeys[]).forEach((key) => {
|
||||
if (
|
||||
!dashboardRefetchDiff[key]?.(
|
||||
this.getInputAsValueType()[key],
|
||||
this.controlGroup!.getInput()[key]
|
||||
)
|
||||
!dashboardRefetchDiff[key]?.(this.getInput()[key], this.controlGroup!.getInput()[key])
|
||||
) {
|
||||
newInput[key] = this.getInputAsValueType()[key];
|
||||
newInput[key] = this.getInput()[key];
|
||||
}
|
||||
});
|
||||
if (Object.keys(newInput).length > 0) {
|
||||
|
@ -155,24 +107,24 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
);
|
||||
|
||||
// dashboard may reset the control group input when discarding changes. Subscribe to these changes and update accordingly
|
||||
subscriptions.add(
|
||||
(this.getInput$() as Readonly<Observable<DashboardContainerByValueInput>>)
|
||||
this.subscriptions.add(
|
||||
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||
.pipe(debounceTime(10), distinctUntilKeyChanged('controlGroupInput'))
|
||||
.subscribe(() => {
|
||||
if (!isControlGroupInputEqual()) {
|
||||
if (!this.getInputAsValueType().controlGroupInput) {
|
||||
if (!this.getInput().controlGroupInput) {
|
||||
this.controlGroup!.updateInput(getDefaultControlGroupInput());
|
||||
return;
|
||||
}
|
||||
this.controlGroup!.updateInput({
|
||||
...this.getInputAsValueType().controlGroupInput,
|
||||
...this.getInput().controlGroupInput,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// when control group outputs filters, force a refresh!
|
||||
subscriptions.add(
|
||||
this.subscriptions.add(
|
||||
this.controlGroup
|
||||
.getOutput$()
|
||||
.pipe(
|
||||
|
@ -184,23 +136,23 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
.subscribe(() => this.forceRefresh(false)) // we should not reload the control group when the control group output changes - otherwise, performance is severely impacted
|
||||
);
|
||||
|
||||
subscriptions.add(
|
||||
this.subscriptions.add(
|
||||
this.controlGroup
|
||||
.getOutput$()
|
||||
.pipe(
|
||||
distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) =>
|
||||
_.isEqual(timesliceA, timesliceB)
|
||||
isEqual(timesliceA, timesliceB)
|
||||
)
|
||||
)
|
||||
.subscribe(({ timeslice }) => {
|
||||
if (!_.isEqual(timeslice, this.getInputAsValueType().timeslice)) {
|
||||
this.updateInput({ timeslice });
|
||||
if (!isEqual(timeslice, this.getInput().timeslice)) {
|
||||
this.dispatch.setTimeslice(timeslice);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// the Control Group needs to know when any dashboard children are loading in order to know when to move on to the next time slice when playing.
|
||||
subscriptions.add(
|
||||
this.subscriptions.add(
|
||||
this.getAnyChildOutputChange$().subscribe(() => {
|
||||
if (!this.controlGroup) {
|
||||
return;
|
||||
|
@ -216,12 +168,6 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
this.controlGroup.anyControlOutputConsumerLoading$.next(false);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
stopSyncingWithControlGroup: () => {
|
||||
subscriptions.unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const combineDashboardFiltersWithControlGroupFilters = (
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* 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 {
|
||||
ContactCardEmbeddableInput,
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
} from '@kbn/embeddable-plugin/public/lib/test_samples';
|
||||
import {
|
||||
ControlGroupInput,
|
||||
ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { createDashboard } from './create_dashboard';
|
||||
import { getSampleDashboardPanel } from '../../../mocks';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
const embeddableId = 'create-dat-dashboard';
|
||||
|
||||
test('throws error when no data views are available', async () => {
|
||||
pluginServices.getServices().data.dataViews.getDefaultDataView = jest
|
||||
.fn()
|
||||
.mockReturnValue(undefined);
|
||||
await expect(async () => {
|
||||
await createDashboard(embeddableId);
|
||||
}).rejects.toThrow('Dashboard requires at least one data view before it can be initialized.');
|
||||
|
||||
// reset get default data view
|
||||
pluginServices.getServices().data.dataViews.getDefaultDataView = jest.fn().mockResolvedValue({});
|
||||
});
|
||||
|
||||
test('throws error when provided validation function returns invalid', async () => {
|
||||
const creationOptions: DashboardCreationOptions = {
|
||||
validateLoadedSavedObject: jest.fn().mockImplementation(() => false),
|
||||
};
|
||||
await expect(async () => {
|
||||
await createDashboard(embeddableId, creationOptions, 0, 'test-id');
|
||||
}).rejects.toThrow('Dashboard failed saved object result validation');
|
||||
});
|
||||
|
||||
test('pulls state from dashboard saved object when given a saved object id', async () => {
|
||||
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ dashboardInput: { description: 'wow would you look at that? Wow.' } });
|
||||
const dashboard = await createDashboard(embeddableId, {}, 0, 'wow-such-id');
|
||||
expect(
|
||||
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject
|
||||
).toHaveBeenCalledWith({ id: 'wow-such-id' });
|
||||
expect(dashboard.getState().explicitInput.description).toBe('wow would you look at that? Wow.');
|
||||
});
|
||||
|
||||
test('pulls state from session storage which overrides state from saved object', async () => {
|
||||
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ dashboardInput: { description: 'wow this description is okay' } });
|
||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||
.fn()
|
||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||
const dashboard = await createDashboard(
|
||||
embeddableId,
|
||||
{ useSessionStorageIntegration: true },
|
||||
0,
|
||||
'wow-such-id'
|
||||
);
|
||||
expect(dashboard.getState().explicitInput.description).toBe(
|
||||
'wow this description marginally better'
|
||||
);
|
||||
});
|
||||
|
||||
test('pulls state from creation options initial input which overrides all other state sources', async () => {
|
||||
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ dashboardInput: { description: 'wow this description is okay' } });
|
||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||
.fn()
|
||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||
const dashboard = await createDashboard(
|
||||
embeddableId,
|
||||
{
|
||||
useSessionStorageIntegration: true,
|
||||
initialInput: { description: 'wow this description is a masterpiece' },
|
||||
},
|
||||
0,
|
||||
'wow-such-id'
|
||||
);
|
||||
expect(dashboard.getState().explicitInput.description).toBe(
|
||||
'wow this description is a masterpiece'
|
||||
);
|
||||
});
|
||||
|
||||
test('applies filters and query from state to query service', async () => {
|
||||
const filters: Filter[] = [
|
||||
{ meta: { alias: 'test', disabled: false, negate: false, index: 'test' } },
|
||||
];
|
||||
const query = { language: 'kql', query: 'query' };
|
||||
await createDashboard(embeddableId, {
|
||||
useUnifiedSearchIntegration: true,
|
||||
unifiedSearchSettings: {
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
},
|
||||
initialInput: { filters, query },
|
||||
});
|
||||
expect(pluginServices.getServices().data.query.queryString.setQuery).toHaveBeenCalledWith(query);
|
||||
expect(pluginServices.getServices().data.query.filterManager.setAppFilters).toHaveBeenCalledWith(
|
||||
filters
|
||||
);
|
||||
});
|
||||
|
||||
test('applies time range and refresh interval from initial input to query service if time restore is on', async () => {
|
||||
const timeRange = { from: new Date().toISOString(), to: new Date().toISOString() };
|
||||
const refreshInterval = { pause: false, value: 42 };
|
||||
await createDashboard(embeddableId, {
|
||||
useUnifiedSearchIntegration: true,
|
||||
unifiedSearchSettings: {
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
},
|
||||
initialInput: { timeRange, refreshInterval, timeRestore: true },
|
||||
});
|
||||
expect(
|
||||
pluginServices.getServices().data.query.timefilter.timefilter.setTime
|
||||
).toHaveBeenCalledWith(timeRange);
|
||||
expect(
|
||||
pluginServices.getServices().data.query.timefilter.timefilter.setRefreshInterval
|
||||
).toHaveBeenCalledWith(refreshInterval);
|
||||
});
|
||||
|
||||
test('applied time range from query service to initial input if time restore is off', async () => {
|
||||
const timeRange = { from: new Date().toISOString(), to: new Date().toISOString() };
|
||||
pluginServices.getServices().data.query.timefilter.timefilter.getTime = jest
|
||||
.fn()
|
||||
.mockReturnValue(timeRange);
|
||||
const dashboard = await createDashboard(embeddableId, {
|
||||
useUnifiedSearchIntegration: true,
|
||||
unifiedSearchSettings: {
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
},
|
||||
});
|
||||
expect(dashboard.getState().explicitInput.timeRange).toEqual(timeRange);
|
||||
});
|
||||
|
||||
test('replaces panel with incoming embeddable if id matches existing panel', async () => {
|
||||
const incomingEmbeddable: EmbeddablePackageState = {
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
input: {
|
||||
id: 'i_match',
|
||||
firstName: 'wow look at this replacement wow',
|
||||
} as ContactCardEmbeddableInput,
|
||||
embeddableId: 'i_match',
|
||||
};
|
||||
const dashboard = await createDashboard(embeddableId, {
|
||||
incomingEmbeddable,
|
||||
initialInput: {
|
||||
panels: {
|
||||
i_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: {
|
||||
id: 'i_match',
|
||||
firstName: 'oh no, I am about to get replaced',
|
||||
},
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(dashboard.getState().explicitInput.panels.i_match.explicitInput).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
id: 'i_match',
|
||||
firstName: 'wow look at this replacement wow',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('creates new embeddable with incoming embeddable if id does not match existing panel', async () => {
|
||||
const incomingEmbeddable: EmbeddablePackageState = {
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
input: {
|
||||
id: 'i_match',
|
||||
firstName: 'wow look at this new panel wow',
|
||||
} as ContactCardEmbeddableInput,
|
||||
embeddableId: 'i_match',
|
||||
};
|
||||
const mockContactCardFactory = {
|
||||
create: jest.fn().mockReturnValue({ destroy: jest.fn() }),
|
||||
getDefaultInput: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockContactCardFactory);
|
||||
|
||||
await createDashboard(embeddableId, {
|
||||
incomingEmbeddable,
|
||||
initialInput: {
|
||||
panels: {
|
||||
i_do_not_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: {
|
||||
id: 'i_do_not_match',
|
||||
firstName: 'phew... I will not be replaced',
|
||||
},
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
expect(mockContactCardFactory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'i_match',
|
||||
firstName: 'wow look at this new panel wow',
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates a control group from the control group factory and waits for it to be initialized', async () => {
|
||||
const mockControlGroupContainer = {
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
updateInput: jest.fn(),
|
||||
untilInitialized: jest.fn(),
|
||||
getInput: jest.fn().mockReturnValue({}),
|
||||
getInput$: jest.fn().mockReturnValue(new Observable()),
|
||||
getOutput$: jest.fn().mockReturnValue(new Observable()),
|
||||
} as unknown as ControlGroupContainer;
|
||||
const mockControlGroupFactory = {
|
||||
create: jest.fn().mockReturnValue(mockControlGroupContainer),
|
||||
} as unknown as ControlGroupContainerFactory;
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockControlGroupFactory);
|
||||
await createDashboard(embeddableId, {
|
||||
useControlGroupIntegration: true,
|
||||
initialInput: {
|
||||
controlGroupInput: { controlStyle: 'twoLine' } as unknown as ControlGroupInput,
|
||||
},
|
||||
});
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
expect(pluginServices.getServices().embeddable.getEmbeddableFactory).toHaveBeenCalledWith(
|
||||
'control_group'
|
||||
);
|
||||
expect(mockControlGroupFactory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ controlStyle: 'twoLine' })
|
||||
);
|
||||
expect(mockControlGroupContainer.untilInitialized).toHaveBeenCalled();
|
||||
});
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* 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 { Subject } from 'rxjs';
|
||||
import { cloneDeep, identity, pickBy } from 'lodash';
|
||||
|
||||
import {
|
||||
ControlGroupInput,
|
||||
CONTROL_GROUP_TYPE,
|
||||
getDefaultControlGroupInput,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public';
|
||||
import { isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { type ControlGroupContainer, ControlGroupOutput } from '@kbn/controls-plugin/public';
|
||||
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
|
||||
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
||||
import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data_views';
|
||||
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
|
||||
import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration';
|
||||
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param creationOptions
|
||||
*/
|
||||
export const createDashboard = async (
|
||||
embeddableId: string,
|
||||
creationOptions?: DashboardCreationOptions,
|
||||
dashboardCreationStartTime?: number,
|
||||
savedObjectId?: string
|
||||
): Promise<DashboardContainer> => {
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Unpack services & Options
|
||||
// --------------------------------------------------------------------------------------
|
||||
const {
|
||||
dashboardSessionStorage,
|
||||
embeddable: { getEmbeddableFactory },
|
||||
data: {
|
||||
dataViews,
|
||||
query: queryService,
|
||||
search: { session },
|
||||
},
|
||||
dashboardSavedObject: { loadDashboardStateFromSavedObject },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
queryString,
|
||||
filterManager,
|
||||
timefilter: { timefilter: timefilterService },
|
||||
} = queryService;
|
||||
|
||||
const {
|
||||
searchSessionSettings,
|
||||
unifiedSearchSettings,
|
||||
validateLoadedSavedObject,
|
||||
useControlGroupIntegration,
|
||||
useUnifiedSearchIntegration,
|
||||
initialInput: overrideInput,
|
||||
useSessionStorageIntegration,
|
||||
} = creationOptions ?? {};
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Create method which allows work to be done on the dashboard container when it's ready.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const dashboardContainerReady$ = new Subject<DashboardContainer>();
|
||||
const untilDashboardReady = () =>
|
||||
new Promise<DashboardContainer>((resolve) => {
|
||||
const subscription = dashboardContainerReady$.subscribe((container) => {
|
||||
subscription.unsubscribe();
|
||||
resolve(container);
|
||||
});
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Lazy load required systems and Dashboard saved object.
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
const reduxEmbeddablePackagePromise = lazyLoadReduxToolsPackage();
|
||||
const defaultDataViewAssignmentPromise = dataViews.getDefaultDataView();
|
||||
const dashboardSavedObjectPromise = savedObjectId
|
||||
? loadDashboardStateFromSavedObject({ id: savedObjectId })
|
||||
: Promise.resolve(undefined);
|
||||
|
||||
const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([
|
||||
reduxEmbeddablePackagePromise,
|
||||
dashboardSavedObjectPromise,
|
||||
defaultDataViewAssignmentPromise,
|
||||
]);
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Run validations.
|
||||
// --------------------------------------------------------------------------------------
|
||||
if (!defaultDataView) {
|
||||
throw new Error('Dashboard requires at least one data view before it can be initialized.');
|
||||
}
|
||||
|
||||
if (
|
||||
savedObjectResult &&
|
||||
validateLoadedSavedObject &&
|
||||
!validateLoadedSavedObject(savedObjectResult)
|
||||
) {
|
||||
throw new Error('Dashboard failed saved object result validation');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Gather input from session storage if integration is used.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const sessionStorageInput = ((): Partial<DashboardContainerInput> | undefined => {
|
||||
if (!useSessionStorageIntegration) return;
|
||||
return dashboardSessionStorage.getState(savedObjectId);
|
||||
})();
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Combine input from saved object, session storage, & passed input to create initial input.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const initialInput: DashboardContainerInput = cloneDeep({
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
...(savedObjectResult?.dashboardInput ?? {}),
|
||||
...sessionStorageInput,
|
||||
...overrideInput,
|
||||
id: embeddableId,
|
||||
});
|
||||
|
||||
initialInput.executionContext = {
|
||||
type: 'dashboard',
|
||||
description: initialInput.title,
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set up unified search integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
if (useUnifiedSearchIntegration && unifiedSearchSettings?.kbnUrlStateStorage) {
|
||||
const { filters, query, timeRestore, timeRange, refreshInterval } = initialInput;
|
||||
const { kbnUrlStateStorage } = unifiedSearchSettings;
|
||||
|
||||
// apply filters and query to the query service
|
||||
filterManager.setAppFilters(cloneDeep(filters ?? []));
|
||||
queryString.setQuery(query ?? queryString.getDefaultQuery());
|
||||
|
||||
/**
|
||||
* If a global time range is not set explicitly and the time range was saved with the dashboard, apply
|
||||
* time range and refresh interval to the query service. Otherwise, set the current dashboard time range
|
||||
* from the query service. The order of the following lines is very important.
|
||||
*/
|
||||
if (timeRestore) {
|
||||
if (timeRange) timefilterService.setTime(timeRange);
|
||||
if (refreshInterval) timefilterService.setRefreshInterval(refreshInterval);
|
||||
}
|
||||
|
||||
const { stop: stopSyncingQueryServiceStateWithUrl } = syncGlobalQueryStateWithUrl(
|
||||
queryService,
|
||||
kbnUrlStateStorage
|
||||
);
|
||||
|
||||
if (!timeRestore) {
|
||||
initialInput.timeRange = timefilterService.getTime();
|
||||
}
|
||||
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
const stopSyncingUnifiedSearchState =
|
||||
syncUnifiedSearchState.bind(dashboardContainer)(kbnUrlStateStorage);
|
||||
dashboardContainer.stopSyncingWithUnifiedSearch = () => {
|
||||
stopSyncingUnifiedSearchState();
|
||||
stopSyncingQueryServiceStateWithUrl();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Place the incoming embeddable if there is one
|
||||
// --------------------------------------------------------------------------------------
|
||||
const incomingEmbeddable = creationOptions?.incomingEmbeddable;
|
||||
if (incomingEmbeddable) {
|
||||
initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable.
|
||||
if (
|
||||
incomingEmbeddable.embeddableId &&
|
||||
Boolean(initialInput.panels[incomingEmbeddable.embeddableId])
|
||||
) {
|
||||
// this embeddable already exists, we will update the explicit input.
|
||||
const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId];
|
||||
const sameType = panelToUpdate.type === incomingEmbeddable.type;
|
||||
|
||||
panelToUpdate.type = incomingEmbeddable.type;
|
||||
panelToUpdate.explicitInput = {
|
||||
// if the incoming panel is the same type as what was there before we can safely spread the old panel's explicit input
|
||||
...(sameType ? panelToUpdate.explicitInput : {}),
|
||||
|
||||
...incomingEmbeddable.input,
|
||||
id: incomingEmbeddable.embeddableId,
|
||||
|
||||
// maintain hide panel titles setting.
|
||||
hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles,
|
||||
};
|
||||
} else {
|
||||
// otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created.
|
||||
untilDashboardReady().then((container) =>
|
||||
container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set up search sessions integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
if (searchSessionSettings) {
|
||||
const { sessionIdToRestore } = searchSessionSettings;
|
||||
|
||||
// if this incoming embeddable has a session, continue it.
|
||||
if (incomingEmbeddable?.searchSessionId) {
|
||||
session.continue(incomingEmbeddable.searchSessionId);
|
||||
}
|
||||
if (sessionIdToRestore) {
|
||||
session.restore(sessionIdToRestore);
|
||||
}
|
||||
const existingSession = session.getSessionId();
|
||||
|
||||
const initialSearchSessionId =
|
||||
sessionIdToRestore ??
|
||||
(existingSession && incomingEmbeddable ? existingSession : session.start());
|
||||
|
||||
untilDashboardReady().then((container) => {
|
||||
startDashboardSearchSessionIntegration.bind(container)(
|
||||
creationOptions?.searchSessionSettings
|
||||
);
|
||||
});
|
||||
initialInput.searchSessionId = initialSearchSessionId;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Start the control group integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
if (useControlGroupIntegration) {
|
||||
const controlsGroupFactory = getEmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
>(CONTROL_GROUP_TYPE);
|
||||
const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput;
|
||||
const controlGroup = await controlsGroupFactory?.create({
|
||||
id: `control_group_${id ?? 'new_dashboard'}`,
|
||||
...getDefaultControlGroupInput(),
|
||||
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
|
||||
timeRange,
|
||||
viewMode,
|
||||
filters,
|
||||
query,
|
||||
});
|
||||
if (!controlGroup || isErrorEmbeddable(controlGroup)) {
|
||||
throw new Error('Error in control group startup');
|
||||
}
|
||||
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
dashboardContainer.controlGroup = controlGroup;
|
||||
startSyncingDashboardControlGroup.bind(dashboardContainer)();
|
||||
});
|
||||
await controlGroup.untilInitialized();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Start the data views integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
dashboardContainer.subscriptions.add(startSyncingDashboardDataViews.bind(dashboardContainer)());
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Build and return the dashboard container.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const dashboardContainer = new DashboardContainer(
|
||||
initialInput,
|
||||
reduxEmbeddablePackage,
|
||||
savedObjectResult?.dashboardInput,
|
||||
dashboardCreationStartTime,
|
||||
undefined,
|
||||
creationOptions,
|
||||
savedObjectId
|
||||
);
|
||||
dashboardContainerReady$.next(dashboardContainer);
|
||||
return dashboardContainer;
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { debounceTime, pairwise, skip } from 'rxjs/operators';
|
||||
|
||||
import { noSearchSessionStorageCapabilityMessage } from '@kbn/data-plugin/public';
|
||||
|
||||
import { DashboardContainer } from '../../dashboard_container';
|
||||
import { DashboardContainerInput } from '../../../../../common';
|
||||
import { pluginServices } from '../../../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../../../dashboard_constants';
|
||||
import { DashboardCreationOptions } from '../../dashboard_container_factory';
|
||||
import { getShouldRefresh } from '../../../state/diffing/dashboard_diffing_integration';
|
||||
|
||||
/**
|
||||
* Enables dashboard search sessions.
|
||||
*/
|
||||
export function startDashboardSearchSessionIntegration(
|
||||
this: DashboardContainer,
|
||||
searchSessionSettings: DashboardCreationOptions['searchSessionSettings']
|
||||
) {
|
||||
if (!searchSessionSettings) return;
|
||||
|
||||
const {
|
||||
data: {
|
||||
search: { session },
|
||||
},
|
||||
dashboardCapabilities: { storeSearchSession: canStoreSearchSession },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const {
|
||||
sessionIdUrlChangeObservable,
|
||||
getSearchSessionIdFromURL,
|
||||
removeSessionIdFromUrl,
|
||||
createSessionRestorationDataProvider,
|
||||
} = searchSessionSettings;
|
||||
|
||||
session.enableStorage(createSessionRestorationDataProvider(this), {
|
||||
isDisabled: () =>
|
||||
canStoreSearchSession
|
||||
? { disabled: false }
|
||||
: {
|
||||
disabled: true,
|
||||
reasonText: noSearchSessionStorageCapabilityMessage,
|
||||
},
|
||||
});
|
||||
|
||||
// force refresh when the session id in the URL changes. This will also fire off the "handle search session change" below.
|
||||
const searchSessionIdChangeSubscription = sessionIdUrlChangeObservable
|
||||
?.pipe(skip(1))
|
||||
.subscribe(() => this.forceRefresh());
|
||||
|
||||
// listen to and compare states to determine when to launch a new session.
|
||||
this.getInput$()
|
||||
.pipe(pairwise(), debounceTime(CHANGE_CHECK_DEBOUNCE))
|
||||
.subscribe(async (states) => {
|
||||
const [previous, current] = states as DashboardContainerInput[];
|
||||
const shouldRefetch = await getShouldRefresh.bind(this)(previous, current);
|
||||
if (!shouldRefetch) return;
|
||||
|
||||
const currentSearchSessionId = this.getState().explicitInput.searchSessionId;
|
||||
|
||||
const updatedSearchSessionId: string | undefined = (() => {
|
||||
// do not update session id if this is irrelevant state change to prevent excessive searches
|
||||
if (!shouldRefetch) return;
|
||||
|
||||
let searchSessionIdFromURL = getSearchSessionIdFromURL();
|
||||
if (searchSessionIdFromURL) {
|
||||
if (session.isRestore() && session.isCurrentSession(searchSessionIdFromURL)) {
|
||||
// we had previously been in a restored session but have now changed state so remove the session id from the URL.
|
||||
removeSessionIdFromUrl();
|
||||
searchSessionIdFromURL = undefined;
|
||||
} else {
|
||||
session.restore(searchSessionIdFromURL);
|
||||
}
|
||||
}
|
||||
return searchSessionIdFromURL ?? session.start();
|
||||
})();
|
||||
|
||||
if (updatedSearchSessionId && updatedSearchSessionId !== currentSearchSessionId) {
|
||||
this.dispatch.setSearchSessionId(updatedSearchSessionId);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscriptions.add(searchSessionIdChangeSubscription);
|
||||
}
|
|
@ -31,19 +31,12 @@ export function syncUnifiedSearchState(
|
|||
const { queryString, timefilter } = queryService;
|
||||
const { timefilter: timefilterService } = timefilter;
|
||||
|
||||
const {
|
||||
getState,
|
||||
dispatch,
|
||||
onStateChange,
|
||||
actions: { setFiltersAndQuery, setTimeRange, setRefreshInterval },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
|
||||
// get Observable for when the dashboard's saved filters or query change.
|
||||
const OnFiltersChange$ = new Subject<{ filters: Filter[]; query: Query }>();
|
||||
const unsubscribeFromSavedFilterChanges = onStateChange(() => {
|
||||
const unsubscribeFromSavedFilterChanges = this.onStateChange(() => {
|
||||
const {
|
||||
explicitInput: { filters, query },
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
OnFiltersChange$.next({
|
||||
filters: filters ?? [],
|
||||
query: query ?? queryString.getDefaultQuery(),
|
||||
|
@ -53,7 +46,7 @@ export function syncUnifiedSearchState(
|
|||
// starts syncing app filters between dashboard state and filterManager
|
||||
const {
|
||||
explicitInput: { filters, query },
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
const intermediateFilterState: { filters: Filter[]; query: Query } = {
|
||||
query: query ?? queryString.getDefaultQuery(),
|
||||
filters: filters ?? [],
|
||||
|
@ -66,7 +59,7 @@ export function syncUnifiedSearchState(
|
|||
set: ({ filters: newFilters, query: newQuery }) => {
|
||||
intermediateFilterState.filters = cleanFiltersForSerialize(newFilters);
|
||||
intermediateFilterState.query = newQuery;
|
||||
dispatch(setFiltersAndQuery(intermediateFilterState));
|
||||
this.dispatch.setFiltersAndQuery(intermediateFilterState);
|
||||
},
|
||||
state$: OnFiltersChange$.pipe(distinctUntilChanged()),
|
||||
},
|
||||
|
@ -78,11 +71,11 @@ export function syncUnifiedSearchState(
|
|||
|
||||
const timeUpdateSubscription = timefilterService
|
||||
.getTimeUpdate$()
|
||||
.subscribe(() => dispatch(setTimeRange(timefilterService.getTime())));
|
||||
.subscribe(() => this.dispatch.setTimeRange(timefilterService.getTime()));
|
||||
|
||||
const refreshIntervalSubscription = timefilterService
|
||||
.getRefreshIntervalUpdate$()
|
||||
.subscribe(() => dispatch(setRefreshInterval(timefilterService.getRefreshInterval())));
|
||||
.subscribe(() => this.dispatch.setRefreshInterval(timefilterService.getRefreshInterval()));
|
||||
|
||||
const autoRefreshSubscription = timefilterService
|
||||
.getAutoRefreshFetch$()
|
|
@ -29,8 +29,7 @@ import { applicationServiceMock, coreMock } from '@kbn/core/public/mocks';
|
|||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { createEditModeActionDefinition } from '@kbn/embeddable-plugin/public/lib/test_samples';
|
||||
|
||||
import { DashboardContainer } from './dashboard_container';
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../../mocks';
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../../mocks';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||
|
||||
|
@ -47,7 +46,7 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
test('DashboardContainer initializes embeddables', (done) => {
|
||||
const initialInput = getSampleDashboardInput({
|
||||
const container = buildMockDashboard({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
|
@ -55,7 +54,6 @@ test('DashboardContainer initializes embeddables', (done) => {
|
|||
}),
|
||||
},
|
||||
});
|
||||
const container = new DashboardContainer(initialInput);
|
||||
|
||||
const subscription = container.getOutput$().subscribe((output) => {
|
||||
if (container.getOutput().embeddableLoaded['123']) {
|
||||
|
@ -76,8 +74,7 @@ test('DashboardContainer initializes embeddables', (done) => {
|
|||
});
|
||||
|
||||
test('DashboardContainer.addNewEmbeddable', async () => {
|
||||
const container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
const container = buildMockDashboard();
|
||||
const embeddable = await container.addNewEmbeddable<ContactCardEmbeddableInput>(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
{
|
||||
|
@ -99,7 +96,8 @@ test('DashboardContainer.addNewEmbeddable', async () => {
|
|||
|
||||
test('DashboardContainer.replacePanel', (done) => {
|
||||
const ID = '123';
|
||||
const initialInput = getSampleDashboardInput({
|
||||
|
||||
const container = buildMockDashboard({
|
||||
panels: {
|
||||
[ID]: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: ID },
|
||||
|
@ -107,8 +105,6 @@ test('DashboardContainer.replacePanel', (done) => {
|
|||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const container = new DashboardContainer(initialInput);
|
||||
let counter = 0;
|
||||
|
||||
const subscription = container.getInput$().subscribe(
|
||||
|
@ -141,7 +137,7 @@ test('DashboardContainer.replacePanel', (done) => {
|
|||
});
|
||||
|
||||
test('Container view mode change propagates to existing children', async () => {
|
||||
const initialInput = getSampleDashboardInput({
|
||||
const container = buildMockDashboard({
|
||||
panels: {
|
||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: { firstName: 'Sam', id: '123' },
|
||||
|
@ -149,8 +145,6 @@ test('Container view mode change propagates to existing children', async () => {
|
|||
}),
|
||||
},
|
||||
});
|
||||
const container = new DashboardContainer(initialInput);
|
||||
await container.untilInitialized();
|
||||
|
||||
const embeddable = await container.untilEmbeddableLoaded('123');
|
||||
expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW);
|
||||
|
@ -159,8 +153,7 @@ test('Container view mode change propagates to existing children', async () => {
|
|||
});
|
||||
|
||||
test('Container view mode change propagates to new children', async () => {
|
||||
const container = new DashboardContainer(getSampleDashboardInput());
|
||||
await container.untilInitialized();
|
||||
const container = buildMockDashboard();
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
@ -178,10 +171,7 @@ test('Container view mode change propagates to new children', async () => {
|
|||
|
||||
test('searchSessionId propagates to children', async () => {
|
||||
const searchSessionId1 = 'searchSessionId1';
|
||||
const container = new DashboardContainer(
|
||||
getSampleDashboardInput({ searchSessionId: searchSessionId1 })
|
||||
);
|
||||
await container.untilInitialized();
|
||||
const container = buildMockDashboard({ searchSessionId: searchSessionId1 });
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
@ -205,9 +195,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
|||
uiActionsSetup.registerAction(editModeAction);
|
||||
uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction);
|
||||
|
||||
const initialInput = getSampleDashboardInput({ viewMode: ViewMode.VIEW });
|
||||
const container = new DashboardContainer(initialInput);
|
||||
await container.untilInitialized();
|
||||
const container = buildMockDashboard({ viewMode: ViewMode.VIEW });
|
||||
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -6,19 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { cloneDeep, omit } from 'lodash';
|
||||
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
||||
import {
|
||||
lazyLoadReduxEmbeddablePackage,
|
||||
ReduxEmbeddableTools,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||
import {
|
||||
ViewMode,
|
||||
Container,
|
||||
type Embeddable,
|
||||
type IEmbeddable,
|
||||
type EmbeddableInput,
|
||||
type EmbeddableOutput,
|
||||
|
@ -44,36 +39,19 @@ import {
|
|||
showPlaceholderUntil,
|
||||
addOrUpdateEmbeddable,
|
||||
} from './api';
|
||||
import {
|
||||
DashboardReduxState,
|
||||
DashboardContainerOutput,
|
||||
DashboardRenderPerformanceStats,
|
||||
} from '../types';
|
||||
import {
|
||||
DashboardPanelState,
|
||||
DashboardContainerInput,
|
||||
DashboardContainerByValueInput,
|
||||
} from '../../../common';
|
||||
import {
|
||||
startDiffingDashboardState,
|
||||
startControlGroupIntegration,
|
||||
startUnifiedSearchIntegration,
|
||||
startSyncingDashboardDataViews,
|
||||
startDashboardSearchSessionIntegration,
|
||||
combineDashboardFiltersWithControlGroupFilters,
|
||||
getUnsavedChanges,
|
||||
keysNotConsideredUnsavedChanges,
|
||||
} from './integrations';
|
||||
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||
import { createPanelState } from '../component/panel';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { DASHBOARD_LOADED_EVENT } from '../../dashboard_constants';
|
||||
import { DashboardCreationOptions } from './dashboard_container_factory';
|
||||
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
||||
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
|
||||
import { DashboardPanelState, DashboardContainerInput } from '../../../common';
|
||||
import { DashboardReduxState, DashboardRenderPerformanceStats } from '../types';
|
||||
import { dashboardContainerReducers } from '../state/dashboard_container_reducers';
|
||||
import { DashboardSavedObjectService } from '../../services/dashboard_saved_object/types';
|
||||
import { dashboardContainerInputIsByValue } from '../../../common/dashboard_container/type_guards';
|
||||
import { startDiffingDashboardState } from '../state/diffing/dashboard_diffing_integration';
|
||||
import { DASHBOARD_LOADED_EVENT, DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
|
||||
import { combineDashboardFiltersWithControlGroupFilters } from './create/controls/dashboard_control_group_integration';
|
||||
|
||||
export interface InheritedChildInput {
|
||||
filters: Filter[];
|
||||
|
@ -91,25 +69,40 @@ export interface InheritedChildInput {
|
|||
executionContext?: KibanaExecutionContext;
|
||||
}
|
||||
|
||||
type DashboardReduxEmbeddableTools = ReduxEmbeddableTools<
|
||||
DashboardReduxState,
|
||||
typeof dashboardContainerReducers
|
||||
>;
|
||||
|
||||
export const DashboardContainerContext = createContext<DashboardContainer | null>(null);
|
||||
export const useDashboardContainer = (): DashboardContainer => {
|
||||
const dashboard = useContext<DashboardContainer | null>(DashboardContainerContext);
|
||||
if (dashboard == null) {
|
||||
throw new Error('useDashboardContainer must be used inside DashboardContainerContext.');
|
||||
}
|
||||
return dashboard!;
|
||||
};
|
||||
|
||||
export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
|
||||
public readonly type = DASHBOARD_CONTAINER_TYPE;
|
||||
|
||||
// state management
|
||||
public select: DashboardReduxEmbeddableTools['select'];
|
||||
public getState: DashboardReduxEmbeddableTools['getState'];
|
||||
public dispatch: DashboardReduxEmbeddableTools['dispatch'];
|
||||
public onStateChange: DashboardReduxEmbeddableTools['onStateChange'];
|
||||
|
||||
public subscriptions: Subscription = new Subscription();
|
||||
public controlGroup?: ControlGroupContainer;
|
||||
|
||||
// Dashboard State
|
||||
public onDestroyControlGroup?: () => void;
|
||||
private subscriptions: Subscription = new Subscription();
|
||||
// cleanup
|
||||
public stopSyncingWithUnifiedSearch?: () => void;
|
||||
private cleanupStateTools: () => void;
|
||||
|
||||
private initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
// performance monitoring
|
||||
private dashboardCreationStartTime?: number;
|
||||
private savedObjectLoadTime?: number;
|
||||
|
||||
private initialSavedDashboardId?: string;
|
||||
|
||||
private reduxEmbeddableTools?: ReduxEmbeddableTools<
|
||||
DashboardReduxState,
|
||||
typeof dashboardContainerReducers
|
||||
>;
|
||||
|
||||
private domNode?: HTMLElement;
|
||||
private overlayRef?: OverlayRef;
|
||||
private allDataViews: DataView[] = [];
|
||||
|
@ -117,19 +110,19 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
// Services that are used in the Dashboard container code
|
||||
private creationOptions?: DashboardCreationOptions;
|
||||
private analyticsService: DashboardAnalyticsService;
|
||||
private dashboardSavedObjectService: DashboardSavedObjectService;
|
||||
private theme$;
|
||||
private chrome;
|
||||
private customBranding;
|
||||
|
||||
constructor(
|
||||
initialInput: DashboardContainerInput,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
initialLastSavedInput?: DashboardContainerInput,
|
||||
dashboardCreationStartTime?: number,
|
||||
parent?: Container,
|
||||
creationOptions?: DashboardCreationOptions
|
||||
creationOptions?: DashboardCreationOptions,
|
||||
savedObjectId?: string
|
||||
) {
|
||||
// we won't initialize any embeddable children until after the dashboard is done initializing.
|
||||
const readyToInitializeChildren$ = new Subject<DashboardContainerInput>();
|
||||
const {
|
||||
embeddable: { getEmbeddableFactory },
|
||||
} = pluginServices.getServices();
|
||||
|
@ -140,15 +133,11 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
},
|
||||
{ embeddableLoaded: {} },
|
||||
getEmbeddableFactory,
|
||||
parent,
|
||||
{
|
||||
readyToInitializeChildren$,
|
||||
}
|
||||
parent
|
||||
);
|
||||
|
||||
({
|
||||
analytics: this.analyticsService,
|
||||
dashboardSavedObject: this.dashboardSavedObjectService,
|
||||
settings: {
|
||||
theme: { theme$: this.theme$ },
|
||||
},
|
||||
|
@ -156,215 +145,44 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
customBranding: this.customBranding,
|
||||
} = pluginServices.getServices());
|
||||
|
||||
this.initialSavedDashboardId = dashboardContainerInputIsByValue(this.input)
|
||||
? undefined
|
||||
: this.input.savedObjectId;
|
||||
this.creationOptions = creationOptions;
|
||||
|
||||
this.dashboardCreationStartTime = dashboardCreationStartTime;
|
||||
this.initializeDashboard(readyToInitializeChildren$, creationOptions);
|
||||
}
|
||||
|
||||
public getDashboardSavedObjectId() {
|
||||
if (this.initialized$.value) {
|
||||
return this.getReduxEmbeddableTools().getState().componentState.lastSavedId;
|
||||
}
|
||||
return this.initialSavedDashboardId;
|
||||
}
|
||||
|
||||
public getInputAsValueType = () => {
|
||||
if (!dashboardContainerInputIsByValue(this.input)) {
|
||||
throw new Error('cannot get input as value type until after dashboard input is unwrapped.');
|
||||
}
|
||||
return this.getInput() as DashboardContainerByValueInput;
|
||||
};
|
||||
|
||||
private async unwrapDashboardContainerInput(): Promise<
|
||||
DashboardContainerByValueInput | undefined
|
||||
> {
|
||||
if (dashboardContainerInputIsByValue(this.input)) {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
// if this dashboard is loaded by reference, unwrap it and track the saved object load time.
|
||||
const savedObjectLoadStartTime = performance.now();
|
||||
const unwrapResult = await this.dashboardSavedObjectService.loadDashboardStateFromSavedObject({
|
||||
id: this.input.savedObjectId,
|
||||
});
|
||||
this.updateInput({ savedObjectId: undefined });
|
||||
this.savedObjectLoadTime = performance.now() - savedObjectLoadStartTime;
|
||||
if (
|
||||
!this.creationOptions?.validateLoadedSavedObject ||
|
||||
this.creationOptions.validateLoadedSavedObject(unwrapResult)
|
||||
) {
|
||||
return unwrapResult.dashboardInput;
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeDashboard(
|
||||
readyToInitializeChildren$: Subject<DashboardContainerInput>,
|
||||
creationOptions?: DashboardCreationOptions
|
||||
) {
|
||||
const {
|
||||
data: { dataViews },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const reduxEmbeddablePackagePromise = lazyLoadReduxEmbeddablePackage();
|
||||
const defaultDataViewAssignmentPromise = dataViews.getDefaultDataView();
|
||||
const dashboardStateUnwrapPromise = this.unwrapDashboardContainerInput();
|
||||
|
||||
const [reduxEmbeddablePackage, inputFromSavedObject, defaultDataView] = await Promise.all([
|
||||
reduxEmbeddablePackagePromise,
|
||||
dashboardStateUnwrapPromise,
|
||||
defaultDataViewAssignmentPromise,
|
||||
]);
|
||||
|
||||
if (!defaultDataView) {
|
||||
throw new Error('Dashboard requires at least one data view before it can be initialized.');
|
||||
}
|
||||
|
||||
// inputFromSavedObject will only be undefined if the provided valiation function returns false.
|
||||
if (!inputFromSavedObject) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather input from session storage if integration is used
|
||||
let sessionStorageInput: Partial<DashboardContainerByValueInput> = {};
|
||||
if (creationOptions?.useSessionStorageIntegration) {
|
||||
const { dashboardSessionStorage } = pluginServices.getServices();
|
||||
const sessionInput = dashboardSessionStorage.getState(this.initialSavedDashboardId);
|
||||
if (sessionInput) sessionStorageInput = sessionInput;
|
||||
}
|
||||
|
||||
// Combine input from saved object with override input.
|
||||
const initialInput: DashboardContainerByValueInput = cloneDeep({
|
||||
...inputFromSavedObject,
|
||||
...sessionStorageInput,
|
||||
...creationOptions?.overrideInput,
|
||||
});
|
||||
|
||||
// set up execution context
|
||||
initialInput.executionContext = {
|
||||
type: 'dashboard',
|
||||
description: initialInput.title,
|
||||
};
|
||||
|
||||
// set up unified search integration
|
||||
if (
|
||||
creationOptions?.useUnifiedSearchIntegration &&
|
||||
creationOptions.unifiedSearchSettings?.kbnUrlStateStorage
|
||||
) {
|
||||
const { kbnUrlStateStorage } = creationOptions.unifiedSearchSettings;
|
||||
const initialTimeRange = startUnifiedSearchIntegration.bind(this)({
|
||||
initialInput,
|
||||
kbnUrlStateStorage,
|
||||
setCleanupFunction: (cleanup) => {
|
||||
this.stopSyncingWithUnifiedSearch = cleanup;
|
||||
},
|
||||
});
|
||||
if (initialTimeRange) initialInput.timeRange = initialTimeRange;
|
||||
}
|
||||
|
||||
// place the incoming embeddable if there is one
|
||||
const incomingEmbeddable = creationOptions?.incomingEmbeddable;
|
||||
if (incomingEmbeddable) {
|
||||
initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable.
|
||||
if (
|
||||
incomingEmbeddable.embeddableId &&
|
||||
Boolean(initialInput.panels[incomingEmbeddable.embeddableId])
|
||||
) {
|
||||
// this embeddable already exists, we will update the explicit input.
|
||||
const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId];
|
||||
const sameType = panelToUpdate.type === incomingEmbeddable.type;
|
||||
|
||||
panelToUpdate.type = incomingEmbeddable.type;
|
||||
panelToUpdate.explicitInput = {
|
||||
// if the incoming panel is the same type as what was there before we can safely spread the old panel's explicit input
|
||||
...(sameType ? panelToUpdate.explicitInput : {}),
|
||||
|
||||
...incomingEmbeddable.input,
|
||||
id: incomingEmbeddable.embeddableId,
|
||||
|
||||
// maintain hide panel titles setting.
|
||||
hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles,
|
||||
};
|
||||
} else {
|
||||
// otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created.
|
||||
this.untilInitialized().then(() =>
|
||||
this.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// start search sessions integration
|
||||
if (creationOptions?.useSearchSessionsIntegration) {
|
||||
const { initialSearchSessionId, stopSyncingDashboardSearchSessions } =
|
||||
startDashboardSearchSessionIntegration.bind(this)(
|
||||
creationOptions?.searchSessionSettings,
|
||||
incomingEmbeddable
|
||||
);
|
||||
initialInput.searchSessionId = initialSearchSessionId;
|
||||
this.stopSyncingDashboardSearchSessions = stopSyncingDashboardSearchSessions;
|
||||
}
|
||||
|
||||
// update input so the redux embeddable tools get the unwrapped, initial input
|
||||
this.updateInput({ ...initialInput });
|
||||
|
||||
// build Control Group
|
||||
if (creationOptions?.useControlGroupIntegration) {
|
||||
this.controlGroup = await startControlGroupIntegration.bind(this)(initialInput);
|
||||
}
|
||||
|
||||
// now that the input with the initial panel state has been set and the control group is ready, we can tell the container class it's time to start loading children.
|
||||
readyToInitializeChildren$.next(initialInput);
|
||||
|
||||
// start diffing dashboard state
|
||||
const diffingMiddleware = startDiffingDashboardState.bind(this)({
|
||||
useSessionBackup: creationOptions?.useSessionStorageIntegration,
|
||||
setCleanupFunction: (cleanup) => {
|
||||
this.stopDiffingDashboardState = cleanup;
|
||||
},
|
||||
});
|
||||
|
||||
// set up data views integration
|
||||
this.dataViewsChangeSubscription = startSyncingDashboardDataViews.bind(this)();
|
||||
const diffingMiddleware = startDiffingDashboardState.bind(this)(creationOptions);
|
||||
|
||||
// build redux embeddable tools
|
||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
||||
const reduxTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||
DashboardReduxState,
|
||||
typeof dashboardContainerReducers
|
||||
>({
|
||||
embeddable: this as Embeddable<DashboardContainerByValueInput, DashboardContainerOutput>, // cast to unwrapped state type
|
||||
embeddable: this,
|
||||
reducers: dashboardContainerReducers,
|
||||
additionalMiddleware: [diffingMiddleware],
|
||||
initialComponentState: {
|
||||
lastSavedInput: inputFromSavedObject,
|
||||
lastSavedInput: initialLastSavedInput ?? {
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
id: initialInput.id,
|
||||
},
|
||||
hasUnsavedChanges: false, // if there is initial unsaved changes, the initial diff will catch them.
|
||||
lastSavedId: this.initialSavedDashboardId,
|
||||
lastSavedId: savedObjectId,
|
||||
},
|
||||
});
|
||||
|
||||
this.initialized$.next(true);
|
||||
this.onStateChange = reduxTools.onStateChange;
|
||||
this.cleanupStateTools = reduxTools.cleanup;
|
||||
this.getState = reduxTools.getState;
|
||||
this.dispatch = reduxTools.dispatch;
|
||||
this.select = reduxTools.select;
|
||||
}
|
||||
|
||||
public async untilInitialized() {
|
||||
if (this.initialized$.value) return Promise.resolve();
|
||||
return new Promise<void>((resolve) => {
|
||||
const subscription = this.initialized$.subscribe((isInitialized) => {
|
||||
if (isInitialized) {
|
||||
resolve();
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
public getDashboardSavedObjectId() {
|
||||
return this.getState().componentState.lastSavedId;
|
||||
}
|
||||
|
||||
public reportPerformanceMetrics(stats: DashboardRenderPerformanceStats) {
|
||||
if (this.analyticsService && this.dashboardCreationStartTime) {
|
||||
const panelCount = Object.keys(
|
||||
this.getReduxEmbeddableTools().getState().explicitInput.panels
|
||||
).length;
|
||||
const panelCount = Object.keys(this.getState().explicitInput.panels).length;
|
||||
const totalDuration = stats.panelsRenderDoneTime - this.dashboardCreationStartTime;
|
||||
reportPerformanceMetricEvent(this.analyticsService, {
|
||||
eventName: DASHBOARD_LOADED_EVENT,
|
||||
|
@ -393,44 +211,22 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
return newPanel;
|
||||
}
|
||||
|
||||
public async getExplicitInputIsEqual(lastExplicitInput: DashboardContainerByValueInput) {
|
||||
const currentInput = this.getReduxEmbeddableTools().getState().explicitInput;
|
||||
return (
|
||||
omit(
|
||||
Object.keys(await getUnsavedChanges.bind(this)(lastExplicitInput, currentInput)),
|
||||
keysNotConsideredUnsavedChanges
|
||||
).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
public getReduxEmbeddableTools() {
|
||||
if (!this.reduxEmbeddableTools) {
|
||||
throw new Error('Dashboard must be initialized before accessing redux embeddable tools');
|
||||
}
|
||||
return this.reduxEmbeddableTools;
|
||||
}
|
||||
|
||||
public render(dom: HTMLElement) {
|
||||
if (!this.reduxEmbeddableTools) {
|
||||
throw new Error('Dashboard must be initialized before it can be rendered');
|
||||
}
|
||||
if (this.domNode) {
|
||||
ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
this.domNode = dom;
|
||||
|
||||
this.domNode.className = 'dashboardContainer';
|
||||
|
||||
const { Wrapper: DashboardReduxWrapper } = this.reduxEmbeddableTools;
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<ExitFullScreenButtonKibanaProvider
|
||||
coreStart={{ chrome: this.chrome, customBranding: this.customBranding }}
|
||||
>
|
||||
<KibanaThemeProvider theme$={this.theme$}>
|
||||
<DashboardReduxWrapper>
|
||||
<DashboardContainerContext.Provider value={this}>
|
||||
<DashboardViewport />
|
||||
</DashboardReduxWrapper>
|
||||
</DashboardContainerContext.Provider>
|
||||
</KibanaThemeProvider>
|
||||
</ExitFullScreenButtonKibanaProvider>
|
||||
</I18nProvider>,
|
||||
|
@ -451,7 +247,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
searchSessionId,
|
||||
refreshInterval,
|
||||
executionContext,
|
||||
} = this.input as DashboardContainerByValueInput;
|
||||
} = this.input;
|
||||
|
||||
let combinedFilters = filters;
|
||||
if (this.controlGroup) {
|
||||
|
@ -476,20 +272,12 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
// ------------------------------------------------------------------------------------------------------
|
||||
// Cleanup
|
||||
// ------------------------------------------------------------------------------------------------------
|
||||
private stopDiffingDashboardState?: () => void;
|
||||
private stopSyncingWithUnifiedSearch?: () => void;
|
||||
private dataViewsChangeSubscription?: Subscription = undefined;
|
||||
private stopSyncingDashboardSearchSessions: (() => void) | undefined;
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.onDestroyControlGroup?.();
|
||||
this.cleanupStateTools();
|
||||
this.controlGroup?.destroy();
|
||||
this.subscriptions.unsubscribe();
|
||||
this.stopDiffingDashboardState?.();
|
||||
this.reduxEmbeddableTools?.cleanup();
|
||||
this.stopSyncingWithUnifiedSearch?.();
|
||||
this.stopSyncingDashboardSearchSessions?.();
|
||||
this.dataViewsChangeSubscription?.unsubscribe();
|
||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
|
||||
|
@ -529,29 +317,20 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
public addOrUpdateEmbeddable = addOrUpdateEmbeddable;
|
||||
|
||||
public forceRefresh(refreshControlGroup: boolean = true) {
|
||||
const {
|
||||
dispatch,
|
||||
actions: { setLastReloadRequestTimeToNow },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
dispatch(setLastReloadRequestTimeToNow({}));
|
||||
this.dispatch.setLastReloadRequestTimeToNow({});
|
||||
if (refreshControlGroup) this.controlGroup?.reload();
|
||||
}
|
||||
|
||||
public onDataViewsUpdate$ = new Subject<DataView[]>();
|
||||
|
||||
public resetToLastSavedState() {
|
||||
const {
|
||||
dispatch,
|
||||
getState,
|
||||
actions: { resetToLastSavedInput },
|
||||
} = this.getReduxEmbeddableTools();
|
||||
dispatch(resetToLastSavedInput({}));
|
||||
this.dispatch.resetToLastSavedInput({});
|
||||
const {
|
||||
explicitInput: { timeRange, refreshInterval },
|
||||
componentState: {
|
||||
lastSavedInput: { timeRestore: lastSavedTimeRestore },
|
||||
},
|
||||
} = getState();
|
||||
} = this.getState();
|
||||
|
||||
// if we are using the unified search integration, we need to force reset the time picker.
|
||||
if (this.creationOptions?.useUnifiedSearchIntegration && lastSavedTimeRestore) {
|
||||
|
@ -585,8 +364,11 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
};
|
||||
|
||||
public getExpandedPanelId = () => {
|
||||
if (!this.reduxEmbeddableTools) throw new Error();
|
||||
return this.reduxEmbeddableTools.getState().componentState.expandedPanelId;
|
||||
return this.getState().componentState.expandedPanelId;
|
||||
};
|
||||
|
||||
public setExpandedPanelId = (newId?: string) => {
|
||||
this.dispatch.setExpandedPanelId(newId);
|
||||
};
|
||||
|
||||
public openOverlay = (ref: OverlayRef) => {
|
||||
|
@ -599,15 +381,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
this.overlayRef?.close();
|
||||
};
|
||||
|
||||
public setExpandedPanelId = (newId?: string) => {
|
||||
if (!this.reduxEmbeddableTools) throw new Error();
|
||||
const {
|
||||
actions: { setExpandedPanelId },
|
||||
dispatch,
|
||||
} = this.reduxEmbeddableTools;
|
||||
dispatch(setExpandedPanelId(newId));
|
||||
};
|
||||
|
||||
public getPanelCount = () => {
|
||||
return Object.keys(this.getInput().panels).length;
|
||||
};
|
||||
|
|
|
@ -21,15 +21,10 @@ import { SearchSessionInfoProvider } from '@kbn/data-plugin/public';
|
|||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||
|
||||
import {
|
||||
createInject,
|
||||
createExtract,
|
||||
DashboardContainerInput,
|
||||
DashboardContainerByValueInput,
|
||||
} from '../../../common';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
||||
import type { DashboardContainer } from './dashboard_container';
|
||||
import { DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
|
||||
import { createInject, createExtract, DashboardContainerInput } from '../../../common';
|
||||
import { LoadDashboardFromSavedObjectReturn } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
|
||||
|
||||
export type DashboardContainerFactory = EmbeddableFactory<
|
||||
|
@ -40,7 +35,6 @@ export type DashboardContainerFactory = EmbeddableFactory<
|
|||
|
||||
export interface DashboardCreationOptions {
|
||||
initialInput?: Partial<DashboardContainerInput>;
|
||||
overrideInput?: Partial<DashboardContainerByValueInput>;
|
||||
|
||||
incomingEmbeddable?: EmbeddablePackageState;
|
||||
|
||||
|
@ -97,19 +91,17 @@ export class DashboardContainerFactoryDefinition
|
|||
public create = async (
|
||||
initialInput: DashboardContainerInput,
|
||||
parent?: Container,
|
||||
creationOptions?: DashboardCreationOptions
|
||||
creationOptions?: DashboardCreationOptions,
|
||||
savedObjectId?: string
|
||||
): Promise<DashboardContainer | ErrorEmbeddable> => {
|
||||
const dashboardCreationStartTime = performance.now();
|
||||
const { DashboardContainer: DashboardContainerEmbeddable } = await import(
|
||||
'./dashboard_container'
|
||||
);
|
||||
return Promise.resolve(
|
||||
new DashboardContainerEmbeddable(
|
||||
initialInput,
|
||||
dashboardCreationStartTime,
|
||||
parent,
|
||||
creationOptions
|
||||
)
|
||||
);
|
||||
const { createDashboard } = await import('./create/create_dashboard');
|
||||
try {
|
||||
return Promise.resolve(
|
||||
createDashboard(initialInput.id, creationOptions, dashboardCreationStartTime, savedObjectId)
|
||||
);
|
||||
} catch (e) {
|
||||
return new ErrorEmbeddable(e.text, { id: e.id });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue