mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Dashboard] Fast Navigation Between Dashboards (#157437)
## Summary Makes all navigation from one Dashboard to another feel snappier.
This commit is contained in:
parent
83b7939e03
commit
5342563a22
24 changed files with 299 additions and 230 deletions
|
@ -14,7 +14,11 @@ import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||||
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
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 { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
|
import {
|
||||||
|
AwaitingDashboardAPI,
|
||||||
|
DashboardRenderer,
|
||||||
|
DashboardCreationOptions,
|
||||||
|
} from '@kbn/dashboard-plugin/public';
|
||||||
|
|
||||||
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
|
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
|
||||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||||
|
@ -48,7 +52,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiPanel hasBorder={true}>
|
<EuiPanel hasBorder={true}>
|
||||||
<DashboardRenderer
|
<DashboardRenderer
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async (): Promise<DashboardCreationOptions> => {
|
||||||
const builder = controlGroupInputBuilder;
|
const builder = controlGroupInputBuilder;
|
||||||
const controlGroupInput = getDefaultControlGroupInput();
|
const controlGroupInput = getDefaultControlGroupInput();
|
||||||
await builder.addDataControlFromField(controlGroupInput, {
|
await builder.addDataControlFromField(controlGroupInput, {
|
||||||
|
@ -68,11 +72,11 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useControlGroupIntegration: true,
|
useControlGroupIntegration: true,
|
||||||
initialInput: {
|
getInitialInput: () => ({
|
||||||
timeRange: { from: 'now-30d', to: 'now' },
|
timeRange: { from: 'now-30d', to: 'now' },
|
||||||
viewMode: ViewMode.VIEW,
|
viewMode: ViewMode.VIEW,
|
||||||
controlGroupInput,
|
controlGroupInput,
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
ref={setDashboard}
|
ref={setDashboard}
|
||||||
|
|
|
@ -136,10 +136,10 @@ export const DynamicByReferenceExample = () => {
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async () => {
|
||||||
const persistedInput = getPersistableInput();
|
const persistedInput = getPersistableInput();
|
||||||
return {
|
return {
|
||||||
initialInput: {
|
getInitialInput: () => ({
|
||||||
...persistedInput,
|
...persistedInput,
|
||||||
timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis
|
timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
ref={setdashboard}
|
ref={setdashboard}
|
||||||
|
|
|
@ -50,12 +50,15 @@ export const StaticByReferenceExample = ({
|
||||||
const field = dataView.getFieldByName('machine.os.keyword');
|
const field = dataView.getFieldByName('machine.os.keyword');
|
||||||
let filter: Filter;
|
let filter: Filter;
|
||||||
let creationOptions: DashboardCreationOptions = {
|
let creationOptions: DashboardCreationOptions = {
|
||||||
initialInput: { viewMode: ViewMode.VIEW },
|
getInitialInput: () => ({ viewMode: ViewMode.VIEW }),
|
||||||
};
|
};
|
||||||
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, initialInput: { filters: [filter] } };
|
creationOptions = {
|
||||||
|
...creationOptions,
|
||||||
|
getInitialInput: () => ({ 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
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -29,11 +29,11 @@ export const StaticByValueExample = () => {
|
||||||
<DashboardRenderer
|
<DashboardRenderer
|
||||||
getCreationOptions={async () => {
|
getCreationOptions={async () => {
|
||||||
return {
|
return {
|
||||||
initialInput: {
|
getInitialInput: () => ({
|
||||||
timeRange: { from: 'now-30d', to: 'now' },
|
timeRange: { from: 'now-30d', to: 'now' },
|
||||||
viewMode: ViewMode.VIEW,
|
viewMode: ViewMode.VIEW,
|
||||||
panels: panelsJson as DashboardPanelMap,
|
panels: panelsJson as DashboardPanelMap,
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -190,6 +190,19 @@ export class ControlGroupContainer extends Container<
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public updateInputAndReinitialize = (newInput: Partial<ControlGroupInput>) => {
|
||||||
|
this.subscriptions.unsubscribe();
|
||||||
|
this.subscriptions = new Subscription();
|
||||||
|
this.initialized$.next(false);
|
||||||
|
this.updateInput(newInput);
|
||||||
|
this.untilAllChildrenReady().then(() => {
|
||||||
|
this.recalculateDataViews();
|
||||||
|
this.recalculateFilters();
|
||||||
|
this.setupSubscriptions();
|
||||||
|
this.initialized$.next(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public setLastUsedDataViewId = (lastUsedDataViewId: string) => {
|
public setLastUsedDataViewId = (lastUsedDataViewId: string) => {
|
||||||
this.lastUsedDataViewId = lastUsedDataViewId;
|
this.lastUsedDataViewId = lastUsedDataViewId;
|
||||||
};
|
};
|
||||||
|
|
|
@ -83,11 +83,6 @@ export function DashboardApp({
|
||||||
customBranding,
|
customBranding,
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
|
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
|
||||||
|
|
||||||
const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage(
|
|
||||||
DASHBOARD_APP_ID,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const { scopedHistory: getScopedHistory } = useDashboardMountContext();
|
const { scopedHistory: getScopedHistory } = useDashboardMountContext();
|
||||||
|
|
||||||
useExecutionContext(executionContext, {
|
useExecutionContext(executionContext, {
|
||||||
|
@ -125,13 +120,28 @@ export function DashboardApp({
|
||||||
/**
|
/**
|
||||||
* Create options to pass into the dashboard renderer
|
* Create options to pass into the dashboard renderer
|
||||||
*/
|
*/
|
||||||
const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
|
|
||||||
const getCreationOptions = useCallback((): Promise<DashboardCreationOptions> => {
|
const getCreationOptions = useCallback((): Promise<DashboardCreationOptions> => {
|
||||||
const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
|
||||||
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
||||||
|
const getInitialInput = () => {
|
||||||
|
const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
|
||||||
|
const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||||
|
|
||||||
return Promise.resolve({
|
// Override all state with URL + Locator input
|
||||||
incomingEmbeddable,
|
return {
|
||||||
|
// State loaded from the dashboard app URL and from the locator overrides all other dashboard state.
|
||||||
|
...initialUrlState,
|
||||||
|
...stateFromLocator,
|
||||||
|
|
||||||
|
// if print mode is active, force viewMode.PRINT
|
||||||
|
...(isScreenshotMode() && getScreenshotContext('layout') === 'print'
|
||||||
|
? { viewMode: ViewMode.PRINT }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.resolve<DashboardCreationOptions>({
|
||||||
|
getIncomingEmbeddable: () =>
|
||||||
|
getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, true),
|
||||||
|
|
||||||
// integrations
|
// integrations
|
||||||
useControlGroupIntegration: true,
|
useControlGroupIntegration: true,
|
||||||
|
@ -148,28 +158,16 @@ export function DashboardApp({
|
||||||
getSearchSessionIdFromURL: () => getSearchSessionIdFromURL(history),
|
getSearchSessionIdFromURL: () => getSearchSessionIdFromURL(history),
|
||||||
removeSessionIdFromUrl: () => removeSearchSessionIdFromURL(kbnUrlStateStorage),
|
removeSessionIdFromUrl: () => removeSearchSessionIdFromURL(kbnUrlStateStorage),
|
||||||
},
|
},
|
||||||
|
getInitialInput,
|
||||||
// Override all state with URL + Locator input
|
|
||||||
initialInput: {
|
|
||||||
// State loaded from the dashboard app URL and from the locator overrides all other dashboard state.
|
|
||||||
...initialUrlState,
|
|
||||||
...stateFromLocator,
|
|
||||||
|
|
||||||
// if print mode is active, force viewMode.PRINT
|
|
||||||
...(isScreenshotMode() && getScreenshotContext('layout') === 'print'
|
|
||||||
? { viewMode: ViewMode.PRINT }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
|
|
||||||
validateLoadedSavedObject: validateOutcome,
|
validateLoadedSavedObject: validateOutcome,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
history,
|
history,
|
||||||
validateOutcome,
|
validateOutcome,
|
||||||
stateFromLocator,
|
getScopedHistory,
|
||||||
isScreenshotMode,
|
isScreenshotMode,
|
||||||
|
getStateTransfer,
|
||||||
kbnUrlStateStorage,
|
kbnUrlStateStorage,
|
||||||
incomingEmbeddable,
|
|
||||||
getScreenshotContext,
|
getScreenshotContext,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -183,7 +181,7 @@ export function DashboardApp({
|
||||||
dashboardAPI,
|
dashboardAPI,
|
||||||
});
|
});
|
||||||
return () => stopWatchingAppStateInUrl();
|
return () => stopWatchingAppStateInUrl();
|
||||||
}, [dashboardAPI, kbnUrlStateStorage]);
|
}, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dshAppWrapper">
|
<div className="dshAppWrapper">
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
import './_dashboard_app.scss';
|
import './_dashboard_app.scss';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { History } from 'history';
|
|
||||||
import { parse, ParsedQuery } from 'query-string';
|
import { parse, ParsedQuery } from 'query-string';
|
||||||
import { render, unmountComponentAtNode } from 'react-dom';
|
import { render, unmountComponentAtNode } from 'react-dom';
|
||||||
import { Switch, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom';
|
import { Switch, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom';
|
||||||
import { Route } from '@kbn/shared-ux-router';
|
import { Route } from '@kbn/shared-ux-router';
|
||||||
|
|
||||||
import { I18nProvider } from '@kbn/i18n-react';
|
import { I18nProvider } from '@kbn/i18n-react';
|
||||||
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { AppMountParameters, CoreSetup } from '@kbn/core/public';
|
import { AppMountParameters, CoreSetup } from '@kbn/core/public';
|
||||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
||||||
|
@ -56,6 +56,7 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
||||||
chrome: { setBadge, docTitle, setHelpExtension },
|
chrome: { setBadge, docTitle, setHelpExtension },
|
||||||
dashboardCapabilities: { showWriteControls },
|
dashboardCapabilities: { showWriteControls },
|
||||||
documentationLinks: { dashboardDocLink },
|
documentationLinks: { dashboardDocLink },
|
||||||
|
application: { navigateToApp },
|
||||||
settings: { uiSettings },
|
settings: { uiSettings },
|
||||||
data: dataStart,
|
data: dataStart,
|
||||||
notifications,
|
notifications,
|
||||||
|
@ -63,7 +64,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
||||||
let globalEmbedSettings: DashboardEmbedSettings | undefined;
|
let globalEmbedSettings: DashboardEmbedSettings | undefined;
|
||||||
let routerHistory: History;
|
|
||||||
|
|
||||||
const getUrlStateStorage = (history: RouteComponentProps['history']) =>
|
const getUrlStateStorage = (history: RouteComponentProps['history']) =>
|
||||||
createKbnUrlStateStorage({
|
createKbnUrlStateStorage({
|
||||||
|
@ -73,17 +73,17 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirect = (redirectTo: RedirectToProps) => {
|
const redirect = (redirectTo: RedirectToProps) => {
|
||||||
if (!routerHistory) return;
|
let path;
|
||||||
const historyFunction = redirectTo.useReplace ? routerHistory.replace : routerHistory.push;
|
let state;
|
||||||
let destination;
|
|
||||||
if (redirectTo.destination === 'dashboard') {
|
if (redirectTo.destination === 'dashboard') {
|
||||||
destination = redirectTo.id
|
path = redirectTo.id ? createDashboardEditUrl(redirectTo.id) : CREATE_NEW_DASHBOARD_URL;
|
||||||
? createDashboardEditUrl(redirectTo.id, redirectTo.editMode)
|
if (redirectTo.editMode) {
|
||||||
: CREATE_NEW_DASHBOARD_URL;
|
state = { viewMode: ViewMode.EDIT };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
destination = createDashboardListingFilterUrl(redirectTo.filter);
|
path = createDashboardListingFilterUrl(redirectTo.filter);
|
||||||
}
|
}
|
||||||
historyFunction(destination);
|
navigateToApp(DASHBOARD_APP_ID, { path: `#/${path}`, state, replace: redirectTo.useReplace });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDashboardEmbedSettings = (
|
const getDashboardEmbedSettings = (
|
||||||
|
@ -102,9 +102,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
||||||
if (routeParams.embed && !globalEmbedSettings) {
|
if (routeParams.embed && !globalEmbedSettings) {
|
||||||
globalEmbedSettings = getDashboardEmbedSettings(routeParams);
|
globalEmbedSettings = getDashboardEmbedSettings(routeParams);
|
||||||
}
|
}
|
||||||
if (!routerHistory) {
|
|
||||||
routerHistory = routeProps.history;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<DashboardApp
|
<DashboardApp
|
||||||
history={routeProps.history}
|
history={routeProps.history}
|
||||||
|
@ -120,9 +117,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
||||||
const routeParams = parse(routeProps.history.location.search);
|
const routeParams = parse(routeProps.history.location.search);
|
||||||
const title = (routeParams.title as string) || undefined;
|
const title = (routeParams.title as string) || undefined;
|
||||||
const filter = (routeParams.filter as string) || undefined;
|
const filter = (routeParams.filter as string) || undefined;
|
||||||
if (!routerHistory) {
|
|
||||||
routerHistory = routeProps.history;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<DashboardListingPage
|
<DashboardListingPage
|
||||||
initialFilter={filter}
|
initialFilter={filter}
|
||||||
|
|
|
@ -82,8 +82,9 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
const title = dashboard.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[]>(dashboard.getAllDataViews());
|
const [allDataViews, setAllDataViews] = useState<DataView[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setAllDataViews(dashboard.getAllDataViews());
|
||||||
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||||
setAllDataViews(dataViews)
|
setAllDataViews(dataViews)
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,7 +11,6 @@ import 'react-grid-layout/css/styles.css';
|
||||||
|
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useEffectOnce } from 'react-use/lib';
|
|
||||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout';
|
import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout';
|
||||||
|
|
||||||
|
@ -31,19 +30,20 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
||||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||||
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
|
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
|
||||||
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
||||||
|
const animatePanelTransforms = dashboard.select(
|
||||||
|
(state) => state.componentState.animatePanelTransforms
|
||||||
|
);
|
||||||
|
|
||||||
// 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);
|
* Track panel maximized state delayed by one tick and use it to prevent
|
||||||
useEffectOnce(() => {
|
* panel sliding animations on maximize and minimize.
|
||||||
setTimeout(() => setAnimatePanelTransforms(true), 500);
|
*/
|
||||||
});
|
const [delayedIsPanelExpanded, setDelayedIsPanelMaximized] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expandedPanelId) {
|
if (expandedPanelId) {
|
||||||
setAnimatePanelTransforms(false);
|
setDelayedIsPanelMaximized(true);
|
||||||
} else {
|
} else {
|
||||||
// delaying enabling CSS transforms to the next tick prevents a panel slide animation on minimize
|
setTimeout(() => setDelayedIsPanelMaximized(false), 0);
|
||||||
setTimeout(() => setAnimatePanelTransforms(true), 0);
|
|
||||||
}
|
}
|
||||||
}, [expandedPanelId]);
|
}, [expandedPanelId]);
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
||||||
'dshLayout-withoutMargins': !useMargins,
|
'dshLayout-withoutMargins': !useMargins,
|
||||||
'dshLayout--viewing': viewMode === ViewMode.VIEW,
|
'dshLayout--viewing': viewMode === ViewMode.VIEW,
|
||||||
'dshLayout--editing': viewMode !== ViewMode.VIEW,
|
'dshLayout--editing': viewMode !== ViewMode.VIEW,
|
||||||
'dshLayout--noAnimation': !animatePanelTransforms || expandedPanelId,
|
'dshLayout--noAnimation': !animatePanelTransforms || delayedIsPanelExpanded,
|
||||||
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
|
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,6 @@ export function runSaveAs(this: DashboardContainer) {
|
||||||
this.dispatch.setLastSavedInput(stateToSave);
|
this.dispatch.setLastSavedInput(stateToSave);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (newCopyOnSave || !lastSavedId) this.expectIdChange();
|
|
||||||
resolve(saveResult);
|
resolve(saveResult);
|
||||||
return saveResult;
|
return saveResult;
|
||||||
};
|
};
|
||||||
|
@ -175,10 +174,7 @@ export async function runClone(this: DashboardContainer) {
|
||||||
saveOptions: { saveAsCopy: true },
|
saveOptions: { saveAsCopy: true },
|
||||||
currentState: { ...currentState, title: newTitle },
|
currentState: { ...currentState, title: newTitle },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dispatch.setTitle(newTitle);
|
|
||||||
resolve(saveResult);
|
resolve(saveResult);
|
||||||
this.expectIdChange();
|
|
||||||
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
||||||
};
|
};
|
||||||
showCloneModal({ onClone, title: currentState.title, onClose: () => resolve(undefined) });
|
showCloneModal({ onClone, title: currentState.title, onClose: () => resolve(undefined) });
|
||||||
|
|
|
@ -50,7 +50,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||||
chainingSystem: deepEqual,
|
chainingSystem: deepEqual,
|
||||||
ignoreParentSettings: deepEqual,
|
ignoreParentSettings: deepEqual,
|
||||||
};
|
};
|
||||||
this.subscriptions.add(
|
this.integrationSubscriptions.add(
|
||||||
this.controlGroup
|
this.controlGroup
|
||||||
.getInput$()
|
.getInput$()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -83,7 +83,7 @@ export 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
|
||||||
this.subscriptions.add(
|
this.integrationSubscriptions.add(
|
||||||
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged((a, b) =>
|
distinctUntilChanged((a, b) =>
|
||||||
|
@ -106,7 +106,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// when control group outputs filters, force a refresh!
|
// when control group outputs filters, force a refresh!
|
||||||
this.subscriptions.add(
|
this.integrationSubscriptions.add(
|
||||||
this.controlGroup
|
this.controlGroup
|
||||||
.getOutput$()
|
.getOutput$()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -118,7 +118,7 @@ export 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
|
||||||
);
|
);
|
||||||
|
|
||||||
this.subscriptions.add(
|
this.integrationSubscriptions.add(
|
||||||
this.controlGroup
|
this.controlGroup
|
||||||
.getOutput$()
|
.getOutput$()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -134,7 +134,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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.
|
||||||
this.subscriptions.add(
|
this.integrationSubscriptions.add(
|
||||||
this.getAnyChildOutputChange$().subscribe(() => {
|
this.getAnyChildOutputChange$().subscribe(() => {
|
||||||
if (!this.controlGroup) {
|
if (!this.controlGroup) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -30,14 +30,12 @@ import { pluginServices } from '../../../services/plugin_services';
|
||||||
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
||||||
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
|
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
|
||||||
|
|
||||||
const embeddableId = 'create-dat-dashboard';
|
|
||||||
|
|
||||||
test('throws error when no data views are available', async () => {
|
test('throws error when no data views are available', async () => {
|
||||||
pluginServices.getServices().data.dataViews.getDefaultDataView = jest
|
pluginServices.getServices().data.dataViews.getDefaultDataView = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(undefined);
|
.mockReturnValue(undefined);
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await createDashboard(embeddableId);
|
await createDashboard();
|
||||||
}).rejects.toThrow('Dashboard requires at least one data view before it can be initialized.');
|
}).rejects.toThrow('Dashboard requires at least one data view before it can be initialized.');
|
||||||
|
|
||||||
// reset get default data view
|
// reset get default data view
|
||||||
|
@ -49,7 +47,7 @@ test('throws error when provided validation function returns invalid', async ()
|
||||||
validateLoadedSavedObject: jest.fn().mockImplementation(() => false),
|
validateLoadedSavedObject: jest.fn().mockImplementation(() => false),
|
||||||
};
|
};
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await createDashboard(embeddableId, creationOptions, 0, 'test-id');
|
await createDashboard(creationOptions, 0, 'test-id');
|
||||||
}).rejects.toThrow('Dashboard failed saved object result validation');
|
}).rejects.toThrow('Dashboard failed saved object result validation');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -62,7 +60,7 @@ test('pulls state from dashboard saved object when given a saved object id', asy
|
||||||
description: `wow would you look at that? Wow.`,
|
description: `wow would you look at that? Wow.`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const dashboard = await createDashboard(embeddableId, {}, 0, 'wow-such-id');
|
const dashboard = await createDashboard({}, 0, 'wow-such-id');
|
||||||
expect(
|
expect(
|
||||||
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject
|
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject
|
||||||
).toHaveBeenCalledWith({ id: 'wow-such-id' });
|
).toHaveBeenCalledWith({ id: 'wow-such-id' });
|
||||||
|
@ -81,12 +79,7 @@ test('pulls state from session storage which overrides state from saved object',
|
||||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||||
const dashboard = await createDashboard(
|
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id');
|
||||||
embeddableId,
|
|
||||||
{ useSessionStorageIntegration: true },
|
|
||||||
0,
|
|
||||||
'wow-such-id'
|
|
||||||
);
|
|
||||||
expect(dashboard.getState().explicitInput.description).toBe(
|
expect(dashboard.getState().explicitInput.description).toBe(
|
||||||
'wow this description marginally better'
|
'wow this description marginally better'
|
||||||
);
|
);
|
||||||
|
@ -105,10 +98,9 @@ test('pulls state from creation options initial input which overrides all other
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||||
const dashboard = await createDashboard(
|
const dashboard = await createDashboard(
|
||||||
embeddableId,
|
|
||||||
{
|
{
|
||||||
useSessionStorageIntegration: true,
|
useSessionStorageIntegration: true,
|
||||||
initialInput: { description: 'wow this description is a masterpiece' },
|
getInitialInput: () => ({ description: 'wow this description is a masterpiece' }),
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
'wow-such-id'
|
'wow-such-id'
|
||||||
|
@ -123,12 +115,12 @@ test('applies filters and query from state to query service', async () => {
|
||||||
{ meta: { alias: 'test', disabled: false, negate: false, index: 'test' } },
|
{ meta: { alias: 'test', disabled: false, negate: false, index: 'test' } },
|
||||||
];
|
];
|
||||||
const query = { language: 'kql', query: 'query' };
|
const query = { language: 'kql', query: 'query' };
|
||||||
await createDashboard(embeddableId, {
|
await createDashboard({
|
||||||
useUnifiedSearchIntegration: true,
|
useUnifiedSearchIntegration: true,
|
||||||
unifiedSearchSettings: {
|
unifiedSearchSettings: {
|
||||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||||
},
|
},
|
||||||
initialInput: { filters, query },
|
getInitialInput: () => ({ filters, query }),
|
||||||
});
|
});
|
||||||
expect(pluginServices.getServices().data.query.queryString.setQuery).toHaveBeenCalledWith(query);
|
expect(pluginServices.getServices().data.query.queryString.setQuery).toHaveBeenCalledWith(query);
|
||||||
expect(pluginServices.getServices().data.query.filterManager.setAppFilters).toHaveBeenCalledWith(
|
expect(pluginServices.getServices().data.query.filterManager.setAppFilters).toHaveBeenCalledWith(
|
||||||
|
@ -139,12 +131,12 @@ test('applies filters and query from state to query service', async () => {
|
||||||
test('applies time range and refresh interval from initial input to query service if time restore is on', async () => {
|
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 timeRange = { from: new Date().toISOString(), to: new Date().toISOString() };
|
||||||
const refreshInterval = { pause: false, value: 42 };
|
const refreshInterval = { pause: false, value: 42 };
|
||||||
await createDashboard(embeddableId, {
|
await createDashboard({
|
||||||
useUnifiedSearchIntegration: true,
|
useUnifiedSearchIntegration: true,
|
||||||
unifiedSearchSettings: {
|
unifiedSearchSettings: {
|
||||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||||
},
|
},
|
||||||
initialInput: { timeRange, refreshInterval, timeRestore: true },
|
getInitialInput: () => ({ timeRange, refreshInterval, timeRestore: true }),
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
pluginServices.getServices().data.query.timefilter.timefilter.setTime
|
pluginServices.getServices().data.query.timefilter.timefilter.setTime
|
||||||
|
@ -159,7 +151,7 @@ test('applied time range from query service to initial input if time restore is
|
||||||
pluginServices.getServices().data.query.timefilter.timefilter.getTime = jest
|
pluginServices.getServices().data.query.timefilter.timefilter.getTime = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(timeRange);
|
.mockReturnValue(timeRange);
|
||||||
const dashboard = await createDashboard(embeddableId, {
|
const dashboard = await createDashboard({
|
||||||
useUnifiedSearchIntegration: true,
|
useUnifiedSearchIntegration: true,
|
||||||
unifiedSearchSettings: {
|
unifiedSearchSettings: {
|
||||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||||
|
@ -177,9 +169,9 @@ test('replaces panel with incoming embeddable if id matches existing panel', asy
|
||||||
} as ContactCardEmbeddableInput,
|
} as ContactCardEmbeddableInput,
|
||||||
embeddableId: 'i_match',
|
embeddableId: 'i_match',
|
||||||
};
|
};
|
||||||
const dashboard = await createDashboard(embeddableId, {
|
const dashboard = await createDashboard({
|
||||||
incomingEmbeddable,
|
getIncomingEmbeddable: () => incomingEmbeddable,
|
||||||
initialInput: {
|
getInitialInput: () => ({
|
||||||
panels: {
|
panels: {
|
||||||
i_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
i_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: {
|
explicitInput: {
|
||||||
|
@ -189,7 +181,7 @@ test('replaces panel with incoming embeddable if id matches existing panel', asy
|
||||||
type: CONTACT_CARD_EMBEDDABLE,
|
type: CONTACT_CARD_EMBEDDABLE,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
expect(dashboard.getState().explicitInput.panels.i_match.explicitInput).toStrictEqual(
|
expect(dashboard.getState().explicitInput.panels.i_match.explicitInput).toStrictEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
@ -216,9 +208,9 @@ test('creates new embeddable with incoming embeddable if id does not match exist
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(mockContactCardFactory);
|
.mockReturnValue(mockContactCardFactory);
|
||||||
|
|
||||||
await createDashboard(embeddableId, {
|
await createDashboard({
|
||||||
incomingEmbeddable,
|
getIncomingEmbeddable: () => incomingEmbeddable,
|
||||||
initialInput: {
|
getInitialInput: () => ({
|
||||||
panels: {
|
panels: {
|
||||||
i_do_not_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
i_do_not_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||||
explicitInput: {
|
explicitInput: {
|
||||||
|
@ -228,7 +220,7 @@ test('creates new embeddable with incoming embeddable if id does not match exist
|
||||||
type: CONTACT_CARD_EMBEDDABLE,
|
type: CONTACT_CARD_EMBEDDABLE,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// flush promises
|
// flush promises
|
||||||
|
@ -258,11 +250,11 @@ test('creates a control group from the control group factory and waits for it to
|
||||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(mockControlGroupFactory);
|
.mockReturnValue(mockControlGroupFactory);
|
||||||
await createDashboard(embeddableId, {
|
await createDashboard({
|
||||||
useControlGroupIntegration: true,
|
useControlGroupIntegration: true,
|
||||||
initialInput: {
|
getInitialInput: () => ({
|
||||||
controlGroupInput: { controlStyle: 'twoLine' } as unknown as ControlGroupInput,
|
controlGroupInput: { controlStyle: 'twoLine' } as unknown as ControlGroupInput,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
// flush promises
|
// flush promises
|
||||||
await new Promise((r) => setTimeout(r, 1));
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
@ -302,7 +294,7 @@ test('searchSessionId is updated prior to child embeddable parent subscription e
|
||||||
sessionCount++;
|
sessionCount++;
|
||||||
return `searchSessionId${sessionCount}`;
|
return `searchSessionId${sessionCount}`;
|
||||||
};
|
};
|
||||||
const dashboard = await createDashboard(embeddableId, {
|
const dashboard = await createDashboard({
|
||||||
searchSessionSettings: {
|
searchSessionSettings: {
|
||||||
getSearchSessionIdFromURL: () => undefined,
|
getSearchSessionIdFromURL: () => undefined,
|
||||||
removeSessionIdFromUrl: () => {},
|
removeSessionIdFromUrl: () => {},
|
||||||
|
|
|
@ -27,47 +27,21 @@ import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data
|
||||||
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
|
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
|
||||||
import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration';
|
import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration';
|
||||||
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
|
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
|
||||||
|
import { LoadDashboardFromSavedObjectReturn } from '../../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Builds a new Dashboard from scratch.
|
||||||
* @param creationOptions
|
|
||||||
*/
|
*/
|
||||||
export const createDashboard = async (
|
export const createDashboard = async (
|
||||||
embeddableId: string,
|
|
||||||
creationOptions?: DashboardCreationOptions,
|
creationOptions?: DashboardCreationOptions,
|
||||||
dashboardCreationStartTime?: number,
|
dashboardCreationStartTime?: number,
|
||||||
savedObjectId?: string
|
savedObjectId?: string
|
||||||
): Promise<DashboardContainer> => {
|
): Promise<DashboardContainer> => {
|
||||||
// --------------------------------------------------------------------------------------
|
|
||||||
// Unpack services & Options
|
|
||||||
// --------------------------------------------------------------------------------------
|
|
||||||
const {
|
const {
|
||||||
dashboardSessionStorage,
|
data: { dataViews },
|
||||||
embeddable: { getEmbeddableFactory },
|
|
||||||
data: {
|
|
||||||
dataViews,
|
|
||||||
query: queryService,
|
|
||||||
search: { session },
|
|
||||||
},
|
|
||||||
dashboardSavedObject: { loadDashboardStateFromSavedObject },
|
dashboardSavedObject: { loadDashboardStateFromSavedObject },
|
||||||
} = pluginServices.getServices();
|
} = 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.
|
// Create method which allows work to be done on the dashboard container when it's ready.
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
|
@ -83,12 +57,9 @@ export const createDashboard = async (
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
// Lazy load required systems and Dashboard saved object.
|
// Lazy load required systems and Dashboard saved object.
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const reduxEmbeddablePackagePromise = lazyLoadReduxToolsPackage();
|
const reduxEmbeddablePackagePromise = lazyLoadReduxToolsPackage();
|
||||||
const defaultDataViewAssignmentPromise = dataViews.getDefaultDataView();
|
const defaultDataViewAssignmentPromise = dataViews.getDefaultDataView();
|
||||||
const dashboardSavedObjectPromise = savedObjectId
|
const dashboardSavedObjectPromise = loadDashboardStateFromSavedObject({ id: savedObjectId });
|
||||||
? loadDashboardStateFromSavedObject({ id: savedObjectId })
|
|
||||||
: Promise.resolve(undefined);
|
|
||||||
|
|
||||||
const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([
|
const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([
|
||||||
reduxEmbeddablePackagePromise,
|
reduxEmbeddablePackagePromise,
|
||||||
|
@ -96,17 +67,82 @@ export const createDashboard = async (
|
||||||
defaultDataViewAssignmentPromise,
|
defaultDataViewAssignmentPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------
|
|
||||||
// Run validations.
|
|
||||||
// --------------------------------------------------------------------------------------
|
|
||||||
if (!defaultDataView) {
|
if (!defaultDataView) {
|
||||||
throw new Error('Dashboard requires at least one data view before it can be initialized.');
|
throw new Error('Dashboard requires at least one data view before it can be initialized.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
// Initialize Dashboard integrations
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
const { input, searchSessionId } = await initializeDashboard({
|
||||||
|
loadDashboardReturn: savedObjectResult,
|
||||||
|
untilDashboardReady,
|
||||||
|
creationOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
// Build and return the dashboard container.
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
const dashboardContainer = new DashboardContainer(
|
||||||
|
input,
|
||||||
|
reduxEmbeddablePackage,
|
||||||
|
searchSessionId,
|
||||||
|
savedObjectResult?.dashboardInput,
|
||||||
|
dashboardCreationStartTime,
|
||||||
|
undefined,
|
||||||
|
creationOptions,
|
||||||
|
savedObjectId
|
||||||
|
);
|
||||||
|
dashboardContainerReady$.next(dashboardContainer);
|
||||||
|
return dashboardContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a Dashboard and starts all of its integrations
|
||||||
|
*/
|
||||||
|
export const initializeDashboard = async ({
|
||||||
|
loadDashboardReturn,
|
||||||
|
untilDashboardReady,
|
||||||
|
creationOptions,
|
||||||
|
controlGroup,
|
||||||
|
}: {
|
||||||
|
loadDashboardReturn: LoadDashboardFromSavedObjectReturn;
|
||||||
|
untilDashboardReady: () => Promise<DashboardContainer>;
|
||||||
|
creationOptions?: DashboardCreationOptions;
|
||||||
|
controlGroup?: ControlGroupContainer;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
dashboardSessionStorage,
|
||||||
|
embeddable: { getEmbeddableFactory },
|
||||||
|
data: {
|
||||||
|
query: queryService,
|
||||||
|
search: { session },
|
||||||
|
},
|
||||||
|
} = pluginServices.getServices();
|
||||||
|
const {
|
||||||
|
queryString,
|
||||||
|
filterManager,
|
||||||
|
timefilter: { timefilter: timefilterService },
|
||||||
|
} = queryService;
|
||||||
|
|
||||||
|
const {
|
||||||
|
getInitialInput,
|
||||||
|
searchSessionSettings,
|
||||||
|
unifiedSearchSettings,
|
||||||
|
validateLoadedSavedObject,
|
||||||
|
useControlGroupIntegration,
|
||||||
|
useUnifiedSearchIntegration,
|
||||||
|
useSessionStorageIntegration,
|
||||||
|
} = creationOptions ?? {};
|
||||||
|
const overrideInput = getInitialInput?.();
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
// Run validation.
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
if (
|
if (
|
||||||
savedObjectResult &&
|
loadDashboardReturn &&
|
||||||
validateLoadedSavedObject &&
|
validateLoadedSavedObject &&
|
||||||
!validateLoadedSavedObject(savedObjectResult)
|
!validateLoadedSavedObject(loadDashboardReturn)
|
||||||
) {
|
) {
|
||||||
throw new Error('Dashboard failed saved object result validation');
|
throw new Error('Dashboard failed saved object result validation');
|
||||||
}
|
}
|
||||||
|
@ -116,7 +152,7 @@ export const createDashboard = async (
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
const sessionStorageInput = ((): Partial<DashboardContainerInput> | undefined => {
|
const sessionStorageInput = ((): Partial<DashboardContainerInput> | undefined => {
|
||||||
if (!useSessionStorageIntegration) return;
|
if (!useSessionStorageIntegration) return;
|
||||||
return dashboardSessionStorage.getState(savedObjectId);
|
return dashboardSessionStorage.getState(loadDashboardReturn.dashboardId);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
|
@ -124,10 +160,9 @@ export const createDashboard = async (
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
const initialInput: DashboardContainerInput = cloneDeep({
|
const initialInput: DashboardContainerInput = cloneDeep({
|
||||||
...DEFAULT_DASHBOARD_INPUT,
|
...DEFAULT_DASHBOARD_INPUT,
|
||||||
...(savedObjectResult?.dashboardInput ?? {}),
|
...(loadDashboardReturn?.dashboardInput ?? {}),
|
||||||
...sessionStorageInput,
|
...sessionStorageInput,
|
||||||
...overrideInput,
|
...overrideInput,
|
||||||
id: embeddableId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
initialInput.executionContext = {
|
initialInput.executionContext = {
|
||||||
|
@ -166,8 +201,7 @@ export const createDashboard = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
untilDashboardReady().then((dashboardContainer) => {
|
untilDashboardReady().then((dashboardContainer) => {
|
||||||
const stopSyncingUnifiedSearchState =
|
const stopSyncingUnifiedSearchState = syncUnifiedSearchState.bind(dashboardContainer)();
|
||||||
syncUnifiedSearchState.bind(dashboardContainer)(kbnUrlStateStorage);
|
|
||||||
dashboardContainer.stopSyncingWithUnifiedSearch = () => {
|
dashboardContainer.stopSyncingWithUnifiedSearch = () => {
|
||||||
stopSyncingUnifiedSearchState();
|
stopSyncingUnifiedSearchState();
|
||||||
stopSyncingQueryServiceStateWithUrl();
|
stopSyncingQueryServiceStateWithUrl();
|
||||||
|
@ -178,16 +212,20 @@ export const createDashboard = async (
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
// Place the incoming embeddable if there is one
|
// Place the incoming embeddable if there is one
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
const incomingEmbeddable = creationOptions?.incomingEmbeddable;
|
const incomingEmbeddable = creationOptions?.getIncomingEmbeddable?.();
|
||||||
if (incomingEmbeddable) {
|
if (incomingEmbeddable) {
|
||||||
initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable.
|
const scrolltoIncomingEmbeddable = (container: DashboardContainer, id: string) => {
|
||||||
|
container.setScrollToPanelId(id);
|
||||||
|
container.setHighlightPanelId(id);
|
||||||
|
};
|
||||||
|
|
||||||
const panelExists =
|
initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable.
|
||||||
|
if (
|
||||||
incomingEmbeddable.embeddableId &&
|
incomingEmbeddable.embeddableId &&
|
||||||
Boolean(initialInput.panels[incomingEmbeddable.embeddableId]);
|
Boolean(initialInput.panels[incomingEmbeddable.embeddableId])
|
||||||
if (panelExists) {
|
) {
|
||||||
// this embeddable already exists, we will update the explicit input.
|
// this embeddable already exists, we will update the explicit input.
|
||||||
const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId as string];
|
const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId];
|
||||||
const sameType = panelToUpdate.type === incomingEmbeddable.type;
|
const sameType = panelToUpdate.type === incomingEmbeddable.type;
|
||||||
|
|
||||||
panelToUpdate.type = incomingEmbeddable.type;
|
panelToUpdate.type = incomingEmbeddable.type;
|
||||||
|
@ -196,22 +234,24 @@ export const createDashboard = async (
|
||||||
...(sameType ? panelToUpdate.explicitInput : {}),
|
...(sameType ? panelToUpdate.explicitInput : {}),
|
||||||
|
|
||||||
...incomingEmbeddable.input,
|
...incomingEmbeddable.input,
|
||||||
id: incomingEmbeddable.embeddableId as string,
|
id: incomingEmbeddable.embeddableId,
|
||||||
|
|
||||||
// maintain hide panel titles setting.
|
// maintain hide panel titles setting.
|
||||||
hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles,
|
hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles,
|
||||||
};
|
};
|
||||||
|
untilDashboardReady().then((container) =>
|
||||||
|
scrolltoIncomingEmbeddable(container, incomingEmbeddable.embeddableId as string)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created.
|
// otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created.
|
||||||
untilDashboardReady().then(async (container) => {
|
untilDashboardReady().then(async (container) => {
|
||||||
container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input);
|
const embeddable = await container.addNewEmbeddable(
|
||||||
|
incomingEmbeddable.type,
|
||||||
|
incomingEmbeddable.input
|
||||||
|
);
|
||||||
|
scrolltoIncomingEmbeddable(container, embeddable.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
untilDashboardReady().then(async (container) => {
|
|
||||||
container.setScrollToPanelId(incomingEmbeddable.embeddableId);
|
|
||||||
container.setHighlightPanelId(incomingEmbeddable.embeddableId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
|
@ -251,7 +291,7 @@ export const createDashboard = async (
|
||||||
ControlGroupContainer
|
ControlGroupContainer
|
||||||
>(CONTROL_GROUP_TYPE);
|
>(CONTROL_GROUP_TYPE);
|
||||||
const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput;
|
const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput;
|
||||||
const controlGroup = await controlsGroupFactory?.create({
|
const fullControlGroupInput = {
|
||||||
id: `control_group_${id ?? 'new_dashboard'}`,
|
id: `control_group_${id ?? 'new_dashboard'}`,
|
||||||
...getDefaultControlGroupInput(),
|
...getDefaultControlGroupInput(),
|
||||||
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
|
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
|
||||||
|
@ -259,9 +299,15 @@ export const createDashboard = async (
|
||||||
viewMode,
|
viewMode,
|
||||||
filters,
|
filters,
|
||||||
query,
|
query,
|
||||||
});
|
};
|
||||||
if (!controlGroup || isErrorEmbeddable(controlGroup)) {
|
if (controlGroup) {
|
||||||
throw new Error('Error in control group startup');
|
controlGroup.updateInputAndReinitialize(fullControlGroupInput);
|
||||||
|
} else {
|
||||||
|
const newControlGroup = await controlsGroupFactory?.create(fullControlGroupInput);
|
||||||
|
if (!newControlGroup || isErrorEmbeddable(newControlGroup)) {
|
||||||
|
throw new Error('Error in control group startup');
|
||||||
|
}
|
||||||
|
controlGroup = newControlGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
untilDashboardReady().then((dashboardContainer) => {
|
untilDashboardReady().then((dashboardContainer) => {
|
||||||
|
@ -275,22 +321,17 @@ export const createDashboard = async (
|
||||||
// Start the data views integration.
|
// Start the data views integration.
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
untilDashboardReady().then((dashboardContainer) => {
|
untilDashboardReady().then((dashboardContainer) => {
|
||||||
dashboardContainer.subscriptions.add(startSyncingDashboardDataViews.bind(dashboardContainer)());
|
dashboardContainer.integrationSubscriptions.add(
|
||||||
|
startSyncingDashboardDataViews.bind(dashboardContainer)()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
// Build and return the dashboard container.
|
// Start animating panel transforms 500 ms after dashboard is created.
|
||||||
// --------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------
|
||||||
const dashboardContainer = new DashboardContainer(
|
untilDashboardReady().then((dashboard) =>
|
||||||
initialInput,
|
setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500)
|
||||||
reduxEmbeddablePackage,
|
|
||||||
initialSearchSessionId,
|
|
||||||
savedObjectResult?.dashboardInput,
|
|
||||||
dashboardCreationStartTime,
|
|
||||||
undefined,
|
|
||||||
creationOptions,
|
|
||||||
savedObjectId
|
|
||||||
);
|
);
|
||||||
dashboardContainerReady$.next(dashboardContainer);
|
|
||||||
return dashboardContainer;
|
return { input: initialInput, searchSessionId: initialSearchSessionId };
|
||||||
};
|
};
|
||||||
|
|
|
@ -86,5 +86,5 @@ export function startDashboardSearchSessionIntegration(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscriptions.add(searchSessionIdChangeSubscription);
|
this.integrationSubscriptions.add(searchSessionIdChangeSubscription);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged, finalize, switchMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, finalize, switchMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import type { Filter, Query } from '@kbn/es-query';
|
import type { Filter, Query } from '@kbn/es-query';
|
||||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
|
||||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
||||||
import { connectToQueryState, waitUntilNextSessionCompletes$ } from '@kbn/data-plugin/public';
|
import { connectToQueryState, waitUntilNextSessionCompletes$ } from '@kbn/data-plugin/public';
|
||||||
|
|
||||||
|
@ -21,10 +20,7 @@ import { pluginServices } from '../../../../services/plugin_services';
|
||||||
* Sets up syncing and subscriptions between the filter state from the Data plugin
|
* Sets up syncing and subscriptions between the filter state from the Data plugin
|
||||||
* and the dashboard Redux store.
|
* and the dashboard Redux store.
|
||||||
*/
|
*/
|
||||||
export function syncUnifiedSearchState(
|
export function syncUnifiedSearchState(this: DashboardContainer) {
|
||||||
this: DashboardContainer,
|
|
||||||
kbnUrlStateStorage: IKbnUrlStateStorage
|
|
||||||
) {
|
|
||||||
const {
|
const {
|
||||||
data: { query: queryService, search },
|
data: { query: queryService, search },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { batch } from 'react-redux';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
|
||||||
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||||
import {
|
import {
|
||||||
|
@ -20,10 +21,10 @@ import {
|
||||||
type EmbeddableFactory,
|
type EmbeddableFactory,
|
||||||
} from '@kbn/embeddable-plugin/public';
|
} from '@kbn/embeddable-plugin/public';
|
||||||
import { I18nProvider } from '@kbn/i18n-react';
|
import { I18nProvider } from '@kbn/i18n-react';
|
||||||
|
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||||
import type { Filter, TimeRange, Query } from '@kbn/es-query';
|
import type { Filter, TimeRange, Query } from '@kbn/es-query';
|
||||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||||
import type { RefreshInterval } from '@kbn/data-plugin/public';
|
|
||||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||||
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
|
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
|
||||||
|
@ -44,6 +45,7 @@ import {
|
||||||
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 { initializeDashboard } from './create/create_dashboard';
|
||||||
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';
|
||||||
|
@ -93,7 +95,8 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
public dispatch: DashboardReduxEmbeddableTools['dispatch'];
|
public dispatch: DashboardReduxEmbeddableTools['dispatch'];
|
||||||
public onStateChange: DashboardReduxEmbeddableTools['onStateChange'];
|
public onStateChange: DashboardReduxEmbeddableTools['onStateChange'];
|
||||||
|
|
||||||
public subscriptions: Subscription = new Subscription();
|
public integrationSubscriptions: Subscription = new Subscription();
|
||||||
|
public diffingSubscription: Subscription = new Subscription();
|
||||||
public controlGroup?: ControlGroupContainer;
|
public controlGroup?: ControlGroupContainer;
|
||||||
|
|
||||||
public searchSessionId?: string;
|
public searchSessionId?: string;
|
||||||
|
@ -169,6 +172,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
...DEFAULT_DASHBOARD_INPUT,
|
...DEFAULT_DASHBOARD_INPUT,
|
||||||
id: initialInput.id,
|
id: initialInput.id,
|
||||||
},
|
},
|
||||||
|
animatePanelTransforms: false, // set panel transforms to false initially to avoid panels animating on initial render.
|
||||||
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: savedObjectId,
|
lastSavedId: savedObjectId,
|
||||||
},
|
},
|
||||||
|
@ -280,7 +284,8 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
super.destroy();
|
super.destroy();
|
||||||
this.cleanupStateTools();
|
this.cleanupStateTools();
|
||||||
this.controlGroup?.destroy();
|
this.controlGroup?.destroy();
|
||||||
this.subscriptions.unsubscribe();
|
this.diffingSubscription.unsubscribe();
|
||||||
|
this.integrationSubscriptions.unsubscribe();
|
||||||
this.stopSyncingWithUnifiedSearch?.();
|
this.stopSyncingWithUnifiedSearch?.();
|
||||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||||
}
|
}
|
||||||
|
@ -289,26 +294,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
// Dashboard API
|
// Dashboard API
|
||||||
// ------------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
|
||||||
* Sometimes when the ID changes, it's due to a clone operation, or a save as operation. In these cases,
|
|
||||||
* most of the state hasn't actually changed, so there isn't any reason to destroy this container and
|
|
||||||
* load up a fresh one. When an id change is in progress, the renderer can check this method, and if it returns
|
|
||||||
* true, the renderer can safely skip destroying and rebuilding the container.
|
|
||||||
*/
|
|
||||||
public isExpectingIdChange() {
|
|
||||||
return this.expectingIdChange;
|
|
||||||
}
|
|
||||||
private expectingIdChange = false;
|
|
||||||
public expectIdChange() {
|
|
||||||
/**
|
|
||||||
* this.expectingIdChange = true; TODO - re-enable this for saving speed-ups. It causes some functional test failures because the _g param is not carried over.
|
|
||||||
* See https://github.com/elastic/kibana/issues/147491 for more information.
|
|
||||||
**/
|
|
||||||
setTimeout(() => {
|
|
||||||
this.expectingIdChange = false;
|
|
||||||
}, 1); // turn this off after the next update.
|
|
||||||
}
|
|
||||||
|
|
||||||
public runClone = runClone;
|
public runClone = runClone;
|
||||||
public runSaveAs = runSaveAs;
|
public runSaveAs = runSaveAs;
|
||||||
public runQuickSave = runQuickSave;
|
public runQuickSave = runQuickSave;
|
||||||
|
@ -361,6 +346,49 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public navigateToDashboard = async (
|
||||||
|
newSavedObjectId?: string,
|
||||||
|
newCreationOptions?: Partial<DashboardCreationOptions>
|
||||||
|
) => {
|
||||||
|
this.integrationSubscriptions.unsubscribe();
|
||||||
|
this.integrationSubscriptions = new Subscription();
|
||||||
|
this.stopSyncingWithUnifiedSearch?.();
|
||||||
|
|
||||||
|
const {
|
||||||
|
dashboardSavedObject: { loadDashboardStateFromSavedObject },
|
||||||
|
} = pluginServices.getServices();
|
||||||
|
if (newCreationOptions) {
|
||||||
|
this.creationOptions = { ...this.creationOptions, ...newCreationOptions };
|
||||||
|
}
|
||||||
|
const loadDashboardReturn = await loadDashboardStateFromSavedObject({ id: newSavedObjectId });
|
||||||
|
|
||||||
|
const dashboardContainerReady$ = new Subject<DashboardContainer>();
|
||||||
|
const untilDashboardReady = () =>
|
||||||
|
new Promise<DashboardContainer>((resolve) => {
|
||||||
|
const subscription = dashboardContainerReady$.subscribe((container) => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
resolve(container);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { input: newInput, searchSessionId } = await initializeDashboard({
|
||||||
|
creationOptions: this.creationOptions,
|
||||||
|
controlGroup: this.controlGroup,
|
||||||
|
untilDashboardReady,
|
||||||
|
loadDashboardReturn,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.searchSessionId = searchSessionId;
|
||||||
|
|
||||||
|
this.updateInput(newInput);
|
||||||
|
batch(() => {
|
||||||
|
this.dispatch.setLastSavedInput(loadDashboardReturn?.dashboardInput);
|
||||||
|
this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate.
|
||||||
|
this.dispatch.setLastSavedId(newSavedObjectId);
|
||||||
|
});
|
||||||
|
dashboardContainerReady$.next(this);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all the dataviews that are actively being used in the dashboard
|
* Gets all the dataviews that are actively being used in the dashboard
|
||||||
* @returns An array of dataviews
|
* @returns An array of dataviews
|
||||||
|
|
|
@ -34,9 +34,9 @@ export type DashboardContainerFactory = EmbeddableFactory<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface DashboardCreationOptions {
|
export interface DashboardCreationOptions {
|
||||||
initialInput?: Partial<DashboardContainerInput>;
|
getInitialInput?: () => Partial<DashboardContainerInput>;
|
||||||
|
|
||||||
incomingEmbeddable?: EmbeddablePackageState;
|
getIncomingEmbeddable?: () => EmbeddablePackageState | undefined;
|
||||||
|
|
||||||
useSearchSessionsIntegration?: boolean;
|
useSearchSessionsIntegration?: boolean;
|
||||||
searchSessionSettings?: {
|
searchSessionSettings?: {
|
||||||
|
@ -98,7 +98,7 @@ export class DashboardContainerFactoryDefinition
|
||||||
const { createDashboard } = await import('./create/create_dashboard');
|
const { createDashboard } = await import('./create/create_dashboard');
|
||||||
try {
|
try {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
createDashboard(initialInput.id, creationOptions, dashboardCreationStartTime, savedObjectId)
|
createDashboard(creationOptions, dashboardCreationStartTime, savedObjectId)
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return new ErrorEmbeddable(e.text, { id: e.id });
|
return new ErrorEmbeddable(e.text, { id: e.id });
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('dashboard renderer', () => {
|
||||||
mockDashboardContainer = {
|
mockDashboardContainer = {
|
||||||
destroy: jest.fn(),
|
destroy: jest.fn(),
|
||||||
render: jest.fn(),
|
render: jest.fn(),
|
||||||
isExpectingIdChange: jest.fn().mockReturnValue(false),
|
navigateToDashboard: jest.fn(),
|
||||||
} as unknown as DashboardContainer;
|
} as unknown as DashboardContainer;
|
||||||
mockDashboardFactory = {
|
mockDashboardFactory = {
|
||||||
create: jest.fn().mockReturnValue(mockDashboardContainer),
|
create: jest.fn().mockReturnValue(mockDashboardContainer),
|
||||||
|
@ -77,7 +77,7 @@ describe('dashboard renderer', () => {
|
||||||
expect(mockDashboardContainer.destroy).toHaveBeenCalledTimes(1);
|
expect(mockDashboardContainer.destroy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('destroys dashboard container on unexpected ID change', async () => {
|
test('calls navigate and does not destroy dashboard container on ID change', async () => {
|
||||||
let wrapper: ReactWrapper;
|
let wrapper: ReactWrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = await mountWithIntl(<DashboardRenderer savedObjectId="saved_object_kibanana" />);
|
wrapper = await mountWithIntl(<DashboardRenderer savedObjectId="saved_object_kibanana" />);
|
||||||
|
@ -85,18 +85,9 @@ describe('dashboard renderer', () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await wrapper.setProps({ savedObjectId: 'saved_object_kibanakiwi' });
|
await wrapper.setProps({ savedObjectId: 'saved_object_kibanakiwi' });
|
||||||
});
|
});
|
||||||
expect(mockDashboardContainer.destroy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not destroy dashboard container on expected ID change', async () => {
|
|
||||||
let wrapper: ReactWrapper;
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = await mountWithIntl(<DashboardRenderer savedObjectId="saved_object_kibanana" />);
|
|
||||||
});
|
|
||||||
mockDashboardContainer.isExpectingIdChange = jest.fn().mockReturnValue(true);
|
|
||||||
await act(async () => {
|
|
||||||
await wrapper.setProps({ savedObjectId: 'saved_object_kibanakiwi' });
|
|
||||||
});
|
|
||||||
expect(mockDashboardContainer.destroy).not.toHaveBeenCalled();
|
expect(mockDashboardContainer.destroy).not.toHaveBeenCalled();
|
||||||
|
expect(mockDashboardContainer.navigateToDashboard).toHaveBeenCalledWith(
|
||||||
|
'saved_object_kibanakiwi'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,7 +47,6 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [screenshotMode, setScreenshotMode] = useState(false);
|
const [screenshotMode, setScreenshotMode] = useState(false);
|
||||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
||||||
const [dashboardIdToBuild, setDashboardIdToBuild] = useState<string | undefined>(savedObjectId);
|
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
|
@ -67,9 +66,10 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
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) return;
|
||||||
if (!dashboardContainer.isExpectingIdChange()) setDashboardIdToBuild(savedObjectId);
|
|
||||||
|
// When a dashboard already exists, don't rebuild it, just set a new id.
|
||||||
|
dashboardContainer.navigateToDashboard(savedObjectId);
|
||||||
|
|
||||||
// Disabling exhaustive deps because this useEffect should only be triggered when the savedObjectId changes.
|
// Disabling exhaustive deps because this useEffect should only be triggered when the savedObjectId changes.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -115,9 +115,9 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
||||||
canceled = true;
|
canceled = true;
|
||||||
destroyContainer?.();
|
destroyContainer?.();
|
||||||
};
|
};
|
||||||
// Disabling exhaustive deps because embeddable should only be created when the dashboardIdToBuild changes.
|
// Disabling exhaustive deps because embeddable should only be created on first render.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [dashboardIdToBuild]);
|
}, []);
|
||||||
|
|
||||||
const viewportClasses = classNames(
|
const viewportClasses = classNames(
|
||||||
'dashboardViewport',
|
'dashboardViewport',
|
||||||
|
|
|
@ -56,6 +56,10 @@ export const dashboardContainerReducers = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setLastSavedId: (state: DashboardReduxState, action: PayloadAction<string | undefined>) => {
|
||||||
|
state.componentState.lastSavedId = action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
setStateFromSettingsFlyout: (
|
setStateFromSettingsFlyout: (
|
||||||
state: DashboardReduxState,
|
state: DashboardReduxState,
|
||||||
action: PayloadAction<DashboardStateFromSettingsFlyout>
|
action: PayloadAction<DashboardStateFromSettingsFlyout>
|
||||||
|
@ -218,4 +222,11 @@ export const dashboardContainerReducers = {
|
||||||
setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction<string | undefined>) => {
|
setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction<string | undefined>) => {
|
||||||
state.componentState.highlightPanelId = action.payload;
|
state.componentState.highlightPanelId = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAnimatePanelTransforms: (
|
||||||
|
state: DashboardReduxState,
|
||||||
|
action: PayloadAction<boolean | undefined>
|
||||||
|
) => {
|
||||||
|
state.componentState.animatePanelTransforms = action.payload;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -84,7 +84,7 @@ export function startDiffingDashboardState(
|
||||||
creationOptions?: DashboardCreationOptions
|
creationOptions?: DashboardCreationOptions
|
||||||
) {
|
) {
|
||||||
const checkForUnsavedChangesSubject$ = new Subject<null>();
|
const checkForUnsavedChangesSubject$ = new Subject<null>();
|
||||||
this.subscriptions.add(
|
this.diffingSubscription.add(
|
||||||
checkForUnsavedChangesSubject$
|
checkForUnsavedChangesSubject$
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
|
|
|
@ -26,6 +26,7 @@ export type DashboardStateFromSettingsFlyout = DashboardStateFromSaveModal & Das
|
||||||
|
|
||||||
export interface DashboardPublicState {
|
export interface DashboardPublicState {
|
||||||
lastSavedInput: DashboardContainerInput;
|
lastSavedInput: DashboardContainerInput;
|
||||||
|
animatePanelTransforms?: boolean;
|
||||||
isEmbeddedExternally?: boolean;
|
isEmbeddedExternally?: boolean;
|
||||||
hasUnsavedChanges?: boolean;
|
hasUnsavedChanges?: boolean;
|
||||||
hasOverlays?: boolean;
|
hasOverlays?: boolean;
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class DashboardAddPanelService extends FtrService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await button.click();
|
await button.click();
|
||||||
await this.common.closeToast();
|
await this.common.closeToastIfExists();
|
||||||
embeddableList.push(name);
|
embeddableList.push(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,7 +44,7 @@ const DashboardRendererComponent = ({
|
||||||
const getCreationOptions = useCallback(
|
const getCreationOptions = useCallback(
|
||||||
() =>
|
() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
initialInput: { timeRange, viewMode: ViewMode.VIEW, query, filters },
|
getInitialInput: () => ({ timeRange, viewMode: ViewMode.VIEW, query, filters }),
|
||||||
}),
|
}),
|
||||||
[filters, query, timeRange]
|
[filters, query, timeRange]
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue