mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [[Dashboard] Fast Navigation Between Dashboards (#157437)](https://github.com/elastic/kibana/pull/157437) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Devon Thomson","email":"devon.thomson@elastic.co"},"sourceCommit":{"committedDate":"2023-05-25T18:40:48Z","message":"[Dashboard] Fast Navigation Between Dashboards (#157437)\n\n## Summary\r\nMakes all navigation from one Dashboard to another feel snappier.","sha":"5342563a22a54223180a727a542a7b6c99caea7f","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Feature:Dashboard","Team:Presentation","loe:days","impact:medium","backport:skip","v8.8.0","v8.9.0"],"number":157437,"url":"https://github.com/elastic/kibana/pull/157437","mergeCommit":{"message":"[Dashboard] Fast Navigation Between Dashboards (#157437)\n\n## Summary\r\nMakes all navigation from one Dashboard to another feel snappier.","sha":"5342563a22a54223180a727a542a7b6c99caea7f"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/157437","number":157437,"mergeCommit":{"message":"[Dashboard] Fast Navigation Between Dashboards (#157437)\n\n## Summary\r\nMakes all navigation from one Dashboard to another feel snappier.","sha":"5342563a22a54223180a727a542a7b6c99caea7f"}}]}] BACKPORT--> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
parent
1bdfebab70
commit
9e48a57755
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 { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
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 }) => {
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
|
@ -48,7 +52,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
|||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<DashboardRenderer
|
||||
getCreationOptions={async () => {
|
||||
getCreationOptions={async (): Promise<DashboardCreationOptions> => {
|
||||
const builder = controlGroupInputBuilder;
|
||||
const controlGroupInput = getDefaultControlGroupInput();
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
|
@ -68,11 +72,11 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
|||
|
||||
return {
|
||||
useControlGroupIntegration: true,
|
||||
initialInput: {
|
||||
getInitialInput: () => ({
|
||||
timeRange: { from: 'now-30d', to: 'now' },
|
||||
viewMode: ViewMode.VIEW,
|
||||
controlGroupInput,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}}
|
||||
ref={setDashboard}
|
||||
|
|
|
@ -136,10 +136,10 @@ export const DynamicByReferenceExample = () => {
|
|||
getCreationOptions={async () => {
|
||||
const persistedInput = getPersistableInput();
|
||||
return {
|
||||
initialInput: {
|
||||
getInitialInput: () => ({
|
||||
...persistedInput,
|
||||
timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis
|
||||
},
|
||||
}),
|
||||
};
|
||||
}}
|
||||
ref={setdashboard}
|
||||
|
|
|
@ -50,12 +50,15 @@ export const StaticByReferenceExample = ({
|
|||
const field = dataView.getFieldByName('machine.os.keyword');
|
||||
let filter: Filter;
|
||||
let creationOptions: DashboardCreationOptions = {
|
||||
initialInput: { viewMode: ViewMode.VIEW },
|
||||
getInitialInput: () => ({ viewMode: ViewMode.VIEW }),
|
||||
};
|
||||
if (field) {
|
||||
filter = buildPhraseFilter(field, 'win xp', dataView);
|
||||
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
|
||||
}}
|
||||
|
|
|
@ -29,11 +29,11 @@ export const StaticByValueExample = () => {
|
|||
<DashboardRenderer
|
||||
getCreationOptions={async () => {
|
||||
return {
|
||||
initialInput: {
|
||||
getInitialInput: () => ({
|
||||
timeRange: { from: 'now-30d', to: 'now' },
|
||||
viewMode: ViewMode.VIEW,
|
||||
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) => {
|
||||
this.lastUsedDataViewId = lastUsedDataViewId;
|
||||
};
|
||||
|
|
|
@ -83,11 +83,6 @@ export function DashboardApp({
|
|||
customBranding,
|
||||
} = pluginServices.getServices();
|
||||
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
|
||||
|
||||
const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage(
|
||||
DASHBOARD_APP_ID,
|
||||
true
|
||||
);
|
||||
const { scopedHistory: getScopedHistory } = useDashboardMountContext();
|
||||
|
||||
useExecutionContext(executionContext, {
|
||||
|
@ -125,13 +120,28 @@ export function DashboardApp({
|
|||
/**
|
||||
* Create options to pass into the dashboard renderer
|
||||
*/
|
||||
const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
|
||||
const getCreationOptions = useCallback((): Promise<DashboardCreationOptions> => {
|
||||
const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
||||
const getInitialInput = () => {
|
||||
const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
|
||||
const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||
|
||||
return Promise.resolve({
|
||||
incomingEmbeddable,
|
||||
// Override all state with URL + Locator input
|
||||
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
|
||||
useControlGroupIntegration: true,
|
||||
|
@ -148,28 +158,16 @@ export function DashboardApp({
|
|||
getSearchSessionIdFromURL: () => getSearchSessionIdFromURL(history),
|
||||
removeSessionIdFromUrl: () => removeSearchSessionIdFromURL(kbnUrlStateStorage),
|
||||
},
|
||||
|
||||
// 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 }
|
||||
: {}),
|
||||
},
|
||||
|
||||
getInitialInput,
|
||||
validateLoadedSavedObject: validateOutcome,
|
||||
});
|
||||
}, [
|
||||
history,
|
||||
validateOutcome,
|
||||
stateFromLocator,
|
||||
getScopedHistory,
|
||||
isScreenshotMode,
|
||||
getStateTransfer,
|
||||
kbnUrlStateStorage,
|
||||
incomingEmbeddable,
|
||||
getScreenshotContext,
|
||||
]);
|
||||
|
||||
|
@ -183,7 +181,7 @@ export function DashboardApp({
|
|||
dashboardAPI,
|
||||
});
|
||||
return () => stopWatchingAppStateInUrl();
|
||||
}, [dashboardAPI, kbnUrlStateStorage]);
|
||||
}, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]);
|
||||
|
||||
return (
|
||||
<div className="dshAppWrapper">
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
import './_dashboard_app.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { History } from 'history';
|
||||
import { parse, ParsedQuery } from 'query-string';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Switch, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom';
|
||||
import { Route } from '@kbn/shared-ux-router';
|
||||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { AppMountParameters, CoreSetup } from '@kbn/core/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-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 },
|
||||
dashboardCapabilities: { showWriteControls },
|
||||
documentationLinks: { dashboardDocLink },
|
||||
application: { navigateToApp },
|
||||
settings: { uiSettings },
|
||||
data: dataStart,
|
||||
notifications,
|
||||
|
@ -63,7 +64,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
|||
} = pluginServices.getServices();
|
||||
|
||||
let globalEmbedSettings: DashboardEmbedSettings | undefined;
|
||||
let routerHistory: History;
|
||||
|
||||
const getUrlStateStorage = (history: RouteComponentProps['history']) =>
|
||||
createKbnUrlStateStorage({
|
||||
|
@ -73,17 +73,17 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
|||
});
|
||||
|
||||
const redirect = (redirectTo: RedirectToProps) => {
|
||||
if (!routerHistory) return;
|
||||
const historyFunction = redirectTo.useReplace ? routerHistory.replace : routerHistory.push;
|
||||
let destination;
|
||||
let path;
|
||||
let state;
|
||||
if (redirectTo.destination === 'dashboard') {
|
||||
destination = redirectTo.id
|
||||
? createDashboardEditUrl(redirectTo.id, redirectTo.editMode)
|
||||
: CREATE_NEW_DASHBOARD_URL;
|
||||
path = redirectTo.id ? createDashboardEditUrl(redirectTo.id) : CREATE_NEW_DASHBOARD_URL;
|
||||
if (redirectTo.editMode) {
|
||||
state = { viewMode: ViewMode.EDIT };
|
||||
}
|
||||
} else {
|
||||
destination = createDashboardListingFilterUrl(redirectTo.filter);
|
||||
path = createDashboardListingFilterUrl(redirectTo.filter);
|
||||
}
|
||||
historyFunction(destination);
|
||||
navigateToApp(DASHBOARD_APP_ID, { path: `#/${path}`, state, replace: redirectTo.useReplace });
|
||||
};
|
||||
|
||||
const getDashboardEmbedSettings = (
|
||||
|
@ -102,9 +102,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
|||
if (routeParams.embed && !globalEmbedSettings) {
|
||||
globalEmbedSettings = getDashboardEmbedSettings(routeParams);
|
||||
}
|
||||
if (!routerHistory) {
|
||||
routerHistory = routeProps.history;
|
||||
}
|
||||
return (
|
||||
<DashboardApp
|
||||
history={routeProps.history}
|
||||
|
@ -120,9 +117,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
|
|||
const routeParams = parse(routeProps.history.location.search);
|
||||
const title = (routeParams.title as string) || undefined;
|
||||
const filter = (routeParams.filter as string) || undefined;
|
||||
if (!routerHistory) {
|
||||
routerHistory = routeProps.history;
|
||||
}
|
||||
return (
|
||||
<DashboardListingPage
|
||||
initialFilter={filter}
|
||||
|
|
|
@ -82,8 +82,9 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
const title = dashboard.select((state) => state.explicitInput.title);
|
||||
|
||||
// store data views in state & subscribe to dashboard data view changes.
|
||||
const [allDataViews, setAllDataViews] = useState<DataView[]>(dashboard.getAllDataViews());
|
||||
const [allDataViews, setAllDataViews] = useState<DataView[]>([]);
|
||||
useEffect(() => {
|
||||
setAllDataViews(dashboard.getAllDataViews());
|
||||
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||
setAllDataViews(dataViews)
|
||||
);
|
||||
|
|
|
@ -11,7 +11,6 @@ import 'react-grid-layout/css/styles.css';
|
|||
|
||||
import { pick } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { useEffectOnce } from 'react-use/lib';
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
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 useMargins = dashboard.select((state) => state.explicitInput.useMargins);
|
||||
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);
|
||||
useEffectOnce(() => {
|
||||
setTimeout(() => setAnimatePanelTransforms(true), 500);
|
||||
});
|
||||
|
||||
/**
|
||||
* Track panel maximized state delayed by one tick and use it to prevent
|
||||
* panel sliding animations on maximize and minimize.
|
||||
*/
|
||||
const [delayedIsPanelExpanded, setDelayedIsPanelMaximized] = useState(false);
|
||||
useEffect(() => {
|
||||
if (expandedPanelId) {
|
||||
setAnimatePanelTransforms(false);
|
||||
setDelayedIsPanelMaximized(true);
|
||||
} else {
|
||||
// delaying enabling CSS transforms to the next tick prevents a panel slide animation on minimize
|
||||
setTimeout(() => setAnimatePanelTransforms(true), 0);
|
||||
setTimeout(() => setDelayedIsPanelMaximized(false), 0);
|
||||
}
|
||||
}, [expandedPanelId]);
|
||||
|
||||
|
@ -107,7 +107,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
|||
'dshLayout-withoutMargins': !useMargins,
|
||||
'dshLayout--viewing': viewMode === ViewMode.VIEW,
|
||||
'dshLayout--editing': viewMode !== ViewMode.VIEW,
|
||||
'dshLayout--noAnimation': !animatePanelTransforms || expandedPanelId,
|
||||
'dshLayout--noAnimation': !animatePanelTransforms || delayedIsPanelExpanded,
|
||||
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
|
||||
});
|
||||
|
||||
|
|
|
@ -102,7 +102,6 @@ export function runSaveAs(this: DashboardContainer) {
|
|||
this.dispatch.setLastSavedInput(stateToSave);
|
||||
});
|
||||
}
|
||||
if (newCopyOnSave || !lastSavedId) this.expectIdChange();
|
||||
resolve(saveResult);
|
||||
return saveResult;
|
||||
};
|
||||
|
@ -175,10 +174,7 @@ export async function runClone(this: DashboardContainer) {
|
|||
saveOptions: { saveAsCopy: true },
|
||||
currentState: { ...currentState, title: newTitle },
|
||||
});
|
||||
|
||||
this.dispatch.setTitle(newTitle);
|
||||
resolve(saveResult);
|
||||
this.expectIdChange();
|
||||
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
||||
};
|
||||
showCloneModal({ onClone, title: currentState.title, onClose: () => resolve(undefined) });
|
||||
|
|
|
@ -50,7 +50,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
chainingSystem: deepEqual,
|
||||
ignoreParentSettings: deepEqual,
|
||||
};
|
||||
this.subscriptions.add(
|
||||
this.integrationSubscriptions.add(
|
||||
this.controlGroup
|
||||
.getInput$()
|
||||
.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
|
||||
this.subscriptions.add(
|
||||
this.integrationSubscriptions.add(
|
||||
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
|
@ -106,7 +106,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
|||
);
|
||||
|
||||
// when control group outputs filters, force a refresh!
|
||||
this.subscriptions.add(
|
||||
this.integrationSubscriptions.add(
|
||||
this.controlGroup
|
||||
.getOutput$()
|
||||
.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
|
||||
);
|
||||
|
||||
this.subscriptions.add(
|
||||
this.integrationSubscriptions.add(
|
||||
this.controlGroup
|
||||
.getOutput$()
|
||||
.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.
|
||||
this.subscriptions.add(
|
||||
this.integrationSubscriptions.add(
|
||||
this.getAnyChildOutputChange$().subscribe(() => {
|
||||
if (!this.controlGroup) {
|
||||
return;
|
||||
|
|
|
@ -30,14 +30,12 @@ import { pluginServices } from '../../../services/plugin_services';
|
|||
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
||||
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
|
||||
|
||||
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);
|
||||
await createDashboard();
|
||||
}).rejects.toThrow('Dashboard requires at least one data view before it can be initialized.');
|
||||
|
||||
// reset get default data view
|
||||
|
@ -49,7 +47,7 @@ test('throws error when provided validation function returns invalid', async ()
|
|||
validateLoadedSavedObject: jest.fn().mockImplementation(() => false),
|
||||
};
|
||||
await expect(async () => {
|
||||
await createDashboard(embeddableId, creationOptions, 0, 'test-id');
|
||||
await createDashboard(creationOptions, 0, 'test-id');
|
||||
}).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.`,
|
||||
},
|
||||
});
|
||||
const dashboard = await createDashboard(embeddableId, {}, 0, 'wow-such-id');
|
||||
const dashboard = await createDashboard({}, 0, 'wow-such-id');
|
||||
expect(
|
||||
pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject
|
||||
).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
|
||||
.fn()
|
||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||
const dashboard = await createDashboard(
|
||||
embeddableId,
|
||||
{ useSessionStorageIntegration: true },
|
||||
0,
|
||||
'wow-such-id'
|
||||
);
|
||||
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id');
|
||||
expect(dashboard.getState().explicitInput.description).toBe(
|
||||
'wow this description marginally better'
|
||||
);
|
||||
|
@ -105,10 +98,9 @@ test('pulls state from creation options initial input which overrides all other
|
|||
.fn()
|
||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||
const dashboard = await createDashboard(
|
||||
embeddableId,
|
||||
{
|
||||
useSessionStorageIntegration: true,
|
||||
initialInput: { description: 'wow this description is a masterpiece' },
|
||||
getInitialInput: () => ({ description: 'wow this description is a masterpiece' }),
|
||||
},
|
||||
0,
|
||||
'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' } },
|
||||
];
|
||||
const query = { language: 'kql', query: 'query' };
|
||||
await createDashboard(embeddableId, {
|
||||
await createDashboard({
|
||||
useUnifiedSearchIntegration: true,
|
||||
unifiedSearchSettings: {
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
},
|
||||
initialInput: { filters, query },
|
||||
getInitialInput: () => ({ filters, query }),
|
||||
});
|
||||
expect(pluginServices.getServices().data.query.queryString.setQuery).toHaveBeenCalledWith(query);
|
||||
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 () => {
|
||||
const timeRange = { from: new Date().toISOString(), to: new Date().toISOString() };
|
||||
const refreshInterval = { pause: false, value: 42 };
|
||||
await createDashboard(embeddableId, {
|
||||
await createDashboard({
|
||||
useUnifiedSearchIntegration: true,
|
||||
unifiedSearchSettings: {
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
},
|
||||
initialInput: { timeRange, refreshInterval, timeRestore: true },
|
||||
getInitialInput: () => ({ timeRange, refreshInterval, timeRestore: true }),
|
||||
});
|
||||
expect(
|
||||
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
|
||||
.fn()
|
||||
.mockReturnValue(timeRange);
|
||||
const dashboard = await createDashboard(embeddableId, {
|
||||
const dashboard = await createDashboard({
|
||||
useUnifiedSearchIntegration: true,
|
||||
unifiedSearchSettings: {
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
|
@ -177,9 +169,9 @@ test('replaces panel with incoming embeddable if id matches existing panel', asy
|
|||
} as ContactCardEmbeddableInput,
|
||||
embeddableId: 'i_match',
|
||||
};
|
||||
const dashboard = await createDashboard(embeddableId, {
|
||||
incomingEmbeddable,
|
||||
initialInput: {
|
||||
const dashboard = await createDashboard({
|
||||
getIncomingEmbeddable: () => incomingEmbeddable,
|
||||
getInitialInput: () => ({
|
||||
panels: {
|
||||
i_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: {
|
||||
|
@ -189,7 +181,7 @@ test('replaces panel with incoming embeddable if id matches existing panel', asy
|
|||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(dashboard.getState().explicitInput.panels.i_match.explicitInput).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -216,9 +208,9 @@ test('creates new embeddable with incoming embeddable if id does not match exist
|
|||
.fn()
|
||||
.mockReturnValue(mockContactCardFactory);
|
||||
|
||||
await createDashboard(embeddableId, {
|
||||
incomingEmbeddable,
|
||||
initialInput: {
|
||||
await createDashboard({
|
||||
getIncomingEmbeddable: () => incomingEmbeddable,
|
||||
getInitialInput: () => ({
|
||||
panels: {
|
||||
i_do_not_match: getSampleDashboardPanel<ContactCardEmbeddableInput>({
|
||||
explicitInput: {
|
||||
|
@ -228,7 +220,7 @@ test('creates new embeddable with incoming embeddable if id does not match exist
|
|||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// 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
|
||||
.fn()
|
||||
.mockReturnValue(mockControlGroupFactory);
|
||||
await createDashboard(embeddableId, {
|
||||
await createDashboard({
|
||||
useControlGroupIntegration: true,
|
||||
initialInput: {
|
||||
getInitialInput: () => ({
|
||||
controlGroupInput: { controlStyle: 'twoLine' } as unknown as ControlGroupInput,
|
||||
},
|
||||
}),
|
||||
});
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
@ -302,7 +294,7 @@ test('searchSessionId is updated prior to child embeddable parent subscription e
|
|||
sessionCount++;
|
||||
return `searchSessionId${sessionCount}`;
|
||||
};
|
||||
const dashboard = await createDashboard(embeddableId, {
|
||||
const dashboard = await createDashboard({
|
||||
searchSessionSettings: {
|
||||
getSearchSessionIdFromURL: () => undefined,
|
||||
removeSessionIdFromUrl: () => {},
|
||||
|
|
|
@ -27,47 +27,21 @@ import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data
|
|||
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';
|
||||
import { LoadDashboardFromSavedObjectReturn } from '../../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param creationOptions
|
||||
* Builds a new Dashboard from scratch.
|
||||
*/
|
||||
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 },
|
||||
},
|
||||
data: { dataViews },
|
||||
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.
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -83,12 +57,9 @@ export const createDashboard = async (
|
|||
// --------------------------------------------------------------------------------------
|
||||
// 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 dashboardSavedObjectPromise = loadDashboardStateFromSavedObject({ id: savedObjectId });
|
||||
|
||||
const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([
|
||||
reduxEmbeddablePackagePromise,
|
||||
|
@ -96,17 +67,82 @@ export const createDashboard = async (
|
|||
defaultDataViewAssignmentPromise,
|
||||
]);
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Run validations.
|
||||
// --------------------------------------------------------------------------------------
|
||||
if (!defaultDataView) {
|
||||
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 (
|
||||
savedObjectResult &&
|
||||
loadDashboardReturn &&
|
||||
validateLoadedSavedObject &&
|
||||
!validateLoadedSavedObject(savedObjectResult)
|
||||
!validateLoadedSavedObject(loadDashboardReturn)
|
||||
) {
|
||||
throw new Error('Dashboard failed saved object result validation');
|
||||
}
|
||||
|
@ -116,7 +152,7 @@ export const createDashboard = async (
|
|||
// --------------------------------------------------------------------------------------
|
||||
const sessionStorageInput = ((): Partial<DashboardContainerInput> | undefined => {
|
||||
if (!useSessionStorageIntegration) return;
|
||||
return dashboardSessionStorage.getState(savedObjectId);
|
||||
return dashboardSessionStorage.getState(loadDashboardReturn.dashboardId);
|
||||
})();
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -124,10 +160,9 @@ export const createDashboard = async (
|
|||
// --------------------------------------------------------------------------------------
|
||||
const initialInput: DashboardContainerInput = cloneDeep({
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
...(savedObjectResult?.dashboardInput ?? {}),
|
||||
...(loadDashboardReturn?.dashboardInput ?? {}),
|
||||
...sessionStorageInput,
|
||||
...overrideInput,
|
||||
id: embeddableId,
|
||||
});
|
||||
|
||||
initialInput.executionContext = {
|
||||
|
@ -166,8 +201,7 @@ export const createDashboard = async (
|
|||
}
|
||||
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
const stopSyncingUnifiedSearchState =
|
||||
syncUnifiedSearchState.bind(dashboardContainer)(kbnUrlStateStorage);
|
||||
const stopSyncingUnifiedSearchState = syncUnifiedSearchState.bind(dashboardContainer)();
|
||||
dashboardContainer.stopSyncingWithUnifiedSearch = () => {
|
||||
stopSyncingUnifiedSearchState();
|
||||
stopSyncingQueryServiceStateWithUrl();
|
||||
|
@ -178,16 +212,20 @@ export const createDashboard = async (
|
|||
// --------------------------------------------------------------------------------------
|
||||
// Place the incoming embeddable if there is one
|
||||
// --------------------------------------------------------------------------------------
|
||||
const incomingEmbeddable = creationOptions?.incomingEmbeddable;
|
||||
const incomingEmbeddable = creationOptions?.getIncomingEmbeddable?.();
|
||||
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 &&
|
||||
Boolean(initialInput.panels[incomingEmbeddable.embeddableId]);
|
||||
if (panelExists) {
|
||||
Boolean(initialInput.panels[incomingEmbeddable.embeddableId])
|
||||
) {
|
||||
// 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;
|
||||
|
||||
panelToUpdate.type = incomingEmbeddable.type;
|
||||
|
@ -196,22 +234,24 @@ export const createDashboard = async (
|
|||
...(sameType ? panelToUpdate.explicitInput : {}),
|
||||
|
||||
...incomingEmbeddable.input,
|
||||
id: incomingEmbeddable.embeddableId as string,
|
||||
id: incomingEmbeddable.embeddableId,
|
||||
|
||||
// maintain hide panel titles setting.
|
||||
hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles,
|
||||
};
|
||||
untilDashboardReady().then((container) =>
|
||||
scrolltoIncomingEmbeddable(container, incomingEmbeddable.embeddableId as string)
|
||||
);
|
||||
} else {
|
||||
// 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) => {
|
||||
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
|
||||
>(CONTROL_GROUP_TYPE);
|
||||
const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput;
|
||||
const controlGroup = await controlsGroupFactory?.create({
|
||||
const fullControlGroupInput = {
|
||||
id: `control_group_${id ?? 'new_dashboard'}`,
|
||||
...getDefaultControlGroupInput(),
|
||||
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
|
||||
|
@ -259,9 +299,15 @@ export const createDashboard = async (
|
|||
viewMode,
|
||||
filters,
|
||||
query,
|
||||
});
|
||||
if (!controlGroup || isErrorEmbeddable(controlGroup)) {
|
||||
throw new Error('Error in control group startup');
|
||||
};
|
||||
if (controlGroup) {
|
||||
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) => {
|
||||
|
@ -275,22 +321,17 @@ export const createDashboard = async (
|
|||
// Start the data views integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
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(
|
||||
initialInput,
|
||||
reduxEmbeddablePackage,
|
||||
initialSearchSessionId,
|
||||
savedObjectResult?.dashboardInput,
|
||||
dashboardCreationStartTime,
|
||||
undefined,
|
||||
creationOptions,
|
||||
savedObjectId
|
||||
untilDashboardReady().then((dashboard) =>
|
||||
setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500)
|
||||
);
|
||||
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 type { Filter, Query } from '@kbn/es-query';
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-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
|
||||
* and the dashboard Redux store.
|
||||
*/
|
||||
export function syncUnifiedSearchState(
|
||||
this: DashboardContainer,
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage
|
||||
) {
|
||||
export function syncUnifiedSearchState(this: DashboardContainer) {
|
||||
const {
|
||||
data: { query: queryService, search },
|
||||
} = pluginServices.getServices();
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { batch } from 'react-redux';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||
import {
|
||||
|
@ -20,10 +21,10 @@ import {
|
|||
type EmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import type { Filter, TimeRange, Query } from '@kbn/es-query';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import type { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
|
||||
|
@ -44,6 +45,7 @@ import {
|
|||
import { DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||
import { createPanelState } from '../component/panel';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { initializeDashboard } from './create/create_dashboard';
|
||||
import { DashboardCreationOptions } from './dashboard_container_factory';
|
||||
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
||||
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
|
||||
|
@ -93,7 +95,8 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
public dispatch: DashboardReduxEmbeddableTools['dispatch'];
|
||||
public onStateChange: DashboardReduxEmbeddableTools['onStateChange'];
|
||||
|
||||
public subscriptions: Subscription = new Subscription();
|
||||
public integrationSubscriptions: Subscription = new Subscription();
|
||||
public diffingSubscription: Subscription = new Subscription();
|
||||
public controlGroup?: ControlGroupContainer;
|
||||
|
||||
public searchSessionId?: string;
|
||||
|
@ -169,6 +172,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
...DEFAULT_DASHBOARD_INPUT,
|
||||
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.
|
||||
lastSavedId: savedObjectId,
|
||||
},
|
||||
|
@ -280,7 +284,8 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
super.destroy();
|
||||
this.cleanupStateTools();
|
||||
this.controlGroup?.destroy();
|
||||
this.subscriptions.unsubscribe();
|
||||
this.diffingSubscription.unsubscribe();
|
||||
this.integrationSubscriptions.unsubscribe();
|
||||
this.stopSyncingWithUnifiedSearch?.();
|
||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
|
@ -289,26 +294,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
// 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 runSaveAs = runSaveAs;
|
||||
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
|
||||
* @returns An array of dataviews
|
||||
|
|
|
@ -34,9 +34,9 @@ export type DashboardContainerFactory = EmbeddableFactory<
|
|||
>;
|
||||
|
||||
export interface DashboardCreationOptions {
|
||||
initialInput?: Partial<DashboardContainerInput>;
|
||||
getInitialInput?: () => Partial<DashboardContainerInput>;
|
||||
|
||||
incomingEmbeddable?: EmbeddablePackageState;
|
||||
getIncomingEmbeddable?: () => EmbeddablePackageState | undefined;
|
||||
|
||||
useSearchSessionsIntegration?: boolean;
|
||||
searchSessionSettings?: {
|
||||
|
@ -98,7 +98,7 @@ export class DashboardContainerFactoryDefinition
|
|||
const { createDashboard } = await import('./create/create_dashboard');
|
||||
try {
|
||||
return Promise.resolve(
|
||||
createDashboard(initialInput.id, creationOptions, dashboardCreationStartTime, savedObjectId)
|
||||
createDashboard(creationOptions, dashboardCreationStartTime, savedObjectId)
|
||||
);
|
||||
} catch (e) {
|
||||
return new ErrorEmbeddable(e.text, { id: e.id });
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('dashboard renderer', () => {
|
|||
mockDashboardContainer = {
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
isExpectingIdChange: jest.fn().mockReturnValue(false),
|
||||
navigateToDashboard: jest.fn(),
|
||||
} as unknown as DashboardContainer;
|
||||
mockDashboardFactory = {
|
||||
create: jest.fn().mockReturnValue(mockDashboardContainer),
|
||||
|
@ -77,7 +77,7 @@ describe('dashboard renderer', () => {
|
|||
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;
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(<DashboardRenderer savedObjectId="saved_object_kibanana" />);
|
||||
|
@ -85,18 +85,9 @@ describe('dashboard renderer', () => {
|
|||
await act(async () => {
|
||||
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.navigateToDashboard).toHaveBeenCalledWith(
|
||||
'saved_object_kibanakiwi'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,7 +47,6 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [screenshotMode, setScreenshotMode] = useState(false);
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
||||
const [dashboardIdToBuild, setDashboardIdToBuild] = useState<string | undefined>(savedObjectId);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
|
@ -67,9 +66,10 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
}, []);
|
||||
|
||||
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);
|
||||
|
||||
// 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.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -115,9 +115,9 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
canceled = true;
|
||||
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
|
||||
}, [dashboardIdToBuild]);
|
||||
}, []);
|
||||
|
||||
const viewportClasses = classNames(
|
||||
'dashboardViewport',
|
||||
|
|
|
@ -56,6 +56,10 @@ export const dashboardContainerReducers = {
|
|||
}
|
||||
},
|
||||
|
||||
setLastSavedId: (state: DashboardReduxState, action: PayloadAction<string | undefined>) => {
|
||||
state.componentState.lastSavedId = action.payload;
|
||||
},
|
||||
|
||||
setStateFromSettingsFlyout: (
|
||||
state: DashboardReduxState,
|
||||
action: PayloadAction<DashboardStateFromSettingsFlyout>
|
||||
|
@ -218,4 +222,11 @@ export const dashboardContainerReducers = {
|
|||
setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction<string | undefined>) => {
|
||||
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
|
||||
) {
|
||||
const checkForUnsavedChangesSubject$ = new Subject<null>();
|
||||
this.subscriptions.add(
|
||||
this.diffingSubscription.add(
|
||||
checkForUnsavedChangesSubject$
|
||||
.pipe(
|
||||
startWith(null),
|
||||
|
|
|
@ -26,6 +26,7 @@ export type DashboardStateFromSettingsFlyout = DashboardStateFromSaveModal & Das
|
|||
|
||||
export interface DashboardPublicState {
|
||||
lastSavedInput: DashboardContainerInput;
|
||||
animatePanelTransforms?: boolean;
|
||||
isEmbeddedExternally?: boolean;
|
||||
hasUnsavedChanges?: boolean;
|
||||
hasOverlays?: boolean;
|
||||
|
|
|
@ -94,7 +94,7 @@ export class DashboardAddPanelService extends FtrService {
|
|||
continue;
|
||||
}
|
||||
await button.click();
|
||||
await this.common.closeToast();
|
||||
await this.common.closeToastIfExists();
|
||||
embeddableList.push(name);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ const DashboardRendererComponent = ({
|
|||
const getCreationOptions = useCallback(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
initialInput: { timeRange, viewMode: ViewMode.VIEW, query, filters },
|
||||
getInitialInput: () => ({ timeRange, viewMode: ViewMode.VIEW, query, filters }),
|
||||
}),
|
||||
[filters, query, timeRange]
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue