mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -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 React from 'react';
|
||||||
|
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
|
||||||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||||
import { LazyControlGroupRenderer } from '@kbn/controls-plugin/public';
|
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||||
|
|
||||||
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
|
|
||||||
|
|
||||||
export const AddButtonExample = ({ dataViewId }: { dataViewId: string }) => {
|
export const AddButtonExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,36 +6,18 @@
|
||||||
* Side Public License, v 1.
|
* 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';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
|
import { EuiButtonGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||||
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
|
import { ControlGroupRenderer, ControlStyle, ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||||
|
import { AwaitingControlGroupAPI } from '@kbn/controls-plugin/public/control_group';
|
||||||
|
|
||||||
export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
|
export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
const [controlGroupAPI, setControlGroupApi] = useState<AwaitingControlGroupAPI>(null);
|
||||||
|
|
||||||
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 Buttons = ({ api }: { api: ControlGroupAPI }) => {
|
||||||
|
const controlStyle = api.select((state) => state.explicitInput.controlStyle);
|
||||||
return (
|
return (
|
||||||
<EuiButtonGroup
|
<EuiButtonGroup
|
||||||
legend="Text style"
|
legend="Text style"
|
||||||
|
@ -52,9 +34,7 @@ export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
idSelected={controlStyle}
|
idSelected={controlStyle}
|
||||||
onChange={(id, value) => {
|
onChange={(id, value) => api.dispatch.setControlStyle(value)}
|
||||||
dispatch(setControlStyle(value));
|
|
||||||
}}
|
|
||||||
type="single"
|
type="single"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -70,16 +50,10 @@ export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||||
</EuiText>
|
</EuiText>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiPanel hasBorder={true}>
|
<EuiPanel hasBorder={true}>
|
||||||
{ControlGroupReduxWrapper && (
|
{controlGroupAPI && <Buttons api={controlGroupAPI} />}
|
||||||
<ControlGroupReduxWrapper>
|
|
||||||
<ButtonControls />
|
|
||||||
</ControlGroupReduxWrapper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ControlGroupRenderer
|
<ControlGroupRenderer
|
||||||
onLoadComplete={async (newControlGroup) => {
|
ref={setControlGroupApi}
|
||||||
setControlGroup(newControlGroup);
|
|
||||||
}}
|
|
||||||
getCreationOptions={async (initialInput, builder) => {
|
getCreationOptions={async (initialInput, builder) => {
|
||||||
await builder.addDataControlFromField(initialInput, {
|
await builder.addDataControlFromField(initialInput, {
|
||||||
dataViewId,
|
dataViewId,
|
||||||
|
|
|
@ -23,17 +23,14 @@ import {
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
|
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
|
||||||
import {
|
import {
|
||||||
LazyControlGroupRenderer,
|
type ControlGroupInput,
|
||||||
ControlGroupContainer,
|
ControlGroupRenderer,
|
||||||
ControlGroupInput,
|
AwaitingControlGroupAPI,
|
||||||
ACTION_EDIT_CONTROL,
|
ACTION_EDIT_CONTROL,
|
||||||
ACTION_DELETE_CONTROL,
|
ACTION_DELETE_CONTROL,
|
||||||
} from '@kbn/controls-plugin/public';
|
} from '@kbn/controls-plugin/public';
|
||||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
|
||||||
import { ControlInputTransform } from '@kbn/controls-plugin/common/types';
|
import { ControlInputTransform } from '@kbn/controls-plugin/common/types';
|
||||||
|
|
||||||
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
|
|
||||||
|
|
||||||
const INPUT_KEY = 'kbnControls:saveExample:input';
|
const INPUT_KEY = 'kbnControls:saveExample:input';
|
||||||
|
|
||||||
const WITH_CUSTOM_PLACEHOLDER = 'Custom Placeholder';
|
const WITH_CUSTOM_PLACEHOLDER = 'Custom Placeholder';
|
||||||
|
@ -41,7 +38,7 @@ const WITH_CUSTOM_PLACEHOLDER = 'Custom Placeholder';
|
||||||
export const EditExample = () => {
|
export const EditExample = () => {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>(null);
|
||||||
const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = useState<{
|
const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = useState<{
|
||||||
[id: string]: boolean;
|
[id: string]: boolean;
|
||||||
}>({});
|
}>({});
|
||||||
|
@ -54,20 +51,21 @@ export const EditExample = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (controlGroup) {
|
if (controlGroupAPI) {
|
||||||
const disabledActions: string[] = Object.keys(
|
const disabledActions: string[] = Object.keys(
|
||||||
pickBy(newToggleIconIdToSelectedMapIcon, (value) => value)
|
pickBy(newToggleIconIdToSelectedMapIcon, (value) => value)
|
||||||
);
|
);
|
||||||
controlGroup.updateInput({ disabledActions });
|
controlGroupAPI.updateInput({ disabledActions });
|
||||||
}
|
}
|
||||||
|
|
||||||
setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon);
|
setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave() {
|
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
|
// simulated async save await
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
@ -133,9 +131,9 @@ export const EditExample = () => {
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty
|
||||||
color="primary"
|
color="primary"
|
||||||
iconType="plusInCircle"
|
iconType="plusInCircle"
|
||||||
isDisabled={controlGroup === undefined}
|
isDisabled={controlGroupAPI === undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
controlGroup!.openAddDataControlFlyout(controlInputTransform);
|
controlGroupAPI!.openAddDataControlFlyout(controlInputTransform);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add control
|
Add control
|
||||||
|
@ -171,7 +169,7 @@ export const EditExample = () => {
|
||||||
<EuiButton
|
<EuiButton
|
||||||
fill
|
fill
|
||||||
color="primary"
|
color="primary"
|
||||||
isDisabled={controlGroup === undefined || isSaving}
|
isDisabled={controlGroupAPI === undefined || isSaving}
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
isLoading={isSaving}
|
isLoading={isSaving}
|
||||||
>
|
>
|
||||||
|
@ -186,6 +184,7 @@ export const EditExample = () => {
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<ControlGroupRenderer
|
<ControlGroupRenderer
|
||||||
|
ref={setControlGroupAPI}
|
||||||
getCreationOptions={async (initialInput, builder) => {
|
getCreationOptions={async (initialInput, builder) => {
|
||||||
const persistedInput = await onLoad();
|
const persistedInput = await onLoad();
|
||||||
return {
|
return {
|
||||||
|
@ -196,9 +195,6 @@ export const EditExample = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
onLoadComplete={async (newControlGroup) => {
|
|
||||||
setControlGroup(newControlGroup);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -22,12 +22,9 @@ import {
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { LazyControlGroupRenderer, ControlGroupContainer } from '@kbn/controls-plugin/public';
|
import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
|
||||||
import { PLUGIN_ID } from './constants';
|
import { PLUGIN_ID } from './constants';
|
||||||
|
|
||||||
const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
dataView: DataView;
|
dataView: DataView;
|
||||||
|
@ -36,7 +33,7 @@ interface Props {
|
||||||
|
|
||||||
export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
||||||
const [controlFilters, setControlFilters] = useState<Filter[]>([]);
|
const [controlFilters, setControlFilters] = useState<Filter[]>([]);
|
||||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>();
|
||||||
const [hits, setHits] = useState(0);
|
const [hits, setHits] = useState(0);
|
||||||
const [filters, setFilters] = useState<Filter[]>([]);
|
const [filters, setFilters] = useState<Filter[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
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' });
|
const [timeRange, setTimeRange] = useState<TimeRange>({ from: 'now-7d', to: 'now' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!controlGroup) {
|
if (!controlGroupAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const subscription = controlGroup.onFiltersPublished$.subscribe((newFilters) => {
|
const subscription = controlGroupAPI.onFiltersPublished$.subscribe((newFilters) => {
|
||||||
setControlFilters([...newFilters]);
|
setControlFilters([...newFilters]);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [controlGroup]);
|
}, [controlGroupAPI]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
@ -155,10 +152,8 @@ export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
onLoadComplete={async (newControlGroup) => {
|
|
||||||
setControlGroup(newControlGroup);
|
|
||||||
}}
|
|
||||||
query={query}
|
query={query}
|
||||||
|
ref={setControlGroupAPI}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
/>
|
/>
|
||||||
<EuiCallOut title="Search results">
|
<EuiCallOut title="Search results">
|
||||||
|
|
|
@ -18,10 +18,9 @@
|
||||||
"@kbn/data-plugin",
|
"@kbn/data-plugin",
|
||||||
"@kbn/controls-plugin",
|
"@kbn/controls-plugin",
|
||||||
"@kbn/navigation-plugin",
|
"@kbn/navigation-plugin",
|
||||||
"@kbn/presentation-util-plugin",
|
|
||||||
"@kbn/shared-ux-page-kibana-template",
|
"@kbn/shared-ux-page-kibana-template",
|
||||||
"@kbn/embeddable-plugin",
|
"@kbn/embeddable-plugin",
|
||||||
"@kbn/data-views-plugin",
|
"@kbn/data-views-plugin",
|
||||||
"@kbn/es-query"
|
"@kbn/es-query",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
"unifiedSearch",
|
"unifiedSearch",
|
||||||
"developerExamples",
|
"developerExamples",
|
||||||
"embeddableExamples"
|
"embeddableExamples"
|
||||||
],
|
]
|
||||||
"requiredBundles": ["presentationUtil"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,19 +6,37 @@
|
||||||
* Side Public License, v 1.
|
* 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 { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import type { DataView } from '@kbn/data-views-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 { 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 { FILTER_DEBUGGER_EMBEDDABLE } from '@kbn/embeddable-examples-plugin/public';
|
||||||
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
|
||||||
|
|
||||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
|
||||||
|
|
||||||
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<EuiTitle>
|
<EuiTitle>
|
||||||
|
@ -29,10 +47,10 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
||||||
</EuiText>
|
</EuiText>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiPanel hasBorder={true}>
|
<EuiPanel hasBorder={true}>
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async () => {
|
||||||
const builder = controlGroupInputBuilder;
|
const builder = controlGroupInputBuilder;
|
||||||
const controlGroupInput = {};
|
const controlGroupInput = getDefaultControlGroupInput();
|
||||||
await builder.addDataControlFromField(controlGroupInput, {
|
await builder.addDataControlFromField(controlGroupInput, {
|
||||||
dataViewId: dataView.id ?? '',
|
dataViewId: dataView.id ?? '',
|
||||||
title: 'Destintion country',
|
title: 'Destintion country',
|
||||||
|
@ -57,22 +75,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
onDashboardContainerLoaded={(container) => {
|
ref={setDashboard}
|
||||||
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();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -6,9 +6,8 @@
|
||||||
* Side Public License, v 1.
|
* 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 {
|
import {
|
||||||
EuiButtonGroup,
|
EuiButtonGroup,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
|
@ -18,35 +17,19 @@ import {
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import {
|
||||||
|
AwaitingDashboardAPI,
|
||||||
|
DashboardAPI,
|
||||||
|
DashboardRenderer,
|
||||||
|
} from '@kbn/dashboard-plugin/public';
|
||||||
import { ViewMode } from '@kbn/embeddable-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 = () => {
|
export const DualReduxExample = () => {
|
||||||
const [firstDashboardContainer, setFirstDashboardContainer] = useState<
|
const [firstDashboardContainer, setFirstDashboardContainer] = useState<AwaitingDashboardAPI>();
|
||||||
DashboardContainer | undefined
|
const [secondDashboardContainer, setSecondDashboardContainer] = useState<AwaitingDashboardAPI>();
|
||||||
>();
|
|
||||||
const [secondDashboardContainer, setSecondDashboardContainer] = useState<
|
|
||||||
DashboardContainer | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const FirstDashboardReduxWrapper = useMemo(() => {
|
const ButtonControls = ({ dashboard }: { dashboard: DashboardAPI }) => {
|
||||||
if (firstDashboardContainer) return firstDashboardContainer.getReduxEmbeddableTools().Wrapper;
|
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||||
}, [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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiButtonGroup
|
<EuiButtonGroup
|
||||||
|
@ -64,9 +47,7 @@ export const DualReduxExample = () => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
idSelected={viewMode}
|
idSelected={viewMode}
|
||||||
onChange={(id, value) => {
|
onChange={(id, value) => dashboard.dispatch.setViewMode(value)}
|
||||||
dispatch(setViewMode(value));
|
|
||||||
}}
|
|
||||||
type="single"
|
type="single"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -91,34 +72,18 @@ export const DualReduxExample = () => {
|
||||||
<h3>Dashboard #1</h3>
|
<h3>Dashboard #1</h3>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
{FirstDashboardReduxWrapper && (
|
{firstDashboardContainer && <ButtonControls dashboard={firstDashboardContainer} />}
|
||||||
<FirstDashboardReduxWrapper>
|
|
||||||
<ButtonControls />
|
|
||||||
</FirstDashboardReduxWrapper>
|
|
||||||
)}
|
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer ref={setFirstDashboardContainer} />
|
||||||
onDashboardContainerLoaded={(container) => {
|
|
||||||
setFirstDashboardContainer(container);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiTitle size="xs">
|
<EuiTitle size="xs">
|
||||||
<h3>Dashboard #2</h3>
|
<h3>Dashboard #2</h3>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
{SecondDashboardReduxWrapper && (
|
{secondDashboardContainer && <ButtonControls dashboard={secondDashboardContainer} />}
|
||||||
<SecondDashboardReduxWrapper>
|
|
||||||
<ButtonControls />
|
|
||||||
</SecondDashboardReduxWrapper>
|
|
||||||
)}
|
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer ref={setSecondDashboardContainer} />
|
||||||
onDashboardContainerLoaded={(container) => {
|
|
||||||
setSecondDashboardContainer(container);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DashboardContainer, LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
|
||||||
import {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
|
@ -23,19 +23,17 @@ import {
|
||||||
VisualizeInput,
|
VisualizeInput,
|
||||||
VisualizeOutput,
|
VisualizeOutput,
|
||||||
} from '@kbn/visualizations-plugin/public/embeddable/visualize_embeddable';
|
} from '@kbn/visualizations-plugin/public/embeddable/visualize_embeddable';
|
||||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
const INPUT_KEY = 'portableDashboard:saveExample:input';
|
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 = () => {
|
export const DynamicByReferenceExample = () => {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer | undefined>();
|
const [dashboard, setdashboard] = useState<AwaitingDashboardAPI>();
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
|
if (!dashboard) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboardContainer!.getInput()));
|
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboard.getInput()));
|
||||||
// simulated async save await
|
// simulated async save await
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
|
@ -56,44 +54,37 @@ export const DynamicByReferenceExample = () => {
|
||||||
|
|
||||||
const resetPersistableInput = () => {
|
const resetPersistableInput = () => {
|
||||||
localStorage.removeItem(INPUT_KEY);
|
localStorage.removeItem(INPUT_KEY);
|
||||||
if (dashboardContainer) {
|
if (dashboard) {
|
||||||
const children = dashboardContainer.getChildIds();
|
const children = dashboard.getChildIds();
|
||||||
children.map((childId) => {
|
children.map((childId) => {
|
||||||
dashboardContainer.removeEmbeddable(childId);
|
dashboard.removeEmbeddable(childId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addByReference = () => {
|
|
||||||
if (dashboardContainer) {
|
|
||||||
dashboardContainer.addFromLibrary();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addByValue = async () => {
|
const addByValue = async () => {
|
||||||
if (dashboardContainer) {
|
if (!dashboard) return;
|
||||||
dashboardContainer.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
|
dashboard.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
|
||||||
'visualization',
|
'visualization',
|
||||||
{
|
{
|
||||||
title: 'Sample Markdown Vis',
|
title: 'Sample Markdown Vis',
|
||||||
savedVis: {
|
savedVis: {
|
||||||
type: 'markdown',
|
type: 'markdown',
|
||||||
title: '',
|
title: '',
|
||||||
data: { aggs: [], searchSource: {} },
|
data: { aggs: [], searchSource: {} },
|
||||||
params: {
|
params: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
openLinksInNewTab: false,
|
openLinksInNewTab: false,
|
||||||
markdown: '### By Value Visualization\nThis is a sample by value panel.',
|
markdown: '### By Value Visualization\nThis is a sample by value panel.',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
}
|
||||||
}
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableButtons = useMemo(() => {
|
const disableButtons = useMemo(() => {
|
||||||
return dashboardContainer === undefined || isSaving;
|
return !dashboard || isSaving;
|
||||||
}, [dashboardContainer, isSaving]);
|
}, [dashboard, isSaving]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -114,7 +105,7 @@ export const DynamicByReferenceExample = () => {
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiButton onClick={addByReference} isDisabled={disableButtons}>
|
<EuiButton onClick={() => dashboard?.addFromLibrary()} isDisabled={disableButtons}>
|
||||||
Add visualization from library
|
Add visualization from library
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
@ -141,7 +132,7 @@ export const DynamicByReferenceExample = () => {
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async () => {
|
||||||
const persistedInput = getPersistableInput();
|
const persistedInput = getPersistableInput();
|
||||||
return {
|
return {
|
||||||
|
@ -151,9 +142,7 @@ export const DynamicByReferenceExample = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
onDashboardContainerLoaded={(container) => {
|
ref={setdashboard}
|
||||||
setDashboardContainer(container);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -11,15 +11,9 @@ import { css } from '@emotion/react';
|
||||||
|
|
||||||
import { buildPhraseFilter, Filter } from '@kbn/es-query';
|
import { buildPhraseFilter, Filter } from '@kbn/es-query';
|
||||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import {
|
import { DashboardRenderer, DashboardCreationOptions } from '@kbn/dashboard-plugin/public';
|
||||||
LazyDashboardContainerRenderer,
|
|
||||||
DashboardCreationOptions,
|
|
||||||
} from '@kbn/dashboard-plugin/public';
|
|
||||||
import { EuiCode, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
import { EuiCode, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
|
||||||
|
|
||||||
export const StaticByReferenceExample = ({
|
export const StaticByReferenceExample = ({
|
||||||
dashboardId,
|
dashboardId,
|
||||||
|
@ -50,7 +44,7 @@ export const StaticByReferenceExample = ({
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer
|
||||||
savedObjectId={dashboardId}
|
savedObjectId={dashboardId}
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async () => {
|
||||||
const field = dataView.getFieldByName('machine.os.keyword');
|
const field = dataView.getFieldByName('machine.os.keyword');
|
||||||
|
@ -61,13 +55,10 @@ export const StaticByReferenceExample = ({
|
||||||
if (field) {
|
if (field) {
|
||||||
filter = buildPhraseFilter(field, 'win xp', dataView);
|
filter = buildPhraseFilter(field, 'win xp', dataView);
|
||||||
filter.meta.negate = true;
|
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
|
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>
|
</EuiPanel>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -11,13 +11,10 @@ import React from 'react';
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||||
import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common';
|
import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common';
|
||||||
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
import { DashboardRenderer } from '@kbn/dashboard-plugin/public';
|
||||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import panelsJson from './static_by_value_example_panels.json';
|
import panelsJson from './static_by_value_example_panels.json';
|
||||||
|
|
||||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
|
||||||
|
|
||||||
export const StaticByValueExample = () => {
|
export const StaticByValueExample = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -29,7 +26,7 @@ export const StaticByValueExample = () => {
|
||||||
</EuiText>
|
</EuiText>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiPanel hasBorder={true}>
|
<EuiPanel hasBorder={true}>
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async () => {
|
||||||
return {
|
return {
|
||||||
initialInput: {
|
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>
|
</EuiPanel>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -20,11 +20,9 @@
|
||||||
"@kbn/embeddable-plugin",
|
"@kbn/embeddable-plugin",
|
||||||
"@kbn/data-views-plugin",
|
"@kbn/data-views-plugin",
|
||||||
"@kbn/visualizations-plugin",
|
"@kbn/visualizations-plugin",
|
||||||
"@kbn/presentation-util-plugin",
|
|
||||||
"@kbn/developer-examples-plugin",
|
"@kbn/developer-examples-plugin",
|
||||||
"@kbn/embeddable-examples-plugin",
|
"@kbn/embeddable-examples-plugin",
|
||||||
"@kbn/shared-ux-page-kibana-template",
|
"@kbn/shared-ux-page-kibana-template",
|
||||||
"@kbn/shared-ux-utility",
|
|
||||||
"@kbn/controls-plugin",
|
"@kbn/controls-plugin",
|
||||||
"@kbn/shared-ux-router"
|
"@kbn/shared-ux-router"
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,19 +6,15 @@
|
||||||
* Side Public License, v 1.
|
* 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 { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public';
|
||||||
import { OptionsListComponentState, OptionsListReduxState } from '../../public/options_list/types';
|
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 { ControlFactory, ControlOutput } from '../../public/types';
|
||||||
import { OptionsListEmbeddableInput } from './types';
|
import { OptionsListEmbeddableInput } from './types';
|
||||||
|
|
||||||
|
import * as optionsListStateModule from '../../public/options_list/options_list_reducers';
|
||||||
|
|
||||||
const mockOptionsListComponentState = {
|
const mockOptionsListComponentState = {
|
||||||
...getDefaultComponentState(),
|
searchString: { value: '', valid: true },
|
||||||
field: undefined,
|
field: undefined,
|
||||||
totalCardinality: 0,
|
totalCardinality: 0,
|
||||||
availableOptions: {
|
availableOptions: {
|
||||||
|
@ -29,6 +25,8 @@ const mockOptionsListComponentState = {
|
||||||
moo: { doc_count: 5 },
|
moo: { doc_count: 5 },
|
||||||
},
|
},
|
||||||
invalidSelections: [],
|
invalidSelections: [],
|
||||||
|
allowExpensiveQueries: true,
|
||||||
|
popoverOpen: false,
|
||||||
validSelections: [],
|
validSelections: [],
|
||||||
} as OptionsListComponentState;
|
} as OptionsListComponentState;
|
||||||
|
|
||||||
|
@ -46,26 +44,24 @@ const mockOptionsListOutput = {
|
||||||
loading: false,
|
loading: false,
|
||||||
} as ControlOutput;
|
} as ControlOutput;
|
||||||
|
|
||||||
export const mockOptionsListReduxEmbeddableTools = async (
|
export const mockOptionsListEmbeddable = async (partialState?: Partial<OptionsListReduxState>) => {
|
||||||
partialState?: Partial<OptionsListReduxState>
|
|
||||||
) => {
|
|
||||||
const optionsListFactoryStub = new OptionsListEmbeddableFactory();
|
const optionsListFactoryStub = new OptionsListEmbeddableFactory();
|
||||||
const optionsListControlFactory = optionsListFactoryStub as unknown as ControlFactory;
|
const optionsListControlFactory = optionsListFactoryStub as unknown as ControlFactory;
|
||||||
optionsListControlFactory.getDefaultInput = () => ({});
|
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({
|
const mockEmbeddable = (await optionsListControlFactory.create({
|
||||||
...mockOptionsListEmbeddableInput,
|
...mockOptionsListEmbeddableInput,
|
||||||
...partialState?.explicitInput,
|
...partialState?.explicitInput,
|
||||||
})) as OptionsListEmbeddable;
|
})) as OptionsListEmbeddable;
|
||||||
mockEmbeddable.getOutput = jest.fn().mockReturnValue(mockOptionsListOutput);
|
mockEmbeddable.getOutput = jest.fn().mockReturnValue(mockOptionsListOutput);
|
||||||
|
return mockEmbeddable;
|
||||||
const mockReduxEmbeddableTools = createReduxEmbeddableTools<OptionsListReduxState>({
|
|
||||||
embeddable: mockEmbeddable,
|
|
||||||
reducers: optionsListReducers,
|
|
||||||
initialComponentState: {
|
|
||||||
...mockOptionsListComponentState,
|
|
||||||
...partialState?.componentState,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return mockReduxEmbeddableTools;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
|
||||||
lazyLoadReduxEmbeddablePackage,
|
|
||||||
ReduxEmbeddablePackage,
|
|
||||||
} from '@kbn/presentation-util-plugin/public';
|
|
||||||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
import { ControlOutput } from '../../types';
|
import { ControlOutput } from '../../types';
|
||||||
|
@ -17,17 +13,15 @@ import { ControlGroupInput } from '../types';
|
||||||
import { pluginServices } from '../../services';
|
import { pluginServices } from '../../services';
|
||||||
import { DeleteControlAction } from './delete_control_action';
|
import { DeleteControlAction } from './delete_control_action';
|
||||||
import { OptionsListEmbeddableInput } from '../../options_list';
|
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 { ControlGroupContainer } from '../embeddable/control_group_container';
|
||||||
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';
|
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';
|
||||||
|
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||||
|
|
||||||
let container: ControlGroupContainer;
|
let container: ControlGroupContainer;
|
||||||
let embeddable: OptionsListEmbeddable;
|
let embeddable: OptionsListEmbeddable;
|
||||||
let reduxEmbeddablePackage: ReduxEmbeddablePackage;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
|
||||||
|
|
||||||
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
||||||
controlGroupInputBuilder.addOptionsListControl(controlGroupInput, {
|
controlGroupInputBuilder.addOptionsListControl(controlGroupInput, {
|
||||||
dataViewId: 'test-data-view',
|
dataViewId: 'test-data-view',
|
||||||
|
@ -36,7 +30,7 @@ beforeAll(async () => {
|
||||||
width: 'medium',
|
width: 'medium',
|
||||||
grow: false,
|
grow: false,
|
||||||
});
|
});
|
||||||
container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
container = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||||
await container.untilInitialized();
|
await container.untilInitialized();
|
||||||
|
|
||||||
embeddable = container.getChild(container.getChildIds()[0]);
|
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 () => {
|
test('Execute throws an error when called with an embeddable not in a parent', async () => {
|
||||||
const deleteControlAction = new DeleteControlAction();
|
const deleteControlAction = new DeleteControlAction();
|
||||||
const optionsListEmbeddable = new OptionsListEmbeddable(
|
const optionsListEmbeddable = new OptionsListEmbeddable(
|
||||||
reduxEmbeddablePackage,
|
mockedReduxEmbeddablePackage,
|
||||||
{} as OptionsListEmbeddableInput,
|
{} as OptionsListEmbeddableInput,
|
||||||
{} as ControlOutput
|
{} as ControlOutput
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
|
||||||
lazyLoadReduxEmbeddablePackage,
|
|
||||||
ReduxEmbeddablePackage,
|
|
||||||
} from '@kbn/presentation-util-plugin/public';
|
|
||||||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
import { ControlOutput } from '../../types';
|
import { ControlOutput } from '../../types';
|
||||||
|
@ -21,16 +17,11 @@ import { TimeSliderEmbeddableFactory } from '../../time_slider';
|
||||||
import { OptionsListEmbeddableFactory, OptionsListEmbeddableInput } from '../../options_list';
|
import { OptionsListEmbeddableFactory, OptionsListEmbeddableInput } from '../../options_list';
|
||||||
import { ControlGroupContainer } from '../embeddable/control_group_container';
|
import { ControlGroupContainer } from '../embeddable/control_group_container';
|
||||||
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';
|
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';
|
||||||
|
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||||
let reduxEmbeddablePackage: ReduxEmbeddablePackage;
|
|
||||||
|
|
||||||
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
||||||
const deleteControlAction = new DeleteControlAction();
|
const deleteControlAction = new DeleteControlAction();
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Action is incompatible with Error Embeddables', async () => {
|
test('Action is incompatible with Error Embeddables', async () => {
|
||||||
const editControlAction = new EditControlAction(deleteControlAction);
|
const editControlAction = new EditControlAction(deleteControlAction);
|
||||||
const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' });
|
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;
|
pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory;
|
||||||
|
|
||||||
const editControlAction = new EditControlAction(deleteControlAction);
|
const editControlAction = new EditControlAction(deleteControlAction);
|
||||||
const emptyContainer = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||||
await emptyContainer.untilInitialized();
|
await emptyContainer.untilInitialized();
|
||||||
await emptyContainer.addTimeSliderControl();
|
await emptyContainer.addTimeSliderControl();
|
||||||
|
|
||||||
|
@ -62,7 +53,7 @@ test('Action is compatible with embeddables that are editable', async () => {
|
||||||
pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory;
|
pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory;
|
||||||
|
|
||||||
const editControlAction = new EditControlAction(deleteControlAction);
|
const editControlAction = new EditControlAction(deleteControlAction);
|
||||||
const emptyContainer = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||||
await emptyContainer.untilInitialized();
|
await emptyContainer.untilInitialized();
|
||||||
await emptyContainer.addOptionsListControl({
|
await emptyContainer.addOptionsListControl({
|
||||||
dataViewId: 'test-data-view',
|
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 () => {
|
test('Execute throws an error when called with an embeddable not in a parent', async () => {
|
||||||
const editControlAction = new EditControlAction(deleteControlAction);
|
const editControlAction = new EditControlAction(deleteControlAction);
|
||||||
const optionsListEmbeddable = new OptionsListEmbeddable(
|
const optionsListEmbeddable = new OptionsListEmbeddable(
|
||||||
reduxEmbeddablePackage,
|
mockedReduxEmbeddablePackage,
|
||||||
{} as OptionsListEmbeddableInput,
|
{} as OptionsListEmbeddableInput,
|
||||||
{} as ControlOutput
|
{} as ControlOutput
|
||||||
);
|
);
|
||||||
|
@ -95,7 +86,7 @@ test('Execute should open a flyout', async () => {
|
||||||
const spyOn = jest.fn().mockResolvedValue(undefined);
|
const spyOn = jest.fn().mockResolvedValue(undefined);
|
||||||
pluginServices.getServices().overlays.openFlyout = spyOn;
|
pluginServices.getServices().overlays.openFlyout = spyOn;
|
||||||
|
|
||||||
const emptyContainer = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
|
const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput);
|
||||||
await emptyContainer.untilInitialized();
|
await emptyContainer.untilInitialized();
|
||||||
await emptyContainer.addOptionsListControl({
|
await emptyContainer.addOptionsListControl({
|
||||||
dataViewId: 'test-data-view',
|
dataViewId: 'test-data-view',
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { DeleteControlAction } from './delete_control_action';
|
||||||
import { ControlGroupStrings } from '../control_group_strings';
|
import { ControlGroupStrings } from '../control_group_strings';
|
||||||
import { ACTION_EDIT_CONTROL, ControlGroupContainer } from '..';
|
import { ACTION_EDIT_CONTROL, ControlGroupContainer } from '..';
|
||||||
import { ControlEmbeddable, DataControlInput } from '../../types';
|
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';
|
import { isControlGroup } from '../embeddable/control_group_helpers';
|
||||||
|
|
||||||
export interface EditControlActionContext {
|
export interface EditControlActionContext {
|
||||||
|
@ -91,11 +91,10 @@ export class EditControlAction implements Action<EditControlActionContext> {
|
||||||
throw new IncompatibleActionError();
|
throw new IncompatibleActionError();
|
||||||
}
|
}
|
||||||
const controlGroup = embeddable.parent as ControlGroupContainer;
|
const controlGroup = embeddable.parent as ControlGroupContainer;
|
||||||
const ReduxWrapper = controlGroup.getReduxEmbeddableTools().Wrapper;
|
|
||||||
|
|
||||||
const flyoutInstance = this.openFlyout(
|
const flyoutInstance = this.openFlyout(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<ReduxWrapper>
|
<ControlGroupContainerContext.Provider value={controlGroup}>
|
||||||
<EditControlFlyout
|
<EditControlFlyout
|
||||||
embeddable={embeddable}
|
embeddable={embeddable}
|
||||||
removeControl={() => this.deleteControlAction.execute({ embeddable })}
|
removeControl={() => this.deleteControlAction.execute({ embeddable })}
|
||||||
|
@ -104,7 +103,7 @@ export class EditControlAction implements Action<EditControlActionContext> {
|
||||||
flyoutInstance.close();
|
flyoutInstance.close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ReduxWrapper>,
|
</ControlGroupContainerContext.Provider>,
|
||||||
|
|
||||||
{ theme$: this.theme$ }
|
{ theme$: this.theme$ }
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { EmbeddableFactoryNotFoundError } from '@kbn/embeddable-plugin/public';
|
||||||
import { DataControlInput, ControlEmbeddable, IEditableControlFactory } from '../../types';
|
import { DataControlInput, ControlEmbeddable, IEditableControlFactory } from '../../types';
|
||||||
import { pluginServices } from '../../services';
|
import { pluginServices } from '../../services';
|
||||||
import { ControlGroupStrings } from '../control_group_strings';
|
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';
|
import { ControlEditor } from '../editor/control_editor';
|
||||||
|
|
||||||
export const EditControlFlyout = ({
|
export const EditControlFlyout = ({
|
||||||
|
@ -32,17 +32,10 @@ export const EditControlFlyout = ({
|
||||||
controls: { getControlFactory },
|
controls: { getControlFactory },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
// Redux embeddable container Context
|
// Redux embeddable container Context
|
||||||
const reduxContext = useControlGroupContainerContext();
|
const controlGroup = useControlGroupContainer();
|
||||||
const {
|
|
||||||
embeddableInstance: controlGroup,
|
|
||||||
actions: { setControlWidth, setControlGrow },
|
|
||||||
useEmbeddableSelector,
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
} = reduxContext;
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// current state
|
// current state
|
||||||
const panels = useEmbeddableSelector((state) => state.explicitInput.panels);
|
const panels = controlGroup.select((state) => state.explicitInput.panels);
|
||||||
const panel = panels[embeddable.id];
|
const panel = panels[embeddable.id];
|
||||||
|
|
||||||
const [currentGrow, setCurrentGrow] = useState(panel.grow);
|
const [currentGrow, setCurrentGrow] = useState(panel.grow);
|
||||||
|
@ -86,9 +79,9 @@ export const EditControlFlyout = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentWidth !== panel.width)
|
if (currentWidth !== panel.width)
|
||||||
dispatch(setControlWidth({ width: currentWidth, embeddableId: embeddable.id }));
|
controlGroup.dispatch.setControlWidth({ width: currentWidth, embeddableId: embeddable.id });
|
||||||
if (currentGrow !== panel.grow)
|
if (currentGrow !== panel.grow)
|
||||||
dispatch(setControlGrow({ grow: currentGrow, embeddableId: embeddable.id }));
|
controlGroup.dispatch.setControlGrow({ grow: currentGrow, embeddableId: embeddable.id });
|
||||||
|
|
||||||
closeFlyout();
|
closeFlyout();
|
||||||
await controlGroup.replaceEmbeddable(embeddable.id, inputToReturn, type);
|
await controlGroup.replaceEmbeddable(embeddable.id, inputToReturn, type);
|
||||||
|
|
|
@ -6,8 +6,9 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiButtonEmpty,
|
EuiButtonEmpty,
|
||||||
EuiFormControlLayout,
|
EuiFormControlLayout,
|
||||||
|
@ -17,15 +18,16 @@ import {
|
||||||
EuiPopover,
|
EuiPopover,
|
||||||
EuiToolTip,
|
EuiToolTip,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { Markdown } from '@kbn/kibana-react-plugin/public';
|
import { Markdown } from '@kbn/kibana-react-plugin/public';
|
||||||
import { useReduxEmbeddableContext, FloatingActions } from '@kbn/presentation-util-plugin/public';
|
import { FloatingActions } from '@kbn/presentation-util-plugin/public';
|
||||||
import { ControlGroupReduxState } from '../types';
|
|
||||||
|
import {
|
||||||
|
controlGroupSelector,
|
||||||
|
useControlGroupContainer,
|
||||||
|
} from '../embeddable/control_group_container';
|
||||||
import { ControlGroupStrings } from '../control_group_strings';
|
import { ControlGroupStrings } from '../control_group_strings';
|
||||||
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
|
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
|
||||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
|
||||||
import { ControlGroupContainer } from '..';
|
|
||||||
|
|
||||||
interface ControlFrameErrorProps {
|
interface ControlFrameErrorProps {
|
||||||
error: Error;
|
error: Error;
|
||||||
|
@ -82,16 +84,11 @@ export const ControlFrame = ({
|
||||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||||
const [fatalError, setFatalError] = useState<Error>();
|
const [fatalError, setFatalError] = useState<Error>();
|
||||||
|
|
||||||
const { useEmbeddableSelector: select, embeddableInstance: controlGroup } =
|
const controlGroup = useControlGroupContainer();
|
||||||
useReduxEmbeddableContext<
|
|
||||||
ControlGroupReduxState,
|
|
||||||
typeof controlGroupReducers,
|
|
||||||
ControlGroupContainer
|
|
||||||
>();
|
|
||||||
|
|
||||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
const controlStyle = controlGroupSelector((state) => state.explicitInput.controlStyle);
|
||||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
const viewMode = controlGroupSelector((state) => state.explicitInput.viewMode);
|
||||||
const disabledActions = select((state) => state.explicitInput.disabledActions);
|
const disabledActions = controlGroupSelector((state) => state.explicitInput.disabledActions);
|
||||||
|
|
||||||
const embeddable = useChildEmbeddable({
|
const embeddable = useChildEmbeddable({
|
||||||
untilEmbeddableLoaded: controlGroup.untilEmbeddableLoaded.bind(controlGroup),
|
untilEmbeddableLoaded: controlGroup.untilEmbeddableLoaded.bind(controlGroup),
|
||||||
|
|
|
@ -8,9 +8,6 @@
|
||||||
|
|
||||||
import '../control_group.scss';
|
import '../control_group.scss';
|
||||||
|
|
||||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
import {
|
||||||
arrayMove,
|
arrayMove,
|
||||||
SortableContext,
|
SortableContext,
|
||||||
|
@ -28,28 +25,28 @@ import {
|
||||||
useSensors,
|
useSensors,
|
||||||
LayoutMeasuringStrategy,
|
LayoutMeasuringStrategy,
|
||||||
} from '@dnd-kit/core';
|
} 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 { 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 { 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 = () => {
|
export const ControlGroup = () => {
|
||||||
// Redux embeddable container Context
|
const controlGroup = useControlGroupContainer();
|
||||||
const reduxContext = useControlGroupContainerContext();
|
|
||||||
const {
|
|
||||||
embeddableInstance: controlGroup,
|
|
||||||
actions: { setControlOrders },
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
} = reduxContext;
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// current state
|
// current state
|
||||||
const panels = select((state) => state.explicitInput.panels);
|
const panels = contextSelect((state) => state.explicitInput.panels);
|
||||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
const viewMode = contextSelect((state) => state.explicitInput.viewMode);
|
||||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
const controlStyle = contextSelect((state) => state.explicitInput.controlStyle);
|
||||||
const showAddButton = select((state) => state.componentState.showAddButton);
|
const showAddButton = contextSelect((state) => state.componentState.showAddButton);
|
||||||
|
|
||||||
const isEditable = viewMode === ViewMode.EDIT;
|
const isEditable = viewMode === ViewMode.EDIT;
|
||||||
|
|
||||||
|
@ -80,7 +77,9 @@ export const ControlGroup = () => {
|
||||||
const overIndex = idsInOrder.indexOf(over.id);
|
const overIndex = idsInOrder.indexOf(over.id);
|
||||||
if (draggingIndex !== overIndex) {
|
if (draggingIndex !== overIndex) {
|
||||||
const newIndex = overIndex;
|
const newIndex = overIndex;
|
||||||
dispatch(setControlOrders({ ids: arrayMove([...idsInOrder], draggingIndex, newIndex) }));
|
controlGroup.dispatch.setControlOrders({
|
||||||
|
ids: arrayMove([...idsInOrder], draggingIndex, newIndex),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDraggingId(null);
|
setDraggingId(null);
|
||||||
|
|
|
@ -6,16 +6,15 @@
|
||||||
* Side Public License, v 1.
|
* 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 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 { ControlGroupStrings } from '../control_group_strings';
|
||||||
|
import { ControlFrame, ControlFrameProps } from './control_frame_component';
|
||||||
|
import { controlGroupSelector } from '../embeddable/control_group_container';
|
||||||
|
|
||||||
interface DragInfo {
|
interface DragInfo {
|
||||||
isOver?: boolean;
|
isOver?: boolean;
|
||||||
|
@ -70,8 +69,7 @@ const SortableControlInner = forwardRef<
|
||||||
dragHandleRef
|
dragHandleRef
|
||||||
) => {
|
) => {
|
||||||
const { isOver, isDragging, draggingIndex, index } = dragInfo;
|
const { isOver, isDragging, draggingIndex, index } = dragInfo;
|
||||||
const { useEmbeddableSelector } = useReduxEmbeddableContext<ControlGroupReduxState>();
|
const panels = controlGroupSelector((state) => state.explicitInput.panels);
|
||||||
const panels = useEmbeddableSelector((state) => state.explicitInput.panels);
|
|
||||||
|
|
||||||
const grow = panels[embeddableId].grow;
|
const grow = panels[embeddableId].grow;
|
||||||
const width = panels[embeddableId].width;
|
const width = panels[embeddableId].width;
|
||||||
|
@ -122,9 +120,8 @@ const SortableControlInner = forwardRef<
|
||||||
* can be quite cumbersome.
|
* can be quite cumbersome.
|
||||||
*/
|
*/
|
||||||
export const ControlClone = ({ draggingId }: { draggingId: string }) => {
|
export const ControlClone = ({ draggingId }: { draggingId: string }) => {
|
||||||
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<ControlGroupReduxState>();
|
const panels = controlGroupSelector((state) => state.explicitInput.panels);
|
||||||
const panels = select((state) => state.explicitInput.panels);
|
const controlStyle = controlGroupSelector((state) => state.explicitInput.controlStyle);
|
||||||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
|
||||||
|
|
||||||
const width = panels[draggingId].width;
|
const width = panels[draggingId].width;
|
||||||
const title = panels[draggingId].explicitInput.title;
|
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 { CONTROL_WIDTH_OPTIONS } from './editor_constants';
|
||||||
import { pluginServices } from '../../services';
|
import { pluginServices } from '../../services';
|
||||||
import { getDataControlFieldRegistry } from './data_control_editor_tools';
|
import { getDataControlFieldRegistry } from './data_control_editor_tools';
|
||||||
import { useControlGroupContainerContext } from '../control_group_renderer';
|
import { useControlGroupContainer } from '../embeddable/control_group_container';
|
||||||
|
|
||||||
interface EditControlProps {
|
interface EditControlProps {
|
||||||
embeddable?: ControlEmbeddable<DataControlInput>;
|
embeddable?: ControlEmbeddable<DataControlInput>;
|
||||||
isCreate: boolean;
|
isCreate: boolean;
|
||||||
|
@ -95,9 +96,11 @@ export const ControlEditor = ({
|
||||||
controls: { getControlFactory },
|
controls: { getControlFactory },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
const { useEmbeddableSelector: select } = useControlGroupContainerContext();
|
const controlGroup = useControlGroupContainer();
|
||||||
const editorConfig = select((state) => state.componentState.editorConfig);
|
const editorConfig = controlGroup.select((state) => state.componentState.editorConfig);
|
||||||
const customFilterPredicate = select((state) => state.componentState.fieldFilterPredicate);
|
const customFilterPredicate = controlGroup.select(
|
||||||
|
(state) => state.componentState.fieldFilterPredicate
|
||||||
|
);
|
||||||
|
|
||||||
const [defaultTitle, setDefaultTitle] = useState<string>();
|
const [defaultTitle, setDefaultTitle] = useState<string>();
|
||||||
const [currentTitle, setCurrentTitle] = useState(title ?? '');
|
const [currentTitle, setCurrentTitle] = useState(title ?? '');
|
||||||
|
|
|
@ -8,22 +8,27 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||||
import { ControlInputTransform } from '../../../common/types';
|
|
||||||
|
import {
|
||||||
|
ControlGroupContainer,
|
||||||
|
ControlGroupContainerContext,
|
||||||
|
setFlyoutRef,
|
||||||
|
} from '../embeddable/control_group_container';
|
||||||
import type {
|
import type {
|
||||||
AddDataControlProps,
|
AddDataControlProps,
|
||||||
AddOptionsListControlProps,
|
AddOptionsListControlProps,
|
||||||
AddRangeSliderControlProps,
|
AddRangeSliderControlProps,
|
||||||
} from '../control_group_input_builder';
|
} from '../external_api/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';
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTROL_GROW,
|
DEFAULT_CONTROL_GROW,
|
||||||
DEFAULT_CONTROL_WIDTH,
|
DEFAULT_CONTROL_WIDTH,
|
||||||
} from '../../../common/control_group/control_group_constants';
|
} 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(
|
export function openAddDataControlFlyout(
|
||||||
this: ControlGroupContainer,
|
this: ControlGroupContainer,
|
||||||
|
@ -34,8 +39,6 @@ export function openAddDataControlFlyout(
|
||||||
controls: { getControlFactory },
|
controls: { getControlFactory },
|
||||||
theme: { theme$ },
|
theme: { theme$ },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const ControlsServicesProvider = pluginServices.getContextProvider();
|
|
||||||
const ReduxWrapper = this.getReduxEmbeddableTools().Wrapper;
|
|
||||||
|
|
||||||
let controlInput: Partial<DataControlInput> = {};
|
let controlInput: Partial<DataControlInput> = {};
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
@ -58,51 +61,49 @@ export function openAddDataControlFlyout(
|
||||||
|
|
||||||
const flyoutInstance = openFlyout(
|
const flyoutInstance = openFlyout(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<ControlsServicesProvider>
|
<ControlGroupContainerContext.Provider value={this}>
|
||||||
<ReduxWrapper>
|
<ControlEditor
|
||||||
<ControlEditor
|
setLastUsedDataViewId={(newId) => this.setLastUsedDataViewId(newId)}
|
||||||
setLastUsedDataViewId={(newId) => this.setLastUsedDataViewId(newId)}
|
getRelevantDataViewId={this.getMostRelevantDataViewId}
|
||||||
getRelevantDataViewId={this.getMostRelevantDataViewId}
|
isCreate={true}
|
||||||
isCreate={true}
|
width={this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||||
width={this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
grow={this.getInput().defaultControlGrow ?? DEFAULT_CONTROL_GROW}
|
||||||
grow={this.getInput().defaultControlGrow ?? DEFAULT_CONTROL_GROW}
|
updateTitle={(newTitle) => (controlInput.title = newTitle)}
|
||||||
updateTitle={(newTitle) => (controlInput.title = newTitle)}
|
updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
|
||||||
updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
|
updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })}
|
||||||
updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })}
|
onSave={(type) => {
|
||||||
onSave={(type) => {
|
this.closeAllFlyouts();
|
||||||
this.closeAllFlyouts();
|
if (!type) {
|
||||||
if (!type) {
|
return;
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
</ReduxWrapper>
|
const factory = getControlFactory(type) as IEditableControlFactory;
|
||||||
</ControlsServicesProvider>,
|
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$ }
|
{ theme$ }
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,14 +13,17 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||||
import { pluginServices } from '../../services';
|
import { pluginServices } from '../../services';
|
||||||
import { ControlGroupEditor } from './control_group_editor';
|
import { ControlGroupEditor } from './control_group_editor';
|
||||||
import { ControlGroupStrings } from '../control_group_strings';
|
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) {
|
export function openEditControlGroupFlyout(this: ControlGroupContainer) {
|
||||||
const {
|
const {
|
||||||
overlays: { openFlyout, openConfirm },
|
overlays: { openFlyout, openConfirm },
|
||||||
theme: { theme$ },
|
theme: { theme$ },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const ReduxWrapper = this.getReduxEmbeddableTools().Wrapper;
|
|
||||||
|
|
||||||
const onDeleteAll = (ref: OverlayRef) => {
|
const onDeleteAll = (ref: OverlayRef) => {
|
||||||
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
|
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
|
||||||
|
@ -37,7 +40,7 @@ export function openEditControlGroupFlyout(this: ControlGroupContainer) {
|
||||||
|
|
||||||
const flyoutInstance = openFlyout(
|
const flyoutInstance = openFlyout(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<ReduxWrapper>
|
<ControlGroupContainerContext.Provider value={this}>
|
||||||
<ControlGroupEditor
|
<ControlGroupEditor
|
||||||
initialInput={this.getInput()}
|
initialInput={this.getInput()}
|
||||||
updateInput={(changes) => this.updateInput(changes)}
|
updateInput={(changes) => this.updateInput(changes)}
|
||||||
|
@ -45,7 +48,7 @@ export function openEditControlGroupFlyout(this: ControlGroupContainer) {
|
||||||
onDeleteAll={() => onDeleteAll(flyoutInstance)}
|
onDeleteAll={() => onDeleteAll(flyoutInstance)}
|
||||||
onClose={() => flyoutInstance.close()}
|
onClose={() => flyoutInstance.close()}
|
||||||
/>
|
/>
|
||||||
</ReduxWrapper>,
|
</ControlGroupContainerContext.Provider>,
|
||||||
{ theme$ }
|
{ theme$ }
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface OnChildChangedProps {
|
||||||
interface ChainingSystem {
|
interface ChainingSystem {
|
||||||
getContainerSettings: (
|
getContainerSettings: (
|
||||||
initialInput: ControlGroupInput
|
initialInput: ControlGroupInput
|
||||||
) => EmbeddableContainerSettings<ControlGroupInput> | undefined;
|
) => EmbeddableContainerSettings | undefined;
|
||||||
getPrecedingFilters: (
|
getPrecedingFilters: (
|
||||||
props: GetPrecedingFiltersProps
|
props: GetPrecedingFiltersProps
|
||||||
) => { filters: Filter[]; timeslice?: [number, number] } | undefined;
|
) => { filters: Filter[]; timeslice?: [number, number] } | undefined;
|
||||||
|
|
|
@ -5,18 +5,19 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
import { skip, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
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 { 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 { OverlayRef } from '@kbn/core/public';
|
||||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import { Container, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
import { Container, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||||
|
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ControlGroupInput,
|
ControlGroupInput,
|
||||||
ControlGroupOutput,
|
ControlGroupOutput,
|
||||||
|
@ -31,23 +32,21 @@ import {
|
||||||
ControlGroupChainingSystems,
|
ControlGroupChainingSystems,
|
||||||
controlOrdersAreEqual,
|
controlOrdersAreEqual,
|
||||||
} from './control_group_chaining_system';
|
} 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 {
|
import {
|
||||||
|
type AddDataControlProps,
|
||||||
|
type AddOptionsListControlProps,
|
||||||
|
type AddRangeSliderControlProps,
|
||||||
getDataControlPanelState,
|
getDataControlPanelState,
|
||||||
getOptionsListPanelState,
|
getOptionsListPanelState,
|
||||||
getRangeSliderPanelState,
|
getRangeSliderPanelState,
|
||||||
getTimeSliderPanelState,
|
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';
|
import { openEditControlGroupFlyout } from '../editor/open_edit_control_group_flyout';
|
||||||
|
|
||||||
let flyoutRef: OverlayRef | undefined;
|
let flyoutRef: OverlayRef | undefined;
|
||||||
|
@ -55,6 +54,21 @@ export const setFlyoutRef = (newRef: OverlayRef | undefined) => {
|
||||||
flyoutRef = newRef;
|
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<
|
export class ControlGroupContainer extends Container<
|
||||||
ControlInput,
|
ControlInput,
|
||||||
ControlGroupInput,
|
ControlGroupInput,
|
||||||
|
@ -71,63 +85,21 @@ export class ControlGroupContainer extends Container<
|
||||||
private relevantDataViewId?: string;
|
private relevantDataViewId?: string;
|
||||||
private lastUsedDataViewId?: string;
|
private lastUsedDataViewId?: string;
|
||||||
|
|
||||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
// state management
|
||||||
ControlGroupReduxState,
|
public select: ControlGroupReduxEmbeddableTools['select'];
|
||||||
typeof controlGroupReducers
|
public getState: ControlGroupReduxEmbeddableTools['getState'];
|
||||||
>;
|
public dispatch: ControlGroupReduxEmbeddableTools['dispatch'];
|
||||||
|
public onStateChange: ControlGroupReduxEmbeddableTools['onStateChange'];
|
||||||
|
|
||||||
|
private store: ControlGroupReduxEmbeddableTools['store'];
|
||||||
|
|
||||||
|
private cleanupStateTools: () => void;
|
||||||
|
|
||||||
public onFiltersPublished$: Subject<Filter[]>;
|
public onFiltersPublished$: Subject<Filter[]>;
|
||||||
public onControlRemoved$: Subject<string>;
|
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(
|
constructor(
|
||||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
reduxToolsPackage: ReduxToolsPackage,
|
||||||
initialInput: ControlGroupInput,
|
initialInput: ControlGroupInput,
|
||||||
parent?: Container,
|
parent?: Container,
|
||||||
settings?: ControlGroupSettings
|
settings?: ControlGroupSettings
|
||||||
|
@ -145,7 +117,7 @@ export class ControlGroupContainer extends Container<
|
||||||
this.onControlRemoved$ = new Subject<string>();
|
this.onControlRemoved$ = new Subject<string>();
|
||||||
|
|
||||||
// build redux embeddable tools
|
// build redux embeddable tools
|
||||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||||
ControlGroupReduxState,
|
ControlGroupReduxState,
|
||||||
typeof controlGroupReducers
|
typeof controlGroupReducers
|
||||||
>({
|
>({
|
||||||
|
@ -154,6 +126,14 @@ export class ControlGroupContainer extends Container<
|
||||||
initialComponentState: settings,
|
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
|
// when all children are ready setup subscriptions
|
||||||
this.untilAllChildrenReady().then(() => {
|
this.untilAllChildrenReady().then(() => {
|
||||||
this.recalculateDataViews();
|
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 = () => {
|
public getPanelCount = () => {
|
||||||
return Object.keys(this.getInput().panels).length;
|
return Object.keys(this.getInput().panels).length;
|
||||||
};
|
};
|
||||||
|
@ -225,7 +246,7 @@ export class ControlGroupContainer extends Container<
|
||||||
// if filters are different, publish them
|
// if filters are different, publish them
|
||||||
if (
|
if (
|
||||||
!compareFilters(this.output.filters ?? [], allFilters ?? [], COMPARE_ALL_OPTIONS) ||
|
!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.updateOutput({ filters: uniqFilters(allFilters), timeslice });
|
||||||
this.onFiltersPublished$.next(allFilters);
|
this.onFiltersPublished$.next(allFilters);
|
||||||
|
@ -337,15 +358,13 @@ export class ControlGroupContainer extends Container<
|
||||||
ReactDOM.unmountComponentAtNode(this.domNode);
|
ReactDOM.unmountComponentAtNode(this.domNode);
|
||||||
}
|
}
|
||||||
this.domNode = dom;
|
this.domNode = dom;
|
||||||
const ControlsServicesProvider = pluginServices.getContextProvider();
|
|
||||||
const { Wrapper: ControlGroupReduxWrapper } = this.reduxEmbeddableTools;
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||||
<ControlsServicesProvider>
|
<Provider store={this.store}>
|
||||||
<ControlGroupReduxWrapper>
|
<ControlGroupContainerContext.Provider value={this}>
|
||||||
<ControlGroup />
|
<ControlGroup />
|
||||||
</ControlGroupReduxWrapper>
|
</ControlGroupContainerContext.Provider>
|
||||||
</ControlsServicesProvider>
|
</Provider>
|
||||||
</KibanaThemeProvider>,
|
</KibanaThemeProvider>,
|
||||||
dom
|
dom
|
||||||
);
|
);
|
||||||
|
@ -355,7 +374,7 @@ export class ControlGroupContainer extends Container<
|
||||||
super.destroy();
|
super.destroy();
|
||||||
this.closeAllFlyouts();
|
this.closeAllFlyouts();
|
||||||
this.subscriptions.unsubscribe();
|
this.subscriptions.unsubscribe();
|
||||||
this.reduxEmbeddableTools.cleanup();
|
this.cleanupStateTools();
|
||||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { Container, EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
|
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 { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||||
|
|
||||||
import { ControlGroupInput, ControlGroupSettings, CONTROL_GROUP_TYPE } from '../types';
|
import { ControlGroupInput, ControlGroupSettings, CONTROL_GROUP_TYPE } from '../types';
|
||||||
|
@ -54,7 +54,7 @@ export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition
|
||||||
parent?: Container,
|
parent?: Container,
|
||||||
settings?: ControlGroupSettings
|
settings?: ControlGroupSettings
|
||||||
) => {
|
) => {
|
||||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||||
const { ControlGroupContainer } = await import('./control_group_container');
|
const { ControlGroupContainer } = await import('./control_group_container');
|
||||||
return new ControlGroupContainer(reduxEmbeddablePackage, initialInput, parent, settings);
|
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 { i18n } from '@kbn/i18n';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { ControlPanelState, OptionsListEmbeddableInput } from '../../common';
|
|
||||||
|
import {
|
||||||
|
ControlPanelState,
|
||||||
|
ControlWidth,
|
||||||
|
OptionsListEmbeddableInput,
|
||||||
|
OPTIONS_LIST_CONTROL,
|
||||||
|
TIME_SLIDER_CONTROL,
|
||||||
|
} from '../../../common';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTROL_GROW,
|
DEFAULT_CONTROL_GROW,
|
||||||
DEFAULT_CONTROL_WIDTH,
|
DEFAULT_CONTROL_WIDTH,
|
||||||
} from '../../common/control_group/control_group_constants';
|
} from '../../../common/control_group/control_group_constants';
|
||||||
import { RangeValue } from '../../common/range_slider/types';
|
import { ControlGroupInput } from '../types';
|
||||||
import {
|
import { ControlInput, DataControlInput } from '../../types';
|
||||||
ControlInput,
|
import { RangeValue, RANGE_SLIDER_CONTROL } from '../../../common/range_slider/types';
|
||||||
ControlWidth,
|
import { getCompatibleControlType, getNextPanelOrder } from '../embeddable/control_group_helpers';
|
||||||
DataControlInput,
|
|
||||||
OPTIONS_LIST_CONTROL,
|
|
||||||
RANGE_SLIDER_CONTROL,
|
|
||||||
TIME_SLIDER_CONTROL,
|
|
||||||
} from '..';
|
|
||||||
import { ControlGroupInput } from './types';
|
|
||||||
import { getCompatibleControlType, getNextPanelOrder } from './embeddable/control_group_helpers';
|
|
||||||
|
|
||||||
export interface AddDataControlProps {
|
export interface AddDataControlProps {
|
||||||
controlId?: string;
|
controlId?: string;
|
||||||
|
@ -40,6 +40,8 @@ export type AddRangeSliderControlProps = AddDataControlProps & {
|
||||||
value?: RangeValue;
|
value?: RangeValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ControlGroupInputBuilder = typeof controlGroupInputBuilder;
|
||||||
|
|
||||||
export const controlGroupInputBuilder = {
|
export const controlGroupInputBuilder = {
|
||||||
addDataControlFromField: async (
|
addDataControlFromField: async (
|
||||||
initialInput: Partial<ControlGroupInput>,
|
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.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export type { ControlGroupContainer } from './embeddable/control_group_container';
|
export type { ControlGroupContainer } from './embeddable/control_group_container';
|
||||||
export type { ControlGroupInput, ControlGroupOutput } from './types';
|
export type { ControlGroupInput, ControlGroupOutput } from './types';
|
||||||
|
|
||||||
|
@ -19,12 +17,14 @@ export { ACTION_EDIT_CONTROL, ACTION_DELETE_CONTROL } from './actions';
|
||||||
export {
|
export {
|
||||||
type AddDataControlProps,
|
type AddDataControlProps,
|
||||||
type AddOptionsListControlProps,
|
type AddOptionsListControlProps,
|
||||||
|
type ControlGroupInputBuilder,
|
||||||
type AddRangeSliderControlProps,
|
type AddRangeSliderControlProps,
|
||||||
controlGroupInputBuilder,
|
controlGroupInputBuilder,
|
||||||
} from './control_group_input_builder';
|
} from './external_api/control_group_input_builder';
|
||||||
|
|
||||||
|
export type { ControlGroupAPI, AwaitingControlGroupAPI } from './external_api/control_group_api';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ControlGroupRendererProps,
|
type ControlGroupRendererProps,
|
||||||
useControlGroupContainerContext,
|
ControlGroupRenderer,
|
||||||
} from './control_group_renderer';
|
} from './external_api/control_group_renderer';
|
||||||
export const LazyControlGroupRenderer = React.lazy(() => import('./control_group_renderer'));
|
|
||||||
|
|
|
@ -19,6 +19,12 @@ export const controlGroupReducers = {
|
||||||
) => {
|
) => {
|
||||||
state.explicitInput.controlStyle = action.payload;
|
state.explicitInput.controlStyle = action.payload;
|
||||||
},
|
},
|
||||||
|
setChainingSystem: (
|
||||||
|
state: WritableDraft<ControlGroupReduxState>,
|
||||||
|
action: PayloadAction<ControlGroupInput['chainingSystem']>
|
||||||
|
) => {
|
||||||
|
state.explicitInput.chainingSystem = action.payload;
|
||||||
|
},
|
||||||
setDefaultControlWidth: (
|
setDefaultControlWidth: (
|
||||||
state: WritableDraft<ControlGroupReduxState>,
|
state: WritableDraft<ControlGroupReduxState>,
|
||||||
action: PayloadAction<ControlGroupInput['defaultControlWidth']>
|
action: PayloadAction<ControlGroupInput['defaultControlWidth']>
|
||||||
|
|
|
@ -39,8 +39,11 @@ export {
|
||||||
type ControlGroupContainer,
|
type ControlGroupContainer,
|
||||||
ControlGroupContainerFactory,
|
ControlGroupContainerFactory,
|
||||||
type ControlGroupInput,
|
type ControlGroupInput,
|
||||||
controlGroupInputBuilder,
|
type ControlGroupInputBuilder,
|
||||||
|
type ControlGroupAPI,
|
||||||
|
type AwaitingControlGroupAPI,
|
||||||
type ControlGroupOutput,
|
type ControlGroupOutput,
|
||||||
|
controlGroupInputBuilder,
|
||||||
} from './control_group';
|
} from './control_group';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -56,11 +59,10 @@ export {
|
||||||
} from './range_slider';
|
} from './range_slider';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
LazyControlGroupRenderer,
|
|
||||||
useControlGroupContainerContext,
|
|
||||||
type ControlGroupRendererProps,
|
|
||||||
ACTION_DELETE_CONTROL,
|
|
||||||
ACTION_EDIT_CONTROL,
|
ACTION_EDIT_CONTROL,
|
||||||
|
ACTION_DELETE_CONTROL,
|
||||||
|
ControlGroupRenderer,
|
||||||
|
type ControlGroupRendererProps,
|
||||||
} from './control_group';
|
} from './control_group';
|
||||||
|
|
||||||
export function plugin() {
|
export function plugin() {
|
||||||
|
|
|
@ -11,9 +11,10 @@ import React from 'react';
|
||||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||||
|
|
||||||
|
import { OptionsListEmbeddableContext } from '../embeddable/options_list_embeddable';
|
||||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
||||||
import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks';
|
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||||
import { OptionsListControl } from './options_list_control';
|
import { OptionsListControl } from './options_list_control';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
@ -30,16 +31,16 @@ describe('Options list control', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountComponent(options?: Partial<MountOptions>) {
|
async function mountComponent(options?: Partial<MountOptions>) {
|
||||||
const mockReduxEmbeddableTools = await mockOptionsListReduxEmbeddableTools({
|
const optionsListEmbeddable = await mockOptionsListEmbeddable({
|
||||||
componentState: options?.componentState ?? {},
|
componentState: options?.componentState ?? {},
|
||||||
explicitInput: options?.explicitInput ?? {},
|
explicitInput: options?.explicitInput ?? {},
|
||||||
output: options?.output ?? {},
|
output: options?.output ?? {},
|
||||||
} as Partial<OptionsListReduxState>);
|
} as Partial<OptionsListReduxState>);
|
||||||
|
|
||||||
return mountWithIntl(
|
return mountWithIntl(
|
||||||
<mockReduxEmbeddableTools.Wrapper>
|
<OptionsListEmbeddableContext.Provider value={optionsListEmbeddable}>
|
||||||
<OptionsListControl {...defaultProps} />
|
<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 React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||||
|
|
||||||
import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui';
|
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 { OptionsListStrings } from './options_list_strings';
|
||||||
import { OptionsListPopover } from './options_list_popover';
|
import { OptionsListPopover } from './options_list_popover';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
|
||||||
|
|
||||||
import './options_list.scss';
|
import './options_list.scss';
|
||||||
|
|
||||||
|
@ -29,38 +28,29 @@ export const OptionsListControl = ({
|
||||||
loadMoreSubject: Subject<number>;
|
loadMoreSubject: Subject<number>;
|
||||||
}) => {
|
}) => {
|
||||||
const resizeRef = useRef(null);
|
const resizeRef = useRef(null);
|
||||||
|
const optionsList = useOptionsList();
|
||||||
const dimensions = useResizeObserver(resizeRef.current);
|
const dimensions = useResizeObserver(resizeRef.current);
|
||||||
|
|
||||||
// Redux embeddable Context
|
const isPopoverOpen = optionsList.select((state) => state.componentState.popoverOpen);
|
||||||
const {
|
const validSelections = optionsList.select((state) => state.componentState.validSelections);
|
||||||
useEmbeddableDispatch,
|
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||||
actions: { replaceSelection, setSearchString, setPopoverOpen },
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const id = optionsList.select((state) => state.explicitInput.id);
|
||||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
const exclude = optionsList.select((state) => state.explicitInput.exclude);
|
||||||
const validSelections = select((state) => state.componentState.validSelections);
|
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||||
const isPopoverOpen = select((state) => state.componentState.popoverOpen);
|
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 loading = optionsList.select((state) => state.output.loading);
|
||||||
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);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
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
|
// debounce loading state so loading doesn't flash when user types
|
||||||
const [debouncedLoading, setDebouncedLoading] = useState(true);
|
const [debouncedLoading, setDebouncedLoading] = useState(true);
|
||||||
|
@ -76,16 +66,16 @@ export const OptionsListControl = ({
|
||||||
// remove all other selections if this control is single select
|
// remove all other selections if this control is single select
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (singleSelect && selectedOptions && selectedOptions?.length > 1) {
|
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(
|
const updateSearchString = useCallback(
|
||||||
(newSearchString: string) => {
|
(newSearchString: string) => {
|
||||||
typeaheadSubject.next(newSearchString);
|
typeaheadSubject.next(newSearchString);
|
||||||
dispatch(setSearchString(newSearchString));
|
optionsList.dispatch.setSearchString(newSearchString);
|
||||||
},
|
},
|
||||||
[typeaheadSubject, dispatch, setSearchString]
|
[typeaheadSubject, optionsList.dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadMoreSuggestions = useCallback(
|
const loadMoreSuggestions = useCallback(
|
||||||
|
@ -141,7 +131,7 @@ export const OptionsListControl = ({
|
||||||
'optionsList--filterBtnPlaceholder': !hasSelections,
|
'optionsList--filterBtnPlaceholder': !hasSelections,
|
||||||
})}
|
})}
|
||||||
data-test-subj={`optionsList-control-${id}`}
|
data-test-subj={`optionsList-control-${id}`}
|
||||||
onClick={() => dispatch(setPopoverOpen(!isPopoverOpen))}
|
onClick={() => optionsList.dispatch.setPopoverOpen(!isPopoverOpen)}
|
||||||
isSelected={isPopoverOpen}
|
isSelected={isPopoverOpen}
|
||||||
numActiveFilters={validSelectionsCount}
|
numActiveFilters={validSelectionsCount}
|
||||||
hasActiveFilters={Boolean(validSelectionsCount)}
|
hasActiveFilters={Boolean(validSelectionsCount)}
|
||||||
|
@ -168,7 +158,7 @@ export const OptionsListControl = ({
|
||||||
anchorPosition="downCenter"
|
anchorPosition="downCenter"
|
||||||
initialFocus={'[data-test-subj=optionsList-control-search-input]'}
|
initialFocus={'[data-test-subj=optionsList-control-search-input]'}
|
||||||
className="optionsList__popoverOverride"
|
className="optionsList__popoverOverride"
|
||||||
closePopover={() => dispatch(setPopoverOpen(false))}
|
closePopover={() => optionsList.dispatch.setPopoverOpen(false)}
|
||||||
anchorClassName="optionsList__anchorOverride"
|
anchorClassName="optionsList__anchorOverride"
|
||||||
aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)}
|
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 { findTestSubject } from '@elastic/eui/lib/test';
|
||||||
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||||
|
|
||||||
import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover';
|
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
|
||||||
import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks';
|
|
||||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
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', () => {
|
describe('Options list popover', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -35,16 +37,16 @@ describe('Options list popover', () => {
|
||||||
|
|
||||||
async function mountComponent(options?: Partial<MountOptions>) {
|
async function mountComponent(options?: Partial<MountOptions>) {
|
||||||
const compProps = { ...defaultProps, ...(options?.popoverProps ?? {}) };
|
const compProps = { ...defaultProps, ...(options?.popoverProps ?? {}) };
|
||||||
const mockReduxEmbeddableTools = await mockOptionsListReduxEmbeddableTools({
|
const optionsListEmbeddable = await mockOptionsListEmbeddable({
|
||||||
componentState: options?.componentState ?? {},
|
componentState: options?.componentState ?? {},
|
||||||
explicitInput: options?.explicitInput ?? {},
|
explicitInput: options?.explicitInput ?? {},
|
||||||
output: options?.output ?? {},
|
output: options?.output ?? {},
|
||||||
} as Partial<OptionsListReduxState>);
|
} as Partial<OptionsListReduxState>);
|
||||||
|
|
||||||
return mountWithIntl(
|
return mountWithIntl(
|
||||||
<mockReduxEmbeddableTools.Wrapper>
|
<OptionsListEmbeddableContext.Provider value={optionsListEmbeddable}>
|
||||||
<OptionsListPopover {...compProps} />
|
<OptionsListPopover {...compProps} />
|
||||||
</mockReduxEmbeddableTools.Wrapper>
|
</OptionsListEmbeddableContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,6 +292,9 @@ describe('Options list popover', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ensure warning icon shows up when testAllowExpensiveQueries = false', async () => {
|
test('ensure warning icon shows up when testAllowExpensiveQueries = false', async () => {
|
||||||
|
pluginServices.getServices().optionsList.getAllowExpensiveQueries = jest.fn(() =>
|
||||||
|
Promise.resolve(false)
|
||||||
|
);
|
||||||
const popover = await mountComponent({
|
const popover = await mountComponent({
|
||||||
componentState: {
|
componentState: {
|
||||||
field: { name: 'Test keyword field', type: 'keyword' } as FieldSpec,
|
field: { name: 'Test keyword field', type: 'keyword' } as FieldSpec,
|
||||||
|
|
|
@ -9,16 +9,13 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import { OptionsListReduxState } from '../types';
|
|
||||||
import { OptionsListStrings } from './options_list_strings';
|
import { OptionsListStrings } from './options_list_strings';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
|
||||||
import { OptionsListPopoverTitle } from './options_list_popover_title';
|
import { OptionsListPopoverTitle } from './options_list_popover_title';
|
||||||
import { OptionsListPopoverFooter } from './options_list_popover_footer';
|
import { OptionsListPopoverFooter } from './options_list_popover_footer';
|
||||||
import { OptionsListPopoverActionBar } from './options_list_popover_action_bar';
|
import { OptionsListPopoverActionBar } from './options_list_popover_action_bar';
|
||||||
import { OptionsListPopoverSuggestions } from './options_list_popover_suggestions';
|
import { OptionsListPopoverSuggestions } from './options_list_popover_suggestions';
|
||||||
import { OptionsListPopoverInvalidSelections } from './options_list_popover_invalid_selections';
|
import { OptionsListPopoverInvalidSelections } from './options_list_popover_invalid_selections';
|
||||||
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
|
|
||||||
export interface OptionsListPopoverProps {
|
export interface OptionsListPopoverProps {
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -33,21 +30,16 @@ export const OptionsListPopover = ({
|
||||||
updateSearchString,
|
updateSearchString,
|
||||||
loadMoreSuggestions,
|
loadMoreSuggestions,
|
||||||
}: OptionsListPopoverProps) => {
|
}: OptionsListPopoverProps) => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<
|
|
||||||
OptionsListReduxState,
|
|
||||||
typeof optionsListReducers
|
|
||||||
>();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const field = optionsList.select((state) => state.componentState.field);
|
||||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
const availableOptions = optionsList.select((state) => state.componentState.availableOptions);
|
||||||
const availableOptions = select((state) => state.componentState.availableOptions);
|
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||||
const field = select((state) => state.componentState.field);
|
|
||||||
|
|
||||||
const hideActionBar = select((state) => state.explicitInput.hideActionBar);
|
const id = optionsList.select((state) => state.explicitInput.id);
|
||||||
const hideExclude = select((state) => state.explicitInput.hideExclude);
|
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
const hideExclude = optionsList.select((state) => state.explicitInput.hideExclude);
|
||||||
const id = select((state) => state.explicitInput.id);
|
const hideActionBar = optionsList.select((state) => state.explicitInput.hideActionBar);
|
||||||
|
|
||||||
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,9 @@ import {
|
||||||
EuiToolTip,
|
EuiToolTip,
|
||||||
EuiText,
|
EuiText,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import { OptionsListReduxState } from '../types';
|
|
||||||
import { OptionsListStrings } from './options_list_strings';
|
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';
|
import { OptionsListPopoverSortingButton } from './options_list_popover_sorting_button';
|
||||||
|
|
||||||
interface OptionsListPopoverProps {
|
interface OptionsListPopoverProps {
|
||||||
|
@ -35,20 +33,18 @@ export const OptionsListPopoverActionBar = ({
|
||||||
updateSearchString,
|
updateSearchString,
|
||||||
setShowOnlySelected,
|
setShowOnlySelected,
|
||||||
}: OptionsListPopoverProps) => {
|
}: OptionsListPopoverProps) => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { clearSelections },
|
|
||||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const totalCardinality =
|
||||||
const allowExpensiveQueries = select((state) => state.componentState.allowExpensiveQueries);
|
optionsList.select((state) => state.componentState.totalCardinality) ?? 0;
|
||||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
const searchString = optionsList.select((state) => state.componentState.searchString);
|
||||||
const totalCardinality = select((state) => state.componentState.totalCardinality) ?? 0;
|
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||||
const searchString = select((state) => state.componentState.searchString);
|
|
||||||
const hideSort = select((state) => state.explicitInput.hideSort);
|
const allowExpensiveQueries = optionsList.select(
|
||||||
|
(state) => state.componentState.allowExpensiveQueries
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideSort = optionsList.select((state) => state.explicitInput.hideSort);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="optionsList__actions">
|
<div className="optionsList__actions">
|
||||||
|
@ -142,7 +138,7 @@ export const OptionsListPopoverActionBar = ({
|
||||||
size="xs"
|
size="xs"
|
||||||
color="danger"
|
color="danger"
|
||||||
iconType="eraser"
|
iconType="eraser"
|
||||||
onClick={() => dispatch(clearSelections({}))}
|
onClick={() => optionsList.dispatch.clearSelections({})}
|
||||||
data-test-subj="optionsList-control-clear-all-selections"
|
data-test-subj="optionsList-control-clear-all-selections"
|
||||||
aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()}
|
aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,19 +7,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useEuiBackgroundColor,
|
|
||||||
useEuiPaddingSize,
|
|
||||||
EuiPopoverFooter,
|
|
||||||
EuiButtonGroup,
|
|
||||||
EuiProgress,
|
EuiProgress,
|
||||||
|
EuiButtonGroup,
|
||||||
|
EuiPopoverFooter,
|
||||||
|
useEuiPaddingSize,
|
||||||
|
useEuiBackgroundColor,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import { OptionsListReduxState } from '../types';
|
|
||||||
import { OptionsListStrings } from './options_list_strings';
|
import { OptionsListStrings } from './options_list_strings';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
|
|
||||||
const aggregationToggleButtons = [
|
const aggregationToggleButtons = [
|
||||||
{
|
{
|
||||||
|
@ -33,16 +32,9 @@ const aggregationToggleButtons = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean }) => {
|
export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean }) => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setExclude },
|
|
||||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const exclude = optionsList.select((state) => state.explicitInput.exclude);
|
||||||
const exclude = select((state) => state.explicitInput.exclude);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -71,7 +63,7 @@ export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean })
|
||||||
options={aggregationToggleButtons}
|
options={aggregationToggleButtons}
|
||||||
idSelected={exclude ? 'optionsList__excludeResults' : 'optionsList__includeResults'}
|
idSelected={exclude ? 'optionsList__excludeResults' : 'optionsList__includeResults'}
|
||||||
onChange={(optionId) =>
|
onChange={(optionId) =>
|
||||||
dispatch(setExclude(optionId === 'optionsList__excludeResults'))
|
optionsList.dispatch.setExclude(optionId === 'optionsList__excludeResults')
|
||||||
}
|
}
|
||||||
buttonSize="compressed"
|
buttonSize="compressed"
|
||||||
data-test-subj="optionsList__includeExcludeButtonGroup"
|
data-test-subj="optionsList__includeExcludeButtonGroup"
|
||||||
|
|
|
@ -15,24 +15,15 @@ import {
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
EuiScreenReaderOnly,
|
EuiScreenReaderOnly,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import { OptionsListReduxState } from '../types';
|
|
||||||
import { OptionsListStrings } from './options_list_strings';
|
import { OptionsListStrings } from './options_list_strings';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
|
|
||||||
export const OptionsListPopoverInvalidSelections = () => {
|
export const OptionsListPopoverInvalidSelections = () => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { deselectOption },
|
|
||||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
|
||||||
|
|
||||||
const [selectableOptions, setSelectableOptions] = useState<EuiSelectableOption[]>([]); // will be set in following useEffect
|
const [selectableOptions, setSelectableOptions] = useState<EuiSelectableOption[]>([]); // will be set in following useEffect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -80,7 +71,7 @@ export const OptionsListPopoverInvalidSelections = () => {
|
||||||
listProps={{ onFocusBadge: false, isVirtualized: false }}
|
listProps={{ onFocusBadge: false, isVirtualized: false }}
|
||||||
onChange={(newSuggestions, _, changedOption) => {
|
onChange={(newSuggestions, _, changedOption) => {
|
||||||
setSelectableOptions(newSuggestions);
|
setSelectableOptions(newSuggestions);
|
||||||
dispatch(deselectOption(changedOption.label));
|
optionsList.dispatch.deselectOption(changedOption.label);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(list) => list}
|
{(list) => list}
|
||||||
|
|
|
@ -21,16 +21,14 @@ import {
|
||||||
Direction,
|
Direction,
|
||||||
EuiToolTip,
|
EuiToolTip,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCompatibleSortingTypes,
|
getCompatibleSortingTypes,
|
||||||
OPTIONS_LIST_DEFAULT_SORT,
|
OPTIONS_LIST_DEFAULT_SORT,
|
||||||
OptionsListSortBy,
|
OptionsListSortBy,
|
||||||
} from '../../../common/options_list/suggestions_sorting';
|
} from '../../../common/options_list/suggestions_sorting';
|
||||||
import { OptionsListReduxState } from '../types';
|
|
||||||
import { OptionsListStrings } from './options_list_strings';
|
import { OptionsListStrings } from './options_list_strings';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
|
|
||||||
interface OptionsListSortingPopoverProps {
|
interface OptionsListSortingPopoverProps {
|
||||||
showOnlySelected: boolean;
|
showOnlySelected: boolean;
|
||||||
|
@ -42,17 +40,10 @@ type SortByItem = EuiSelectableOption & {
|
||||||
export const OptionsListPopoverSortingButton = ({
|
export const OptionsListPopoverSortingButton = ({
|
||||||
showOnlySelected,
|
showOnlySelected,
|
||||||
}: OptionsListSortingPopoverProps) => {
|
}: OptionsListSortingPopoverProps) => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setSort },
|
|
||||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const field = optionsList.select((state) => state.componentState.field);
|
||||||
const field = select((state) => state.componentState.field);
|
const sort = optionsList.select((state) => state.explicitInput.sort ?? OPTIONS_LIST_DEFAULT_SORT);
|
||||||
const sort = select((state) => state.explicitInput.sort ?? OPTIONS_LIST_DEFAULT_SORT);
|
|
||||||
|
|
||||||
const [isSortingPopoverOpen, setIsSortingPopoverOpen] = useState(false);
|
const [isSortingPopoverOpen, setIsSortingPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
@ -87,7 +78,7 @@ export const OptionsListPopoverSortingButton = ({
|
||||||
setSortByOptions(updatedOptions);
|
setSortByOptions(updatedOptions);
|
||||||
const selectedOption = updatedOptions.find(({ checked }) => checked === 'on');
|
const selectedOption = updatedOptions.find(({ checked }) => checked === 'on');
|
||||||
if (selectedOption) {
|
if (selectedOption) {
|
||||||
dispatch(setSort({ by: selectedOption.data.sortBy }));
|
optionsList.dispatch.setSort({ by: selectedOption.data.sortBy });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,7 +126,9 @@ export const OptionsListPopoverSortingButton = ({
|
||||||
options={sortOrderOptions}
|
options={sortOrderOptions}
|
||||||
idSelected={sort.direction}
|
idSelected={sort.direction}
|
||||||
legend={OptionsListStrings.editorAndPopover.getSortDirectionLegend()}
|
legend={OptionsListStrings.editorAndPopover.getSortDirectionLegend()}
|
||||||
onChange={(value) => dispatch(setSort({ direction: value as Direction }))}
|
onChange={(value) =>
|
||||||
|
optionsList.dispatch.setSort({ direction: value as Direction })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
|
@ -10,12 +10,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { euiThemeVars } from '@kbn/ui-theme';
|
import { euiThemeVars } from '@kbn/ui-theme';
|
||||||
import { EuiSelectable } from '@elastic/eui';
|
import { EuiSelectable } from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
|
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 { OptionsListStrings } from './options_list_strings';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
|
||||||
import { OptionsListPopoverEmptyMessage } from './options_list_popover_empty_message';
|
import { OptionsListPopoverEmptyMessage } from './options_list_popover_empty_message';
|
||||||
import { OptionsListPopoverSuggestionBadge } from './options_list_popover_suggestion_badge';
|
import { OptionsListPopoverSuggestionBadge } from './options_list_popover_suggestion_badge';
|
||||||
|
|
||||||
|
@ -28,27 +27,21 @@ export const OptionsListPopoverSuggestions = ({
|
||||||
showOnlySelected,
|
showOnlySelected,
|
||||||
loadMoreSuggestions,
|
loadMoreSuggestions,
|
||||||
}: OptionsListPopoverSuggestionsProps) => {
|
}: OptionsListPopoverSuggestionsProps) => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { replaceSelection, deselectOption, selectOption, selectExists },
|
|
||||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const searchString = optionsList.select((state) => state.componentState.searchString);
|
||||||
const invalidSelections = select((state) => state.componentState.invalidSelections);
|
const availableOptions = optionsList.select((state) => state.componentState.availableOptions);
|
||||||
const availableOptions = select((state) => state.componentState.availableOptions);
|
const totalCardinality = optionsList.select((state) => state.componentState.totalCardinality);
|
||||||
const totalCardinality = select((state) => state.componentState.totalCardinality);
|
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||||
const searchString = select((state) => state.componentState.searchString);
|
|
||||||
|
|
||||||
const selectedOptions = select((state) => state.explicitInput.selectedOptions);
|
const sort = optionsList.select((state) => state.explicitInput.sort);
|
||||||
const existsSelected = select((state) => state.explicitInput.existsSelected);
|
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||||
const singleSelect = select((state) => state.explicitInput.singleSelect);
|
const hideExists = optionsList.select((state) => state.explicitInput.hideExists);
|
||||||
const hideExists = select((state) => state.explicitInput.hideExists);
|
const singleSelect = optionsList.select((state) => state.explicitInput.singleSelect);
|
||||||
const isLoading = select((state) => state.output.loading) ?? false;
|
const existsSelected = optionsList.select((state) => state.explicitInput.existsSelected);
|
||||||
const fieldName = select((state) => state.explicitInput.fieldName);
|
const selectedOptions = optionsList.select((state) => state.explicitInput.selectedOptions);
|
||||||
const sort = select((state) => state.explicitInput.sort);
|
|
||||||
|
const isLoading = optionsList.select((state) => state.output.loading) ?? false;
|
||||||
|
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -173,13 +166,13 @@ export const OptionsListPopoverSuggestions = ({
|
||||||
setSelectableOptions(newSuggestions);
|
setSelectableOptions(newSuggestions);
|
||||||
// the order of these checks matters, so be careful if rearranging them
|
// the order of these checks matters, so be careful if rearranging them
|
||||||
if (key === 'exists-option') {
|
if (key === 'exists-option') {
|
||||||
dispatch(selectExists(!Boolean(existsSelected)));
|
optionsList.dispatch.selectExists(!Boolean(existsSelected));
|
||||||
} else if (showOnlySelected || selectedOptionsSet.has(key)) {
|
} else if (showOnlySelected || selectedOptionsSet.has(key)) {
|
||||||
dispatch(deselectOption(key));
|
optionsList.dispatch.deselectOption(key);
|
||||||
} else if (singleSelect) {
|
} else if (singleSelect) {
|
||||||
dispatch(replaceSelection(key));
|
optionsList.dispatch.replaceSelection(key);
|
||||||
} else {
|
} else {
|
||||||
dispatch(selectOption(key));
|
optionsList.dispatch.selectOption(key);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,22 +9,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiIconTip } from '@elastic/eui';
|
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 { OptionsListStrings } from './options_list_strings';
|
||||||
import { optionsListReducers } from '../options_list_reducers';
|
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||||
|
|
||||||
export const OptionsListPopoverTitle = () => {
|
export const OptionsListPopoverTitle = () => {
|
||||||
// Redux embeddable container Context
|
const optionsList = useOptionsList();
|
||||||
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<
|
|
||||||
OptionsListReduxState,
|
|
||||||
typeof optionsListReducers
|
|
||||||
>();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const allowExpensiveQueries = optionsList.select(
|
||||||
const allowExpensiveQueries = select((state) => state.componentState.allowExpensiveQueries);
|
(state) => state.componentState.allowExpensiveQueries
|
||||||
const title = select((state) => state.explicitInput.title);
|
);
|
||||||
|
const title = optionsList.select((state) => state.explicitInput.title);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPopoverTitle paddingSize="s">
|
<EuiPopoverTitle paddingSize="s">
|
||||||
|
|
|
@ -6,15 +6,14 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { batch } from 'react-redux';
|
import { batch } from 'react-redux';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import { isEmpty, isEqual } from 'lodash';
|
import { isEmpty, isEqual } from 'lodash';
|
||||||
import { merge, Subject, Subscription } from 'rxjs';
|
import { merge, Subject, Subscription } from 'rxjs';
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
import { debounceTime, map, distinctUntilChanged, skip } from 'rxjs/operators';
|
import { debounceTime, map, distinctUntilChanged, skip } from 'rxjs/operators';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import {
|
import {
|
||||||
Filter,
|
Filter,
|
||||||
compareFilters,
|
compareFilters,
|
||||||
|
@ -23,23 +22,24 @@ import {
|
||||||
COMPARE_ALL_OPTIONS,
|
COMPARE_ALL_OPTIONS,
|
||||||
buildExistsFilter,
|
buildExistsFilter,
|
||||||
} from '@kbn/es-query';
|
} 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 { DataView, FieldSpec } from '@kbn/data-views-plugin/public';
|
||||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||||
import { KibanaThemeProvider } from '@kbn/kibana-react-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 {
|
import {
|
||||||
ControlInput,
|
ControlInput,
|
||||||
ControlOutput,
|
ControlOutput,
|
||||||
OptionsListEmbeddableInput,
|
|
||||||
OPTIONS_LIST_CONTROL,
|
OPTIONS_LIST_CONTROL,
|
||||||
|
OptionsListEmbeddableInput,
|
||||||
} from '../..';
|
} 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 { OptionsListControl } from '../components/options_list_control';
|
||||||
import { ControlsDataViewsService } from '../../services/data_views/types';
|
import { ControlsDataViewsService } from '../../services/data_views/types';
|
||||||
import { ControlsOptionsListService } from '../../services/options_list/types';
|
import { ControlsOptionsListService } from '../../services/options_list/types';
|
||||||
|
import { getDefaultComponentState, optionsListReducers } from '../options_list_reducers';
|
||||||
|
|
||||||
const diffDataFetchProps = (
|
const diffDataFetchProps = (
|
||||||
last?: OptionsListDataFetchProps,
|
last?: OptionsListDataFetchProps,
|
||||||
|
@ -62,6 +62,20 @@ interface OptionsListDataFetchProps {
|
||||||
filters?: ControlInput['filters'];
|
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> {
|
export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput, ControlOutput> {
|
||||||
public readonly type = OPTIONS_LIST_CONTROL;
|
public readonly type = OPTIONS_LIST_CONTROL;
|
||||||
public deferEmbeddableLoad = true;
|
public deferEmbeddableLoad = true;
|
||||||
|
@ -80,13 +94,16 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
private dataView?: DataView;
|
private dataView?: DataView;
|
||||||
private field?: FieldSpec;
|
private field?: FieldSpec;
|
||||||
|
|
||||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
// state management
|
||||||
OptionsListReduxState,
|
public select: OptionsListReduxEmbeddableTools['select'];
|
||||||
typeof optionsListReducers
|
public getState: OptionsListReduxEmbeddableTools['getState'];
|
||||||
>;
|
public dispatch: OptionsListReduxEmbeddableTools['dispatch'];
|
||||||
|
public onStateChange: OptionsListReduxEmbeddableTools['onStateChange'];
|
||||||
|
|
||||||
|
private cleanupStateTools: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
reduxToolsPackage: ReduxToolsPackage,
|
||||||
input: OptionsListEmbeddableInput,
|
input: OptionsListEmbeddableInput,
|
||||||
output: ControlOutput,
|
output: ControlOutput,
|
||||||
parent?: IContainer
|
parent?: IContainer
|
||||||
|
@ -101,7 +118,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
this.loadMoreSubject = new Subject<number>();
|
this.loadMoreSubject = new Subject<number>();
|
||||||
|
|
||||||
// build redux embeddable tools
|
// build redux embeddable tools
|
||||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||||
OptionsListReduxState,
|
OptionsListReduxState,
|
||||||
typeof optionsListReducers
|
typeof optionsListReducers
|
||||||
>({
|
>({
|
||||||
|
@ -110,6 +127,12 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
initialComponentState: getDefaultComponentState(),
|
initialComponentState: getDefaultComponentState(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.select = reduxEmbeddableTools.select;
|
||||||
|
this.getState = reduxEmbeddableTools.getState;
|
||||||
|
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||||
|
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||||
|
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,11 +140,9 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
const { selectedOptions: initialSelectedOptions } = this.getInput();
|
const { selectedOptions: initialSelectedOptions } = this.getInput();
|
||||||
if (!initialSelectedOptions) this.setInitializationFinished();
|
if (!initialSelectedOptions) this.setInitializationFinished();
|
||||||
|
|
||||||
const {
|
this.dispatch.setAllowExpensiveQueries(
|
||||||
actions: { setAllowExpensiveQueries },
|
await this.optionsListService.getAllowExpensiveQueries()
|
||||||
dispatch,
|
);
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
dispatch(setAllowExpensiveQueries(await this.optionsListService.getAllowExpensiveQueries()));
|
|
||||||
|
|
||||||
this.runOptionsListQuery().then(async () => {
|
this.runOptionsListQuery().then(async () => {
|
||||||
if (initialSelectedOptions) {
|
if (initialSelectedOptions) {
|
||||||
|
@ -183,19 +204,10 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subscribe(async ({ selectedOptions: newSelectedOptions }) => {
|
.subscribe(async ({ selectedOptions: newSelectedOptions }) => {
|
||||||
const {
|
|
||||||
actions: {
|
|
||||||
clearValidAndInvalidSelections,
|
|
||||||
setValidAndInvalidSelections,
|
|
||||||
publishFilters,
|
|
||||||
},
|
|
||||||
dispatch,
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
|
|
||||||
if (!newSelectedOptions || isEmpty(newSelectedOptions)) {
|
if (!newSelectedOptions || isEmpty(newSelectedOptions)) {
|
||||||
dispatch(clearValidAndInvalidSelections({}));
|
this.dispatch.clearValidAndInvalidSelections({});
|
||||||
} else {
|
} else {
|
||||||
const { invalidSelections } = this.reduxEmbeddableTools.getState().componentState ?? {};
|
const { invalidSelections } = this.getState().componentState ?? {};
|
||||||
const newValidSelections: string[] = [];
|
const newValidSelections: string[] = [];
|
||||||
const newInvalidSelections: string[] = [];
|
const newInvalidSelections: string[] = [];
|
||||||
for (const selectedOption of newSelectedOptions) {
|
for (const selectedOption of newSelectedOptions) {
|
||||||
|
@ -205,15 +217,13 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
}
|
}
|
||||||
newValidSelections.push(selectedOption);
|
newValidSelections.push(selectedOption);
|
||||||
}
|
}
|
||||||
dispatch(
|
this.dispatch.setValidAndInvalidSelections({
|
||||||
setValidAndInvalidSelections({
|
validSelections: newValidSelections,
|
||||||
validSelections: newValidSelections,
|
invalidSelections: newInvalidSelections,
|
||||||
invalidSelections: newInvalidSelections,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const newFilters = await this.buildFilter();
|
const newFilters = await this.buildFilter();
|
||||||
dispatch(publishFilters(newFilters));
|
this.dispatch.publishFilters(newFilters);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -222,15 +232,9 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
dataView?: DataView;
|
dataView?: DataView;
|
||||||
field?: FieldSpec;
|
field?: FieldSpec;
|
||||||
}> => {
|
}> => {
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
actions: { setField, setDataViewId },
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
explicitInput: { dataViewId, fieldName },
|
explicitInput: { dataViewId, fieldName },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
|
|
||||||
if (!this.dataView || this.dataView.id !== dataViewId) {
|
if (!this.dataView || this.dataView.id !== dataViewId) {
|
||||||
try {
|
try {
|
||||||
|
@ -246,7 +250,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
this.onFatalError(e);
|
this.onFatalError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(setDataViewId(this.dataView?.id));
|
this.dispatch.setDataViewId(this.dataView?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dataView && (!this.field || this.field.name !== fieldName)) {
|
if (this.dataView && (!this.field || this.field.name !== fieldName)) {
|
||||||
|
@ -265,31 +269,26 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.onFatalError(e);
|
this.onFatalError(e);
|
||||||
}
|
}
|
||||||
dispatch(setField(this.field));
|
this.dispatch.setField(this.field);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { dataView: this.dataView, field: this.field! };
|
return { dataView: this.dataView, field: this.field! };
|
||||||
};
|
};
|
||||||
|
|
||||||
private runOptionsListQuery = async (size: number = MIN_OPTIONS_LIST_REQUEST_SIZE) => {
|
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 previousFieldName = this.field?.name;
|
||||||
const { dataView, field } = await this.getCurrentDataViewAndField();
|
const { dataView, field } = await this.getCurrentDataViewAndField();
|
||||||
if (!dataView || !field) return;
|
if (!dataView || !field) return;
|
||||||
|
|
||||||
if (previousFieldName && field.name !== previousFieldName) {
|
if (previousFieldName && field.name !== previousFieldName) {
|
||||||
dispatch(setSearchString(''));
|
this.dispatch.setSearchString('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
componentState: { searchString, allowExpensiveQueries },
|
componentState: { searchString, allowExpensiveQueries },
|
||||||
explicitInput: { selectedOptions, runPastTimeout, existsSelected, sort },
|
explicitInput: { selectedOptions, runPastTimeout, existsSelected, sort },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
dispatch(setLoading(true));
|
this.dispatch.setLoading(true);
|
||||||
if (searchString.valid) {
|
if (searchString.valid) {
|
||||||
// need to get filters, query, ignoreParentSettings, and timeRange from input for inheritance
|
// need to get filters, query, ignoreParentSettings, and timeRange from input for inheritance
|
||||||
const {
|
const {
|
||||||
|
@ -342,14 +341,12 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
isEmpty(invalidSelections) ||
|
isEmpty(invalidSelections) ||
|
||||||
ignoreParentSettings?.ignoreValidations
|
ignoreParentSettings?.ignoreValidations
|
||||||
) {
|
) {
|
||||||
dispatch(
|
this.dispatch.updateQueryResults({
|
||||||
updateQueryResults({
|
availableOptions: suggestions,
|
||||||
availableOptions: suggestions,
|
invalidSelections: undefined,
|
||||||
invalidSelections: undefined,
|
validSelections: selectedOptions,
|
||||||
validSelections: selectedOptions,
|
totalCardinality,
|
||||||
totalCardinality,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const valid: string[] = [];
|
const valid: string[] = [];
|
||||||
const invalid: string[] = [];
|
const invalid: string[] = [];
|
||||||
|
@ -357,38 +354,33 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
if (invalidSelections?.includes(selectedOption)) invalid.push(selectedOption);
|
if (invalidSelections?.includes(selectedOption)) invalid.push(selectedOption);
|
||||||
else valid.push(selectedOption);
|
else valid.push(selectedOption);
|
||||||
}
|
}
|
||||||
dispatch(
|
this.dispatch.updateQueryResults({
|
||||||
updateQueryResults({
|
availableOptions: suggestions,
|
||||||
availableOptions: suggestions,
|
invalidSelections: invalid,
|
||||||
invalidSelections: invalid,
|
validSelections: valid,
|
||||||
validSelections: valid,
|
totalCardinality,
|
||||||
totalCardinality,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// publish filter
|
// publish filter
|
||||||
const newFilters = await this.buildFilter();
|
const newFilters = await this.buildFilter();
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setLoading(false));
|
this.dispatch.setLoading(false);
|
||||||
dispatch(publishFilters(newFilters));
|
this.dispatch.publishFilters(newFilters);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(
|
this.dispatch.updateQueryResults({
|
||||||
updateQueryResults({
|
availableOptions: {},
|
||||||
availableOptions: {},
|
});
|
||||||
})
|
this.dispatch.setLoading(false);
|
||||||
);
|
|
||||||
dispatch(setLoading(false));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private buildFilter = async () => {
|
private buildFilter = async () => {
|
||||||
const { getState } = this.reduxEmbeddableTools;
|
const { validSelections } = this.getState().componentState ?? {};
|
||||||
const { validSelections } = getState().componentState ?? {};
|
const { existsSelected } = this.getState().explicitInput ?? {};
|
||||||
const { existsSelected } = getState().explicitInput ?? {};
|
|
||||||
const { exclude } = this.getInput();
|
const { exclude } = this.getInput();
|
||||||
|
|
||||||
if ((!validSelections || isEmpty(validSelections)) && !existsSelected) {
|
if ((!validSelections || isEmpty(validSelections)) && !existsSelected) {
|
||||||
|
@ -421,22 +413,18 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
};
|
};
|
||||||
|
|
||||||
public onFatalError = (e: Error) => {
|
public onFatalError = (e: Error) => {
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
actions: { setPopoverOpen, setLoading },
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setLoading(false));
|
this.dispatch.setLoading(false);
|
||||||
dispatch(setPopoverOpen(false));
|
this.dispatch.setPopoverOpen(false);
|
||||||
});
|
});
|
||||||
super.onFatalError(e);
|
super.onFatalError(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
public destroy = () => {
|
public destroy = () => {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
this.cleanupStateTools();
|
||||||
this.abortController?.abort();
|
this.abortController?.abort();
|
||||||
this.subscriptions.unsubscribe();
|
this.subscriptions.unsubscribe();
|
||||||
this.reduxEmbeddableTools.cleanup();
|
|
||||||
if (this.node) ReactDOM.unmountComponentAtNode(this.node);
|
if (this.node) ReactDOM.unmountComponentAtNode(this.node);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -444,16 +432,15 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
||||||
if (this.node) {
|
if (this.node) {
|
||||||
ReactDOM.unmountComponentAtNode(this.node);
|
ReactDOM.unmountComponentAtNode(this.node);
|
||||||
}
|
}
|
||||||
const { Wrapper: OptionsListReduxWrapper } = this.reduxEmbeddableTools;
|
|
||||||
this.node = node;
|
this.node = node;
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||||
<OptionsListReduxWrapper>
|
<OptionsListEmbeddableContext.Provider value={this}>
|
||||||
<OptionsListControl
|
<OptionsListControl
|
||||||
typeaheadSubject={this.typeaheadSubject}
|
typeaheadSubject={this.typeaheadSubject}
|
||||||
loadMoreSubject={this.loadMoreSubject}
|
loadMoreSubject={this.loadMoreSubject}
|
||||||
/>
|
/>
|
||||||
</OptionsListReduxWrapper>
|
</OptionsListEmbeddableContext.Provider>
|
||||||
</KibanaThemeProvider>,
|
</KibanaThemeProvider>,
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import deepEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
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 { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -33,7 +33,7 @@ export class OptionsListEmbeddableFactory
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
public async create(initialInput: OptionsListEmbeddableInput, parent?: IContainer) {
|
public async create(initialInput: OptionsListEmbeddableInput, parent?: IContainer) {
|
||||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||||
const { OptionsListEmbeddable } = await import('./options_list_embeddable');
|
const { OptionsListEmbeddable } = await import('./options_list_embeddable');
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new OptionsListEmbeddable(reduxEmbeddablePackage, initialInput, {}, parent)
|
new OptionsListEmbeddable(reduxEmbeddablePackage, initialInput, {}, parent)
|
||||||
|
|
|
@ -16,10 +16,8 @@ import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
|
||||||
|
|
||||||
import { rangeSliderReducers } from '../range_slider_reducers';
|
import { useRangeSlider } from '../embeddable/range_slider_embeddable';
|
||||||
import { RangeSliderReduxState } from '../types';
|
|
||||||
import { RangeSliderPopover, EuiDualRangeRef } from './range_slider_popover';
|
import { RangeSliderPopover, EuiDualRangeRef } from './range_slider_popover';
|
||||||
|
|
||||||
import './range_slider.scss';
|
import './range_slider.scss';
|
||||||
|
@ -30,21 +28,14 @@ export const RangeSliderControl: FC = () => {
|
||||||
const rangeRef = useRef<EuiDualRangeRef>(null);
|
const rangeRef = useRef<EuiDualRangeRef>(null);
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Controls Services Context
|
const rangeSlider = useRangeSlider();
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setSelectedRange },
|
|
||||||
} = useReduxEmbeddableContext<RangeSliderReduxState, typeof rangeSliderReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
const min = rangeSlider.select((state) => state.componentState.min);
|
||||||
const min = select((state) => state.componentState.min);
|
const max = rangeSlider.select((state) => state.componentState.max);
|
||||||
const max = select((state) => state.componentState.max);
|
const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid);
|
||||||
const isInvalid = select((state) => state.componentState.isInvalid);
|
const id = rangeSlider.select((state) => state.explicitInput.id);
|
||||||
const id = select((state) => state.explicitInput.id);
|
const value = rangeSlider.select((state) => state.explicitInput.value) ?? ['', ''];
|
||||||
const value = select((state) => state.explicitInput.value) ?? ['', ''];
|
const isLoading = rangeSlider.select((state) => state.output.loading);
|
||||||
const isLoading = select((state) => state.output.loading);
|
|
||||||
|
|
||||||
const hasAvailableRange = min !== '' && max !== '';
|
const hasAvailableRange = min !== '' && max !== '';
|
||||||
|
|
||||||
|
@ -76,12 +67,10 @@ export const RangeSliderControl: FC = () => {
|
||||||
}`}
|
}`}
|
||||||
value={hasLowerBoundSelection ? lowerBoundValue : ''}
|
value={hasLowerBoundSelection ? lowerBoundValue : ''}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
dispatch(
|
rangeSlider.dispatch.setSelectedRange([
|
||||||
setSelectedRange([
|
event.target.value,
|
||||||
event.target.value,
|
isNaN(upperBoundValue) ? '' : String(upperBoundValue),
|
||||||
isNaN(upperBoundValue) ? '' : String(upperBoundValue),
|
]);
|
||||||
])
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
placeholder={`${hasAvailableRange ? roundedMin : ''}`}
|
placeholder={`${hasAvailableRange ? roundedMin : ''}`}
|
||||||
|
@ -103,12 +92,10 @@ export const RangeSliderControl: FC = () => {
|
||||||
}`}
|
}`}
|
||||||
value={hasUpperBoundSelection ? upperBoundValue : ''}
|
value={hasUpperBoundSelection ? upperBoundValue : ''}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
dispatch(
|
rangeSlider.dispatch.setSelectedRange([
|
||||||
setSelectedRange([
|
isNaN(lowerBoundValue) ? '' : String(lowerBoundValue),
|
||||||
isNaN(lowerBoundValue) ? '' : String(lowerBoundValue),
|
event.target.value,
|
||||||
event.target.value,
|
]);
|
||||||
])
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
placeholder={`${hasAvailableRange ? roundedMax : ''}`}
|
placeholder={`${hasAvailableRange ? roundedMax : ''}`}
|
||||||
|
|
|
@ -19,13 +19,11 @@ import {
|
||||||
EuiButtonIcon,
|
EuiButtonIcon,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range';
|
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 { pluginServices } from '../../services';
|
||||||
import { rangeSliderReducers } from '../range_slider_reducers';
|
|
||||||
import { RangeSliderReduxState } from '../types';
|
|
||||||
import { RangeSliderStrings } from './range_slider_strings';
|
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
|
// Unfortunately, wrapping EuiDualRange in `withEuiTheme` has created this annoying/verbose typing
|
||||||
export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps<typeof EuiDualRange>;
|
export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps<typeof EuiDualRange>;
|
||||||
|
@ -37,23 +35,17 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
||||||
const {
|
const {
|
||||||
dataViews: { get: getDataViewById },
|
dataViews: { get: getDataViewById },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const {
|
const rangeSlider = useRangeSlider();
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setSelectedRange },
|
|
||||||
} = useReduxEmbeddableContext<RangeSliderReduxState, typeof rangeSliderReducers>();
|
|
||||||
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
// Select current state from Redux using multiple selectors to avoid rerenders.
|
// Select current state from Redux using multiple selectors to avoid rerenders.
|
||||||
const dataViewId = select((state) => state.output.dataViewId);
|
const dataViewId = rangeSlider.select((state) => state.output.dataViewId);
|
||||||
const fieldSpec = select((state) => state.componentState.field);
|
const fieldSpec = rangeSlider.select((state) => state.componentState.field);
|
||||||
const id = select((state) => state.explicitInput.id);
|
const id = rangeSlider.select((state) => state.explicitInput.id);
|
||||||
const isInvalid = select((state) => state.componentState.isInvalid);
|
const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid);
|
||||||
const max = select((state) => state.componentState.max);
|
const max = rangeSlider.select((state) => state.componentState.max);
|
||||||
const min = select((state) => state.componentState.min);
|
const min = rangeSlider.select((state) => state.componentState.min);
|
||||||
const title = select((state) => state.explicitInput.title);
|
const title = rangeSlider.select((state) => state.explicitInput.title);
|
||||||
const value = select((state) => state.explicitInput.value) ?? ['', ''];
|
const value = rangeSlider.select((state) => state.explicitInput.value) ?? ['', ''];
|
||||||
|
|
||||||
const hasAvailableRange = min !== '' && max !== '';
|
const hasAvailableRange = min !== '' && max !== '';
|
||||||
const hasLowerBoundSelection = value[0] !== '';
|
const hasLowerBoundSelection = value[0] !== '';
|
||||||
|
@ -154,7 +146,7 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
||||||
const updatedUpperBound =
|
const updatedUpperBound =
|
||||||
typeof newUpperBound === 'number' ? String(newUpperBound) : value[1];
|
typeof newUpperBound === 'number' ? String(newUpperBound) : value[1];
|
||||||
|
|
||||||
dispatch(setSelectedRange([updatedLowerBound, updatedUpperBound]));
|
rangeSlider.dispatch.setSelectedRange([updatedLowerBound, updatedUpperBound]);
|
||||||
}}
|
}}
|
||||||
value={displayedValue}
|
value={displayedValue}
|
||||||
ticks={hasAvailableRange ? ticks : undefined}
|
ticks={hasAvailableRange ? ticks : undefined}
|
||||||
|
@ -179,7 +171,7 @@ export const RangeSliderPopover: FC<{ rangeRef?: Ref<EuiDualRangeRef> }> = ({ ra
|
||||||
iconType="eraser"
|
iconType="eraser"
|
||||||
color="danger"
|
color="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(setSelectedRange(['', '']));
|
rangeSlider.dispatch.setSelectedRange(['', '']);
|
||||||
}}
|
}}
|
||||||
aria-label={RangeSliderStrings.popover.getClearRangeButtonTitle()}
|
aria-label={RangeSliderStrings.popover.getClearRangeButtonTitle()}
|
||||||
data-test-subj="rangeSlider__clearRangeButton"
|
data-test-subj="rangeSlider__clearRangeButton"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { batch } from 'react-redux';
|
import { batch } from 'react-redux';
|
||||||
|
@ -15,8 +15,6 @@ import deepEqual from 'fast-deep-equal';
|
||||||
import { Subscription, lastValueFrom } from 'rxjs';
|
import { Subscription, lastValueFrom } from 'rxjs';
|
||||||
import { debounceTime, distinctUntilChanged, skip, map } from 'rxjs/operators';
|
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 {
|
import {
|
||||||
compareFilters,
|
compareFilters,
|
||||||
buildRangeFilter,
|
buildRangeFilter,
|
||||||
|
@ -27,7 +25,9 @@ import {
|
||||||
} from '@kbn/es-query';
|
} from '@kbn/es-query';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
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 {
|
import {
|
||||||
ControlInput,
|
ControlInput,
|
||||||
|
@ -36,11 +36,11 @@ import {
|
||||||
RANGE_SLIDER_CONTROL,
|
RANGE_SLIDER_CONTROL,
|
||||||
} from '../..';
|
} from '../..';
|
||||||
import { pluginServices } from '../../services';
|
import { pluginServices } from '../../services';
|
||||||
import { RangeSliderControl } from '../components/range_slider_control';
|
|
||||||
import { getDefaultComponentState, rangeSliderReducers } from '../range_slider_reducers';
|
|
||||||
import { RangeSliderReduxState } from '../types';
|
import { RangeSliderReduxState } from '../types';
|
||||||
import { ControlsDataService } from '../../services/data/types';
|
import { ControlsDataService } from '../../services/data/types';
|
||||||
|
import { RangeSliderControl } from '../components/range_slider_control';
|
||||||
import { ControlsDataViewsService } from '../../services/data_views/types';
|
import { ControlsDataViewsService } from '../../services/data_views/types';
|
||||||
|
import { getDefaultComponentState, rangeSliderReducers } from '../range_slider_reducers';
|
||||||
|
|
||||||
const diffDataFetchProps = (
|
const diffDataFetchProps = (
|
||||||
current?: RangeSliderDataFetchProps,
|
current?: RangeSliderDataFetchProps,
|
||||||
|
@ -65,6 +65,20 @@ interface RangeSliderDataFetchProps {
|
||||||
const fieldMissingError = (fieldName: string) =>
|
const fieldMissingError = (fieldName: string) =>
|
||||||
new Error(`field ${fieldName} not found in index pattern`);
|
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> {
|
export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput, ControlOutput> {
|
||||||
public readonly type = RANGE_SLIDER_CONTROL;
|
public readonly type = RANGE_SLIDER_CONTROL;
|
||||||
public deferEmbeddableLoad = true;
|
public deferEmbeddableLoad = true;
|
||||||
|
@ -80,13 +94,16 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
private dataView?: DataView;
|
private dataView?: DataView;
|
||||||
private field?: DataViewField;
|
private field?: DataViewField;
|
||||||
|
|
||||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
// state management
|
||||||
RangeSliderReduxState,
|
public select: RangeSliderReduxEmbeddableTools['select'];
|
||||||
typeof rangeSliderReducers
|
public getState: RangeSliderReduxEmbeddableTools['getState'];
|
||||||
>;
|
public dispatch: RangeSliderReduxEmbeddableTools['dispatch'];
|
||||||
|
public onStateChange: RangeSliderReduxEmbeddableTools['onStateChange'];
|
||||||
|
|
||||||
|
private cleanupStateTools: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
reduxToolsPackage: ReduxToolsPackage,
|
||||||
input: RangeSliderEmbeddableInput,
|
input: RangeSliderEmbeddableInput,
|
||||||
output: ControlOutput,
|
output: ControlOutput,
|
||||||
parent?: IContainer
|
parent?: IContainer
|
||||||
|
@ -96,7 +113,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
// Destructure controls services
|
// Destructure controls services
|
||||||
({ data: this.dataService, dataViews: this.dataViewsService } = pluginServices.getServices());
|
({ data: this.dataService, dataViews: this.dataViewsService } = pluginServices.getServices());
|
||||||
|
|
||||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||||
RangeSliderReduxState,
|
RangeSliderReduxState,
|
||||||
typeof rangeSliderReducers
|
typeof rangeSliderReducers
|
||||||
>({
|
>({
|
||||||
|
@ -104,6 +121,11 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
reducers: rangeSliderReducers,
|
reducers: rangeSliderReducers,
|
||||||
initialComponentState: getDefaultComponentState(),
|
initialComponentState: getDefaultComponentState(),
|
||||||
});
|
});
|
||||||
|
this.select = reduxEmbeddableTools.select;
|
||||||
|
this.getState = reduxEmbeddableTools.getState;
|
||||||
|
this.dispatch = reduxEmbeddableTools.dispatch;
|
||||||
|
this.onStateChange = reduxEmbeddableTools.onStateChange;
|
||||||
|
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
@ -157,14 +179,9 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
dataView?: DataView;
|
dataView?: DataView;
|
||||||
field?: DataViewField;
|
field?: DataViewField;
|
||||||
}> => {
|
}> => {
|
||||||
const {
|
|
||||||
getState,
|
|
||||||
dispatch,
|
|
||||||
actions: { setField, setDataViewId },
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
const {
|
const {
|
||||||
explicitInput: { dataViewId, fieldName },
|
explicitInput: { dataViewId, fieldName },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
|
|
||||||
if (!this.dataView || this.dataView.id !== dataViewId) {
|
if (!this.dataView || this.dataView.id !== dataViewId) {
|
||||||
try {
|
try {
|
||||||
|
@ -179,7 +196,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(setDataViewId(this.dataView.id));
|
this.dispatch.setDataViewId(this.dataView.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.onFatalError(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! };
|
return { dataView: this.dataView, field: this.field! };
|
||||||
};
|
};
|
||||||
|
|
||||||
private runRangeSliderQuery = async () => {
|
private runRangeSliderQuery = async () => {
|
||||||
const {
|
this.dispatch.setLoading(true);
|
||||||
dispatch,
|
|
||||||
actions: { setLoading, publishFilters, setMinMax },
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
|
|
||||||
dispatch(setLoading(true));
|
|
||||||
const { dataView, field } = await this.getCurrentDataViewAndField();
|
const { dataView, field } = await this.getCurrentDataViewAndField();
|
||||||
if (!dataView || !field) return;
|
if (!dataView || !field) return;
|
||||||
|
|
||||||
|
@ -226,8 +238,8 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setLoading(false));
|
this.dispatch.setLoading(false);
|
||||||
dispatch(publishFilters([]));
|
this.dispatch.publishFilters([]);
|
||||||
});
|
});
|
||||||
throw fieldMissingError(fieldName);
|
throw fieldMissingError(fieldName);
|
||||||
}
|
}
|
||||||
|
@ -258,12 +270,10 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(
|
this.dispatch.setMinMax({
|
||||||
setMinMax({
|
min: `${min ?? ''}`,
|
||||||
min: `${min ?? ''}`,
|
max: `${max ?? ''}`,
|
||||||
max: `${max ?? ''}`,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// build filter with new min/max
|
// build filter with new min/max
|
||||||
await this.buildFilter();
|
await this.buildFilter();
|
||||||
|
@ -323,11 +333,6 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
};
|
};
|
||||||
|
|
||||||
private buildFilter = async () => {
|
private buildFilter = async () => {
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
actions: { setLoading, setIsInvalid, setDataViewId, publishFilters },
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
const {
|
const {
|
||||||
componentState: { min: availableMin, max: availableMax },
|
componentState: { min: availableMin, max: availableMax },
|
||||||
explicitInput: {
|
explicitInput: {
|
||||||
|
@ -337,7 +342,7 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
ignoreParentSettings,
|
ignoreParentSettings,
|
||||||
value: [selectedMin, selectedMax] = ['', ''],
|
value: [selectedMin, selectedMax] = ['', ''],
|
||||||
},
|
},
|
||||||
} = getState();
|
} = this.getState();
|
||||||
|
|
||||||
const hasData = !isEmpty(availableMin) && !isEmpty(availableMax);
|
const hasData = !isEmpty(availableMin) && !isEmpty(availableMax);
|
||||||
const hasLowerSelection = !isEmpty(selectedMin);
|
const hasLowerSelection = !isEmpty(selectedMin);
|
||||||
|
@ -349,10 +354,10 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
|
|
||||||
if (!hasData || !hasEitherSelection) {
|
if (!hasData || !hasEitherSelection) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setLoading(false));
|
this.dispatch.setLoading(false);
|
||||||
dispatch(setIsInvalid(!ignoreParentSettings?.ignoreValidations && hasEitherSelection));
|
this.dispatch.setIsInvalid(!ignoreParentSettings?.ignoreValidations && hasEitherSelection);
|
||||||
dispatch(setDataViewId(dataView.id));
|
this.dispatch.setDataViewId(dataView.id);
|
||||||
dispatch(publishFilters([]));
|
this.dispatch.publishFilters([]);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -404,20 +409,20 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
|
|
||||||
if (!docCount) {
|
if (!docCount) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setLoading(false));
|
this.dispatch.setLoading(false);
|
||||||
dispatch(setIsInvalid(true));
|
this.dispatch.setIsInvalid(true);
|
||||||
dispatch(setDataViewId(dataView.id));
|
this.dispatch.setDataViewId(dataView.id);
|
||||||
dispatch(publishFilters([]));
|
this.dispatch.publishFilters([]);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setLoading(false));
|
this.dispatch.setLoading(false);
|
||||||
dispatch(setIsInvalid(false));
|
this.dispatch.setIsInvalid(false);
|
||||||
dispatch(setDataViewId(dataView.id));
|
this.dispatch.setDataViewId(dataView.id);
|
||||||
dispatch(publishFilters([rangeFilter]));
|
this.dispatch.publishFilters([rangeFilter]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -427,23 +432,22 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
|
||||||
|
|
||||||
public destroy = () => {
|
public destroy = () => {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
this.cleanupStateTools();
|
||||||
this.subscriptions.unsubscribe();
|
this.subscriptions.unsubscribe();
|
||||||
this.reduxEmbeddableTools.cleanup();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public render = (node: HTMLElement) => {
|
public render = (node: HTMLElement) => {
|
||||||
if (this.node) {
|
if (this.node) {
|
||||||
ReactDOM.unmountComponentAtNode(this.node);
|
ReactDOM.unmountComponentAtNode(this.node);
|
||||||
}
|
}
|
||||||
const { Wrapper: RangeSliderReduxWrapper } = this.reduxEmbeddableTools;
|
|
||||||
this.node = node;
|
this.node = node;
|
||||||
const ControlsServicesProvider = pluginServices.getContextProvider();
|
const ControlsServicesProvider = pluginServices.getContextProvider();
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||||
<ControlsServicesProvider>
|
<ControlsServicesProvider>
|
||||||
<RangeSliderReduxWrapper>
|
<RangeSliderControlContext.Provider value={this}>
|
||||||
<RangeSliderControl />
|
<RangeSliderControl />
|
||||||
</RangeSliderReduxWrapper>
|
</RangeSliderControlContext.Provider>
|
||||||
</ControlsServicesProvider>
|
</ControlsServicesProvider>
|
||||||
</KibanaThemeProvider>,
|
</KibanaThemeProvider>,
|
||||||
node
|
node
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
import deepEqual from 'fast-deep-equal';
|
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 { 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 {
|
import {
|
||||||
createRangeSliderExtract,
|
createRangeSliderExtract,
|
||||||
|
@ -45,7 +45,7 @@ export class RangeSliderEmbeddableFactory
|
||||||
public isEditable = () => Promise.resolve(true);
|
public isEditable = () => Promise.resolve(true);
|
||||||
|
|
||||||
public async create(initialInput: RangeSliderEmbeddableInput, parent?: IContainer) {
|
public async create(initialInput: RangeSliderEmbeddableInput, parent?: IContainer) {
|
||||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||||
const { RangeSliderEmbeddable } = await import('./range_slider_embeddable');
|
const { RangeSliderEmbeddable } = await import('./range_slider_embeddable');
|
||||||
|
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
|
|
|
@ -8,14 +8,12 @@
|
||||||
|
|
||||||
import React, { FC, useRef } from 'react';
|
import React, { FC, useRef } from 'react';
|
||||||
import { EuiInputPopover } from '@elastic/eui';
|
import { EuiInputPopover } from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
import { FROM_INDEX, TO_INDEX } from '../time_utils';
|
||||||
import { timeSliderReducers } from '../time_slider_reducers';
|
import { EuiDualRangeRef } from './time_slider_sliding_window_range';
|
||||||
import { TimeSliderReduxState } from '../types';
|
import { getRoundedTimeRangeBounds } from '../time_slider_selectors';
|
||||||
|
import { useTimeSlider } from '../embeddable/time_slider_embeddable';
|
||||||
import { TimeSliderPopoverButton } from './time_slider_popover_button';
|
import { TimeSliderPopoverButton } from './time_slider_popover_button';
|
||||||
import { TimeSliderPopoverContent } from './time_slider_popover_content';
|
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';
|
import './index.scss';
|
||||||
|
|
||||||
|
@ -25,25 +23,21 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeSlider: FC<Props> = (props: Props) => {
|
export const TimeSlider: FC<Props> = (props: Props) => {
|
||||||
const {
|
const timeSlider = useTimeSlider();
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
const stepSize = timeSlider.select((state) => {
|
||||||
actions,
|
|
||||||
} = useReduxEmbeddableContext<TimeSliderReduxState, typeof timeSliderReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
const stepSize = select((state) => {
|
|
||||||
return state.componentState.stepSize;
|
return state.componentState.stepSize;
|
||||||
});
|
});
|
||||||
const ticks = select((state) => {
|
const ticks = timeSlider.select((state) => {
|
||||||
return state.componentState.ticks;
|
return state.componentState.ticks;
|
||||||
});
|
});
|
||||||
const timeRangeBounds = select(getRoundedTimeRangeBounds);
|
const timeRangeBounds = timeSlider.select(getRoundedTimeRangeBounds);
|
||||||
const timeRangeMin = timeRangeBounds[FROM_INDEX];
|
const timeRangeMin = timeRangeBounds[FROM_INDEX];
|
||||||
const timeRangeMax = timeRangeBounds[TO_INDEX];
|
const timeRangeMax = timeRangeBounds[TO_INDEX];
|
||||||
const value = select((state) => {
|
const value = timeSlider.select((state) => {
|
||||||
return state.componentState.value;
|
return state.componentState.value;
|
||||||
});
|
});
|
||||||
const isOpen = select((state) => {
|
const isOpen = timeSlider.select((state) => {
|
||||||
return state.componentState.isOpen;
|
return state.componentState.isOpen;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,7 +58,7 @@ export const TimeSlider: FC<Props> = (props: Props) => {
|
||||||
input={
|
input={
|
||||||
<TimeSliderPopoverButton
|
<TimeSliderPopoverButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(actions.setIsOpen({ isOpen: !isOpen }));
|
timeSlider.dispatch.setIsOpen({ isOpen: !isOpen });
|
||||||
}}
|
}}
|
||||||
formatDate={props.formatDate}
|
formatDate={props.formatDate}
|
||||||
from={from}
|
from={from}
|
||||||
|
@ -72,7 +66,7 @@ export const TimeSlider: FC<Props> = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
closePopover={() => dispatch(actions.setIsOpen({ isOpen: false }))}
|
closePopover={() => timeSlider.dispatch.setIsOpen({ isOpen: false })}
|
||||||
panelPaddingSize="s"
|
panelPaddingSize="s"
|
||||||
anchorPosition="downCenter"
|
anchorPosition="downCenter"
|
||||||
disableFocusTrap
|
disableFocusTrap
|
||||||
|
|
|
@ -8,13 +8,12 @@
|
||||||
|
|
||||||
import React, { Ref } from 'react';
|
import React, { Ref } from 'react';
|
||||||
import { EuiButtonIcon, EuiRangeTick, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
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 { getIsAnchored } from '../time_slider_selectors';
|
||||||
import { TimeSliderStrings } from './time_slider_strings';
|
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 {
|
interface Props {
|
||||||
value: [number, number];
|
value: [number, number];
|
||||||
|
@ -41,13 +40,8 @@ export function TimeSliderPopoverContent(props: Props) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const timeSlider = useTimeSlider();
|
||||||
useEmbeddableDispatch,
|
const isAnchored = timeSlider.select(getIsAnchored);
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setIsAnchored },
|
|
||||||
} = useReduxEmbeddableContext<TimeSliderReduxState, typeof timeSliderReducers>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
const isAnchored = select(getIsAnchored);
|
|
||||||
const rangeInput = isAnchored ? (
|
const rangeInput = isAnchored ? (
|
||||||
<TimeSliderAnchoredRange
|
<TimeSliderAnchoredRange
|
||||||
value={props.value}
|
value={props.value}
|
||||||
|
@ -88,7 +82,7 @@ export function TimeSliderPopoverContent(props: Props) {
|
||||||
if (nextIsAnchored) {
|
if (nextIsAnchored) {
|
||||||
props.onChange([props.timeRangeMin, props.value[1]]);
|
props.onChange([props.timeRangeMin, props.value[1]]);
|
||||||
}
|
}
|
||||||
dispatch(setIsAnchored({ isAnchored: nextIsAnchored }));
|
timeSlider.dispatch.setIsAnchored({ isAnchored: nextIsAnchored });
|
||||||
}}
|
}}
|
||||||
aria-label={anchorStartToggleButtonLabel}
|
aria-label={anchorStartToggleButtonLabel}
|
||||||
data-test-subj="timeSlider__anchorStartToggleButton"
|
data-test-subj="timeSlider__anchorStartToggleButton"
|
||||||
|
|
|
@ -6,14 +6,12 @@
|
||||||
* Side Public License, v 1.
|
* 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 { i18n } from '@kbn/i18n';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import React, { FC, useState } from 'react';
|
||||||
import { EuiButtonIcon } from '@elastic/eui';
|
import { EuiButtonIcon } from '@elastic/eui';
|
||||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { timeSliderReducers } from '../time_slider_reducers';
|
import { useTimeSlider } from '../embeddable/time_slider_embeddable';
|
||||||
import { TimeSliderReduxState } from '../types';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
|
@ -22,11 +20,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeSliderPrepend: FC<Props> = (props: Props) => {
|
export const TimeSliderPrepend: FC<Props> = (props: Props) => {
|
||||||
const { useEmbeddableDispatch, actions } = useReduxEmbeddableContext<
|
const timeSlider = useTimeSlider();
|
||||||
TimeSliderReduxState,
|
|
||||||
typeof timeSliderReducers
|
|
||||||
>();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
const [isPaused, setIsPaused] = useState(true);
|
const [isPaused, setIsPaused] = useState(true);
|
||||||
const [timeoutId, setTimeoutId] = useState<number | undefined>(undefined);
|
const [timeoutId, setTimeoutId] = useState<number | undefined>(undefined);
|
||||||
|
@ -51,13 +45,13 @@ export const TimeSliderPrepend: FC<Props> = (props: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPlay = () => {
|
const onPlay = () => {
|
||||||
dispatch(actions.setIsOpen({ isOpen: true }));
|
timeSlider.dispatch.setIsOpen({ isOpen: true });
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
playNextFrame();
|
playNextFrame();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPause = () => {
|
const onPause = () => {
|
||||||
dispatch(actions.setIsOpen({ isOpen: true }));
|
timeSlider.dispatch.setIsOpen({ isOpen: true });
|
||||||
setIsPaused(true);
|
setIsPaused(true);
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
|
|
|
@ -10,10 +10,10 @@ import _ from 'lodash';
|
||||||
import { debounceTime, first, map } from 'rxjs/operators';
|
import { debounceTime, first, map } from 'rxjs/operators';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
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 type { TimeRange } from '@kbn/es-query';
|
||||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import React from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { TIME_SLIDER_CONTROL } from '../..';
|
import { TIME_SLIDER_CONTROL } from '../..';
|
||||||
|
@ -37,6 +37,20 @@ import {
|
||||||
} from '../time_utils';
|
} from '../time_utils';
|
||||||
import { getIsAnchored, getRoundedTimeRangeBounds } from '../time_slider_selectors';
|
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<
|
export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
TimeSliderControlEmbeddableInput,
|
TimeSliderControlEmbeddableInput,
|
||||||
ControlOutput
|
ControlOutput
|
||||||
|
@ -47,6 +61,14 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
private inputSubscription: Subscription;
|
private inputSubscription: Subscription;
|
||||||
private node?: HTMLElement;
|
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 getTimezone: ControlsSettingsService['getTimezone'];
|
||||||
private timefilter: ControlsDataService['timefilter'];
|
private timefilter: ControlsDataService['timefilter'];
|
||||||
private prevTimeRange: TimeRange | undefined;
|
private prevTimeRange: TimeRange | undefined;
|
||||||
|
@ -56,13 +78,8 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
};
|
};
|
||||||
private readonly waitForControlOutputConsumersToLoad$;
|
private readonly waitForControlOutputConsumersToLoad$;
|
||||||
|
|
||||||
private reduxEmbeddableTools: ReduxEmbeddableTools<
|
|
||||||
TimeSliderReduxState,
|
|
||||||
typeof timeSliderReducers
|
|
||||||
>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
reduxEmbeddablePackage: ReduxEmbeddablePackage,
|
reduxToolsPackage: ReduxToolsPackage,
|
||||||
input: TimeSliderControlEmbeddableInput,
|
input: TimeSliderControlEmbeddableInput,
|
||||||
output: ControlOutput,
|
output: ControlOutput,
|
||||||
parent?: IContainer
|
parent?: IContainer
|
||||||
|
@ -85,7 +102,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
timeRangeBounds[TO_INDEX],
|
timeRangeBounds[TO_INDEX],
|
||||||
this.getTimezone()
|
this.getTimezone()
|
||||||
);
|
);
|
||||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||||
TimeSliderReduxState,
|
TimeSliderReduxState,
|
||||||
typeof timeSliderReducers
|
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.inputSubscription = this.getInput$().subscribe(() => this.onInputChange());
|
||||||
|
|
||||||
this.waitForControlOutputConsumersToLoad$ =
|
this.waitForControlOutputConsumersToLoad$ =
|
||||||
|
@ -125,7 +148,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
|
|
||||||
public destroy = () => {
|
public destroy = () => {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
this.reduxEmbeddableTools.cleanup();
|
this.cleanupStateTools();
|
||||||
if (this.inputSubscription) {
|
if (this.inputSubscription) {
|
||||||
this.inputSubscription.unsubscribe();
|
this.inputSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
@ -136,7 +159,6 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
||||||
this.prevTimesliceAsPercentage ?? {};
|
this.prevTimesliceAsPercentage ?? {};
|
||||||
|
|
||||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
|
||||||
if (
|
if (
|
||||||
timesliceStartAsPercentageOfTimeRange !== input.timesliceStartAsPercentageOfTimeRange ||
|
timesliceStartAsPercentageOfTimeRange !== input.timesliceStartAsPercentageOfTimeRange ||
|
||||||
timesliceEndAsPercentageOfTimeRange !== input.timesliceEndAsPercentageOfTimeRange
|
timesliceEndAsPercentageOfTimeRange !== input.timesliceEndAsPercentageOfTimeRange
|
||||||
|
@ -149,8 +171,8 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
) {
|
) {
|
||||||
// If no selections have been saved into the timeslider, then both `timesliceStartAsPercentageOfTimeRange`
|
// 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
|
// and `timesliceEndAsPercentageOfTimeRange` will be undefined - so, need to reset component state to match
|
||||||
dispatch(actions.publishValue({ value: undefined }));
|
this.dispatch.publishValue({ value: undefined });
|
||||||
dispatch(actions.setValue({ value: undefined }));
|
this.dispatch.setValue({ value: undefined });
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, need to call `syncWithTimeRange` so that the component state value can be calculated and set
|
// Otherwise, need to call `syncWithTimeRange` so that the component state value can be calculated and set
|
||||||
this.syncWithTimeRange();
|
this.syncWithTimeRange();
|
||||||
|
@ -158,31 +180,26 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
} else if (input.timeRange && !_.isEqual(input.timeRange, this.prevTimeRange)) {
|
} else if (input.timeRange && !_.isEqual(input.timeRange, this.prevTimeRange)) {
|
||||||
const nextBounds = this.timeRangeToBounds(input.timeRange);
|
const nextBounds = this.timeRangeToBounds(input.timeRange);
|
||||||
const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], this.getTimezone());
|
const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], this.getTimezone());
|
||||||
dispatch(
|
this.dispatch.setTimeRangeBounds({
|
||||||
actions.setTimeRangeBounds({
|
...getStepSize(ticks),
|
||||||
...getStepSize(ticks),
|
ticks,
|
||||||
ticks,
|
timeRangeBounds: nextBounds,
|
||||||
timeRangeBounds: nextBounds,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
this.syncWithTimeRange();
|
this.syncWithTimeRange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncWithTimeRange() {
|
private syncWithTimeRange() {
|
||||||
this.prevTimeRange = this.getInput().timeRange;
|
this.prevTimeRange = this.getInput().timeRange;
|
||||||
const { actions, dispatch, getState } = this.reduxEmbeddableTools;
|
const stepSize = this.getState().componentState.stepSize;
|
||||||
const stepSize = getState().componentState.stepSize;
|
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
||||||
const timesliceStartAsPercentageOfTimeRange =
|
this.getState().explicitInput;
|
||||||
getState().explicitInput.timesliceStartAsPercentageOfTimeRange;
|
|
||||||
const timesliceEndAsPercentageOfTimeRange =
|
|
||||||
getState().explicitInput.timesliceEndAsPercentageOfTimeRange;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
timesliceStartAsPercentageOfTimeRange !== undefined &&
|
timesliceStartAsPercentageOfTimeRange !== undefined &&
|
||||||
timesliceEndAsPercentageOfTimeRange !== undefined
|
timesliceEndAsPercentageOfTimeRange !== undefined
|
||||||
) {
|
) {
|
||||||
const timeRangeBounds = getState().componentState.timeRangeBounds;
|
const timeRangeBounds = this.getState().componentState.timeRangeBounds;
|
||||||
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
||||||
const from = timeRangeBounds[FROM_INDEX] + timesliceStartAsPercentageOfTimeRange * timeRange;
|
const from = timeRangeBounds[FROM_INDEX] + timesliceStartAsPercentageOfTimeRange * timeRange;
|
||||||
const to = timeRangeBounds[FROM_INDEX] + timesliceEndAsPercentageOfTimeRange * timeRange;
|
const to = timeRangeBounds[FROM_INDEX] + timesliceEndAsPercentageOfTimeRange * timeRange;
|
||||||
|
@ -190,8 +207,8 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
roundDownToNextStepSizeFactor(from, stepSize),
|
roundDownToNextStepSizeFactor(from, stepSize),
|
||||||
roundUpToNextStepSizeFactor(to, stepSize),
|
roundUpToNextStepSizeFactor(to, stepSize),
|
||||||
] as [number, number];
|
] as [number, number];
|
||||||
dispatch(actions.publishValue({ value }));
|
this.dispatch.publishValue({ value });
|
||||||
dispatch(actions.setValue({ value }));
|
this.dispatch.setValue({ value });
|
||||||
this.onRangeChange(value[TO_INDEX] - value[FROM_INDEX]);
|
this.onRangeChange(value[TO_INDEX] - value[FROM_INDEX]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,16 +225,14 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
}
|
}
|
||||||
|
|
||||||
private debouncedPublishChange = _.debounce((value?: [number, number]) => {
|
private debouncedPublishChange = _.debounce((value?: [number, number]) => {
|
||||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
this.dispatch.publishValue({ value });
|
||||||
dispatch(actions.publishValue({ value }));
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
private getTimeSliceAsPercentageOfTimeRange(value?: [number, number]) {
|
private getTimeSliceAsPercentageOfTimeRange(value?: [number, number]) {
|
||||||
const { getState } = this.reduxEmbeddableTools;
|
|
||||||
let timesliceStartAsPercentageOfTimeRange: number | undefined;
|
let timesliceStartAsPercentageOfTimeRange: number | undefined;
|
||||||
let timesliceEndAsPercentageOfTimeRange: number | undefined;
|
let timesliceEndAsPercentageOfTimeRange: number | undefined;
|
||||||
if (value) {
|
if (value) {
|
||||||
const timeRangeBounds = getState().componentState.timeRangeBounds;
|
const timeRangeBounds = this.getState().componentState.timeRangeBounds;
|
||||||
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
||||||
timesliceStartAsPercentageOfTimeRange =
|
timesliceStartAsPercentageOfTimeRange =
|
||||||
(value[FROM_INDEX] - timeRangeBounds[FROM_INDEX]) / timeRange;
|
(value[FROM_INDEX] - timeRangeBounds[FROM_INDEX]) / timeRange;
|
||||||
|
@ -232,39 +247,29 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTimesliceChange = (value?: [number, number]) => {
|
private onTimesliceChange = (value?: [number, number]) => {
|
||||||
const { actions, dispatch } = this.reduxEmbeddableTools;
|
|
||||||
|
|
||||||
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
|
||||||
this.getTimeSliceAsPercentageOfTimeRange(value);
|
this.getTimeSliceAsPercentageOfTimeRange(value);
|
||||||
dispatch(
|
this.dispatch.setValueAsPercentageOfTimeRange({
|
||||||
actions.setValueAsPercentageOfTimeRange({
|
timesliceStartAsPercentageOfTimeRange,
|
||||||
timesliceStartAsPercentageOfTimeRange,
|
timesliceEndAsPercentageOfTimeRange,
|
||||||
timesliceEndAsPercentageOfTimeRange,
|
});
|
||||||
})
|
this.dispatch.setValue({ value });
|
||||||
);
|
|
||||||
dispatch(actions.setValue({ value }));
|
|
||||||
this.debouncedPublishChange(value);
|
this.debouncedPublishChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRangeChange = (range?: number) => {
|
private onRangeChange = (range?: number) => {
|
||||||
const { actions, dispatch, getState } = this.reduxEmbeddableTools;
|
const timeRangeBounds = this.getState().componentState.timeRangeBounds;
|
||||||
const timeRangeBounds = getState().componentState.timeRangeBounds;
|
|
||||||
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
const timeRange = timeRangeBounds[TO_INDEX] - timeRangeBounds[FROM_INDEX];
|
||||||
dispatch(
|
this.dispatch.setRange({
|
||||||
actions.setRange({
|
range: range !== undefined && range < timeRange ? range : undefined,
|
||||||
range: range !== undefined && range < timeRange ? range : undefined,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNext = () => {
|
private onNext = () => {
|
||||||
const { getState } = this.reduxEmbeddableTools;
|
const { value, range, ticks } = this.getState().componentState;
|
||||||
const value = getState().componentState.value;
|
const isAnchored = getIsAnchored(this.getState());
|
||||||
const range = getState().componentState.range;
|
|
||||||
const ticks = getState().componentState.ticks;
|
|
||||||
const isAnchored = getIsAnchored(getState());
|
|
||||||
const tickRange = ticks[1].value - ticks[0].value;
|
const tickRange = ticks[1].value - ticks[0].value;
|
||||||
const timeRangeBounds = getRoundedTimeRangeBounds(getState());
|
const timeRangeBounds = getRoundedTimeRangeBounds(this.getState());
|
||||||
|
|
||||||
if (isAnchored) {
|
if (isAnchored) {
|
||||||
if (value === undefined || value[TO_INDEX] >= timeRangeBounds[TO_INDEX]) {
|
if (value === undefined || value[TO_INDEX] >= timeRangeBounds[TO_INDEX]) {
|
||||||
|
@ -304,13 +309,10 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPrevious = () => {
|
private onPrevious = () => {
|
||||||
const { getState } = this.reduxEmbeddableTools;
|
const { value, range, ticks } = this.getState().componentState;
|
||||||
const value = getState().componentState.value;
|
const isAnchored = getIsAnchored(this.getState());
|
||||||
const range = getState().componentState.range;
|
|
||||||
const ticks = getState().componentState.ticks;
|
|
||||||
const isAnchored = getIsAnchored(getState());
|
|
||||||
const tickRange = ticks[1].value - ticks[0].value;
|
const tickRange = ticks[1].value - ticks[0].value;
|
||||||
const timeRangeBounds = getRoundedTimeRangeBounds(getState());
|
const timeRangeBounds = getRoundedTimeRangeBounds(this.getState());
|
||||||
|
|
||||||
if (isAnchored) {
|
if (isAnchored) {
|
||||||
const prevTick = value
|
const prevTick = value
|
||||||
|
@ -347,10 +349,9 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
};
|
};
|
||||||
|
|
||||||
private formatDate = (epoch: number) => {
|
private formatDate = (epoch: number) => {
|
||||||
const { getState } = this.reduxEmbeddableTools;
|
|
||||||
return moment
|
return moment
|
||||||
.tz(epoch, getMomentTimezone(this.getTimezone()))
|
.tz(epoch, getMomentTimezone(this.getTimezone()))
|
||||||
.format(getState().componentState.format);
|
.format(this.getState().componentState.format);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render = (node: HTMLElement) => {
|
public render = (node: HTMLElement) => {
|
||||||
|
@ -358,12 +359,9 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
ReactDOM.unmountComponentAtNode(this.node);
|
ReactDOM.unmountComponentAtNode(this.node);
|
||||||
}
|
}
|
||||||
this.node = node;
|
this.node = node;
|
||||||
|
|
||||||
const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools;
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
<KibanaThemeProvider theme$={pluginServices.getServices().theme.theme$}>
|
||||||
<TimeSliderControlReduxWrapper>
|
<TimeSliderControlContext.Provider value={this}>
|
||||||
<TimeSlider
|
<TimeSlider
|
||||||
formatDate={this.formatDate}
|
formatDate={this.formatDate}
|
||||||
onChange={(value?: [number, number]) => {
|
onChange={(value?: [number, number]) => {
|
||||||
|
@ -372,22 +370,21 @@ export class TimeSliderControlEmbeddable extends Embeddable<
|
||||||
this.onRangeChange(range);
|
this.onRangeChange(range);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TimeSliderControlReduxWrapper>
|
</TimeSliderControlContext.Provider>
|
||||||
</KibanaThemeProvider>,
|
</KibanaThemeProvider>,
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public renderPrepend() {
|
public renderPrepend() {
|
||||||
const { Wrapper: TimeSliderControlReduxWrapper } = this.reduxEmbeddableTools;
|
|
||||||
return (
|
return (
|
||||||
<TimeSliderControlReduxWrapper>
|
<TimeSliderControlContext.Provider value={this}>
|
||||||
<TimeSliderPrepend
|
<TimeSliderPrepend
|
||||||
onNext={this.onNext}
|
onNext={this.onNext}
|
||||||
onPrevious={this.onPrevious}
|
onPrevious={this.onPrevious}
|
||||||
waitForControlOutputConsumersToLoad$={this.waitForControlOutputConsumersToLoad$}
|
waitForControlOutputConsumersToLoad$={this.waitForControlOutputConsumersToLoad$}
|
||||||
/>
|
/>
|
||||||
</TimeSliderControlReduxWrapper>
|
</TimeSliderControlContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
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 {
|
import {
|
||||||
createTimeSliderExtract,
|
createTimeSliderExtract,
|
||||||
createTimeSliderInject,
|
createTimeSliderInject,
|
||||||
|
@ -24,7 +24,7 @@ export class TimeSliderEmbeddableFactory
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
public async create(initialInput: any, parent?: IContainer) {
|
public async create(initialInput: any, parent?: IContainer) {
|
||||||
const reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();
|
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
|
||||||
const { TimeSliderControlEmbeddable } = await import('./time_slider_embeddable');
|
const { TimeSliderControlEmbeddable } = await import('./time_slider_embeddable');
|
||||||
|
|
||||||
return Promise.resolve(
|
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;
|
panelRefName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DashboardContainerInput =
|
export type DashboardContainerByReferenceInput = SavedObjectEmbeddableInput;
|
||||||
| DashboardContainerByReferenceInput
|
|
||||||
| DashboardContainerByValueInput;
|
|
||||||
|
|
||||||
export type DashboardContainerByReferenceInput = SavedObjectEmbeddableInput & { panels: never };
|
export interface DashboardContainerInput extends EmbeddableInput {
|
||||||
|
|
||||||
export interface DashboardContainerByValueInput extends EmbeddableInput {
|
|
||||||
// filter context to be passed to children
|
// filter context to be passed to children
|
||||||
query: Query;
|
query: Query;
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
|
|
|
@ -17,7 +17,6 @@ export type {
|
||||||
DashboardPanelMap,
|
DashboardPanelMap,
|
||||||
DashboardPanelState,
|
DashboardPanelState,
|
||||||
DashboardContainerInput,
|
DashboardContainerInput,
|
||||||
DashboardContainerByValueInput,
|
|
||||||
DashboardContainerByReferenceInput,
|
DashboardContainerByReferenceInput,
|
||||||
} from './dashboard_container/types';
|
} from './dashboard_container/types';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { EmbeddableInput, EmbeddableStateWithType } from '@kbn/embeddable-plugin
|
||||||
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||||
|
|
||||||
import { SavedDashboardPanel } from './dashboard_saved_object/types';
|
import { SavedDashboardPanel } from './dashboard_saved_object/types';
|
||||||
import { DashboardContainerByValueInput, DashboardPanelState } from './dashboard_container/types';
|
import { DashboardContainerInput, DashboardPanelState } from './dashboard_container/types';
|
||||||
|
|
||||||
export interface DashboardOptions {
|
export interface DashboardOptions {
|
||||||
hidePanelTitles: boolean;
|
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
|
* For BWC reasons, dashboard state is stored with panels as an array instead of a map
|
||||||
*/
|
*/
|
||||||
export type SharedDashboardState = Partial<
|
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';
|
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||||
|
|
||||||
import { getSampleDashboardInput } from '../mocks';
|
import { buildMockDashboard } from '../mocks';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { AddToLibraryAction } from './add_to_library_action';
|
import { AddToLibraryAction } from './add_to_library_action';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
@ -48,8 +48,7 @@ Object.defineProperty(pluginServices.getServices().application, 'capabilities',
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pluginServices.getServices().application.capabilities = defaultCapabilities;
|
pluginServices.getServices().application.capabilities = defaultCapabilities;
|
||||||
|
|
||||||
container = new DashboardContainer(getSampleDashboardInput());
|
container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { ErrorEmbeddable, IContainer, isErrorEmbeddable } from '@kbn/embeddable-
|
||||||
import { DashboardPanelState } from '../../common';
|
import { DashboardPanelState } from '../../common';
|
||||||
import { ClonePanelAction } from './clone_panel_action';
|
import { ClonePanelAction } from './clone_panel_action';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
|
||||||
let container: DashboardContainer;
|
let container: DashboardContainer;
|
||||||
|
@ -37,7 +37,12 @@ beforeEach(async () => {
|
||||||
create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })),
|
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: {
|
panels: {
|
||||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Kibanana', id: '123' },
|
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<
|
const refOrValContactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ExpandPanelAction } from './expand_panel_action';
|
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 { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
|
||||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||||
|
@ -30,7 +30,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||||
.mockReturnValue(mockEmbeddableFactory);
|
.mockReturnValue(mockEmbeddableFactory);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const input = getSampleDashboardInput({
|
container = buildMockDashboard({
|
||||||
panels: {
|
panels: {
|
||||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Sam', id: '123' },
|
explicitInput: { firstName: 'Sam', id: '123' },
|
||||||
|
@ -39,9 +39,6 @@ beforeEach(async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
container = new DashboardContainer(input);
|
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
ContactCardEmbeddableOutput,
|
ContactCardEmbeddableOutput,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '@kbn/embeddable-
|
||||||
|
|
||||||
import { ExportCSVAction } from './export_csv_action';
|
import { ExportCSVAction } from './export_csv_action';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
|
||||||
describe('Export CSV action', () => {
|
describe('Export CSV action', () => {
|
||||||
|
@ -45,7 +45,7 @@ describe('Export CSV action', () => {
|
||||||
create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })),
|
create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })),
|
||||||
};
|
};
|
||||||
|
|
||||||
const input = getSampleDashboardInput({
|
container = buildMockDashboard({
|
||||||
panels: {
|
panels: {
|
||||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Kibanana', id: '123' },
|
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<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -17,9 +17,8 @@ import {
|
||||||
import { type Query, type AggregateQuery, Filter } from '@kbn/es-query';
|
import { type Query, type AggregateQuery, Filter } from '@kbn/es-query';
|
||||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||||
|
|
||||||
import { getSampleDashboardInput } from '../mocks';
|
import { buildMockDashboard } from '../mocks';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
|
||||||
import { FiltersNotificationAction } from './filters_notification_action';
|
import { FiltersNotificationAction } from './filters_notification_action';
|
||||||
|
|
||||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
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 buildEmbeddable = async (input?: Partial<ContactCardEmbeddableInput>) => {
|
||||||
const container = new DashboardContainer(getSampleDashboardInput());
|
const container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
ContactCardEmbeddableOutput,
|
ContactCardEmbeddableOutput,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { FilterableEmbeddable, isErrorEmbeddable, ViewMode } from '@kbn/embeddab
|
||||||
|
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||||
import { getSampleDashboardInput } from '../mocks';
|
import { buildMockDashboard } from '../mocks';
|
||||||
import { EuiPopover } from '@elastic/eui';
|
import { EuiPopover } from '@elastic/eui';
|
||||||
import {
|
import {
|
||||||
FiltersNotificationPopover,
|
FiltersNotificationPopover,
|
||||||
|
@ -40,8 +40,7 @@ describe('filters notification popover', () => {
|
||||||
let defaultProps: FiltersNotificationProps;
|
let defaultProps: FiltersNotificationProps;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = new DashboardContainer(getSampleDashboardInput());
|
container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
ContactCardEmbeddableOutput,
|
ContactCardEmbeddableOutput,
|
||||||
|
|
|
@ -22,11 +22,11 @@ import {
|
||||||
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||||
|
|
||||||
import { getSampleDashboardInput } from '../mocks';
|
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||||
import { LibraryNotificationAction } from './library_notification_action';
|
import { LibraryNotificationAction } from './library_notification_action';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
import { buildMockDashboard } from '../mocks';
|
||||||
|
|
||||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||||
|
@ -43,8 +43,7 @@ beforeEach(async () => {
|
||||||
execute: jest.fn(),
|
execute: jest.fn(),
|
||||||
} as unknown as UnlinkFromLibraryAction;
|
} as unknown as UnlinkFromLibraryAction;
|
||||||
|
|
||||||
container = new DashboardContainer(getSampleDashboardInput());
|
container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {
|
||||||
LibraryNotificationPopover,
|
LibraryNotificationPopover,
|
||||||
LibraryNotificationProps,
|
LibraryNotificationProps,
|
||||||
} from './library_notification_popover';
|
} from './library_notification_popover';
|
||||||
import { getSampleDashboardInput } from '../mocks';
|
import { buildMockDashboard } from '../mocks';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
|
||||||
|
@ -38,8 +38,7 @@ describe('LibraryNotificationPopover', () => {
|
||||||
let defaultProps: LibraryNotificationProps;
|
let defaultProps: LibraryNotificationProps;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = new DashboardContainer(getSampleDashboardInput());
|
container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
import { ReplacePanelAction } from './replace_panel_action';
|
import { ReplacePanelAction } from './replace_panel_action';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
|
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||||
|
|
||||||
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||||
|
@ -28,7 +28,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||||
let container: DashboardContainer;
|
let container: DashboardContainer;
|
||||||
let embeddable: ContactCardEmbeddable;
|
let embeddable: ContactCardEmbeddable;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const input = getSampleDashboardInput({
|
container = buildMockDashboard({
|
||||||
panels: {
|
panels: {
|
||||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Sam', id: '123' },
|
explicitInput: { firstName: 'Sam', id: '123' },
|
||||||
|
@ -36,8 +36,6 @@ beforeEach(async () => {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
container = new DashboardContainer(input);
|
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
@ -54,7 +52,7 @@ beforeEach(async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Executes the replace panel action', async () => {
|
test('Executes the replace panel action', () => {
|
||||||
let SavedObjectFinder: any;
|
let SavedObjectFinder: any;
|
||||||
const action = new ReplacePanelAction(SavedObjectFinder);
|
const action = new ReplacePanelAction(SavedObjectFinder);
|
||||||
action.execute({ embeddable });
|
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);
|
await expect(check()).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Returns title', async () => {
|
test('Returns title', () => {
|
||||||
let SavedObjectFinder: any;
|
let SavedObjectFinder: any;
|
||||||
const action = new ReplacePanelAction(SavedObjectFinder);
|
const action = new ReplacePanelAction(SavedObjectFinder);
|
||||||
expect(action.getDisplayName({ embeddable })).toBeDefined();
|
expect(action.getDisplayName({ embeddable })).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Returns an icon', async () => {
|
test('Returns an icon', () => {
|
||||||
let SavedObjectFinder: any;
|
let SavedObjectFinder: any;
|
||||||
const action = new ReplacePanelAction(SavedObjectFinder);
|
const action = new ReplacePanelAction(SavedObjectFinder);
|
||||||
expect(action.getIconType({ embeddable })).toBeDefined();
|
expect(action.getIconType({ embeddable })).toBeDefined();
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
|
||||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||||
|
|
||||||
import { getSampleDashboardInput } from '../mocks';
|
import { buildMockDashboard } from '../mocks';
|
||||||
import { DashboardPanelState } from '../../common';
|
import { DashboardPanelState } from '../../common';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||||
|
@ -38,8 +38,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||||
.mockReturnValue(mockEmbeddableFactory);
|
.mockReturnValue(mockEmbeddableFactory);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = new DashboardContainer(getSampleDashboardInput());
|
container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import useMount from 'react-use/lib/useMount';
|
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 { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||||
|
@ -28,14 +29,14 @@ import {
|
||||||
removeSearchSessionIdFromURL,
|
removeSearchSessionIdFromURL,
|
||||||
createSessionRestorationDataProvider,
|
createSessionRestorationDataProvider,
|
||||||
} from './url/search_sessions_integration';
|
} from './url/search_sessions_integration';
|
||||||
|
import { DashboardAPI, DashboardRenderer } from '..';
|
||||||
import { DASHBOARD_APP_ID } from '../dashboard_constants';
|
import { DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
|
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 { type DashboardEmbedSettings, DashboardRedirect } from './types';
|
||||||
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
||||||
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
|
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 { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
|
||||||
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
|
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
|
||||||
|
|
||||||
|
@ -46,6 +47,16 @@ export interface DashboardAppProps {
|
||||||
embedSettings?: DashboardEmbedSettings;
|
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({
|
export function DashboardApp({
|
||||||
savedDashboardId,
|
savedDashboardId,
|
||||||
embedSettings,
|
embedSettings,
|
||||||
|
@ -57,10 +68,7 @@ export function DashboardApp({
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
(async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
|
(async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
|
||||||
});
|
});
|
||||||
|
const [dashboardAPI, setDashboardAPI] = useState<AwaitingDashboardAPI>(null);
|
||||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpack & set up dashboard services
|
* Unpack & set up dashboard services
|
||||||
|
@ -72,7 +80,9 @@ export function DashboardApp({
|
||||||
notifications: { toasts },
|
notifications: { toasts },
|
||||||
settings: { uiSettings },
|
settings: { uiSettings },
|
||||||
data: { search },
|
data: { search },
|
||||||
|
customBranding,
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
|
||||||
|
|
||||||
const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage(
|
const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage(
|
||||||
DASHBOARD_APP_ID,
|
DASHBOARD_APP_ID,
|
||||||
|
@ -140,7 +150,7 @@ export function DashboardApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override all state with URL + Locator input
|
// 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.
|
// State loaded from the dashboard app URL and from the locator overrides all other dashboard state.
|
||||||
...initialUrlState,
|
...initialUrlState,
|
||||||
...stateFromLocator,
|
...stateFromLocator,
|
||||||
|
@ -163,26 +173,17 @@ export function DashboardApp({
|
||||||
getScreenshotContext,
|
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
|
* When the dashboard container is created, or re-created, start syncing dashboard state with the URL
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dashboardContainer) return;
|
if (!dashboardAPI) return;
|
||||||
const { stopWatchingAppStateInUrl } = startSyncingDashboardUrlState({
|
const { stopWatchingAppStateInUrl } = startSyncingDashboardUrlState({
|
||||||
kbnUrlStateStorage,
|
kbnUrlStateStorage,
|
||||||
dashboardContainer,
|
dashboardAPI,
|
||||||
});
|
});
|
||||||
return () => stopWatchingAppStateInUrl();
|
return () => stopWatchingAppStateInUrl();
|
||||||
}, [dashboardContainer, kbnUrlStateStorage]);
|
}, [dashboardAPI, kbnUrlStateStorage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dshAppWrapper">
|
<div className="dshAppWrapper">
|
||||||
|
@ -191,18 +192,19 @@ export function DashboardApp({
|
||||||
)}
|
)}
|
||||||
{!showNoDataPage && (
|
{!showNoDataPage && (
|
||||||
<>
|
<>
|
||||||
{DashboardReduxWrapper && (
|
{dashboardAPI && (
|
||||||
<DashboardReduxWrapper>
|
<DashboardAPIContext.Provider value={dashboardAPI}>
|
||||||
<DashboardTopNav redirectTo={redirectTo} embedSettings={embedSettings} />
|
<DashboardTopNav redirectTo={redirectTo} embedSettings={embedSettings} />
|
||||||
</DashboardReduxWrapper>
|
</DashboardAPIContext.Provider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{getLegacyConflictWarning?.()}
|
{getLegacyConflictWarning?.()}
|
||||||
|
|
||||||
<DashboardContainerRenderer
|
<DashboardRenderer
|
||||||
|
ref={setDashboardAPI}
|
||||||
savedObjectId={savedDashboardId}
|
savedObjectId={savedDashboardId}
|
||||||
|
showPlainSpinner={showPlainSpinner}
|
||||||
getCreationOptions={getCreationOptions}
|
getCreationOptions={getCreationOptions}
|
||||||
onDashboardContainerLoaded={(container) => setDashboardContainer(container)}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
import { ScopedHistory } from '@kbn/core-application-browser';
|
import { ScopedHistory } from '@kbn/core-application-browser';
|
||||||
|
|
||||||
import { ForwardedDashboardState } from './locator';
|
import { ForwardedDashboardState } from './locator';
|
||||||
import { convertSavedPanelsToPanelMap, DashboardContainerByValueInput } from '../../../common';
|
import { convertSavedPanelsToPanelMap, DashboardContainerInput } from '../../../common';
|
||||||
|
|
||||||
export const loadDashboardHistoryLocationState = (
|
export const loadDashboardHistoryLocationState = (
|
||||||
getScopedHistory: () => ScopedHistory
|
getScopedHistory: () => ScopedHistory
|
||||||
): Partial<DashboardContainerByValueInput> => {
|
): Partial<DashboardContainerInput> => {
|
||||||
const state = getScopedHistory().location.state as undefined | ForwardedDashboardState;
|
const state = getScopedHistory().location.state as undefined | ForwardedDashboardState;
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'
|
||||||
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
|
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
|
||||||
|
|
||||||
import { DASHBOARD_APP_ID, SEARCH_SESSION_ID } from '../../dashboard_constants';
|
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).
|
* 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<
|
export type DashboardAppLocatorParams = Partial<
|
||||||
Omit<
|
Omit<
|
||||||
DashboardContainerByValueInput,
|
DashboardContainerInput,
|
||||||
'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally'
|
'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally'
|
||||||
>
|
>
|
||||||
> & {
|
> & {
|
||||||
|
|
|
@ -6,25 +6,31 @@
|
||||||
* Side Public License, v 1.
|
* 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 { css } from '@emotion/react';
|
||||||
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
|
import React, { useCallback } from 'react';
|
||||||
import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
|
import { METRIC_TYPE } from '@kbn/analytics';
|
||||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
import { IconType, useEuiTheme } from '@elastic/eui';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
|
||||||
|
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 {
|
import {
|
||||||
getCreateVisualizationButtonTitle,
|
getCreateVisualizationButtonTitle,
|
||||||
getQuickCreateButtonGroupLegend,
|
getQuickCreateButtonGroupLegend,
|
||||||
} from '../_dashboard_app_strings';
|
} from '../_dashboard_app_strings';
|
||||||
import { EditorMenu } from './editor_menu';
|
import { EditorMenu } from './editor_menu';
|
||||||
|
import { useDashboardAPI } from '../dashboard_app';
|
||||||
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { ControlsToolbarButton } from './controls_toolbar_button';
|
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() {
|
export function DashboardEditingToolbar() {
|
||||||
const {
|
const {
|
||||||
|
@ -36,7 +42,7 @@ export function DashboardEditingToolbar() {
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
|
const dashboard = useDashboardAPI();
|
||||||
|
|
||||||
const stateTransferService = getStateTransfer();
|
const stateTransferService = getStateTransfer();
|
||||||
|
|
||||||
|
@ -101,10 +107,7 @@ export function DashboardEditingToolbar() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEmbeddable = await dashboardContainer.addNewEmbeddable(
|
const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput);
|
||||||
embeddableFactory.type,
|
|
||||||
explicitInput
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newEmbeddable) {
|
if (newEmbeddable) {
|
||||||
toasts.addSuccess({
|
toasts.addSuccess({
|
||||||
|
@ -113,7 +116,7 @@ export function DashboardEditingToolbar() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[trackUiMetric, dashboardContainer, toasts]
|
[trackUiMetric, dashboard, toasts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getVisTypeQuickButton = (
|
const getVisTypeQuickButton = (
|
||||||
|
@ -170,12 +173,12 @@ export function DashboardEditingToolbar() {
|
||||||
const extraButtons = [
|
const extraButtons = [
|
||||||
<EditorMenu createNewVisType={createNewVisType} createNewEmbeddable={createNewEmbeddable} />,
|
<EditorMenu createNewVisType={createNewVisType} createNewEmbeddable={createNewEmbeddable} />,
|
||||||
<AddFromLibraryButton
|
<AddFromLibraryButton
|
||||||
onClick={() => dashboardContainer.addFromLibrary()}
|
onClick={() => dashboard.addFromLibrary()}
|
||||||
data-test-subj="dashboardAddPanelButton"
|
data-test-subj="dashboardAddPanelButton"
|
||||||
/>,
|
/>,
|
||||||
];
|
];
|
||||||
if (dashboardContainer.controlGroup) {
|
if (dashboard.controlGroup) {
|
||||||
extraButtons.push(<ControlsToolbarButton controlGroup={dashboardContainer.controlGroup} />);
|
extraButtons.push(<ControlsToolbarButton controlGroup={dashboard.controlGroup} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -26,13 +26,13 @@ import {
|
||||||
unsavedChangesBadgeStrings,
|
unsavedChangesBadgeStrings,
|
||||||
} from '../_dashboard_app_strings';
|
} from '../_dashboard_app_strings';
|
||||||
import { UI_SETTINGS } from '../../../common';
|
import { UI_SETTINGS } from '../../../common';
|
||||||
|
import { useDashboardAPI } from '../dashboard_app';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { useDashboardMenuItems } from './use_dashboard_menu_items';
|
import { useDashboardMenuItems } from './use_dashboard_menu_items';
|
||||||
import { DashboardEmbedSettings, DashboardRedirect } from '../types';
|
import { DashboardEmbedSettings, DashboardRedirect } from '../types';
|
||||||
import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
|
import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
|
||||||
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
|
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
|
||||||
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
|
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
|
||||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
|
||||||
|
|
||||||
import './_dashboard_top_nav.scss';
|
import './_dashboard_top_nav.scss';
|
||||||
export interface DashboardTopNavProps {
|
export interface DashboardTopNavProps {
|
||||||
|
@ -69,36 +69,26 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
||||||
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
||||||
/**
|
|
||||||
* Unpack dashboard state from redux
|
const dashboard = useDashboardAPI();
|
||||||
*/
|
|
||||||
const {
|
|
||||||
useEmbeddableDispatch,
|
|
||||||
actions: { setSavedQueryId },
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
embeddableInstance: dashboardContainer,
|
|
||||||
} = useDashboardContainerContext();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
const PresentationUtilContextProvider = getPresentationUtilContextProvider();
|
const PresentationUtilContextProvider = getPresentationUtilContextProvider();
|
||||||
|
|
||||||
const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges);
|
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
|
||||||
const fullScreenMode = select((state) => state.componentState.fullScreenMode);
|
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
|
||||||
const savedQueryId = select((state) => state.componentState.savedQueryId);
|
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
|
||||||
const lastSavedId = select((state) => state.componentState.lastSavedId);
|
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||||
const query = select((state) => state.explicitInput.query);
|
const query = dashboard.select((state) => state.explicitInput.query);
|
||||||
const title = select((state) => state.explicitInput.title);
|
const title = dashboard.select((state) => state.explicitInput.title);
|
||||||
|
|
||||||
// store data views in state & subscribe to dashboard data view changes.
|
// store data views in state & subscribe to dashboard data view changes.
|
||||||
const [allDataViews, setAllDataViews] = useState<DataView[]>(
|
const [allDataViews, setAllDataViews] = useState<DataView[]>(dashboard.getAllDataViews());
|
||||||
dashboardContainer.getAllDataViews()
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = dashboardContainer.onDataViewsUpdate$.subscribe((dataViews) =>
|
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||||
setAllDataViews(dataViews)
|
setAllDataViews(dataViews)
|
||||||
);
|
);
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [dashboardContainer]);
|
}, [dashboard]);
|
||||||
|
|
||||||
const dashboardTitle = useMemo(() => {
|
const dashboardTitle = useMemo(() => {
|
||||||
return getDashboardTitle(title, viewMode, !lastSavedId);
|
return getDashboardTitle(title, viewMode, !lastSavedId);
|
||||||
|
@ -212,7 +202,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
}, [embedSettings, filterManager, fullScreenMode, isChromeVisible, viewMode]);
|
}, [embedSettings, filterManager, fullScreenMode, isChromeVisible, viewMode]);
|
||||||
|
|
||||||
UseUnmount(() => {
|
UseUnmount(() => {
|
||||||
dashboardContainer.clearOverlays();
|
dashboard.clearOverlays();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -264,12 +254,12 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
}
|
}
|
||||||
onQuerySubmit={(_payload, isUpdate) => {
|
onQuerySubmit={(_payload, isUpdate) => {
|
||||||
if (isUpdate === false) {
|
if (isUpdate === false) {
|
||||||
dashboardContainer.forceRefresh();
|
dashboard.forceRefresh();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSavedQueryIdChange={(newId: string | undefined) => {
|
onSavedQueryIdChange={(newId: string | undefined) =>
|
||||||
dispatch(setSavedQueryId(newId));
|
dashboard.dispatch.setSavedQueryId(newId)
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
{viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? (
|
{viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? (
|
||||||
<PresentationUtilContextProvider>
|
<PresentationUtilContextProvider>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Capabilities } from '@kbn/core/public';
|
import { Capabilities } from '@kbn/core/public';
|
||||||
import { convertPanelMapToSavedPanels, DashboardContainerByValueInput } from '../../../../common';
|
import { convertPanelMapToSavedPanels, DashboardContainerInput } from '../../../../common';
|
||||||
|
|
||||||
import { DashboardAppLocatorParams } from '../../..';
|
import { DashboardAppLocatorParams } from '../../..';
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
import { pluginServices } from '../../../services/plugin_services';
|
||||||
|
@ -68,7 +68,7 @@ describe('ShowShareModal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const getPropsAndShare = (
|
const getPropsAndShare = (
|
||||||
unsavedState?: Partial<DashboardContainerByValueInput>
|
unsavedState?: Partial<DashboardContainerInput>
|
||||||
): ShowShareModalProps => {
|
): ShowShareModalProps => {
|
||||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||||
.fn()
|
.fn()
|
||||||
|
@ -94,7 +94,7 @@ describe('ShowShareModal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('locatorParams unsaved state is properly propagated to locator', () => {
|
it('locatorParams unsaved state is properly propagated to locator', () => {
|
||||||
const unsavedDashboardState: DashboardContainerByValueInput = {
|
const unsavedDashboardState: DashboardContainerInput = {
|
||||||
panels: {
|
panels: {
|
||||||
panel_1: {
|
panel_1: {
|
||||||
type: 'panel_type',
|
type: 'panel_type',
|
||||||
|
@ -121,7 +121,7 @@ describe('ShowShareModal', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
query: { query: 'bye', language: 'kuery' },
|
query: { query: 'bye', language: 'kuery' },
|
||||||
} as unknown as DashboardContainerByValueInput;
|
} as unknown as DashboardContainerInput;
|
||||||
const showModalProps = getPropsAndShare(unsavedDashboardState);
|
const showModalProps = getPropsAndShare(unsavedDashboardState);
|
||||||
ShowShareModal(showModalProps);
|
ShowShareModal(showModalProps);
|
||||||
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
|
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
|
@ -14,13 +14,13 @@ import { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
||||||
|
|
||||||
import { DashboardRedirect } from '../types';
|
import { DashboardRedirect } from '../types';
|
||||||
import { UI_SETTINGS } from '../../../common';
|
import { UI_SETTINGS } from '../../../common';
|
||||||
|
import { useDashboardAPI } from '../dashboard_app';
|
||||||
import { topNavStrings } from '../_dashboard_app_strings';
|
import { topNavStrings } from '../_dashboard_app_strings';
|
||||||
import { ShowShareModal } from './share/show_share_modal';
|
import { ShowShareModal } from './share/show_share_modal';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
||||||
import { SaveDashboardReturn } from '../../services/dashboard_saved_object/types';
|
import { SaveDashboardReturn } from '../../services/dashboard_saved_object/types';
|
||||||
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
|
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
|
||||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
|
||||||
|
|
||||||
export const useDashboardMenuItems = ({
|
export const useDashboardMenuItems = ({
|
||||||
redirectTo,
|
redirectTo,
|
||||||
|
@ -46,18 +46,12 @@ export const useDashboardMenuItems = ({
|
||||||
/**
|
/**
|
||||||
* Unpack dashboard state from redux
|
* Unpack dashboard state from redux
|
||||||
*/
|
*/
|
||||||
const {
|
const dashboard = useDashboardAPI();
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
embeddableInstance: dashboardContainer,
|
|
||||||
actions: { setViewMode, setFullScreenMode },
|
|
||||||
} = useDashboardContainerContext();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges);
|
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
|
||||||
const hasOverlays = select((state) => state.componentState.hasOverlays);
|
const hasOverlays = dashboard.select((state) => state.componentState.hasOverlays);
|
||||||
const lastSavedId = select((state) => state.componentState.lastSavedId);
|
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||||
const dashboardTitle = select((state) => state.explicitInput.title);
|
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the Dashboard app's share menu
|
* Show the Dashboard app's share menu
|
||||||
|
@ -95,41 +89,41 @@ export const useDashboardMenuItems = ({
|
||||||
*/
|
*/
|
||||||
const quickSaveDashboard = useCallback(() => {
|
const quickSaveDashboard = useCallback(() => {
|
||||||
setIsSaveInProgress(true);
|
setIsSaveInProgress(true);
|
||||||
dashboardContainer
|
dashboard
|
||||||
.runQuickSave()
|
.runQuickSave()
|
||||||
.then(() => setTimeout(() => setIsSaveInProgress(false), CHANGE_CHECK_DEBOUNCE));
|
.then(() => setTimeout(() => setIsSaveInProgress(false), CHANGE_CHECK_DEBOUNCE));
|
||||||
}, [dashboardContainer]);
|
}, [dashboard]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the dashboard's save modal
|
* Show the dashboard's save modal
|
||||||
*/
|
*/
|
||||||
const saveDashboardAs = useCallback(() => {
|
const saveDashboardAs = useCallback(() => {
|
||||||
dashboardContainer.runSaveAs().then((result) => maybeRedirect(result));
|
dashboard.runSaveAs().then((result) => maybeRedirect(result));
|
||||||
}, [maybeRedirect, dashboardContainer]);
|
}, [maybeRedirect, dashboard]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone the dashboard
|
* Clone the dashboard
|
||||||
*/
|
*/
|
||||||
const clone = useCallback(() => {
|
const clone = useCallback(() => {
|
||||||
dashboardContainer.runClone().then((result) => maybeRedirect(result));
|
dashboard.runClone().then((result) => maybeRedirect(result));
|
||||||
}, [maybeRedirect, dashboardContainer]);
|
}, [maybeRedirect, dashboard]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns to view mode. If the dashboard has unsaved changes shows a warning and resets to last saved state.
|
* Returns to view mode. If the dashboard has unsaved changes shows a warning and resets to last saved state.
|
||||||
*/
|
*/
|
||||||
const returnToViewMode = useCallback(() => {
|
const returnToViewMode = useCallback(() => {
|
||||||
dashboardContainer.clearOverlays();
|
dashboard.clearOverlays();
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
confirmDiscardUnsavedChanges(() => {
|
confirmDiscardUnsavedChanges(() => {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dashboardContainer.resetToLastSavedState();
|
dashboard.resetToLastSavedState();
|
||||||
dispatch(setViewMode(ViewMode.VIEW));
|
dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(setViewMode(ViewMode.VIEW));
|
dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||||
}, [dashboardContainer, dispatch, hasUnsavedChanges, setViewMode]);
|
}, [dashboard, hasUnsavedChanges]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all of the top nav configs that can be used by dashboard.
|
* Register all of the top nav configs that can be used by dashboard.
|
||||||
|
@ -140,7 +134,7 @@ export const useDashboardMenuItems = ({
|
||||||
...topNavStrings.fullScreen,
|
...topNavStrings.fullScreen,
|
||||||
id: 'full-screen',
|
id: 'full-screen',
|
||||||
testId: 'dashboardFullScreenMode',
|
testId: 'dashboardFullScreenMode',
|
||||||
run: () => dispatch(setFullScreenMode(true)),
|
run: () => dashboard.dispatch.setFullScreenMode(true),
|
||||||
} as TopNavMenuData,
|
} as TopNavMenuData,
|
||||||
|
|
||||||
labs: {
|
labs: {
|
||||||
|
@ -158,8 +152,8 @@ export const useDashboardMenuItems = ({
|
||||||
testId: 'dashboardEditMode',
|
testId: 'dashboardEditMode',
|
||||||
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
|
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
|
||||||
run: () => {
|
run: () => {
|
||||||
dashboardContainer.clearOverlays();
|
dashboard.dispatch.setViewMode(ViewMode.EDIT);
|
||||||
dispatch(setViewMode(ViewMode.EDIT));
|
dashboard.clearOverlays();
|
||||||
},
|
},
|
||||||
} as TopNavMenuData,
|
} as TopNavMenuData,
|
||||||
|
|
||||||
|
@ -206,7 +200,7 @@ export const useDashboardMenuItems = ({
|
||||||
id: 'settings',
|
id: 'settings',
|
||||||
testId: 'dashboardSettingsButton',
|
testId: 'dashboardSettingsButton',
|
||||||
disableButton: isSaveInProgress || hasOverlays,
|
disableButton: isSaveInProgress || hasOverlays,
|
||||||
run: () => dashboardContainer.showSettings(),
|
run: () => dashboard.showSettings(),
|
||||||
} as TopNavMenuData,
|
} as TopNavMenuData,
|
||||||
|
|
||||||
clone: {
|
clone: {
|
||||||
|
@ -219,19 +213,16 @@ export const useDashboardMenuItems = ({
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
quickSaveDashboard,
|
quickSaveDashboard,
|
||||||
dashboardContainer,
|
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
hasOverlays,
|
|
||||||
setFullScreenMode,
|
|
||||||
isSaveInProgress,
|
isSaveInProgress,
|
||||||
returnToViewMode,
|
returnToViewMode,
|
||||||
saveDashboardAs,
|
saveDashboardAs,
|
||||||
setIsLabsShown,
|
setIsLabsShown,
|
||||||
|
hasOverlays,
|
||||||
lastSavedId,
|
lastSavedId,
|
||||||
setViewMode,
|
|
||||||
isLabsShown,
|
isLabsShown,
|
||||||
showShare,
|
showShare,
|
||||||
dispatch,
|
dashboard,
|
||||||
clone,
|
clone,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ function getLocatorParams({
|
||||||
const {
|
const {
|
||||||
componentState: { lastSavedId },
|
componentState: { lastSavedId },
|
||||||
explicitInput: { panels, query, viewMode },
|
explicitInput: { panels, query, viewMode },
|
||||||
} = container.getReduxEmbeddableTools().getState();
|
} = container.getState();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewMode,
|
viewMode,
|
||||||
|
|
|
@ -18,9 +18,9 @@ import {
|
||||||
SavedDashboardPanel,
|
SavedDashboardPanel,
|
||||||
SharedDashboardState,
|
SharedDashboardState,
|
||||||
convertSavedPanelsToPanelMap,
|
convertSavedPanelsToPanelMap,
|
||||||
DashboardContainerByValueInput,
|
DashboardContainerInput,
|
||||||
} from '../../../common';
|
} from '../../../common';
|
||||||
import { DashboardContainer } from '../../dashboard_container';
|
import { DashboardAPI } from '../../dashboard_container';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
|
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
|
||||||
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
|
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
|
||||||
|
@ -59,7 +59,7 @@ function getPanelsMap(appStateInUrl: SharedDashboardState): DashboardPanelMap |
|
||||||
*/
|
*/
|
||||||
export const loadAndRemoveDashboardState = (
|
export const loadAndRemoveDashboardState = (
|
||||||
kbnUrlStateStorage: IKbnUrlStateStorage
|
kbnUrlStateStorage: IKbnUrlStateStorage
|
||||||
): Partial<DashboardContainerByValueInput> => {
|
): Partial<DashboardContainerInput> => {
|
||||||
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
|
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
|
||||||
DASHBOARD_STATE_STORAGE_KEY
|
DASHBOARD_STATE_STORAGE_KEY
|
||||||
);
|
);
|
||||||
|
@ -72,7 +72,7 @@ export const loadAndRemoveDashboardState = (
|
||||||
return hashQuery;
|
return hashQuery;
|
||||||
});
|
});
|
||||||
kbnUrlStateStorage.kbnUrlControls.update(nextUrl, true);
|
kbnUrlStateStorage.kbnUrlControls.update(nextUrl, true);
|
||||||
const partialState: Partial<DashboardContainerByValueInput> = {
|
const partialState: Partial<DashboardContainerInput> = {
|
||||||
..._.omit(rawAppStateInUrl, ['panels', 'query']),
|
..._.omit(rawAppStateInUrl, ['panels', 'query']),
|
||||||
...(panelsMap ? { panels: panelsMap } : {}),
|
...(panelsMap ? { panels: panelsMap } : {}),
|
||||||
...(rawAppStateInUrl.query ? { query: migrateLegacyQuery(rawAppStateInUrl.query) } : {}),
|
...(rawAppStateInUrl.query ? { query: migrateLegacyQuery(rawAppStateInUrl.query) } : {}),
|
||||||
|
@ -83,10 +83,10 @@ export const loadAndRemoveDashboardState = (
|
||||||
|
|
||||||
export const startSyncingDashboardUrlState = ({
|
export const startSyncingDashboardUrlState = ({
|
||||||
kbnUrlStateStorage,
|
kbnUrlStateStorage,
|
||||||
dashboardContainer,
|
dashboardAPI,
|
||||||
}: {
|
}: {
|
||||||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||||
dashboardContainer: DashboardContainer;
|
dashboardAPI: DashboardAPI;
|
||||||
}) => {
|
}) => {
|
||||||
const appStateSubscription = kbnUrlStateStorage
|
const appStateSubscription = kbnUrlStateStorage
|
||||||
.change$(DASHBOARD_STATE_STORAGE_KEY)
|
.change$(DASHBOARD_STATE_STORAGE_KEY)
|
||||||
|
@ -94,7 +94,7 @@ export const startSyncingDashboardUrlState = ({
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
const stateFromUrl = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
const stateFromUrl = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||||
if (Object.keys(stateFromUrl).length === 0) return;
|
if (Object.keys(stateFromUrl).length === 0) return;
|
||||||
dashboardContainer.updateInput(stateFromUrl);
|
dashboardAPI.updateInput(stateFromUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
const stopWatchingAppStateInUrl = () => appStateSubscription.unsubscribe();
|
const stopWatchingAppStateInUrl = () => appStateSubscription.unsubscribe();
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||||
import type { DashboardContainerByValueInput } from '../common';
|
import type { DashboardContainerInput } from '../common';
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// URL Constants
|
// URL Constants
|
||||||
|
@ -69,7 +69,7 @@ export const CHANGE_CHECK_DEBOUNCE = 100;
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Default State
|
// 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.
|
viewMode: ViewMode.EDIT, // new dashboards start in edit mode.
|
||||||
timeRestore: false,
|
timeRestore: false,
|
||||||
query: { query: '', language: 'kuery' },
|
query: { query: '', language: 'kuery' },
|
||||||
|
|
|
@ -6,17 +6,15 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import React from 'react';
|
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 { 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 { DashboardGrid } from './dashboard_grid';
|
||||||
import { DashboardContainer } from '../../embeddable/dashboard_container';
|
import { buildMockDashboard } from '../../../mocks';
|
||||||
import { getSampleDashboardInput } from '../../../mocks';
|
|
||||||
import type { Props as DashboardGridItemProps } from './dashboard_grid_item';
|
import type { Props as DashboardGridItemProps } from './dashboard_grid_item';
|
||||||
|
import { DashboardContainerContext } from '../../embeddable/dashboard_container';
|
||||||
|
|
||||||
jest.mock('./dashboard_grid_item', () => {
|
jest.mock('./dashboard_grid_item', () => {
|
||||||
return {
|
return {
|
||||||
|
@ -39,10 +37,8 @@ jest.mock('./dashboard_grid_item', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const DashboardServicesProvider = pluginServices.getContextProvider();
|
const createAndMountDashboardGrid = () => {
|
||||||
|
const dashboardContainer = buildMockDashboard({
|
||||||
async function getDashboardContainer() {
|
|
||||||
const initialInput = getSampleDashboardInput({
|
|
||||||
panels: {
|
panels: {
|
||||||
'1': {
|
'1': {
|
||||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||||
|
@ -56,55 +52,29 @@ async function getDashboardContainer() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const dashboardContainer = new DashboardContainer(initialInput);
|
const component = mountWithIntl(
|
||||||
await dashboardContainer.untilInitialized();
|
<DashboardContainerContext.Provider value={dashboardContainer}>
|
||||||
return dashboardContainer;
|
<DashboardGrid viewportWidth={1000} />
|
||||||
}
|
</DashboardContainerContext.Provider>
|
||||||
|
);
|
||||||
|
return { dashboardContainer, component };
|
||||||
|
};
|
||||||
|
|
||||||
test('renders DashboardGrid', async () => {
|
test('renders DashboardGrid', async () => {
|
||||||
const dashboardContainer = await getDashboardContainer();
|
const { component } = createAndMountDashboardGrid();
|
||||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
|
||||||
|
|
||||||
const component = mountWithIntl(
|
|
||||||
<DashboardServicesProvider>
|
|
||||||
<DashboardReduxWrapper>
|
|
||||||
<DashboardGrid viewportWidth={1000} />
|
|
||||||
</DashboardReduxWrapper>
|
|
||||||
</DashboardServicesProvider>
|
|
||||||
);
|
|
||||||
const panelElements = component.find('GridItem');
|
const panelElements = component.find('GridItem');
|
||||||
expect(panelElements.length).toBe(2);
|
expect(panelElements.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders DashboardGrid with no visualizations', async () => {
|
test('renders DashboardGrid with no visualizations', async () => {
|
||||||
const dashboardContainer = await getDashboardContainer();
|
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
|
||||||
|
|
||||||
const component = mountWithIntl(
|
|
||||||
<DashboardServicesProvider>
|
|
||||||
<DashboardReduxWrapper>
|
|
||||||
<DashboardGrid viewportWidth={1000} />
|
|
||||||
</DashboardReduxWrapper>
|
|
||||||
</DashboardServicesProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
dashboardContainer.updateInput({ panels: {} });
|
dashboardContainer.updateInput({ panels: {} });
|
||||||
component.update();
|
component.update();
|
||||||
expect(component.find('GridItem').length).toBe(0);
|
expect(component.find('GridItem').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('DashboardGrid removes panel when removed from container', async () => {
|
test('DashboardGrid removes panel when removed from container', async () => {
|
||||||
const dashboardContainer = await getDashboardContainer();
|
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
|
||||||
|
|
||||||
const component = mountWithIntl(
|
|
||||||
<DashboardServicesProvider>
|
|
||||||
<DashboardReduxWrapper>
|
|
||||||
<DashboardGrid viewportWidth={1000} />
|
|
||||||
</DashboardReduxWrapper>
|
|
||||||
</DashboardServicesProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const originalPanels = dashboardContainer.getInput().panels;
|
const originalPanels = dashboardContainer.getInput().panels;
|
||||||
const filteredPanels = { ...originalPanels };
|
const filteredPanels = { ...originalPanels };
|
||||||
delete filteredPanels['1'];
|
delete filteredPanels['1'];
|
||||||
|
@ -115,17 +85,7 @@ test('DashboardGrid removes panel when removed from container', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('DashboardGrid renders expanded panel', async () => {
|
test('DashboardGrid renders expanded panel', async () => {
|
||||||
const dashboardContainer = await getDashboardContainer();
|
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||||
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
|
|
||||||
|
|
||||||
const component = mountWithIntl(
|
|
||||||
<DashboardServicesProvider>
|
|
||||||
<DashboardReduxWrapper>
|
|
||||||
<DashboardGrid viewportWidth={1000} />
|
|
||||||
</DashboardReduxWrapper>
|
|
||||||
</DashboardServicesProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
dashboardContainer.setExpandedPanelId('1');
|
dashboardContainer.setExpandedPanelId('1');
|
||||||
component.update();
|
component.update();
|
||||||
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
|
// 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 { DashboardPanelState } from '../../../../common';
|
||||||
import { DashboardGridItem } from './dashboard_grid_item';
|
import { DashboardGridItem } from './dashboard_grid_item';
|
||||||
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
|
|
||||||
import { useDashboardGridSettings } from './use_dashboard_grid_settings';
|
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 { 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 }) => {
|
export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
||||||
const {
|
const dashboard = useDashboardContainer();
|
||||||
useEmbeddableSelector: select,
|
const panels = dashboard.select((state) => state.explicitInput.panels);
|
||||||
actions: { setPanels },
|
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||||
useEmbeddableDispatch,
|
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
|
||||||
} = useDashboardContainerContext();
|
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
||||||
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);
|
|
||||||
|
|
||||||
// turn off panel transform animations for the first 500ms so that the dashboard doesn't animate on its first render.
|
// 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);
|
const [animatePanelTransforms, setAnimatePanelTransforms] = useState(false);
|
||||||
|
@ -93,10 +88,10 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
||||||
{} as { [key: string]: DashboardPanelState }
|
{} as { [key: string]: DashboardPanelState }
|
||||||
);
|
);
|
||||||
if (!getPanelLayoutsAreEqual(panels, updatedPanels)) {
|
if (!getPanelLayoutsAreEqual(panels, updatedPanels)) {
|
||||||
dispatch(setPanels(updatedPanels));
|
dashboard.dispatch.setPanels(updatedPanels);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, panels, setPanels]
|
[dashboard, panels]
|
||||||
);
|
);
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
|
|
||||||
import { DashboardPanelState } from '../../../../common';
|
import { DashboardPanelState } from '../../../../common';
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
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'>;
|
type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'>;
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ const Item = React.forwardRef<HTMLDivElement, Props>(
|
||||||
const {
|
const {
|
||||||
embeddable: { EmbeddablePanel: PanelComponent },
|
embeddable: { EmbeddablePanel: PanelComponent },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const { embeddableInstance: container } = useDashboardContainerContext();
|
const container = useDashboardContainer();
|
||||||
|
|
||||||
const expandPanel = expandedPanelId !== undefined && expandedPanelId === id;
|
const expandPanel = expandedPanelId !== undefined && expandedPanelId === id;
|
||||||
const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id;
|
const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id;
|
||||||
|
@ -134,9 +134,9 @@ export const DashboardGridItem = React.forwardRef<HTMLDivElement, Props>((props,
|
||||||
settings: { isProjectEnabledInLabs },
|
settings: { isProjectEnabledInLabs },
|
||||||
} = pluginServices.getServices();
|
} = 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');
|
const isEnabled = !isPrintMode && isProjectEnabledInLabs('labs:dashboard:deferBelowFold');
|
||||||
|
|
||||||
return isEnabled ? <ObservedItem ref={ref} {...props} /> : <Item ref={ref} {...props} />;
|
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 { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
|
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[]) => {
|
export const useDashboardGridSettings = (panelsInOrder: string[]) => {
|
||||||
const { useEmbeddableSelector: select } = useDashboardContainerContext();
|
const dashboard = useDashboardContainer();
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
const panels = select((state) => state.explicitInput.panels);
|
const panels = dashboard.select((state) => state.explicitInput.panels);
|
||||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||||
|
|
||||||
const layouts = useMemo(() => {
|
const layouts = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { EmbeddablePhaseEvent } from '@kbn/embeddable-plugin/public';
|
import { EmbeddablePhaseEvent } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||||
import { DashboardLoadedEventStatus, DashboardRenderPerformanceStats } from '../../types';
|
import { DashboardLoadedEventStatus, DashboardRenderPerformanceStats } from '../../types';
|
||||||
|
|
||||||
type DashboardRenderPerformanceTracker = DashboardRenderPerformanceStats & {
|
type DashboardRenderPerformanceTracker = DashboardRenderPerformanceStats & {
|
||||||
|
@ -30,7 +30,7 @@ const getDefaultPerformanceTracker: () => DashboardRenderPerformanceTracker = ()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useDashboardPerformanceTracker = ({ panelCount }: { panelCount: number }) => {
|
export const useDashboardPerformanceTracker = ({ panelCount }: { panelCount: number }) => {
|
||||||
const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
|
const dashboard = useDashboardContainer();
|
||||||
|
|
||||||
// reset performance tracker on each render.
|
// reset performance tracker on each render.
|
||||||
const performanceRefs = useRef<DashboardRenderPerformanceTracker>(getDefaultPerformanceTracker());
|
const performanceRefs = useRef<DashboardRenderPerformanceTracker>(getDefaultPerformanceTracker());
|
||||||
|
@ -52,11 +52,11 @@ export const useDashboardPerformanceTracker = ({ panelCount }: { panelCount: num
|
||||||
performanceRefs.current.doneCount++;
|
performanceRefs.current.doneCount++;
|
||||||
if (performanceRefs.current.doneCount === panelCount) {
|
if (performanceRefs.current.doneCount === panelCount) {
|
||||||
performanceRefs.current.panelsRenderDoneTime = performance.now();
|
performanceRefs.current.panelsRenderDoneTime = performance.now();
|
||||||
dashboardContainer.reportPerformanceMetrics(performanceRefs.current);
|
dashboard.reportPerformanceMetrics(performanceRefs.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dashboardContainer, panelCount]
|
[dashboard, panelCount]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { onPanelStatusChange };
|
return { onPanelStatusChange };
|
||||||
|
|
|
@ -26,9 +26,9 @@ import {
|
||||||
EuiSwitch,
|
EuiSwitch,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { DashboardContainerByValueInput } from '../../../../common';
|
import { DashboardContainerInput } from '../../../../common';
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
import { pluginServices } from '../../../services/plugin_services';
|
||||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||||
|
|
||||||
interface DashboardSettingsProps {
|
interface DashboardSettingsProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
@ -42,23 +42,18 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
|
||||||
dashboardSavedObject: { checkForDuplicateDashboardTitle },
|
dashboardSavedObject: { checkForDuplicateDashboardTitle },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
const {
|
const dashboard = useDashboardContainer();
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setStateFromSettingsFlyout },
|
|
||||||
embeddableInstance: dashboardContainer,
|
|
||||||
} = useDashboardContainerContext();
|
|
||||||
|
|
||||||
const [dashboardSettingsState, setDashboardSettingsState] = useState({
|
const [dashboardSettingsState, setDashboardSettingsState] = useState({
|
||||||
...dashboardContainer.getInputAsValueType(),
|
...dashboard.getInput(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isTitleDuplicate, setIsTitleDuplicate] = useState(false);
|
const [isTitleDuplicate, setIsTitleDuplicate] = useState(false);
|
||||||
const [isTitleDuplicateConfirmed, setIsTitleDuplicateConfirmed] = useState(false);
|
const [isTitleDuplicateConfirmed, setIsTitleDuplicateConfirmed] = useState(false);
|
||||||
const [isApplying, setIsApplying] = useState(false);
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
|
|
||||||
const lastSavedId = select((state) => state.componentState.lastSavedId);
|
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||||
const lastSavedTitle = select((state) => state.explicitInput.title);
|
const lastSavedTitle = dashboard.select((state) => state.explicitInput.title);
|
||||||
|
|
||||||
const isMounted = useMountedState();
|
const isMounted = useMountedState();
|
||||||
|
|
||||||
|
@ -83,24 +78,19 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
|
||||||
setIsApplying(false);
|
setIsApplying(false);
|
||||||
|
|
||||||
if (validTitle) {
|
if (validTitle) {
|
||||||
dispatch(setStateFromSettingsFlyout({ lastSavedId, ...dashboardSettingsState }));
|
dashboard.dispatch.setStateFromSettingsFlyout({ lastSavedId, ...dashboardSettingsState });
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDashboardSetting = useCallback(
|
const updateDashboardSetting = useCallback((newSettings: Partial<DashboardContainerInput>) => {
|
||||||
(newSettings: Partial<DashboardContainerByValueInput>) => {
|
setDashboardSettingsState((prevDashboardSettingsState) => {
|
||||||
setDashboardSettingsState((prevDashboardSettingsState) => {
|
return {
|
||||||
return {
|
...prevDashboardSettingsState,
|
||||||
...prevDashboardSettingsState,
|
...newSettings,
|
||||||
...newSettings,
|
};
|
||||||
};
|
});
|
||||||
});
|
}, []);
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
const renderDuplicateTitleCallout = () => {
|
const renderDuplicateTitleCallout = () => {
|
||||||
if (!isTitleDuplicate) {
|
if (!isTitleDuplicate) {
|
||||||
|
|
|
@ -17,8 +17,8 @@ import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
|
||||||
|
|
||||||
import { DashboardGrid } from '../grid';
|
import { DashboardGrid } from '../grid';
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
import { pluginServices } from '../../../services/plugin_services';
|
||||||
|
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||||
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
||||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
|
||||||
|
|
||||||
export const useDebouncedWidthObserver = (wait = 250) => {
|
export const useDebouncedWidthObserver = (wait = 250) => {
|
||||||
const [width, setWidth] = useState<number>(0);
|
const [width, setWidth] = useState<number>(0);
|
||||||
|
@ -38,26 +38,25 @@ export const DashboardViewportComponent = () => {
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const controlsRoot = useRef(null);
|
const controlsRoot = useRef(null);
|
||||||
|
|
||||||
const { useEmbeddableSelector: select, embeddableInstance: dashboardContainer } =
|
const dashboard = useDashboardContainer();
|
||||||
useDashboardContainerContext();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render Control group
|
* Render Control group
|
||||||
*/
|
*/
|
||||||
const controlGroup = dashboardContainer.controlGroup;
|
const controlGroup = dashboard.controlGroup;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (controlGroup && controlsRoot.current) controlGroup.render(controlsRoot.current);
|
if (controlGroup && controlsRoot.current) controlGroup.render(controlsRoot.current);
|
||||||
}, [controlGroup]);
|
}, [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(
|
const controlCount = Object.keys(
|
||||||
select((state) => state.explicitInput.controlGroupInput?.panels) ?? {}
|
dashboard.select((state) => state.explicitInput.controlGroupInput?.panels) ?? {}
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||||
const dashboardTitle = select((state) => state.explicitInput.title);
|
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||||
const description = select((state) => state.explicitInput.description);
|
const description = dashboard.select((state) => state.explicitInput.description);
|
||||||
const expandedPanelId = select((state) => state.componentState.expandedPanelId);
|
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
||||||
const controlsEnabled = isProjectEnabledInLabs('labs:dashboard:dashboardControls');
|
const controlsEnabled = isProjectEnabledInLabs('labs:dashboard:dashboardControls');
|
||||||
|
|
||||||
const { ref: resizeRef, width: viewportWidth } = useDebouncedWidthObserver();
|
const { ref: resizeRef, width: viewportWidth } = useDebouncedWidthObserver();
|
||||||
|
@ -98,15 +97,12 @@ export const DashboardViewportComponent = () => {
|
||||||
// because ExitFullScreenButton sets isFullscreenMode to false on unmount while rerendering.
|
// because ExitFullScreenButton sets isFullscreenMode to false on unmount while rerendering.
|
||||||
// This specifically fixed maximizing/minimizing panels without exiting fullscreen mode.
|
// This specifically fixed maximizing/minimizing panels without exiting fullscreen mode.
|
||||||
const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
|
const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
|
||||||
const {
|
const dashboard = useDashboardContainer();
|
||||||
useEmbeddableDispatch,
|
|
||||||
useEmbeddableSelector: select,
|
|
||||||
actions: { setFullScreenMode },
|
|
||||||
} = useDashboardContainerContext();
|
|
||||||
const dispatch = useEmbeddableDispatch();
|
|
||||||
|
|
||||||
const isFullScreenMode = select((state) => state.componentState.fullScreenMode);
|
const isFullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
|
||||||
const isEmbeddedExternally = select((state) => state.componentState.isEmbeddedExternally);
|
const isEmbeddedExternally = dashboard.select(
|
||||||
|
(state) => state.componentState.isEmbeddedExternally
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -114,7 +110,7 @@ const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
|
||||||
{isFullScreenMode && (
|
{isFullScreenMode && (
|
||||||
<EuiPortal>
|
<EuiPortal>
|
||||||
<ExitFullScreenButton
|
<ExitFullScreenButton
|
||||||
onExit={() => dispatch(setFullScreenMode(false))}
|
onExit={() => dashboard.dispatch.setFullScreenMode(false)}
|
||||||
toggleChrome={!isEmbeddedExternally}
|
toggleChrome={!isEmbeddedExternally}
|
||||||
/>
|
/>
|
||||||
</EuiPortal>
|
</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 { DashboardContainer } from '../dashboard_container';
|
||||||
import { showCloneModal } from './overlays/show_clone_modal';
|
import { showCloneModal } from './overlays/show_clone_modal';
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
import { pluginServices } from '../../../services/plugin_services';
|
||||||
import { DashboardContainerByValueInput } from '../../../../common';
|
import { DashboardContainerInput } from '../../../../common';
|
||||||
import { SaveDashboardReturn } from '../../../services/dashboard_saved_object/types';
|
import { SaveDashboardReturn } from '../../../services/dashboard_saved_object/types';
|
||||||
|
|
||||||
export function runSaveAs(this: DashboardContainer) {
|
export function runSaveAs(this: DashboardContainer) {
|
||||||
|
@ -31,15 +31,10 @@ export function runSaveAs(this: DashboardContainer) {
|
||||||
dashboardSavedObject: { checkForDuplicateDashboardTitle, saveDashboardStateToSavedObject },
|
dashboardSavedObject: { checkForDuplicateDashboardTitle, saveDashboardStateToSavedObject },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
const {
|
|
||||||
getState,
|
|
||||||
dispatch,
|
|
||||||
actions: { setStateFromSaveModal, setLastSavedInput },
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
const {
|
const {
|
||||||
explicitInput: currentState,
|
explicitInput: currentState,
|
||||||
componentState: { lastSavedId },
|
componentState: { lastSavedId },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
|
|
||||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
||||||
const onSave = async ({
|
const onSave = async ({
|
||||||
|
@ -81,7 +76,7 @@ export function runSaveAs(this: DashboardContainer) {
|
||||||
// do not save if title is duplicate and is unconfirmed
|
// do not save if title is duplicate and is unconfirmed
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const stateToSave: DashboardContainerByValueInput = {
|
const stateToSave: DashboardContainerInput = {
|
||||||
...currentState,
|
...currentState,
|
||||||
...stateFromSaveModal,
|
...stateFromSaveModal,
|
||||||
};
|
};
|
||||||
|
@ -103,8 +98,8 @@ export function runSaveAs(this: DashboardContainer) {
|
||||||
stateFromSaveModal.lastSavedId = saveResult.id;
|
stateFromSaveModal.lastSavedId = saveResult.id;
|
||||||
if (saveResult.id) {
|
if (saveResult.id) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
dispatch(setStateFromSaveModal(stateFromSaveModal));
|
this.dispatch.setStateFromSaveModal(stateFromSaveModal);
|
||||||
dispatch(setLastSavedInput(stateToSave));
|
this.dispatch.setLastSavedInput(stateToSave);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (newCopyOnSave || !lastSavedId) this.expectIdChange();
|
if (newCopyOnSave || !lastSavedId) this.expectIdChange();
|
||||||
|
@ -136,22 +131,17 @@ export async function runQuickSave(this: DashboardContainer) {
|
||||||
dashboardSavedObject: { saveDashboardStateToSavedObject },
|
dashboardSavedObject: { saveDashboardStateToSavedObject },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
const {
|
|
||||||
getState,
|
|
||||||
dispatch,
|
|
||||||
actions: { setLastSavedInput },
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
const {
|
const {
|
||||||
explicitInput: currentState,
|
explicitInput: currentState,
|
||||||
componentState: { lastSavedId },
|
componentState: { lastSavedId },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
|
|
||||||
const saveResult = await saveDashboardStateToSavedObject({
|
const saveResult = await saveDashboardStateToSavedObject({
|
||||||
lastSavedId,
|
lastSavedId,
|
||||||
currentState,
|
currentState,
|
||||||
saveOptions: {},
|
saveOptions: {},
|
||||||
});
|
});
|
||||||
dispatch(setLastSavedInput(currentState));
|
this.dispatch.setLastSavedInput(currentState);
|
||||||
|
|
||||||
return saveResult;
|
return saveResult;
|
||||||
}
|
}
|
||||||
|
@ -161,12 +151,7 @@ export async function runClone(this: DashboardContainer) {
|
||||||
dashboardSavedObject: { saveDashboardStateToSavedObject, checkForDuplicateDashboardTitle },
|
dashboardSavedObject: { saveDashboardStateToSavedObject, checkForDuplicateDashboardTitle },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
const {
|
const { explicitInput: currentState } = this.getState();
|
||||||
getState,
|
|
||||||
dispatch,
|
|
||||||
actions: { setTitle },
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
const { explicitInput: currentState } = getState();
|
|
||||||
|
|
||||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
||||||
const onClone = async (
|
const onClone = async (
|
||||||
|
@ -191,7 +176,7 @@ export async function runClone(this: DashboardContainer) {
|
||||||
currentState: { ...currentState, title: newTitle },
|
currentState: { ...currentState, title: newTitle },
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(setTitle(newTitle));
|
this.dispatch.setTitle(newTitle);
|
||||||
resolve(saveResult);
|
resolve(saveResult);
|
||||||
this.expectIdChange();
|
this.expectIdChange();
|
||||||
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
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 { 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 { pluginServices } from '../../../services/plugin_services';
|
||||||
|
import { DashboardSettings } from '../../component/settings/settings_flyout';
|
||||||
|
import { DashboardContainer, DashboardContainerContext } from '../dashboard_container';
|
||||||
|
|
||||||
export function showSettings(this: DashboardContainer) {
|
export function showSettings(this: DashboardContainer) {
|
||||||
const {
|
const {
|
||||||
|
@ -22,26 +22,20 @@ export function showSettings(this: DashboardContainer) {
|
||||||
overlays,
|
overlays,
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
Wrapper: DashboardReduxWrapper,
|
|
||||||
actions: { setHasOverlays },
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
|
|
||||||
// TODO Move this action into DashboardContainer.openOverlay
|
// TODO Move this action into DashboardContainer.openOverlay
|
||||||
dispatch(setHasOverlays(true));
|
this.dispatch.setHasOverlays(true);
|
||||||
|
|
||||||
this.openOverlay(
|
this.openOverlay(
|
||||||
overlays.openFlyout(
|
overlays.openFlyout(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<DashboardReduxWrapper>
|
<DashboardContainerContext.Provider value={this}>
|
||||||
<DashboardSettings
|
<DashboardSettings
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
dispatch(setHasOverlays(false));
|
this.dispatch.setHasOverlays(false);
|
||||||
this.clearOverlays();
|
this.clearOverlays();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DashboardReduxWrapper>,
|
</DashboardContainerContext.Provider>,
|
||||||
{ theme$ }
|
{ theme$ }
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
@ -49,7 +43,7 @@ export function showSettings(this: DashboardContainer) {
|
||||||
'data-test-subj': 'dashboardSettingsFlyout',
|
'data-test-subj': 'dashboardSettingsFlyout',
|
||||||
onClose: (flyout) => {
|
onClose: (flyout) => {
|
||||||
this.clearOverlays();
|
this.clearOverlays();
|
||||||
dispatch(setHasOverlays(false));
|
this.dispatch.setHasOverlays(false);
|
||||||
flyout.close();
|
flyout.close();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import { mockControlGroupInput } from '@kbn/controls-plugin/common/mocks';
|
import { mockControlGroupInput } from '@kbn/controls-plugin/common/mocks';
|
||||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public/control_group/embeddable/control_group_container';
|
import { ControlGroupContainer } from '@kbn/controls-plugin/public/control_group/embeddable/control_group_container';
|
||||||
import { Filter } from '@kbn/es-query';
|
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';
|
import { combineDashboardFiltersWithControlGroupFilters } from './dashboard_control_group_integration';
|
||||||
|
|
||||||
jest.mock('@kbn/controls-plugin/public/control_group/embeddable/control_group_container');
|
jest.mock('@kbn/controls-plugin/public/control_group/embeddable/control_group_container');
|
||||||
|
@ -52,7 +52,7 @@ const testFilter3: Filter = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockControlGroupContainer = new ControlGroupContainer(
|
const mockControlGroupContainer = new ControlGroupContainer(
|
||||||
{ getTools: () => {} } as unknown as ReduxEmbeddablePackage,
|
{ getTools: () => {} } as unknown as ReduxToolsPackage,
|
||||||
mockControlGroupInput()
|
mockControlGroupInput()
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,24 +6,21 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import _, { identity, pickBy } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
|
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
|
||||||
import { debounceTime, distinctUntilChanged, distinctUntilKeyChanged, skip } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, distinctUntilKeyChanged, skip } from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ControlGroupInput,
|
ControlGroupInput,
|
||||||
CONTROL_GROUP_TYPE,
|
|
||||||
getDefaultControlGroupInput,
|
getDefaultControlGroupInput,
|
||||||
persistableControlGroupInputIsEqual,
|
persistableControlGroupInputIsEqual,
|
||||||
} from '@kbn/controls-plugin/common';
|
} from '@kbn/controls-plugin/common';
|
||||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
import { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||||
import { ControlGroupContainer, ControlGroupOutput } from '@kbn/controls-plugin/public';
|
|
||||||
|
|
||||||
import { DashboardContainer } from '../../dashboard_container';
|
import { DashboardContainer } from '../../dashboard_container';
|
||||||
import { pluginServices } from '../../../../services/plugin_services';
|
import { DashboardContainerInput } from '../../../../../common';
|
||||||
import { DashboardContainerByValueInput } from '../../../../../common';
|
|
||||||
|
|
||||||
interface DiffChecks {
|
interface DiffChecks {
|
||||||
[key: string]: (a?: unknown, b?: unknown) => boolean;
|
[key: string]: (a?: unknown, b?: unknown) => boolean;
|
||||||
|
@ -35,61 +32,16 @@ const distinctUntilDiffCheck = <T extends {}>(a: T, b: T, diffChecks: DiffChecks
|
||||||
.includes(false);
|
.includes(false);
|
||||||
|
|
||||||
type DashboardControlGroupCommonKeys = keyof Pick<
|
type DashboardControlGroupCommonKeys = keyof Pick<
|
||||||
DashboardContainerByValueInput | ControlGroupInput,
|
DashboardContainerInput | ControlGroupInput,
|
||||||
'filters' | 'lastReloadRequestTime' | 'timeRange' | 'query'
|
'filters' | 'lastReloadRequestTime' | 'timeRange' | 'query'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export async function startControlGroupIntegration(
|
export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||||
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) {
|
|
||||||
if (!this.controlGroup) return;
|
if (!this.controlGroup) return;
|
||||||
const subscriptions = new Subscription();
|
|
||||||
|
|
||||||
const {
|
|
||||||
actions: { setControlGroupState },
|
|
||||||
dispatch,
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
|
|
||||||
const isControlGroupInputEqual = () =>
|
const isControlGroupInputEqual = () =>
|
||||||
persistableControlGroupInputIsEqual(
|
persistableControlGroupInputIsEqual(
|
||||||
this.controlGroup!.getInput(),
|
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
|
// 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,
|
chainingSystem: deepEqual,
|
||||||
ignoreParentSettings: deepEqual,
|
ignoreParentSettings: deepEqual,
|
||||||
};
|
};
|
||||||
subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.controlGroup
|
this.controlGroup
|
||||||
.getInput$()
|
.getInput$()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -111,9 +63,12 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||||
const { panels, controlStyle, chainingSystem, ignoreParentSettings } =
|
const { panels, controlStyle, chainingSystem, ignoreParentSettings } =
|
||||||
this.controlGroup!.getInput();
|
this.controlGroup!.getInput();
|
||||||
if (!isControlGroupInputEqual()) {
|
if (!isControlGroupInputEqual()) {
|
||||||
dispatch(
|
this.dispatch.setControlGroupState({
|
||||||
setControlGroupState({ panels, controlStyle, chainingSystem, ignoreParentSettings })
|
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
|
// pass down any pieces of input needed to refetch or force refetch data for the controls
|
||||||
subscriptions.add(
|
this.subscriptions.add(
|
||||||
(this.getInput$() as Readonly<Observable<DashboardContainerByValueInput>>)
|
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged((a, b) =>
|
distinctUntilChanged((a, b) =>
|
||||||
distinctUntilDiffCheck<DashboardContainerByValueInput>(a, b, dashboardRefetchDiff)
|
distinctUntilDiffCheck<DashboardContainerInput>(a, b, dashboardRefetchDiff)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
const newInput: { [key: string]: unknown } = {};
|
const newInput: { [key: string]: unknown } = {};
|
||||||
(Object.keys(dashboardRefetchDiff) as DashboardControlGroupCommonKeys[]).forEach((key) => {
|
(Object.keys(dashboardRefetchDiff) as DashboardControlGroupCommonKeys[]).forEach((key) => {
|
||||||
if (
|
if (
|
||||||
!dashboardRefetchDiff[key]?.(
|
!dashboardRefetchDiff[key]?.(this.getInput()[key], this.controlGroup!.getInput()[key])
|
||||||
this.getInputAsValueType()[key],
|
|
||||||
this.controlGroup!.getInput()[key]
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
newInput[key] = this.getInputAsValueType()[key];
|
newInput[key] = this.getInput()[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (Object.keys(newInput).length > 0) {
|
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
|
// dashboard may reset the control group input when discarding changes. Subscribe to these changes and update accordingly
|
||||||
subscriptions.add(
|
this.subscriptions.add(
|
||||||
(this.getInput$() as Readonly<Observable<DashboardContainerByValueInput>>)
|
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||||
.pipe(debounceTime(10), distinctUntilKeyChanged('controlGroupInput'))
|
.pipe(debounceTime(10), distinctUntilKeyChanged('controlGroupInput'))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
if (!isControlGroupInputEqual()) {
|
if (!isControlGroupInputEqual()) {
|
||||||
if (!this.getInputAsValueType().controlGroupInput) {
|
if (!this.getInput().controlGroupInput) {
|
||||||
this.controlGroup!.updateInput(getDefaultControlGroupInput());
|
this.controlGroup!.updateInput(getDefaultControlGroupInput());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.controlGroup!.updateInput({
|
this.controlGroup!.updateInput({
|
||||||
...this.getInputAsValueType().controlGroupInput,
|
...this.getInput().controlGroupInput,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// when control group outputs filters, force a refresh!
|
// when control group outputs filters, force a refresh!
|
||||||
subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.controlGroup
|
this.controlGroup
|
||||||
.getOutput$()
|
.getOutput$()
|
||||||
.pipe(
|
.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
|
.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
|
this.controlGroup
|
||||||
.getOutput$()
|
.getOutput$()
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) =>
|
distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) =>
|
||||||
_.isEqual(timesliceA, timesliceB)
|
isEqual(timesliceA, timesliceB)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subscribe(({ timeslice }) => {
|
.subscribe(({ timeslice }) => {
|
||||||
if (!_.isEqual(timeslice, this.getInputAsValueType().timeslice)) {
|
if (!isEqual(timeslice, this.getInput().timeslice)) {
|
||||||
this.updateInput({ 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.
|
// 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(() => {
|
this.getAnyChildOutputChange$().subscribe(() => {
|
||||||
if (!this.controlGroup) {
|
if (!this.controlGroup) {
|
||||||
return;
|
return;
|
||||||
|
@ -216,12 +168,6 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||||
this.controlGroup.anyControlOutputConsumerLoading$.next(false);
|
this.controlGroup.anyControlOutputConsumerLoading$.next(false);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
|
||||||
stopSyncingWithControlGroup: () => {
|
|
||||||
subscriptions.unsubscribe();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const combineDashboardFiltersWithControlGroupFilters = (
|
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 { queryString, timefilter } = queryService;
|
||||||
const { timefilter: timefilterService } = timefilter;
|
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.
|
// get Observable for when the dashboard's saved filters or query change.
|
||||||
const OnFiltersChange$ = new Subject<{ filters: Filter[]; query: Query }>();
|
const OnFiltersChange$ = new Subject<{ filters: Filter[]; query: Query }>();
|
||||||
const unsubscribeFromSavedFilterChanges = onStateChange(() => {
|
const unsubscribeFromSavedFilterChanges = this.onStateChange(() => {
|
||||||
const {
|
const {
|
||||||
explicitInput: { filters, query },
|
explicitInput: { filters, query },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
OnFiltersChange$.next({
|
OnFiltersChange$.next({
|
||||||
filters: filters ?? [],
|
filters: filters ?? [],
|
||||||
query: query ?? queryString.getDefaultQuery(),
|
query: query ?? queryString.getDefaultQuery(),
|
||||||
|
@ -53,7 +46,7 @@ export function syncUnifiedSearchState(
|
||||||
// starts syncing app filters between dashboard state and filterManager
|
// starts syncing app filters between dashboard state and filterManager
|
||||||
const {
|
const {
|
||||||
explicitInput: { filters, query },
|
explicitInput: { filters, query },
|
||||||
} = getState();
|
} = this.getState();
|
||||||
const intermediateFilterState: { filters: Filter[]; query: Query } = {
|
const intermediateFilterState: { filters: Filter[]; query: Query } = {
|
||||||
query: query ?? queryString.getDefaultQuery(),
|
query: query ?? queryString.getDefaultQuery(),
|
||||||
filters: filters ?? [],
|
filters: filters ?? [],
|
||||||
|
@ -66,7 +59,7 @@ export function syncUnifiedSearchState(
|
||||||
set: ({ filters: newFilters, query: newQuery }) => {
|
set: ({ filters: newFilters, query: newQuery }) => {
|
||||||
intermediateFilterState.filters = cleanFiltersForSerialize(newFilters);
|
intermediateFilterState.filters = cleanFiltersForSerialize(newFilters);
|
||||||
intermediateFilterState.query = newQuery;
|
intermediateFilterState.query = newQuery;
|
||||||
dispatch(setFiltersAndQuery(intermediateFilterState));
|
this.dispatch.setFiltersAndQuery(intermediateFilterState);
|
||||||
},
|
},
|
||||||
state$: OnFiltersChange$.pipe(distinctUntilChanged()),
|
state$: OnFiltersChange$.pipe(distinctUntilChanged()),
|
||||||
},
|
},
|
||||||
|
@ -78,11 +71,11 @@ export function syncUnifiedSearchState(
|
||||||
|
|
||||||
const timeUpdateSubscription = timefilterService
|
const timeUpdateSubscription = timefilterService
|
||||||
.getTimeUpdate$()
|
.getTimeUpdate$()
|
||||||
.subscribe(() => dispatch(setTimeRange(timefilterService.getTime())));
|
.subscribe(() => this.dispatch.setTimeRange(timefilterService.getTime()));
|
||||||
|
|
||||||
const refreshIntervalSubscription = timefilterService
|
const refreshIntervalSubscription = timefilterService
|
||||||
.getRefreshIntervalUpdate$()
|
.getRefreshIntervalUpdate$()
|
||||||
.subscribe(() => dispatch(setRefreshInterval(timefilterService.getRefreshInterval())));
|
.subscribe(() => this.dispatch.setRefreshInterval(timefilterService.getRefreshInterval()));
|
||||||
|
|
||||||
const autoRefreshSubscription = timefilterService
|
const autoRefreshSubscription = timefilterService
|
||||||
.getAutoRefreshFetch$()
|
.getAutoRefreshFetch$()
|
|
@ -29,8 +29,7 @@ import { applicationServiceMock, coreMock } from '@kbn/core/public/mocks';
|
||||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||||
import { createEditModeActionDefinition } from '@kbn/embeddable-plugin/public/lib/test_samples';
|
import { createEditModeActionDefinition } from '@kbn/embeddable-plugin/public/lib/test_samples';
|
||||||
|
|
||||||
import { DashboardContainer } from './dashboard_container';
|
import { buildMockDashboard, getSampleDashboardPanel } from '../../mocks';
|
||||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../../mocks';
|
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||||
|
|
||||||
|
@ -47,7 +46,7 @@ beforeEach(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('DashboardContainer initializes embeddables', (done) => {
|
test('DashboardContainer initializes embeddables', (done) => {
|
||||||
const initialInput = getSampleDashboardInput({
|
const container = buildMockDashboard({
|
||||||
panels: {
|
panels: {
|
||||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Sam', id: '123' },
|
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) => {
|
const subscription = container.getOutput$().subscribe((output) => {
|
||||||
if (container.getOutput().embeddableLoaded['123']) {
|
if (container.getOutput().embeddableLoaded['123']) {
|
||||||
|
@ -76,8 +74,7 @@ test('DashboardContainer initializes embeddables', (done) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('DashboardContainer.addNewEmbeddable', async () => {
|
test('DashboardContainer.addNewEmbeddable', async () => {
|
||||||
const container = new DashboardContainer(getSampleDashboardInput());
|
const container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
const embeddable = await container.addNewEmbeddable<ContactCardEmbeddableInput>(
|
const embeddable = await container.addNewEmbeddable<ContactCardEmbeddableInput>(
|
||||||
CONTACT_CARD_EMBEDDABLE,
|
CONTACT_CARD_EMBEDDABLE,
|
||||||
{
|
{
|
||||||
|
@ -99,7 +96,8 @@ test('DashboardContainer.addNewEmbeddable', async () => {
|
||||||
|
|
||||||
test('DashboardContainer.replacePanel', (done) => {
|
test('DashboardContainer.replacePanel', (done) => {
|
||||||
const ID = '123';
|
const ID = '123';
|
||||||
const initialInput = getSampleDashboardInput({
|
|
||||||
|
const container = buildMockDashboard({
|
||||||
panels: {
|
panels: {
|
||||||
[ID]: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
[ID]: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Sam', id: ID },
|
explicitInput: { firstName: 'Sam', id: ID },
|
||||||
|
@ -107,8 +105,6 @@ test('DashboardContainer.replacePanel', (done) => {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const container = new DashboardContainer(initialInput);
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
const subscription = container.getInput$().subscribe(
|
const subscription = container.getInput$().subscribe(
|
||||||
|
@ -141,7 +137,7 @@ test('DashboardContainer.replacePanel', (done) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Container view mode change propagates to existing children', async () => {
|
test('Container view mode change propagates to existing children', async () => {
|
||||||
const initialInput = getSampleDashboardInput({
|
const container = buildMockDashboard({
|
||||||
panels: {
|
panels: {
|
||||||
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: { firstName: 'Sam', id: '123' },
|
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');
|
const embeddable = await container.untilEmbeddableLoaded('123');
|
||||||
expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW);
|
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 () => {
|
test('Container view mode change propagates to new children', async () => {
|
||||||
const container = new DashboardContainer(getSampleDashboardInput());
|
const container = buildMockDashboard();
|
||||||
await container.untilInitialized();
|
|
||||||
const embeddable = await container.addNewEmbeddable<
|
const embeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
ContactCardEmbeddableOutput,
|
ContactCardEmbeddableOutput,
|
||||||
|
@ -178,10 +171,7 @@ test('Container view mode change propagates to new children', async () => {
|
||||||
|
|
||||||
test('searchSessionId propagates to children', async () => {
|
test('searchSessionId propagates to children', async () => {
|
||||||
const searchSessionId1 = 'searchSessionId1';
|
const searchSessionId1 = 'searchSessionId1';
|
||||||
const container = new DashboardContainer(
|
const container = buildMockDashboard({ searchSessionId: searchSessionId1 });
|
||||||
getSampleDashboardInput({ searchSessionId: searchSessionId1 })
|
|
||||||
);
|
|
||||||
await container.untilInitialized();
|
|
||||||
const embeddable = await container.addNewEmbeddable<
|
const embeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
ContactCardEmbeddableOutput,
|
ContactCardEmbeddableOutput,
|
||||||
|
@ -205,9 +195,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
||||||
uiActionsSetup.registerAction(editModeAction);
|
uiActionsSetup.registerAction(editModeAction);
|
||||||
uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction);
|
uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction);
|
||||||
|
|
||||||
const initialInput = getSampleDashboardInput({ viewMode: ViewMode.VIEW });
|
const container = buildMockDashboard({ viewMode: ViewMode.VIEW });
|
||||||
const container = new DashboardContainer(initialInput);
|
|
||||||
await container.untilInitialized();
|
|
||||||
|
|
||||||
const embeddable = await container.addNewEmbeddable<
|
const embeddable = await container.addNewEmbeddable<
|
||||||
ContactCardEmbeddableInput,
|
ContactCardEmbeddableInput,
|
||||||
|
|
|
@ -6,19 +6,14 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { cloneDeep, omit } from 'lodash';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
import {
|
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||||
lazyLoadReduxEmbeddablePackage,
|
|
||||||
ReduxEmbeddableTools,
|
|
||||||
} from '@kbn/presentation-util-plugin/public';
|
|
||||||
import {
|
import {
|
||||||
ViewMode,
|
ViewMode,
|
||||||
Container,
|
Container,
|
||||||
type Embeddable,
|
|
||||||
type IEmbeddable,
|
type IEmbeddable,
|
||||||
type EmbeddableInput,
|
type EmbeddableInput,
|
||||||
type EmbeddableOutput,
|
type EmbeddableOutput,
|
||||||
|
@ -44,36 +39,19 @@ import {
|
||||||
showPlaceholderUntil,
|
showPlaceholderUntil,
|
||||||
addOrUpdateEmbeddable,
|
addOrUpdateEmbeddable,
|
||||||
} from './api';
|
} 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 { DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||||
import { createPanelState } from '../component/panel';
|
import { createPanelState } from '../component/panel';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { DASHBOARD_LOADED_EVENT } from '../../dashboard_constants';
|
|
||||||
import { DashboardCreationOptions } from './dashboard_container_factory';
|
import { DashboardCreationOptions } from './dashboard_container_factory';
|
||||||
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
||||||
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
|
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 { dashboardContainerReducers } from '../state/dashboard_container_reducers';
|
||||||
import { DashboardSavedObjectService } from '../../services/dashboard_saved_object/types';
|
import { startDiffingDashboardState } from '../state/diffing/dashboard_diffing_integration';
|
||||||
import { dashboardContainerInputIsByValue } from '../../../common/dashboard_container/type_guards';
|
import { DASHBOARD_LOADED_EVENT, DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
|
||||||
|
import { combineDashboardFiltersWithControlGroupFilters } from './create/controls/dashboard_control_group_integration';
|
||||||
|
|
||||||
export interface InheritedChildInput {
|
export interface InheritedChildInput {
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
|
@ -91,25 +69,40 @@ export interface InheritedChildInput {
|
||||||
executionContext?: KibanaExecutionContext;
|
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> {
|
export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
|
||||||
public readonly type = DASHBOARD_CONTAINER_TYPE;
|
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;
|
public controlGroup?: ControlGroupContainer;
|
||||||
|
|
||||||
// Dashboard State
|
// cleanup
|
||||||
public onDestroyControlGroup?: () => void;
|
public stopSyncingWithUnifiedSearch?: () => void;
|
||||||
private subscriptions: Subscription = new Subscription();
|
private cleanupStateTools: () => void;
|
||||||
|
|
||||||
private initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
// performance monitoring
|
||||||
private dashboardCreationStartTime?: number;
|
private dashboardCreationStartTime?: number;
|
||||||
private savedObjectLoadTime?: number;
|
private savedObjectLoadTime?: number;
|
||||||
|
|
||||||
private initialSavedDashboardId?: string;
|
|
||||||
|
|
||||||
private reduxEmbeddableTools?: ReduxEmbeddableTools<
|
|
||||||
DashboardReduxState,
|
|
||||||
typeof dashboardContainerReducers
|
|
||||||
>;
|
|
||||||
|
|
||||||
private domNode?: HTMLElement;
|
private domNode?: HTMLElement;
|
||||||
private overlayRef?: OverlayRef;
|
private overlayRef?: OverlayRef;
|
||||||
private allDataViews: DataView[] = [];
|
private allDataViews: DataView[] = [];
|
||||||
|
@ -117,19 +110,19 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
// Services that are used in the Dashboard container code
|
// Services that are used in the Dashboard container code
|
||||||
private creationOptions?: DashboardCreationOptions;
|
private creationOptions?: DashboardCreationOptions;
|
||||||
private analyticsService: DashboardAnalyticsService;
|
private analyticsService: DashboardAnalyticsService;
|
||||||
private dashboardSavedObjectService: DashboardSavedObjectService;
|
|
||||||
private theme$;
|
private theme$;
|
||||||
private chrome;
|
private chrome;
|
||||||
private customBranding;
|
private customBranding;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
initialInput: DashboardContainerInput,
|
initialInput: DashboardContainerInput,
|
||||||
|
reduxToolsPackage: ReduxToolsPackage,
|
||||||
|
initialLastSavedInput?: DashboardContainerInput,
|
||||||
dashboardCreationStartTime?: number,
|
dashboardCreationStartTime?: number,
|
||||||
parent?: Container,
|
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 {
|
const {
|
||||||
embeddable: { getEmbeddableFactory },
|
embeddable: { getEmbeddableFactory },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
@ -140,15 +133,11 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
},
|
},
|
||||||
{ embeddableLoaded: {} },
|
{ embeddableLoaded: {} },
|
||||||
getEmbeddableFactory,
|
getEmbeddableFactory,
|
||||||
parent,
|
parent
|
||||||
{
|
|
||||||
readyToInitializeChildren$,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
({
|
({
|
||||||
analytics: this.analyticsService,
|
analytics: this.analyticsService,
|
||||||
dashboardSavedObject: this.dashboardSavedObjectService,
|
|
||||||
settings: {
|
settings: {
|
||||||
theme: { theme$: this.theme$ },
|
theme: { theme$: this.theme$ },
|
||||||
},
|
},
|
||||||
|
@ -156,215 +145,44 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
customBranding: this.customBranding,
|
customBranding: this.customBranding,
|
||||||
} = pluginServices.getServices());
|
} = pluginServices.getServices());
|
||||||
|
|
||||||
this.initialSavedDashboardId = dashboardContainerInputIsByValue(this.input)
|
|
||||||
? undefined
|
|
||||||
: this.input.savedObjectId;
|
|
||||||
this.creationOptions = creationOptions;
|
this.creationOptions = creationOptions;
|
||||||
|
|
||||||
this.dashboardCreationStartTime = dashboardCreationStartTime;
|
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
|
// start diffing dashboard state
|
||||||
const diffingMiddleware = startDiffingDashboardState.bind(this)({
|
const diffingMiddleware = startDiffingDashboardState.bind(this)(creationOptions);
|
||||||
useSessionBackup: creationOptions?.useSessionStorageIntegration,
|
|
||||||
setCleanupFunction: (cleanup) => {
|
|
||||||
this.stopDiffingDashboardState = cleanup;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// set up data views integration
|
|
||||||
this.dataViewsChangeSubscription = startSyncingDashboardDataViews.bind(this)();
|
|
||||||
|
|
||||||
// build redux embeddable tools
|
// build redux embeddable tools
|
||||||
this.reduxEmbeddableTools = reduxEmbeddablePackage.createTools<
|
const reduxTools = reduxToolsPackage.createReduxEmbeddableTools<
|
||||||
DashboardReduxState,
|
DashboardReduxState,
|
||||||
typeof dashboardContainerReducers
|
typeof dashboardContainerReducers
|
||||||
>({
|
>({
|
||||||
embeddable: this as Embeddable<DashboardContainerByValueInput, DashboardContainerOutput>, // cast to unwrapped state type
|
embeddable: this,
|
||||||
reducers: dashboardContainerReducers,
|
reducers: dashboardContainerReducers,
|
||||||
additionalMiddleware: [diffingMiddleware],
|
additionalMiddleware: [diffingMiddleware],
|
||||||
initialComponentState: {
|
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.
|
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() {
|
public getDashboardSavedObjectId() {
|
||||||
if (this.initialized$.value) return Promise.resolve();
|
return this.getState().componentState.lastSavedId;
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
const subscription = this.initialized$.subscribe((isInitialized) => {
|
|
||||||
if (isInitialized) {
|
|
||||||
resolve();
|
|
||||||
subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reportPerformanceMetrics(stats: DashboardRenderPerformanceStats) {
|
public reportPerformanceMetrics(stats: DashboardRenderPerformanceStats) {
|
||||||
if (this.analyticsService && this.dashboardCreationStartTime) {
|
if (this.analyticsService && this.dashboardCreationStartTime) {
|
||||||
const panelCount = Object.keys(
|
const panelCount = Object.keys(this.getState().explicitInput.panels).length;
|
||||||
this.getReduxEmbeddableTools().getState().explicitInput.panels
|
|
||||||
).length;
|
|
||||||
const totalDuration = stats.panelsRenderDoneTime - this.dashboardCreationStartTime;
|
const totalDuration = stats.panelsRenderDoneTime - this.dashboardCreationStartTime;
|
||||||
reportPerformanceMetricEvent(this.analyticsService, {
|
reportPerformanceMetricEvent(this.analyticsService, {
|
||||||
eventName: DASHBOARD_LOADED_EVENT,
|
eventName: DASHBOARD_LOADED_EVENT,
|
||||||
|
@ -393,44 +211,22 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
return newPanel;
|
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) {
|
public render(dom: HTMLElement) {
|
||||||
if (!this.reduxEmbeddableTools) {
|
|
||||||
throw new Error('Dashboard must be initialized before it can be rendered');
|
|
||||||
}
|
|
||||||
if (this.domNode) {
|
if (this.domNode) {
|
||||||
ReactDOM.unmountComponentAtNode(this.domNode);
|
ReactDOM.unmountComponentAtNode(this.domNode);
|
||||||
}
|
}
|
||||||
this.domNode = dom;
|
this.domNode = dom;
|
||||||
|
|
||||||
this.domNode.className = 'dashboardContainer';
|
this.domNode.className = 'dashboardContainer';
|
||||||
|
|
||||||
const { Wrapper: DashboardReduxWrapper } = this.reduxEmbeddableTools;
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ExitFullScreenButtonKibanaProvider
|
<ExitFullScreenButtonKibanaProvider
|
||||||
coreStart={{ chrome: this.chrome, customBranding: this.customBranding }}
|
coreStart={{ chrome: this.chrome, customBranding: this.customBranding }}
|
||||||
>
|
>
|
||||||
<KibanaThemeProvider theme$={this.theme$}>
|
<KibanaThemeProvider theme$={this.theme$}>
|
||||||
<DashboardReduxWrapper>
|
<DashboardContainerContext.Provider value={this}>
|
||||||
<DashboardViewport />
|
<DashboardViewport />
|
||||||
</DashboardReduxWrapper>
|
</DashboardContainerContext.Provider>
|
||||||
</KibanaThemeProvider>
|
</KibanaThemeProvider>
|
||||||
</ExitFullScreenButtonKibanaProvider>
|
</ExitFullScreenButtonKibanaProvider>
|
||||||
</I18nProvider>,
|
</I18nProvider>,
|
||||||
|
@ -451,7 +247,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
searchSessionId,
|
searchSessionId,
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
executionContext,
|
executionContext,
|
||||||
} = this.input as DashboardContainerByValueInput;
|
} = this.input;
|
||||||
|
|
||||||
let combinedFilters = filters;
|
let combinedFilters = filters;
|
||||||
if (this.controlGroup) {
|
if (this.controlGroup) {
|
||||||
|
@ -476,20 +272,12 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
// ------------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------------
|
||||||
// Cleanup
|
// Cleanup
|
||||||
// ------------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------------
|
||||||
private stopDiffingDashboardState?: () => void;
|
|
||||||
private stopSyncingWithUnifiedSearch?: () => void;
|
|
||||||
private dataViewsChangeSubscription?: Subscription = undefined;
|
|
||||||
private stopSyncingDashboardSearchSessions: (() => void) | undefined;
|
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
this.onDestroyControlGroup?.();
|
this.cleanupStateTools();
|
||||||
|
this.controlGroup?.destroy();
|
||||||
this.subscriptions.unsubscribe();
|
this.subscriptions.unsubscribe();
|
||||||
this.stopDiffingDashboardState?.();
|
|
||||||
this.reduxEmbeddableTools?.cleanup();
|
|
||||||
this.stopSyncingWithUnifiedSearch?.();
|
this.stopSyncingWithUnifiedSearch?.();
|
||||||
this.stopSyncingDashboardSearchSessions?.();
|
|
||||||
this.dataViewsChangeSubscription?.unsubscribe();
|
|
||||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,29 +317,20 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
public addOrUpdateEmbeddable = addOrUpdateEmbeddable;
|
public addOrUpdateEmbeddable = addOrUpdateEmbeddable;
|
||||||
|
|
||||||
public forceRefresh(refreshControlGroup: boolean = true) {
|
public forceRefresh(refreshControlGroup: boolean = true) {
|
||||||
const {
|
this.dispatch.setLastReloadRequestTimeToNow({});
|
||||||
dispatch,
|
|
||||||
actions: { setLastReloadRequestTimeToNow },
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
dispatch(setLastReloadRequestTimeToNow({}));
|
|
||||||
if (refreshControlGroup) this.controlGroup?.reload();
|
if (refreshControlGroup) this.controlGroup?.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDataViewsUpdate$ = new Subject<DataView[]>();
|
public onDataViewsUpdate$ = new Subject<DataView[]>();
|
||||||
|
|
||||||
public resetToLastSavedState() {
|
public resetToLastSavedState() {
|
||||||
const {
|
this.dispatch.resetToLastSavedInput({});
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
actions: { resetToLastSavedInput },
|
|
||||||
} = this.getReduxEmbeddableTools();
|
|
||||||
dispatch(resetToLastSavedInput({}));
|
|
||||||
const {
|
const {
|
||||||
explicitInput: { timeRange, refreshInterval },
|
explicitInput: { timeRange, refreshInterval },
|
||||||
componentState: {
|
componentState: {
|
||||||
lastSavedInput: { timeRestore: lastSavedTimeRestore },
|
lastSavedInput: { timeRestore: lastSavedTimeRestore },
|
||||||
},
|
},
|
||||||
} = getState();
|
} = this.getState();
|
||||||
|
|
||||||
// if we are using the unified search integration, we need to force reset the time picker.
|
// if we are using the unified search integration, we need to force reset the time picker.
|
||||||
if (this.creationOptions?.useUnifiedSearchIntegration && lastSavedTimeRestore) {
|
if (this.creationOptions?.useUnifiedSearchIntegration && lastSavedTimeRestore) {
|
||||||
|
@ -585,8 +364,11 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
};
|
};
|
||||||
|
|
||||||
public getExpandedPanelId = () => {
|
public getExpandedPanelId = () => {
|
||||||
if (!this.reduxEmbeddableTools) throw new Error();
|
return this.getState().componentState.expandedPanelId;
|
||||||
return this.reduxEmbeddableTools.getState().componentState.expandedPanelId;
|
};
|
||||||
|
|
||||||
|
public setExpandedPanelId = (newId?: string) => {
|
||||||
|
this.dispatch.setExpandedPanelId(newId);
|
||||||
};
|
};
|
||||||
|
|
||||||
public openOverlay = (ref: OverlayRef) => {
|
public openOverlay = (ref: OverlayRef) => {
|
||||||
|
@ -599,15 +381,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
this.overlayRef?.close();
|
this.overlayRef?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
public setExpandedPanelId = (newId?: string) => {
|
|
||||||
if (!this.reduxEmbeddableTools) throw new Error();
|
|
||||||
const {
|
|
||||||
actions: { setExpandedPanelId },
|
|
||||||
dispatch,
|
|
||||||
} = this.reduxEmbeddableTools;
|
|
||||||
dispatch(setExpandedPanelId(newId));
|
|
||||||
};
|
|
||||||
|
|
||||||
public getPanelCount = () => {
|
public getPanelCount = () => {
|
||||||
return Object.keys(this.getInput().panels).length;
|
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 { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||||
|
|
||||||
import {
|
|
||||||
createInject,
|
|
||||||
createExtract,
|
|
||||||
DashboardContainerInput,
|
|
||||||
DashboardContainerByValueInput,
|
|
||||||
} from '../../../common';
|
|
||||||
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
||||||
import type { DashboardContainer } from './dashboard_container';
|
import type { DashboardContainer } from './dashboard_container';
|
||||||
import { DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
|
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';
|
import { LoadDashboardFromSavedObjectReturn } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
|
||||||
|
|
||||||
export type DashboardContainerFactory = EmbeddableFactory<
|
export type DashboardContainerFactory = EmbeddableFactory<
|
||||||
|
@ -40,7 +35,6 @@ export type DashboardContainerFactory = EmbeddableFactory<
|
||||||
|
|
||||||
export interface DashboardCreationOptions {
|
export interface DashboardCreationOptions {
|
||||||
initialInput?: Partial<DashboardContainerInput>;
|
initialInput?: Partial<DashboardContainerInput>;
|
||||||
overrideInput?: Partial<DashboardContainerByValueInput>;
|
|
||||||
|
|
||||||
incomingEmbeddable?: EmbeddablePackageState;
|
incomingEmbeddable?: EmbeddablePackageState;
|
||||||
|
|
||||||
|
@ -97,19 +91,17 @@ export class DashboardContainerFactoryDefinition
|
||||||
public create = async (
|
public create = async (
|
||||||
initialInput: DashboardContainerInput,
|
initialInput: DashboardContainerInput,
|
||||||
parent?: Container,
|
parent?: Container,
|
||||||
creationOptions?: DashboardCreationOptions
|
creationOptions?: DashboardCreationOptions,
|
||||||
|
savedObjectId?: string
|
||||||
): Promise<DashboardContainer | ErrorEmbeddable> => {
|
): Promise<DashboardContainer | ErrorEmbeddable> => {
|
||||||
const dashboardCreationStartTime = performance.now();
|
const dashboardCreationStartTime = performance.now();
|
||||||
const { DashboardContainer: DashboardContainerEmbeddable } = await import(
|
const { createDashboard } = await import('./create/create_dashboard');
|
||||||
'./dashboard_container'
|
try {
|
||||||
);
|
return Promise.resolve(
|
||||||
return Promise.resolve(
|
createDashboard(initialInput.id, creationOptions, dashboardCreationStartTime, savedObjectId)
|
||||||
new DashboardContainerEmbeddable(
|
);
|
||||||
initialInput,
|
} catch (e) {
|
||||||
dashboardCreationStartTime,
|
return new ErrorEmbeddable(e.text, { id: e.id });
|
||||||
parent,
|
}
|
||||||
creationOptions
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
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