[dashboard] Lazy DashboardRenderer (#192754)

Changes
1. expose DashboardRenderer as lazy loaded component to reduce static
page load size
2. Use `onApiAvailable` prop to pass DashboardApi to parent instead of
`forwardRef`
3. Decouple DashboardApi from legacy embeddable system. This changed
functions such as `updateInput` and using redux `select` to access
state.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-09-17 13:15:50 -06:00 committed by GitHub
parent 2d78f23dde
commit e2380afd7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 667 additions and 736 deletions

View file

@ -12,55 +12,65 @@ import React, { useMemo } from 'react';
import { useAsync } from 'react-use/lib';
import { Redirect } from 'react-router-dom';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { AppMountParameters } from '@kbn/core/public';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { DashboardListingTable } from '@kbn/dashboard-plugin/public';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { DualReduxExample } from './dual_redux_example';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { DualDashboardsExample } from './dual_dashboards_example';
import { StartDeps } from './plugin';
import { StaticByValueExample } from './static_by_value_example';
import { StaticByReferenceExample } from './static_by_reference_example';
import { DynamicByReferenceExample } from './dynamically_add_panels_example';
import { DashboardWithControlsExample } from './dashboard_with_controls_example';
const DASHBOARD_DEMO_PATH = '/dashboardDemo';
const DASHBOARD_LIST_PATH = '/listingDemo';
export const renderApp = async (
coreStart: CoreStart,
{ data, dashboard }: StartDeps,
{ element, history }: AppMountParameters
) => {
ReactDOM.render(
<PortableDashboardsDemos data={data} history={history} dashboard={dashboard} />,
<PortableDashboardsDemos
coreStart={coreStart}
data={data}
history={history}
dashboard={dashboard}
/>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};
const PortableDashboardsDemos = ({
coreStart,
data,
dashboard,
history,
}: {
coreStart: CoreStart;
data: StartDeps['data'];
dashboard: StartDeps['dashboard'];
history: AppMountParameters['history'];
}) => {
return (
<Router history={history}>
<Routes>
<Route exact path="/">
<Redirect to={DASHBOARD_DEMO_PATH} />
</Route>
<Route path={DASHBOARD_LIST_PATH}>
<PortableDashboardListingDemo history={history} />
</Route>
<Route path={DASHBOARD_DEMO_PATH}>
<DashboardsDemo data={data} dashboard={dashboard} history={history} />
</Route>
</Routes>
</Router>
<KibanaRenderContextProvider i18n={coreStart.i18n} theme={coreStart.theme}>
<Router history={history}>
<Routes>
<Route exact path="/">
<Redirect to={DASHBOARD_DEMO_PATH} />
</Route>
<Route path={DASHBOARD_LIST_PATH}>
<PortableDashboardListingDemo history={history} />
</Route>
<Route path={DASHBOARD_DEMO_PATH}>
<DashboardsDemo data={data} dashboard={dashboard} history={history} />
</Route>
</Routes>
</Router>
</KibanaRenderContextProvider>
);
};
@ -91,9 +101,7 @@ const DashboardsDemo = ({
<>
<DashboardWithControlsExample dataView={dataViews[0]} />
<EuiSpacer size="xl" />
<DynamicByReferenceExample />
<EuiSpacer size="xl" />
<DualReduxExample />
<DualDashboardsExample />
<EuiSpacer size="xl" />
<StaticByReferenceExample dashboardId={logsSampleDashboardId} dataView={dataViews[0]} />
<EuiSpacer size="xl" />

View file

@ -14,41 +14,29 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { controlGroupStateBuilder } from '@kbn/controls-plugin/public';
import {
AwaitingDashboardAPI,
DashboardApi,
DashboardRenderer,
DashboardCreationOptions,
} from '@kbn/dashboard-plugin/public';
import { apiHasUniqueId } from '@kbn/presentation-publishing';
import { FILTER_DEBUGGER_EMBEDDABLE_ID } from './constants';
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
const [dashboard, setDashboard] = useState<DashboardApi | undefined>();
// add a filter debugger panel as soon as the dashboard becomes available
useEffect(() => {
if (!dashboard) return;
(async () => {
const api = await dashboard.addNewPanel(
dashboard
.addNewPanel(
{
panelType: FILTER_DEBUGGER_EMBEDDABLE_ID,
initialState: {},
},
true
);
if (!apiHasUniqueId(api)) {
return;
}
const prevPanelState = dashboard.getExplicitInput().panels[api.uuid];
// resize the new panel so that it fills up the entire width of the dashboard
dashboard.updateInput({
panels: {
[api.uuid]: {
...prevPanelState,
gridData: { i: api.uuid, x: 0, y: 0, w: 48, h: 12 },
},
},
)
.catch(() => {
// ignore error - its an example
});
})();
}, [dashboard]);
return (
@ -88,7 +76,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
}),
};
}}
ref={setDashboard}
onApiAvailable={setDashboard}
/>
</EuiPanel>
</>

View file

@ -18,19 +18,16 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import {
AwaitingDashboardAPI,
DashboardAPI,
DashboardRenderer,
} from '@kbn/dashboard-plugin/public';
import { DashboardApi, DashboardRenderer } from '@kbn/dashboard-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
export const DualReduxExample = () => {
const [firstDashboardContainer, setFirstDashboardContainer] = useState<AwaitingDashboardAPI>();
const [secondDashboardContainer, setSecondDashboardContainer] = useState<AwaitingDashboardAPI>();
export const DualDashboardsExample = () => {
const [firstDashboardApi, setFirstDashboardApi] = useState<DashboardApi | undefined>();
const [secondDashboardApi, setSecondDashboardApi] = useState<DashboardApi | undefined>();
const ButtonControls = ({ dashboard }: { dashboard: DashboardAPI }) => {
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const ButtonControls = ({ dashboardApi }: { dashboardApi: DashboardApi }) => {
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode);
return (
<EuiButtonGroup
@ -48,7 +45,7 @@ export const DualReduxExample = () => {
},
]}
idSelected={viewMode}
onChange={(id, value) => dashboard.dispatch.setViewMode(value)}
onChange={(id, value) => dashboardApi.setViewMode(value)}
type="single"
/>
);
@ -57,12 +54,12 @@ export const DualReduxExample = () => {
return (
<>
<EuiTitle>
<h2>Dual redux example</h2>
<h2>Dual dashboards example</h2>
</EuiTitle>
<EuiText>
<p>
Use the redux contexts from two different dashboard containers to independently set the
view mode of each dashboard.
Use the APIs from different dashboards to independently set the view mode of each
dashboard.
</p>
</EuiText>
<EuiSpacer size="m" />
@ -73,18 +70,18 @@ export const DualReduxExample = () => {
<h3>Dashboard #1</h3>
</EuiTitle>
<EuiSpacer size="m" />
{firstDashboardContainer && <ButtonControls dashboard={firstDashboardContainer} />}
{firstDashboardApi && <ButtonControls dashboardApi={firstDashboardApi} />}
<EuiSpacer size="m" />
<DashboardRenderer ref={setFirstDashboardContainer} />
<DashboardRenderer onApiAvailable={setFirstDashboardApi} />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>Dashboard #2</h3>
</EuiTitle>
<EuiSpacer size="m" />
{secondDashboardContainer && <ButtonControls dashboard={secondDashboardContainer} />}
{secondDashboardApi && <ButtonControls dashboardApi={secondDashboardApi} />}
<EuiSpacer size="m" />
<DashboardRenderer ref={setSecondDashboardContainer} />
<DashboardRenderer onApiAvailable={setSecondDashboardApi} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -1,151 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useMemo, useState } from 'react';
import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import {
VisualizeEmbeddable,
VisualizeInput,
VisualizeOutput,
} from '@kbn/visualizations-plugin/public/legacy/embeddable/visualize_embeddable';
const INPUT_KEY = 'portableDashboard:saveExample:input';
export const DynamicByReferenceExample = () => {
const [isSaving, setIsSaving] = useState(false);
const [dashboard, setdashboard] = useState<AwaitingDashboardAPI>();
const onSave = async () => {
if (!dashboard) return;
setIsSaving(true);
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboard.getInput()));
// simulated async save await
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsSaving(false);
};
const getPersistableInput = () => {
let input = {};
const inputAsString = localStorage.getItem(INPUT_KEY);
if (inputAsString) {
try {
input = JSON.parse(inputAsString);
} catch (e) {
// ignore parse errors
}
return input;
}
};
const resetPersistableInput = () => {
localStorage.removeItem(INPUT_KEY);
if (dashboard) {
const children = dashboard.getChildIds();
children.map((childId) => {
dashboard.removeEmbeddable(childId);
});
}
};
const addByValue = async () => {
if (!dashboard) return;
dashboard.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
'visualization',
{
title: 'Sample Markdown Vis',
savedVis: {
type: 'markdown',
title: '',
data: { aggs: [], searchSource: {} },
params: {
fontSize: 12,
openLinksInNewTab: false,
markdown: '### By Value Visualization\nThis is a sample by value panel.',
},
},
}
);
};
const disableButtons = useMemo(() => {
return !dashboard || isSaving;
}, [dashboard, isSaving]);
return (
<>
<EuiTitle>
<h2>Edit and save example</h2>
</EuiTitle>
<EuiText>
<p>Customize the dashboard and persist the state to local storage.</p>
</EuiText>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true}>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiButton onClick={addByValue} isDisabled={disableButtons}>
Add visualization by value
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton onClick={() => dashboard?.addFromLibrary()} isDisabled={disableButtons}>
Add visualization from library
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiButton fill onClick={onSave} isLoading={isSaving} isDisabled={disableButtons}>
Save to local storage
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
onClick={resetPersistableInput}
isLoading={isSaving}
isDisabled={disableButtons}
>
Empty dashboard and reset local storage
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<DashboardRenderer
getCreationOptions={async () => {
const persistedInput = getPersistableInput();
return {
getInitialInput: () => ({
...persistedInput,
timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis
}),
};
}}
ref={setdashboard}
/>
</EuiPanel>
</>
);
};

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { AppMountParameters, CoreSetup, Plugin } from '@kbn/core/public';
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
@ -34,9 +34,9 @@ export class PortableDashboardsExamplePlugin implements Plugin<void, void, Setup
title: 'Portable dashboard examples',
visibleIn: [],
async mount(params: AppMountParameters) {
const [, depsStart] = await core.getStartServices();
const [coreStart, depsStart] = await core.getStartServices();
const { renderApp } = await import('./app');
return renderApp(depsStart, params);
return renderApp(coreStart, depsStart, params);
},
});
@ -53,7 +53,12 @@ export class PortableDashboardsExamplePlugin implements Plugin<void, void, Setup
});
}
public async start() {}
public async start(core: CoreStart, deps: StartDeps) {
deps.dashboard.registerDashboardPanelPlacementSetting(FILTER_DEBUGGER_EMBEDDABLE_ID, () => ({
width: 48,
height: 12,
}));
}
public stop() {}
}

View file

@ -19,11 +19,11 @@
"@kbn/navigation-plugin",
"@kbn/embeddable-plugin",
"@kbn/data-views-plugin",
"@kbn/visualizations-plugin",
"@kbn/developer-examples-plugin",
"@kbn/shared-ux-page-kibana-template",
"@kbn/controls-plugin",
"@kbn/shared-ux-router",
"@kbn/presentation-publishing"
"@kbn/presentation-publishing",
"@kbn/react-kibana-context-render"
]
}

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
CanExpandPanels,
PresentationContainer,
TracksOverlays,
} from '@kbn/presentation-containers';
import {
HasAppContext,
HasType,
PublishesDataViews,
PublishesPanelTitle,
PublishesSavedObjectId,
PublishesUnifiedSearch,
PublishesViewMode,
PublishingSubject,
ViewMode,
} from '@kbn/presentation-publishing';
import { ControlGroupApi } from '@kbn/controls-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { DashboardPanelMap } from '../../common';
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
export type DashboardApi = CanExpandPanels &
HasAppContext &
HasType<'dashboard'> &
PresentationContainer &
PublishesDataViews &
Pick<PublishesPanelTitle, 'panelTitle'> &
PublishesSavedObjectId &
PublishesUnifiedSearch &
PublishesViewMode &
TracksOverlays & {
addFromLibrary: () => void;
asyncResetToLastSavedState: () => Promise<void>;
controlGroupApi$: PublishingSubject<ControlGroupApi | undefined>;
fullScreenMode$: PublishingSubject<boolean | undefined>;
focusedPanelId$: PublishingSubject<string | undefined>;
forceRefresh: () => void;
getPanelsState: () => DashboardPanelMap;
hasOverlays$: PublishingSubject<boolean | undefined>;
hasRunMigrations$: PublishingSubject<boolean | undefined>;
hasUnsavedChanges$: PublishingSubject<boolean | undefined>;
managed$: PublishingSubject<boolean | undefined>;
runInteractiveSave: (interactionMode: ViewMode) => Promise<SaveDashboardReturn | undefined>;
runQuickSave: () => Promise<void>;
scrollToTop: () => void;
setFilters: (filters?: Filter[] | undefined) => void;
setFullScreenMode: (fullScreenMode: boolean) => void;
setQuery: (query?: Query | undefined) => void;
setTags: (tags: string[]) => void;
setTimeRange: (timeRange?: TimeRange | undefined) => void;
setViewMode: (viewMode: ViewMode) => void;
openSettingsFlyout: () => void;
};

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { createContext, useContext } from 'react';
import { DashboardApi } from './types';
export const DashboardContext = createContext<DashboardApi | undefined>(undefined);
export const useDashboardApi = (): DashboardApi => {
const api = useContext<DashboardApi | undefined>(DashboardContext);
if (!api) {
throw new Error('useDashboardApi must be used inside DashboardContext');
}
return api;
};

View file

@ -8,7 +8,7 @@
*/
import { i18n } from '@kbn/i18n';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { ViewMode } from '@kbn/presentation-publishing';
export const getDashboardPageTitle = () =>
i18n.translate('dashboard.dashboardPageTitle', {
@ -42,9 +42,13 @@ export const dashboardManagedBadge = {
* @param viewMode {DashboardViewMode} the current mode. If in editing state, prepends 'Editing ' to the title.
* @returns {string} A title to display to the user based on the above parameters.
*/
export function getDashboardTitle(title: string, viewMode: ViewMode, isNew: boolean): string {
const isEditMode = viewMode === ViewMode.EDIT;
const dashboardTitle = isNew ? getNewDashboardTitle() : title;
export function getDashboardTitle(
title: string | undefined,
viewMode: ViewMode,
isNew: boolean
): string {
const isEditMode = viewMode === 'edit';
const dashboardTitle = isNew || !Boolean(title) ? getNewDashboardTitle() : (title as string);
return isEditMode
? i18n.translate('dashboard.strings.dashboardEditTitle', {
defaultMessage: 'Editing {title}',

View file

@ -7,43 +7,46 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { v4 as uuidv4 } from 'uuid';
import { History } from 'history';
import useMount from 'react-use/lib/useMount';
import useObservable from 'react-use/lib/useObservable';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { debounceTime } from 'rxjs';
import {
DashboardAppNoDataPage,
isDashboardAppInNoDataState,
} from './no_data/dashboard_app_no_data';
import {
loadAndRemoveDashboardState,
startSyncingDashboardUrlState,
} from './url/sync_dashboard_url_state';
import { loadAndRemoveDashboardState } from './url/url_utils';
import {
getSessionURLObservable,
getSearchSessionIdFromURL,
removeSearchSessionIdFromURL,
createSessionRestorationDataProvider,
} from './url/search_sessions_integration';
import { DashboardAPI, DashboardRenderer } from '..';
import { DashboardApi, DashboardRenderer } from '..';
import { type DashboardEmbedSettings } from './types';
import { pluginServices } from '../services/plugin_services';
import { AwaitingDashboardAPI } from '../dashboard_container';
import { DashboardRedirect } from '../dashboard_container/types';
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
import { createDashboardEditUrl, DASHBOARD_APP_ID } from '../dashboard_constants';
import {
createDashboardEditUrl,
DASHBOARD_APP_ID,
DASHBOARD_STATE_STORAGE_KEY,
} from '../dashboard_constants';
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
import { DashboardTopNav } from '../dashboard_top_nav';
import { DashboardTabTitleSetter } from './tab_title_setter/dashboard_tab_title_setter';
import { useObservabilityAIAssistantContext } from './hooks/use_observability_ai_assistant_context';
import { SharedDashboardState } from '../../common';
export interface DashboardAppProps {
history: History;
@ -52,16 +55,6 @@ export interface DashboardAppProps {
embedSettings?: DashboardEmbedSettings;
}
export const DashboardAPIContext = createContext<AwaitingDashboardAPI>(null);
export const useDashboardAPI = (): DashboardAPI => {
const api = useContext<AwaitingDashboardAPI>(DashboardAPIContext);
if (api == null) {
throw new Error('useDashboardAPI must be used inside DashboardAPIContext');
}
return api!;
};
export function DashboardApp({
savedDashboardId,
embedSettings,
@ -69,11 +62,12 @@ export function DashboardApp({
history,
}: DashboardAppProps) {
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
const [regenerateId, setRegenerateId] = useState(uuidv4());
useMount(() => {
(async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
});
const [dashboardAPI, setDashboardAPI] = useState<AwaitingDashboardAPI>(null);
const [dashboardApi, setDashboardApi] = useState<DashboardApi | undefined>(undefined);
/**
* Unpack & set up dashboard services
@ -94,7 +88,7 @@ export function DashboardApp({
useObservabilityAIAssistantContext({
observabilityAIAssistant: observabilityAIAssistant.start,
dashboardAPI,
dashboardApi,
search,
dataViews,
});
@ -193,45 +187,46 @@ export function DashboardApp({
* When the dashboard container is created, or re-created, start syncing dashboard state with the URL
*/
useEffect(() => {
if (!dashboardAPI) return;
const { stopWatchingAppStateInUrl } = startSyncingDashboardUrlState({
kbnUrlStateStorage,
dashboardAPI,
});
return () => stopWatchingAppStateInUrl();
}, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]);
if (!dashboardApi) return;
const appStateSubscription = kbnUrlStateStorage
.change$(DASHBOARD_STATE_STORAGE_KEY)
.pipe(debounceTime(10)) // debounce URL updates so react has time to unsubscribe when changing URLs
.subscribe(() => {
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
DASHBOARD_STATE_STORAGE_KEY
);
if (rawAppStateInUrl) setRegenerateId(uuidv4());
});
return () => appStateSubscription.unsubscribe();
}, [dashboardApi, kbnUrlStateStorage, savedDashboardId]);
const locator = useMemo(() => url?.locators.get(DASHBOARD_APP_LOCATOR), [url]);
return (
return showNoDataPage ? (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
) : (
<>
{showNoDataPage && (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
)}
{!showNoDataPage && (
{dashboardApi && (
<>
{dashboardAPI && (
<>
<DashboardTabTitleSetter dashboardContainer={dashboardAPI} />
<DashboardTopNav
redirectTo={redirectTo}
embedSettings={embedSettings}
dashboardContainer={dashboardAPI}
/>
</>
)}
{getLegacyConflictWarning?.()}
<DashboardRenderer
locator={locator}
ref={setDashboardAPI}
dashboardRedirect={redirectTo}
savedObjectId={savedDashboardId}
showPlainSpinner={showPlainSpinner}
getCreationOptions={getCreationOptions}
<DashboardTabTitleSetter dashboardApi={dashboardApi} />
<DashboardTopNav
redirectTo={redirectTo}
embedSettings={embedSettings}
dashboardApi={dashboardApi}
/>
</>
)}
{getLegacyConflictWarning?.()}
<DashboardRenderer
key={regenerateId}
locator={locator}
onApiAvailable={setDashboardApi}
dashboardRedirect={redirectTo}
savedObjectId={savedDashboardId}
showPlainSpinner={showPlainSpinner}
getCreationOptions={getCreationOptions}
/>
</>
);
}

View file

@ -9,7 +9,6 @@
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import { useEffect } from 'react';
import type { Embeddable } from '@kbn/embeddable-plugin/public';
import { getESQLQueryColumns } from '@kbn/esql-utils';
import type { ISearchStart } from '@kbn/data-plugin/public';
import {
@ -29,7 +28,7 @@ import {
} from '@kbn/lens-embeddable-utils/config_builder';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { LensEmbeddableInput } from '@kbn/lens-plugin/public';
import type { AwaitingDashboardAPI } from '../../dashboard_container';
import { DashboardApi } from '../../dashboard_api/types';
const chartTypes = [
'xy',
@ -47,12 +46,12 @@ const chartTypes = [
export function useObservabilityAIAssistantContext({
observabilityAIAssistant,
dashboardAPI,
dashboardApi,
search,
dataViews,
}: {
observabilityAIAssistant: ObservabilityAIAssistantPublicStart | undefined;
dashboardAPI: AwaitingDashboardAPI;
dashboardApi: DashboardApi | undefined;
search: ISearchStart;
dataViews: DataViewsPublicPluginStart;
}) {
@ -69,7 +68,7 @@ export function useObservabilityAIAssistantContext({
return setScreenContext({
screenDescription:
'The user is looking at the dashboard app. Here they can add visualizations to a dashboard and save them',
actions: dashboardAPI
actions: dashboardApi
? [
createScreenContextAction(
{
@ -361,8 +360,8 @@ export function useObservabilityAIAssistantContext({
query: dataset,
})) as LensEmbeddableInput;
return dashboardAPI
.addNewPanel<Embeddable>({
return dashboardApi
.addNewPanel({
panelType: 'lens',
initialState: embeddableInput,
})
@ -383,5 +382,5 @@ export function useObservabilityAIAssistantContext({
]
: [],
});
}, [observabilityAIAssistant, dashboardAPI, search, dataViews]);
}, [observabilityAIAssistant, dashboardApi, search, dataViews]);
}

View file

@ -9,29 +9,25 @@
import { useEffect } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/common';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { pluginServices } from '../../services/plugin_services';
import { DashboardAPI } from '../..';
import { getDashboardTitle } from '../_dashboard_app_strings';
import { DashboardApi } from '../..';
import { getNewDashboardTitle } from '../_dashboard_app_strings';
export const DashboardTabTitleSetter = ({
dashboardContainer,
}: {
dashboardContainer: DashboardAPI;
}) => {
export const DashboardTabTitleSetter = ({ dashboardApi }: { dashboardApi: DashboardApi }) => {
const {
chrome: { docTitle: chromeDocTitle },
} = pluginServices.getServices();
const title = dashboardContainer.select((state) => state.explicitInput.title);
const lastSavedId = dashboardContainer.select((state) => state.componentState.lastSavedId);
const [title, lastSavedId] = useBatchedPublishingSubjects(
dashboardApi.panelTitle,
dashboardApi.savedObjectId
);
/**
* Set chrome tab title when dashboard's title changes
*/
useEffect(() => {
/** We do not want the tab title to include the "Editing" prefix, so always send in view mode */
chromeDocTitle.change(getDashboardTitle(title, ViewMode.VIEW, !lastSavedId));
chromeDocTitle.change(!lastSavedId ? getNewDashboardTitle() : title ?? lastSavedId);
}, [title, chromeDocTitle, lastSavedId]);
return null;

View file

@ -11,7 +11,7 @@ import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { ControlGroupApi } from '@kbn/controls-plugin/public';
import { getAddControlButtonTitle } from '../../_dashboard_app_strings';
import { useDashboardAPI } from '../../dashboard_app';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
interface Props {
closePopover: () => void;
@ -19,9 +19,9 @@ interface Props {
}
export const AddDataControlButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
const dashboard = useDashboardAPI();
const dashboardApi = useDashboardApi();
const onSave = () => {
dashboard.scrollToTop();
dashboardApi.scrollToTop();
};
return (

View file

@ -18,7 +18,7 @@ import {
getAddTimeSliderControlButtonTitle,
getOnlyOneTimeSliderControlMsg,
} from '../../_dashboard_app_strings';
import { useDashboardAPI } from '../../dashboard_app';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
interface Props {
closePopover: () => void;
@ -27,7 +27,7 @@ interface Props {
export const AddTimeSliderControlButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
const [hasTimeSliderControl, setHasTimeSliderControl] = useState(false);
const dashboard = useDashboardAPI();
const dashboardApi = useDashboardApi();
useEffect(() => {
if (!controlGroupApi) {
@ -58,7 +58,7 @@ export const AddTimeSliderControlButton = ({ closePopover, controlGroupApi, ...r
id: uuidv4(),
},
});
dashboard.scrollToTop();
dashboardApi.scrollToTop();
closePopover();
}}
data-test-subj="controls-create-timeslider-button"

View file

@ -18,10 +18,10 @@ import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
import { EditorMenu } from './editor_menu';
import { useDashboardAPI } from '../dashboard_app';
import { pluginServices } from '../../services/plugin_services';
import { ControlsToolbarButton } from './controls_toolbar_button';
import { DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
const {
@ -32,7 +32,7 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
} = pluginServices.getServices();
const { euiTheme } = useEuiTheme();
const dashboard = useDashboardAPI();
const dashboardApi = useDashboardApi();
const stateTransferService = getStateTransfer();
@ -70,13 +70,13 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
stateTransferService.navigateToEditor(appId, {
path,
state: {
originatingApp: dashboard.getAppContext()?.currentAppId,
originatingPath: dashboard.getAppContext()?.getCurrentPath?.(),
originatingApp: dashboardApi.getAppContext()?.currentAppId,
originatingPath: dashboardApi.getAppContext()?.getCurrentPath?.(),
searchSessionId: search.session.getSessionId(),
},
});
},
[stateTransferService, dashboard, search.session, trackUiMetric]
[stateTransferService, dashboardApi, search.session, trackUiMetric]
);
/**
@ -85,11 +85,11 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
* dismissNotification: Optional, if not passed a toast will appear in the dashboard
*/
const controlGroupApi = useStateFromPublishingSubject(dashboard.controlGroupApi$);
const controlGroupApi = useStateFromPublishingSubject(dashboardApi.controlGroupApi$);
const extraButtons = [
<EditorMenu createNewVisType={createNewVisType} isDisabled={isDisabled} api={dashboard} />,
<EditorMenu createNewVisType={createNewVisType} isDisabled={isDisabled} />,
<AddFromLibraryButton
onClick={() => dashboard.addFromLibrary()}
onClick={() => dashboardApi.addFromLibrary()}
size="s"
data-test-subj="dashboardAddFromLibraryButton"
isDisabled={isDisabled}

View file

@ -7,14 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { ComponentProps } from 'react';
import React from 'react';
import { render } from '@testing-library/react';
import { PresentationContainer } from '@kbn/presentation-containers';
import { EditorMenu } from './editor_menu';
import { DashboardAPIContext } from '../dashboard_app';
import { buildMockDashboard } from '../../mocks';
import { pluginServices } from '../../services/plugin_services';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../dashboard_api/types';
jest.mock('../../services/plugin_services', () => {
const module = jest.requireActual('../../services/plugin_services');
@ -37,21 +37,14 @@ jest.mock('../../services/plugin_services', () => {
};
});
const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked<PresentationContainer>;
describe('editor menu', () => {
const defaultProps: ComponentProps<typeof EditorMenu> = {
api: mockApi,
createNewVisType: jest.fn(),
};
it('renders without crashing', async () => {
render(<EditorMenu {...defaultProps} />, {
render(<EditorMenu createNewVisType={jest.fn()} />, {
wrapper: ({ children }) => {
return (
<DashboardAPIContext.Provider value={buildMockDashboard()}>
<DashboardContext.Provider value={buildMockDashboard() as DashboardApi}>
{children}
</DashboardAPIContext.Provider>
</DashboardContext.Provider>
);
},
});

View file

@ -17,15 +17,15 @@ import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
import { useGetDashboardPanels, DashboardPanelSelectionListFlyout } from './add_new_panel';
import { pluginServices } from '../../services/plugin_services';
import { useDashboardAPI } from '../dashboard_app';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
interface EditorMenuProps
extends Pick<Parameters<typeof useGetDashboardPanels>[0], 'api' | 'createNewVisType'> {
extends Pick<Parameters<typeof useGetDashboardPanels>[0], 'createNewVisType'> {
isDisabled?: boolean;
}
export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProps) => {
const dashboardAPI = useDashboardAPI();
export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) => {
const dashboardApi = useDashboardApi();
const {
overlays,
@ -34,16 +34,16 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
} = pluginServices.getServices();
const fetchDashboardPanels = useGetDashboardPanels({
api,
api: dashboardApi,
createNewVisType,
});
useEffect(() => {
// ensure opened dashboard is closed if a navigation event happens;
return () => {
dashboardAPI.clearOverlays();
dashboardApi.clearOverlays();
};
}, [dashboardAPI]);
}, [dashboardApi]);
const openDashboardPanelSelectionFlyout = useCallback(
function openDashboardPanelSelectionFlyout() {
@ -55,10 +55,10 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
React.createElement(function () {
return (
<DashboardPanelSelectionListFlyout
close={dashboardAPI.clearOverlays}
close={dashboardApi.clearOverlays}
{...{
paddingSize: flyoutPanelPaddingSize,
fetchDashboardPanels: fetchDashboardPanels.bind(null, dashboardAPI.clearOverlays),
fetchDashboardPanels: fetchDashboardPanels.bind(null, dashboardApi.clearOverlays),
}}
/>
);
@ -66,7 +66,7 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
{ analytics, theme, i18n: i18nStart }
);
dashboardAPI.openOverlay(
dashboardApi.openOverlay(
overlays.openFlyout(mount, {
size: 'm',
maxWidth: 500,
@ -74,13 +74,13 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
'aria-labelledby': 'addPanelsFlyout',
'data-test-subj': 'dashboardPanelSelectionFlyout',
onClose(overlayRef) {
dashboardAPI.clearOverlays();
dashboardApi.clearOverlays();
overlayRef.close();
},
})
);
},
[analytics, theme, i18nStart, dashboardAPI, overlays, fetchDashboardPanels]
[analytics, theme, i18nStart, dashboardApi, overlays, fetchDashboardPanels]
);
return (

View file

@ -77,7 +77,7 @@ describe('ShowShareModal', () => {
return {
isDirty: true,
anchorElement: document.createElement('div'),
getDashboardState: () => ({} as DashboardContainerInput),
getPanelsState: () => ({}),
};
};
@ -125,19 +125,17 @@ describe('ShowShareModal', () => {
query: { query: 'bye', language: 'kuery' },
} as unknown as DashboardContainerInput;
const showModalProps = getPropsAndShare(unsavedDashboardState);
showModalProps.getDashboardState = () => {
showModalProps.getPanelsState = () => {
return {
panels: {
panel_1: {
type: 'panel_type',
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
panelRefName: 'superPanel',
explicitInput: {
id: 'superPanel',
},
panel_1: {
type: 'panel_type',
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
panelRefName: 'superPanel',
explicitInput: {
id: 'superPanel',
},
},
} as unknown as DashboardContainerInput;
};
};
ShowShareModal(showModalProps);
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
@ -171,36 +169,6 @@ describe('ShowShareModal', () => {
},
};
const props = getPropsAndShare(unsavedDashboardState);
const getCurrentState: () => DashboardContainerInput = () => {
return {
panels: {
panel_1: {
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
type: 'superType',
explicitInput: {
id: 'whatever',
changedKey1: 'NOT changed',
},
},
panel_2: {
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
type: 'superType',
explicitInput: {
id: 'whatever2',
changedKey2: 'definitely NOT changed',
},
},
panel_3: {
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
type: 'superType',
explicitInput: {
id: 'whatever2',
changedKey3: 'should still exist',
},
},
},
} as unknown as DashboardContainerInput;
};
pluginServices.getServices().dashboardBackup.getState = jest.fn().mockReturnValue({
dashboardState: unsavedDashboardState,
panels: {
@ -208,7 +176,32 @@ describe('ShowShareModal', () => {
panel_2: { changedKey2: 'definitely changed' },
},
});
props.getDashboardState = getCurrentState;
props.getPanelsState = () => ({
panel_1: {
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
type: 'superType',
explicitInput: {
id: 'whatever',
changedKey1: 'NOT changed',
},
},
panel_2: {
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
type: 'superType',
explicitInput: {
id: 'whatever2',
changedKey2: 'definitely NOT changed',
},
},
panel_3: {
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
type: 'superType',
explicitInput: {
id: 'whatever2',
changedKey3: 'should still exist',
},
},
});
ShowShareModal(props);
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
const shareLocatorParams = (

View file

@ -17,11 +17,7 @@ import { getStateFromKbnUrl, setStateToKbnUrl, unhashUrl } from '@kbn/kibana-uti
import { omit } from 'lodash';
import moment from 'moment';
import React, { ReactElement, useState } from 'react';
import {
convertPanelMapToSavedPanels,
DashboardContainerInput,
DashboardPanelMap,
} from '../../../../common';
import { convertPanelMapToSavedPanels, DashboardPanelMap } from '../../../../common';
import { DashboardLocatorParams } from '../../../dashboard_container';
import { pluginServices } from '../../../services/plugin_services';
import { dashboardUrlParams } from '../../dashboard_router';
@ -35,7 +31,7 @@ export interface ShowShareModalProps {
savedObjectId?: string;
dashboardTitle?: string;
anchorElement: HTMLElement;
getDashboardState: () => DashboardContainerInput;
getPanelsState: () => DashboardPanelMap;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
@ -51,7 +47,7 @@ export function ShowShareModal({
anchorElement,
savedObjectId,
dashboardTitle,
getDashboardState,
getPanelsState,
}: ShowShareModalProps) {
const {
dashboardCapabilities: { createShortUrl: allowShortUrl },
@ -140,7 +136,7 @@ export function ShowShareModal({
return;
}
const latestPanels = getDashboardState().panels;
const latestPanels = getPanelsState();
// apply modifications to panels.
const modifiedPanels = panelModifications
? Object.entries(panelModifications).reduce((acc, [panelId, unsavedPanel]) => {

View file

@ -14,14 +14,15 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import { TopNavMenuData } from '@kbn/navigation-plugin/public';
import useMountedState from 'react-use/lib/useMountedState';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { UI_SETTINGS } from '../../../common';
import { useDashboardAPI } from '../dashboard_app';
import { topNavStrings } from '../_dashboard_app_strings';
import { ShowShareModal } from './share/show_share_modal';
import { pluginServices } from '../../services/plugin_services';
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
export const useDashboardMenuItems = ({
isLabsShown,
@ -52,17 +53,25 @@ export const useDashboardMenuItems = ({
/**
* Unpack dashboard state from redux
*/
const dashboard = useDashboardAPI();
const dashboardApi = useDashboardApi();
const hasRunMigrations = dashboard.select(
(state) => state.componentState.hasRunClientsideMigrations
const [
dashboardTitle,
hasOverlays,
hasRunMigrations,
hasUnsavedChanges,
lastSavedId,
managed,
viewMode,
] = useBatchedPublishingSubjects(
dashboardApi.panelTitle,
dashboardApi.hasOverlays$,
dashboardApi.hasRunMigrations$,
dashboardApi.hasUnsavedChanges$,
dashboardApi.savedObjectId,
dashboardApi.managed$,
dashboardApi.viewMode
);
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
const hasOverlays = dashboard.select((state) => state.componentState.hasOverlays);
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const managed = dashboard.select((state) => state.componentState.managed);
const disableTopNav = isSaveInProgress || hasOverlays;
/**
@ -75,10 +84,10 @@ export const useDashboardMenuItems = ({
anchorElement,
savedObjectId: lastSavedId,
isDirty: Boolean(hasUnsavedChanges),
getDashboardState: () => dashboard.getState().explicitInput,
getPanelsState: dashboardApi.getPanelsState,
});
},
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboard]
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboardApi]
);
/**
@ -86,17 +95,17 @@ export const useDashboardMenuItems = ({
*/
const quickSaveDashboard = useCallback(() => {
setIsSaveInProgress(true);
dashboard
dashboardApi
.runQuickSave()
.then(() => setTimeout(() => setIsSaveInProgress(false), CHANGE_CHECK_DEBOUNCE));
}, [dashboard]);
}, [dashboardApi]);
/**
* initiate interactive dashboard copy action
*/
const dashboardInteractiveSave = useCallback(() => {
dashboard.runInteractiveSave(viewMode).then((result) => maybeRedirect(result));
}, [maybeRedirect, dashboard, viewMode]);
dashboardApi.runInteractiveSave(viewMode).then((result) => maybeRedirect(result));
}, [maybeRedirect, dashboardApi, viewMode]);
/**
* Show the dashboard's "Confirm reset changes" modal. If confirmed:
@ -106,10 +115,10 @@ export const useDashboardMenuItems = ({
const [isResetting, setIsResetting] = useState(false);
const resetChanges = useCallback(
(switchToViewMode: boolean = false) => {
dashboard.clearOverlays();
dashboardApi.clearOverlays();
const switchModes = switchToViewMode
? () => {
dashboard.dispatch.setViewMode(ViewMode.VIEW);
dashboardApi.setViewMode(ViewMode.VIEW);
dashboardBackup.storeViewMode(ViewMode.VIEW);
}
: undefined;
@ -120,15 +129,15 @@ export const useDashboardMenuItems = ({
confirmDiscardUnsavedChanges(() => {
batch(async () => {
setIsResetting(true);
await dashboard.asyncResetToLastSavedState();
await dashboardApi.asyncResetToLastSavedState();
if (isMounted()) {
setIsResetting(false);
switchModes?.();
}
});
}, viewMode);
}, viewMode as ViewMode);
},
[dashboard, dashboardBackup, hasUnsavedChanges, viewMode, isMounted]
[dashboardApi, dashboardBackup, hasUnsavedChanges, viewMode, isMounted]
);
/**
@ -141,7 +150,7 @@ export const useDashboardMenuItems = ({
...topNavStrings.fullScreen,
id: 'full-screen',
testId: 'dashboardFullScreenMode',
run: () => dashboard.dispatch.setFullScreenMode(true),
run: () => dashboardApi.setFullScreenMode(true),
disableButton: disableTopNav,
} as TopNavMenuData,
@ -161,8 +170,8 @@ export const useDashboardMenuItems = ({
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
run: () => {
dashboardBackup.storeViewMode(ViewMode.EDIT);
dashboard.dispatch.setViewMode(ViewMode.EDIT);
dashboard.clearOverlays();
dashboardApi.setViewMode(ViewMode.EDIT);
dashboardApi.clearOverlays();
},
disableButton: disableTopNav,
} as TopNavMenuData,
@ -218,7 +227,7 @@ export const useDashboardMenuItems = ({
id: 'settings',
testId: 'dashboardSettingsButton',
disableButton: disableTopNav,
run: () => dashboard.showSettings(),
run: () => dashboardApi.openSettingsFlyout(),
},
};
}, [
@ -230,7 +239,7 @@ export const useDashboardMenuItems = ({
dashboardInteractiveSave,
viewMode,
showShare,
dashboard,
dashboardApi,
setIsLabsShown,
isLabsShown,
dashboardBackup,

View file

@ -8,7 +8,6 @@
*/
import _ from 'lodash';
import { debounceTime } from 'rxjs';
import semverSatisfies from 'semver/functions/satisfies';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
@ -20,7 +19,6 @@ import {
convertSavedPanelsToPanelMap,
DashboardContainerInput,
} from '../../../common';
import { DashboardAPI } from '../../dashboard_container';
import { pluginServices } from '../../services/plugin_services';
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
@ -86,23 +84,3 @@ export const loadAndRemoveDashboardState = (
return partialState;
};
export const startSyncingDashboardUrlState = ({
kbnUrlStateStorage,
dashboardAPI,
}: {
kbnUrlStateStorage: IKbnUrlStateStorage;
dashboardAPI: DashboardAPI;
}) => {
const appStateSubscription = kbnUrlStateStorage
.change$(DASHBOARD_STATE_STORAGE_KEY)
.pipe(debounceTime(10)) // debounce URL updates so react has time to unsubscribe when changing URLs
.subscribe(() => {
const stateFromUrl = loadAndRemoveDashboardState(kbnUrlStateStorage);
if (Object.keys(stateFromUrl).length === 0) return;
dashboardAPI.updateInput(stateFromUrl);
});
const stopWatchingAppStateInUrl = () => appStateSubscription.unsubscribe();
return { stopWatchingAppStateInUrl };
};

View file

@ -301,23 +301,42 @@ export class DashboardContainer
this.select = reduxTools.select;
this.savedObjectId = new BehaviorSubject(this.getDashboardSavedObjectId());
this.expandedPanelId = new BehaviorSubject(this.getExpandedPanelId());
this.focusedPanelId$ = new BehaviorSubject(this.getState().componentState.focusedPanelId);
this.managed$ = new BehaviorSubject(this.getState().componentState.managed);
this.fullScreenMode$ = new BehaviorSubject(this.getState().componentState.fullScreenMode);
this.hasRunMigrations$ = new BehaviorSubject(
this.getState().componentState.hasRunClientsideMigrations
);
this.hasUnsavedChanges$ = new BehaviorSubject(this.getState().componentState.hasUnsavedChanges);
this.hasOverlays$ = new BehaviorSubject(this.getState().componentState.hasOverlays);
this.publishingSubscription.add(
this.onStateChange(() => {
if (this.savedObjectId.value === this.getDashboardSavedObjectId()) return;
this.savedObjectId.next(this.getDashboardSavedObjectId());
})
);
this.publishingSubscription.add(
this.savedObjectId.subscribe(() => {
this.hadContentfulRender = false;
})
);
this.expandedPanelId = new BehaviorSubject(this.getDashboardSavedObjectId());
this.publishingSubscription.add(
this.onStateChange(() => {
if (this.expandedPanelId.value === this.getExpandedPanelId()) return;
this.expandedPanelId.next(this.getExpandedPanelId());
const state = this.getState();
if (this.savedObjectId.value !== this.getDashboardSavedObjectId()) {
this.savedObjectId.next(this.getDashboardSavedObjectId());
}
if (this.expandedPanelId.value !== this.getExpandedPanelId()) {
this.expandedPanelId.next(this.getExpandedPanelId());
}
if (this.focusedPanelId$.value !== state.componentState.focusedPanelId) {
this.focusedPanelId$.next(state.componentState.focusedPanelId);
}
if (this.managed$.value !== state.componentState.managed) {
this.managed$.next(state.componentState.managed);
}
if (this.fullScreenMode$.value !== state.componentState.fullScreenMode) {
this.fullScreenMode$.next(state.componentState.fullScreenMode);
}
if (this.hasRunMigrations$.value !== state.componentState.hasRunClientsideMigrations) {
this.hasRunMigrations$.next(state.componentState.hasRunClientsideMigrations);
}
if (this.hasUnsavedChanges$.value !== state.componentState.hasUnsavedChanges) {
this.hasUnsavedChanges$.next(state.componentState.hasUnsavedChanges);
}
if (this.hasOverlays$.value !== state.componentState.hasOverlays) {
this.hasOverlays$.next(state.componentState.hasOverlays);
}
})
);
@ -519,7 +538,7 @@ export class DashboardContainer
public runInteractiveSave = runInteractiveSave;
public runQuickSave = runQuickSave;
public showSettings = showSettings;
public openSettingsFlyout = showSettings;
public addFromLibrary = addFromLibrary;
public duplicatePanel(id: string) {
@ -533,6 +552,12 @@ export class DashboardContainer
public savedObjectId: BehaviorSubject<string | undefined>;
public expandedPanelId: BehaviorSubject<string | undefined>;
public focusedPanelId$: BehaviorSubject<string | undefined>;
public managed$: BehaviorSubject<boolean | undefined>;
public fullScreenMode$: BehaviorSubject<boolean | undefined>;
public hasRunMigrations$: BehaviorSubject<boolean | undefined>;
public hasUnsavedChanges$: BehaviorSubject<boolean | undefined>;
public hasOverlays$: BehaviorSubject<boolean | undefined>;
public async replacePanel(idToRemove: string, { panelType, initialState }: PanelPackage) {
const newId = await this.replaceEmbeddable(
@ -795,10 +820,30 @@ export class DashboardContainer
return this.getState().componentState.expandedPanelId;
};
public getPanelsState = () => {
return this.getState().explicitInput.panels;
};
public setExpandedPanelId = (newId?: string) => {
this.dispatch.setExpandedPanelId(newId);
};
public setViewMode = (viewMode: ViewMode) => {
this.dispatch.setViewMode(viewMode);
};
public setFullScreenMode = (fullScreenMode: boolean) => {
this.dispatch.setFullScreenMode(fullScreenMode);
};
public setQuery = (query?: Query | undefined) => this.updateInput({ query });
public setFilters = (filters?: Filter[] | undefined) => this.updateInput({ filters });
public setTags = (tags: string[]) => {
this.updateInput({ tags });
};
public openOverlay = (ref: OverlayRef, options?: { focusedPanelId?: string }) => {
this.clearOverlays();
this.dispatch.setHasOverlays(true);

View file

@ -9,23 +9,10 @@
import type { DataView } from '@kbn/data-views-plugin/public';
import { CanDuplicatePanels, CanExpandPanels, TracksOverlays } from '@kbn/presentation-containers';
import {
HasType,
HasTypeDisplayName,
PublishesUnifiedSearch,
PublishesPanelTitle,
PublishesSavedObjectId,
} from '@kbn/presentation-publishing';
import { HasTypeDisplayName, PublishesSavedObjectId } from '@kbn/presentation-publishing';
import { DashboardPanelState } from '../../../common';
import { DashboardContainer } from '../embeddable/dashboard_container';
// TODO lock down DashboardAPI
export type DashboardAPI = DashboardContainer &
Partial<
HasType<'dashboard'> & PublishesUnifiedSearch & PublishesPanelTitle & PublishesSavedObjectId
>;
export type AwaitingDashboardAPI = DashboardAPI | null;
export const buildApiFromDashboardContainer = (container?: DashboardContainer) => container ?? null;
export type DashboardExternallyAccessibleApi = HasTypeDisplayName &

View file

@ -22,6 +22,7 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { DashboardContainer } from '../embeddable/dashboard_container';
import { DashboardCreationOptions } from '../embeddable/dashboard_container_factory';
import { setStubKibanaServices as setPresentationPanelMocks } from '@kbn/presentation-panel-plugin/public/mocks';
import { BehaviorSubject } from 'rxjs';
describe('dashboard renderer', () => {
let mockDashboardContainer: DashboardContainer;
@ -246,6 +247,7 @@ describe('dashboard renderer', () => {
navigateToDashboard: jest.fn(),
select: jest.fn().mockReturnValue('WhatAnExpandedPanel'),
getInput: jest.fn().mockResolvedValue({}),
expandedPanelId: new BehaviorSubject('panel1'),
} as unknown as DashboardContainer;
const mockSuccessFactory = {
create: jest.fn().mockReturnValue(mockSuccessEmbeddable),

View file

@ -10,15 +10,7 @@
import '../_dashboard_container.scss';
import classNames from 'classnames';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import useUnmount from 'react-use/lib/useUnmount';
import { v4 as uuidv4 } from 'uuid';
@ -27,6 +19,7 @@ import { ErrorEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/publi
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { DASHBOARD_CONTAINER_TYPE } from '..';
import { DashboardContainerInput } from '../../../common';
import type { DashboardContainer } from '../embeddable/dashboard_container';
@ -37,13 +30,11 @@ import {
} from '../embeddable/dashboard_container_factory';
import { DashboardLocatorParams, DashboardRedirect } from '../types';
import { Dashboard404Page } from './dashboard_404';
import {
AwaitingDashboardAPI,
buildApiFromDashboardContainer,
DashboardAPI,
} from './dashboard_api';
import { DashboardApi } from '../../dashboard_api/types';
import { pluginServices } from '../../services/plugin_services';
export interface DashboardRendererProps {
onApiAvailable?: (api: DashboardApi) => void;
savedObjectId?: string;
showPlainSpinner?: boolean;
dashboardRedirect?: DashboardRedirect;
@ -51,150 +42,136 @@ export interface DashboardRendererProps {
locator?: Pick<LocatorPublic<DashboardLocatorParams>, 'navigate' | 'getRedirectUrl'>;
}
export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRendererProps>(
({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner, locator }, ref) => {
const dashboardRoot = useRef(null);
const dashboardViewport = useRef(null);
const [loading, setLoading] = useState(true);
const [screenshotMode, setScreenshotMode] = useState(false);
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
const [fatalError, setFatalError] = useState<ErrorEmbeddable | undefined>();
const [dashboardMissing, setDashboardMissing] = useState(false);
export function DashboardRenderer({
savedObjectId,
getCreationOptions,
dashboardRedirect,
showPlainSpinner,
locator,
onApiAvailable,
}: DashboardRendererProps) {
const dashboardRoot = useRef(null);
const dashboardViewport = useRef(null);
const [loading, setLoading] = useState(true);
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
const [fatalError, setFatalError] = useState<ErrorEmbeddable | undefined>();
const [dashboardMissing, setDashboardMissing] = useState(false);
useImperativeHandle(
ref,
() => buildApiFromDashboardContainer(dashboardContainer) as DashboardAPI,
[dashboardContainer]
);
const { embeddable, screenshotMode } = pluginServices.getServices();
useEffect(() => {
(async () => {
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
const { pluginServices } = await import('../../services/plugin_services');
const {
screenshotMode: { isScreenshotMode },
} = pluginServices.getServices();
setScreenshotMode(isScreenshotMode());
})();
}, []);
const id = useMemo(() => uuidv4(), []);
const id = useMemo(() => uuidv4(), []);
useEffect(() => {
/* In case the locator prop changes, we need to reassign the value in the container */
if (dashboardContainer) dashboardContainer.locator = locator;
}, [dashboardContainer, locator]);
useEffect(() => {
/* In case the locator prop changes, we need to reassign the value in the container */
if (dashboardContainer) dashboardContainer.locator = locator;
}, [dashboardContainer, locator]);
useEffect(() => {
/**
* Here we attempt to build a dashboard or navigate to a new dashboard. Clear all error states
* if they exist in case this dashboard loads correctly.
*/
fatalError?.destroy();
setDashboardMissing(false);
setFatalError(undefined);
useEffect(() => {
/**
* Here we attempt to build a dashboard or navigate to a new dashboard. Clear all error states
* if they exist in case this dashboard loads correctly.
*/
fatalError?.destroy();
setDashboardMissing(false);
setFatalError(undefined);
if (dashboardContainer) {
// When a dashboard already exists, don't rebuild it, just set a new id.
dashboardContainer.navigateToDashboard(savedObjectId).catch((e) => {
dashboardContainer?.destroy();
setDashboardContainer(undefined);
setFatalError(new ErrorEmbeddable(e, { id }));
if (e instanceof SavedObjectNotFound) {
setDashboardMissing(true);
}
});
return;
}
if (dashboardContainer) {
// When a dashboard already exists, don't rebuild it, just set a new id.
dashboardContainer.navigateToDashboard(savedObjectId).catch((e) => {
dashboardContainer?.destroy();
setDashboardContainer(undefined);
setFatalError(new ErrorEmbeddable(e, { id }));
if (e instanceof SavedObjectNotFound) {
setDashboardMissing(true);
}
});
setLoading(true);
let canceled = false;
(async () => {
const creationOptions = await getCreationOptions?.();
const dashboardFactory = embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
) as DashboardContainerFactory & {
create: DashboardContainerFactoryDefinition['create'];
};
const container = await dashboardFactory?.create(
{ id } as unknown as DashboardContainerInput, // Input from creationOptions is used instead.
undefined,
creationOptions,
savedObjectId
);
setLoading(false);
if (canceled || !container) {
setDashboardContainer(undefined);
container?.destroy();
return;
}
setLoading(true);
let canceled = false;
(async () => {
const creationOptions = await getCreationOptions?.();
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
const { pluginServices } = await import('../../services/plugin_services');
const { embeddable } = pluginServices.getServices();
const dashboardFactory = embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
) as DashboardContainerFactory & {
create: DashboardContainerFactoryDefinition['create'];
};
const container = await dashboardFactory?.create(
{ id } as unknown as DashboardContainerInput, // Input from creationOptions is used instead.
undefined,
creationOptions,
savedObjectId
);
setLoading(false);
if (canceled || !container) {
setDashboardContainer(undefined);
container?.destroy();
return;
if (isErrorEmbeddable(container)) {
setFatalError(container);
if (container.error instanceof SavedObjectNotFound) {
setDashboardMissing(true);
}
return;
}
if (isErrorEmbeddable(container)) {
setFatalError(container);
if (container.error instanceof SavedObjectNotFound) {
setDashboardMissing(true);
}
return;
}
if (dashboardRoot.current) {
container.render(dashboardRoot.current);
}
if (dashboardRoot.current) {
container.render(dashboardRoot.current);
}
setDashboardContainer(container);
})();
return () => {
canceled = true;
};
// Disabling exhaustive deps because embeddable should only be created on first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [savedObjectId]);
useUnmount(() => {
fatalError?.destroy();
dashboardContainer?.destroy();
});
const viewportClasses = classNames(
'dashboardViewport',
{ 'dashboardViewport--screenshotMode': screenshotMode },
{ 'dashboardViewport--loading': loading }
);
const loadingSpinner = showPlainSpinner ? (
<EuiLoadingSpinner size="xxl" />
) : (
<EuiLoadingElastic size="xxl" />
);
const renderDashboardContents = () => {
if (dashboardMissing) return <Dashboard404Page dashboardRedirect={dashboardRedirect} />;
if (fatalError) return fatalError.render();
if (loading) return loadingSpinner;
return <div ref={dashboardRoot} />;
setDashboardContainer(container);
onApiAvailable?.(container as DashboardApi);
})();
return () => {
canceled = true;
};
// Disabling exhaustive deps because embeddable should only be created on first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [savedObjectId]);
return (
<div ref={dashboardViewport} className={viewportClasses}>
{dashboardViewport?.current &&
dashboardContainer &&
!isErrorEmbeddable(dashboardContainer) && (
<ParentClassController
viewportRef={dashboardViewport.current}
dashboard={dashboardContainer}
/>
)}
{renderDashboardContents()}
</div>
);
}
);
useUnmount(() => {
fatalError?.destroy();
dashboardContainer?.destroy();
});
const viewportClasses = classNames(
'dashboardViewport',
{ 'dashboardViewport--screenshotMode': screenshotMode },
{ 'dashboardViewport--loading': loading }
);
const loadingSpinner = showPlainSpinner ? (
<EuiLoadingSpinner size="xxl" />
) : (
<EuiLoadingElastic size="xxl" />
);
const renderDashboardContents = () => {
if (dashboardMissing) return <Dashboard404Page dashboardRedirect={dashboardRedirect} />;
if (fatalError) return fatalError.render();
if (loading) return loadingSpinner;
return <div ref={dashboardRoot} />;
};
return (
<div ref={dashboardViewport} className={viewportClasses}>
{dashboardViewport?.current &&
dashboardContainer &&
!isErrorEmbeddable(dashboardContainer) && (
<ParentClassController
viewportRef={dashboardViewport.current}
dashboardApi={dashboardContainer as DashboardApi}
/>
)}
{renderDashboardContents()}
</div>
);
}
/**
* Maximizing a panel in Dashboard only works if the parent div has a certain class. This
@ -202,13 +179,13 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
* the class to whichever element renders the Dashboard.
*/
const ParentClassController = ({
dashboard,
dashboardApi,
viewportRef,
}: {
dashboard: DashboardContainer;
dashboardApi: DashboardApi;
viewportRef: HTMLDivElement;
}) => {
const maximizedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
const maximizedPanelId = useStateFromPublishingSubject(dashboardApi.expandedPanelId);
useLayoutEffect(() => {
const parentDiv = viewportRef.parentElement;

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { dynamic } from '@kbn/shared-ux-utility';
import type { DashboardRendererProps } from './dashboard_renderer';
const Component = dynamic(async () => {
const { DashboardRenderer } = await import('./dashboard_renderer');
return {
default: DashboardRenderer,
};
});
export function LazyDashboardRenderer(props: DashboardRendererProps) {
return <Component {...props} />;
}

View file

@ -21,7 +21,6 @@ export {
DashboardContainerFactoryDefinition,
} from './embeddable/dashboard_container_factory';
export { DashboardRenderer } from './external_api/dashboard_renderer';
export type { DashboardAPI, AwaitingDashboardAPI } from './external_api/dashboard_api';
export { LazyDashboardRenderer } from './external_api/lazy_dashboard_renderer';
export type { DashboardLocatorParams } from './types';
export type { IProvidesLegacyPanelPlacementSettings } from './panel_placement';

View file

@ -8,20 +8,20 @@
*/
import React from 'react';
import { DashboardAPIContext } from '../dashboard_app/dashboard_app';
import { DashboardContainer } from '../dashboard_container';
import {
InternalDashboardTopNav,
InternalDashboardTopNavProps,
} from './internal_dashboard_top_nav';
import { DashboardContext } from '../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../dashboard_api/types';
export interface DashboardTopNavProps extends InternalDashboardTopNavProps {
dashboardContainer: DashboardContainer;
dashboardApi: DashboardApi;
}
export const DashboardTopNavWithContext = (props: DashboardTopNavProps) => (
<DashboardAPIContext.Provider value={props.dashboardContainer}>
<DashboardContext.Provider value={props.dashboardApi}>
<InternalDashboardTopNav {...props} />
</DashboardAPIContext.Provider>
</DashboardContext.Provider>
);
// eslint-disable-next-line import/no-default-export

View file

@ -13,8 +13,9 @@ import { buildMockDashboard } from '../mocks';
import { InternalDashboardTopNav } from './internal_dashboard_top_nav';
import { setMockedPresentationUtilServices } from '@kbn/presentation-util-plugin/public/mocks';
import { pluginServices } from '../services/plugin_services';
import { DashboardAPIContext } from '../dashboard_app/dashboard_app';
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
import { DashboardContext } from '../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../dashboard_api/types';
describe('Internal dashboard top nav', () => {
const mockTopNav = (badges: TopNavMenuProps['badges'] | undefined[]) => {
@ -42,9 +43,9 @@ describe('Internal dashboard top nav', () => {
it('should not render the managed badge by default', async () => {
const component = render(
<DashboardAPIContext.Provider value={buildMockDashboard()}>
<DashboardContext.Provider value={buildMockDashboard() as DashboardApi}>
<InternalDashboardTopNav redirectTo={jest.fn()} />
</DashboardAPIContext.Provider>
</DashboardContext.Provider>
);
expect(component.queryByText('Managed')).toBeNull();
@ -54,9 +55,9 @@ describe('Internal dashboard top nav', () => {
const container = buildMockDashboard();
container.dispatch.setManaged(true);
const component = render(
<DashboardAPIContext.Provider value={container}>
<DashboardContext.Provider value={container as DashboardApi}>
<InternalDashboardTopNav redirectTo={jest.fn()} />
</DashboardAPIContext.Provider>
</DashboardContext.Provider>
);
expect(component.getByText('Managed')).toBeInTheDocument();

View file

@ -15,7 +15,6 @@ import {
LazyLabsFlyout,
getContextProvider as getPresentationUtilContextProvider,
} from '@kbn/presentation-util-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public';
import {
EuiBreadcrumb,
@ -29,7 +28,8 @@ import {
import { MountPoint } from '@kbn/core/public';
import { getManagedContentBadge } from '@kbn/managed-content-badge';
import { FormattedMessage } from '@kbn/i18n-react';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { Query } from '@kbn/es-query';
import {
getDashboardTitle,
leaveConfirmStrings,
@ -38,7 +38,6 @@ import {
dashboardManagedBadge,
} from '../dashboard_app/_dashboard_app_strings';
import { UI_SETTINGS } from '../../common';
import { useDashboardAPI } from '../dashboard_app/dashboard_app';
import { pluginServices } from '../services/plugin_services';
import { useDashboardMenuItems } from '../dashboard_app/top_nav/use_dashboard_menu_items';
import { DashboardEmbedSettings } from '../dashboard_app/types';
@ -48,6 +47,7 @@ import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants
import './_dashboard_top_nav.scss';
import { DashboardRedirect } from '../dashboard_container/types';
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
import { useDashboardApi } from '../dashboard_api/use_dashboard_api';
export interface InternalDashboardTopNavProps {
customLeadingBreadCrumbs?: EuiBreadcrumb[];
@ -98,24 +98,35 @@ export function InternalDashboardTopNav({
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
const dashboard = useDashboardAPI();
const dashboardApi = useDashboardApi();
const PresentationUtilContextProvider = getPresentationUtilContextProvider();
const hasRunMigrations = dashboard.select(
(state) => state.componentState.hasRunClientsideMigrations
const [
allDataViews,
focusedPanelId,
fullScreenMode,
hasRunMigrations,
hasUnsavedChanges,
lastSavedId,
managed,
query,
title,
viewMode,
] = useBatchedPublishingSubjects(
dashboardApi.dataViews,
dashboardApi.focusedPanelId$,
dashboardApi.fullScreenMode$,
dashboardApi.hasRunMigrations$,
dashboardApi.hasUnsavedChanges$,
dashboardApi.savedObjectId,
dashboardApi.managed$,
dashboardApi.query$,
dashboardApi.panelTitle,
dashboardApi.viewMode
);
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
const focusedPanelId = dashboard.select((state) => state.componentState.focusedPanelId);
const managed = dashboard.select((state) => state.componentState.managed);
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const query = dashboard.select((state) => state.explicitInput.query);
const title = dashboard.select((state) => state.explicitInput.title);
const [savedQueryId, setSavedQueryId] = useState<string | undefined>();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const allDataViews = useStateFromPublishingSubject(dashboard.dataViews);
const dashboardTitle = useMemo(() => {
return getDashboardTitle(title, viewMode, !lastSavedId);
@ -132,7 +143,7 @@ export function InternalDashboardTopNav({
* Manage chrome visibility when dashboard is embedded.
*/
useEffect(() => {
if (!embedSettings) setChromeVisibility(viewMode !== ViewMode.PRINT);
if (!embedSettings) setChromeVisibility(viewMode !== 'print');
}, [embedSettings, setChromeVisibility, viewMode]);
/**
@ -142,12 +153,12 @@ export function InternalDashboardTopNav({
const subscription = getChromeIsVisible$().subscribe((visible) => setIsChromeVisible(visible));
if (lastSavedId && title) {
chromeRecentlyAccessed.add(
getFullEditPath(lastSavedId, viewMode === ViewMode.EDIT),
getFullEditPath(lastSavedId, viewMode === 'edit'),
title,
lastSavedId
);
dashboardRecentlyAccessed.add(
getFullEditPath(lastSavedId, viewMode === ViewMode.EDIT),
getFullEditPath(lastSavedId, viewMode === 'edit'),
title,
lastSavedId
);
@ -170,14 +181,14 @@ export function InternalDashboardTopNav({
const dashboardTitleBreadcrumbs = [
{
text:
viewMode === ViewMode.EDIT ? (
viewMode === 'edit' ? (
<>
{dashboardTitle}
<EuiIcon
size="s"
type="pencil"
className="dshTitleBreadcrumbs__updateIcon"
onClick={() => dashboard.showSettings()}
onClick={() => dashboardApi.openSettingsFlyout()}
/>
</>
) : (
@ -213,7 +224,7 @@ export function InternalDashboardTopNav({
setBreadcrumbs,
redirectTo,
dashboardTitle,
dashboard,
dashboardApi,
viewMode,
serverless,
customLeadingBreadCrumbs,
@ -224,11 +235,7 @@ export function InternalDashboardTopNav({
*/
useEffect(() => {
onAppLeave((actions) => {
if (
viewMode === ViewMode.EDIT &&
hasUnsavedChanges &&
!getStateTransfer().isTransferInProgress
) {
if (viewMode === 'edit' && hasUnsavedChanges && !getStateTransfer().isTransferInProgress) {
return actions.confirm(
leaveConfirmStrings.getLeaveSubtitle(),
leaveConfirmStrings.getLeaveTitle()
@ -252,7 +259,7 @@ export function InternalDashboardTopNav({
const showQueryInput = Boolean(forceHideUnifiedSearch)
? false
: shouldShowNavBarComponent(
Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
Boolean(embedSettings?.forceShowQueryInput || viewMode === 'edit')
);
const showDatePicker = Boolean(forceHideUnifiedSearch)
? false
@ -300,12 +307,12 @@ export function InternalDashboardTopNav({
});
UseUnmount(() => {
dashboard.clearOverlays();
dashboardApi.clearOverlays();
});
const badges = useMemo(() => {
const allBadges: TopNavMenuProps['badges'] = [];
if (hasUnsavedChanges && viewMode === ViewMode.EDIT) {
if (hasUnsavedChanges && viewMode === 'edit') {
allBadges.push({
'data-test-subj': 'dashboardUnsavedChangesBadge',
badgeText: unsavedChangesBadgeStrings.getUnsavedChangedBadgeText(),
@ -317,7 +324,7 @@ export function InternalDashboardTopNav({
} as EuiToolTipProps,
});
}
if (hasRunMigrations && viewMode === ViewMode.EDIT) {
if (hasRunMigrations && viewMode === 'edit') {
allBadges.push({
'data-test-subj': 'dashboardSaveRecommendedBadge',
badgeText: unsavedChangesBadgeStrings.getHasRunMigrationsText(),
@ -357,7 +364,7 @@ export function InternalDashboardTopNav({
<EuiLink
id="dashboardManagedContentPopoverButton"
onClick={() => {
dashboard
dashboardApi
.runInteractiveSave(viewMode)
.then((result) => maybeRedirect(result));
}}
@ -385,7 +392,7 @@ export function InternalDashboardTopNav({
showWriteControls,
managed,
isPopoverOpen,
dashboard,
dashboardApi,
maybeRedirect,
]);
@ -399,7 +406,7 @@ export function InternalDashboardTopNav({
>{`${getDashboardBreadcrumb()} - ${dashboardTitle}`}</h1>
<TopNavMenu
{...visibilityProps}
query={query}
query={query as Query | undefined}
badges={badges}
screenTitle={title}
useDefaultBehaviors={true}
@ -407,7 +414,7 @@ export function InternalDashboardTopNav({
indexPatterns={allDataViews ?? []}
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
appName={LEGACY_DASHBOARD_APP_ID}
visible={viewMode !== ViewMode.PRINT}
visible={viewMode !== 'print'}
setMenuMountPoint={
embedSettings || fullScreenMode
? setCustomHeaderActionMenu ?? undefined
@ -416,28 +423,24 @@ export function InternalDashboardTopNav({
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
config={
visibilityProps.showTopNavMenu
? viewMode === ViewMode.EDIT
? viewMode === 'edit'
? editModeTopNavConfig
: viewModeTopNavConfig
: undefined
}
onQuerySubmit={(_payload, isUpdate) => {
if (isUpdate === false) {
dashboard.forceRefresh();
dashboardApi.forceRefresh();
}
}}
onSavedQueryIdChange={(newId: string | undefined) =>
dashboard.dispatch.setSavedQueryId(newId)
}
onSavedQueryIdChange={setSavedQueryId}
/>
{viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? (
{viewMode !== 'print' && isLabsEnabled && isLabsShown ? (
<PresentationUtilContextProvider>
<LabsFlyout solutions={['dashboard']} onClose={() => setIsLabsShown(false)} />
</PresentationUtilContextProvider>
) : null}
{viewMode === ViewMode.EDIT ? (
<DashboardEditingToolbar isDisabled={!!focusedPanelId} />
) : null}
{viewMode === 'edit' ? <DashboardEditingToolbar isDisabled={!!focusedPanelId} /> : null}
{showBorderBottom && <EuiHorizontalRule margin="none" />}
</div>
);

View file

@ -17,10 +17,9 @@ export {
DASHBOARD_GRID_COLUMN_COUNT,
PanelPlacementStrategy,
} from './dashboard_constants';
export type { DashboardApi } from './dashboard_api/types';
export {
type DashboardAPI,
type AwaitingDashboardAPI,
DashboardRenderer,
LazyDashboardRenderer as DashboardRenderer,
DASHBOARD_CONTAINER_TYPE,
type DashboardCreationOptions,
type DashboardLocatorParams,

View file

@ -200,6 +200,7 @@ export const legacyEmbeddableToApi = (
const filters$: BehaviorSubject<Filter[] | undefined> = new BehaviorSubject<Filter[] | undefined>(
undefined
);
const query$: BehaviorSubject<Query | AggregateQuery | undefined> = new BehaviorSubject<
Query | AggregateQuery | undefined
>(undefined);

View file

@ -12,7 +12,7 @@ import type { TimefilterContract } from '@kbn/data-plugin/public';
import { firstValueFrom } from 'rxjs';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type {
DashboardAPI,
DashboardApi,
DashboardLocatorParams,
DashboardStart,
} from '@kbn/dashboard-plugin/public';
@ -78,7 +78,7 @@ export class QuickJobCreatorBase {
end: number | undefined;
startJob: boolean;
runInRealTime: boolean;
dashboard?: DashboardAPI;
dashboard?: DashboardApi;
}) {
const datafeedId = createDatafeedId(jobId);
const datafeed = { ...datafeedConfig, job_id: jobId, datafeed_id: datafeedId };
@ -225,7 +225,7 @@ export class QuickJobCreatorBase {
return mergedQueries;
}
private async createDashboardLink(dashboard: DashboardAPI, datafeedConfig: estypes.MlDatafeed) {
private async createDashboardLink(dashboard: DashboardApi, datafeedConfig: estypes.MlDatafeed) {
const savedObjectId = dashboard.savedObjectId?.value;
if (!savedObjectId) {
return null;
@ -260,7 +260,7 @@ export class QuickJobCreatorBase {
return { url_name: urlName, url_value: url, time_range: 'auto' };
}
private async getCustomUrls(dashboard: DashboardAPI, datafeedConfig: estypes.MlDatafeed) {
private async getCustomUrls(dashboard: DashboardApi, datafeedConfig: estypes.MlDatafeed) {
const customUrls = await this.createDashboardLink(dashboard, datafeedConfig);
return dashboard !== undefined && customUrls !== null ? { custom_urls: [customUrls] } : {};
}

View file

@ -20,7 +20,7 @@ import { layerTypes } from '@kbn/lens-plugin/public';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import { ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
import type { LensApi } from '@kbn/lens-plugin/public';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
import { ML_PAGES, ML_APP_LOCATOR } from '../../../../../common/constants/locator';
export const COMPATIBLE_SERIES_TYPES = [
@ -78,7 +78,7 @@ export async function getJobsItemsFromEmbeddable(embeddable: LensApi, lens?: Len
}
const dashboardApi = apiIsOfType(embeddable.parentApi, 'dashboard')
? (embeddable.parentApi as DashboardAPI)
? (embeddable.parentApi as DashboardApi)
: undefined;
const timeRange = embeddable.timeRange$?.value ?? dashboardApi?.timeRange$?.value;

View file

@ -10,7 +10,7 @@ import type { Query } from '@kbn/es-query';
import { apiIsOfType } from '@kbn/presentation-publishing';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { MapApi } from '@kbn/maps-plugin/public';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
import { ML_PAGES, ML_APP_LOCATOR } from '../../../../../common/constants/locator';
export async function redirectToGeoJobWizard(
@ -53,7 +53,7 @@ export function isCompatibleMapVisualization(api: MapApi) {
export async function getJobsItemsFromEmbeddable(embeddable: MapApi) {
const dashboardApi = apiIsOfType(embeddable.parentApi, 'dashboard')
? (embeddable.parentApi as DashboardAPI)
? (embeddable.parentApi as DashboardApi)
: undefined;
const timeRange = embeddable.timeRange$?.value ?? dashboardApi?.timeRange$?.value;
if (timeRange === undefined) {

View file

@ -9,7 +9,7 @@ import React, { useState, useEffect } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import {
AwaitingDashboardAPI,
DashboardApi,
DashboardCreationOptions,
DashboardRenderer,
} from '@kbn/dashboard-plugin/public';
@ -28,7 +28,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
import { convertSavedDashboardToPanels, MetricsDashboardProps } from './helper';
export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
const [dashboard, setDashboard] = useState<DashboardApi | undefined>(undefined);
const { dataView } = dashboardProps;
const {
query: { environment, kuery, rangeFrom, rangeTo },
@ -42,24 +42,20 @@ export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
useEffect(() => {
if (!dashboard) return;
dashboard.updateInput({
timeRange: { from: rangeFrom, to: rangeTo },
query: { query: kuery, language: 'kuery' },
});
dashboard.setTimeRange({ from: rangeFrom, to: rangeTo });
dashboard.setQuery({ query: kuery, language: 'kuery' });
}, [kuery, dashboard, rangeFrom, rangeTo]);
useEffect(() => {
if (!dashboard) return;
dashboard.updateInput({
filters: dataView ? getFilters(serviceName, environment, dataView) : [],
});
dashboard.setFilters(dataView ? getFilters(serviceName, environment, dataView) : []);
}, [dataView, serviceName, environment, dashboard]);
return (
<DashboardRenderer
getCreationOptions={() => getCreationOptions(dashboardProps, notifications, dataView)}
ref={setDashboard}
onApiAvailable={setDashboard}
/>
);
}

View file

@ -19,7 +19,7 @@ import {
import { ViewMode } from '@kbn/embeddable-plugin/public';
import {
AwaitingDashboardAPI,
DashboardApi,
DashboardCreationOptions,
DashboardRenderer,
} from '@kbn/dashboard-plugin/public';
@ -53,7 +53,7 @@ export function ServiceDashboards({ checkForEntities = false }: { checkForEntiti
'/services/{serviceName}/dashboards',
'/mobile-services/{serviceName}/dashboards'
);
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
const [dashboard, setDashboard] = useState<DashboardApi | undefined>();
const [serviceDashboards, setServiceDashboards] = useState<MergedServiceDashboard[]>([]);
const [currentDashboard, setCurrentDashboard] = useState<MergedServiceDashboard>();
const { data: allAvailableDashboards } = useDashboardFetcher();
@ -110,16 +110,15 @@ export function ServiceDashboards({ checkForEntities = false }: { checkForEntiti
useEffect(() => {
if (!dashboard) return;
dashboard.updateInput({
filters:
dataView &&
dashboard.setFilters(
dataView &&
currentDashboard?.serviceEnvironmentFilterEnabled &&
currentDashboard?.serviceNameFilterEnabled
? getFilters(serviceName, environment, dataView)
: [],
timeRange: { from: rangeFrom, to: rangeTo },
query: { query: kuery, language: 'kuery' },
});
? getFilters(serviceName, environment, dataView)
: []
);
dashboard.setQuery({ query: kuery, language: 'kuery' });
dashboard.setTimeRange({ from: rangeFrom, to: rangeTo });
}, [dataView, serviceName, environment, kuery, dashboard, rangeFrom, rangeTo, currentDashboard]);
const getLocatorParams = useCallback(
@ -213,7 +212,7 @@ export function ServiceDashboards({ checkForEntities = false }: { checkForEntiti
locator={locator}
savedObjectId={dashboardId}
getCreationOptions={getCreationOptions}
ref={setDashboard}
onApiAvailable={setDashboard}
/>
)}
</EuiFlexItem>

View file

@ -19,7 +19,7 @@ import {
import { ViewMode } from '@kbn/embeddable-plugin/public';
import {
AwaitingDashboardAPI,
DashboardApi,
DashboardCreationOptions,
DashboardRenderer,
} from '@kbn/dashboard-plugin/public';
@ -61,7 +61,7 @@ export function Dashboards() {
const {
services: { share, telemetry },
} = useKibanaContextForPlugin();
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
const [dashboard, setDashboard] = useState<DashboardApi | undefined>();
const [customDashboards, setCustomDashboards] = useState<DashboardItemWithTitle[]>([]);
const [currentDashboard, setCurrentDashboard] = useState<DashboardItemWithTitle>();
const [trackingEventProperties, setTrackingEventProperties] = useState({});
@ -143,15 +143,13 @@ export function Dashboards() {
useEffect(() => {
if (!dashboard) return;
dashboard.updateInput({
filters:
metrics.dataView && currentDashboard?.dashboardFilterAssetIdEnabled
? buildAssetIdFilter(asset.name, asset.type, metrics.dataView)
: [],
timeRange: { from: dateRange.from, to: dateRange.to },
// forces data reload
lastReloadRequestTime: Date.now(),
});
dashboard.setFilters(
metrics.dataView && currentDashboard?.dashboardFilterAssetIdEnabled
? buildAssetIdFilter(asset.name, asset.type, metrics.dataView)
: []
);
dashboard.setTimeRange({ from: dateRange.from, to: dateRange.to });
dashboard.forceRefresh();
}, [
metrics.dataView,
asset.name,
@ -274,7 +272,7 @@ export function Dashboards() {
<DashboardRenderer
savedObjectId={urlState?.dashboardId}
getCreationOptions={getCreationOptions}
ref={setDashboard}
onApiAvailable={setDashboard}
locator={locator}
/>
)}

View file

@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { DashboardContainerInput } from '@kbn/dashboard-plugin/common';
import type {
DashboardAPI,
DashboardApi,
DashboardCreationOptions,
DashboardLocatorParams,
} from '@kbn/dashboard-plugin/public';
@ -43,11 +43,11 @@ const DashboardRendererComponent = ({
viewMode = ViewMode.VIEW,
}: {
canReadDashboard: boolean;
dashboardContainer?: DashboardAPI;
dashboardContainer?: DashboardApi;
filters?: Filter[];
id: string;
inputId?: InputsModelId.global | InputsModelId.timeline;
onDashboardContainerLoaded?: (dashboardContainer: DashboardAPI) => void;
onDashboardContainerLoaded?: (dashboardContainer: DashboardApi) => void;
query?: Query;
savedObjectId: string | undefined;
timeRange: {
@ -142,12 +142,19 @@ const DashboardRendererComponent = ({
}, [dispatch, id, inputId, refetchByForceRefresh]);
useEffect(() => {
dashboardContainer?.updateInput({ timeRange, query, filters });
}, [dashboardContainer, filters, query, timeRange]);
dashboardContainer?.setFilters(filters);
}, [dashboardContainer, filters]);
useEffect(() => {
if (isCreateDashboard && firstSecurityTagId)
dashboardContainer?.updateInput({ tags: [firstSecurityTagId] });
dashboardContainer?.setQuery(query);
}, [dashboardContainer, query]);
useEffect(() => {
dashboardContainer?.setTimeRange(timeRange);
}, [dashboardContainer, timeRange]);
useEffect(() => {
if (isCreateDashboard && firstSecurityTagId) dashboardContainer?.setTags([firstSecurityTagId]);
}, [dashboardContainer, firstSecurityTagId, isCreateDashboard]);
useEffect(() => {
@ -166,7 +173,7 @@ const DashboardRendererComponent = ({
setDashboardContainerRenderer(
<DashboardContainerRenderer
locator={locator}
ref={onDashboardContainerLoaded}
onApiAvailable={onDashboardContainerLoaded}
savedObjectId={savedObjectId}
getCreationOptions={getCreationOptions}
/>

View file

@ -5,21 +5,23 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { EDIT_DASHBOARD_TITLE } from '../pages/details/translations';
const DashboardTitleComponent = ({
dashboardContainer,
onTitleLoaded,
}: {
dashboardContainer: DashboardAPI;
dashboardContainer: DashboardApi;
onTitleLoaded: (title: string) => void;
}) => {
const dashboardTitle = dashboardContainer.select((state) => state.explicitInput.title).trim();
const title =
dashboardTitle && dashboardTitle.length !== 0 ? dashboardTitle : EDIT_DASHBOARD_TITLE;
const dashboardTitle = useStateFromPublishingSubject(dashboardContainer.panelTitle);
const title = useMemo(() => {
return dashboardTitle && dashboardTitle.length !== 0 ? dashboardTitle : EDIT_DASHBOARD_TITLE;
}, [dashboardTitle]);
useEffect(() => {
onTitleLoaded(title);

View file

@ -8,7 +8,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { DashboardToolBar } from './dashboard_tool_bar';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
import { coreMock } from '@kbn/core/public/mocks';
import { DashboardTopNav } from '@kbn/dashboard-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
@ -34,9 +34,7 @@ jest.mock('@kbn/dashboard-plugin/public', () => ({
const mockCore = coreMock.createStart();
const mockNavigateTo = jest.fn();
const mockGetAppUrl = jest.fn();
const mockDashboardContainer = {
select: jest.fn(),
} as unknown as DashboardAPI;
const mockDashboardContainer = {} as unknown as DashboardApi;
const wrapper = ({ children }: { children: React.ReactNode }) => (
<TestProviders>

View file

@ -6,13 +6,14 @@
*/
import React, { useCallback, useEffect, useMemo } from 'react';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
import { DashboardTopNav, LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { ViewMode } from '@kbn/embeddable-plugin/public';
import type { ChromeBreadcrumb } from '@kbn/core/public';
import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common';
import type { RedirectToProps } from '@kbn/dashboard-plugin/public/dashboard_container/types';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { SecurityPageName } from '../../../common';
import { useCapabilities, useKibana, useNavigation } from '../../common/lib/kibana';
import { APP_NAME } from '../../../common/constants';
@ -21,13 +22,12 @@ const DashboardToolBarComponent = ({
dashboardContainer,
onLoad,
}: {
dashboardContainer: DashboardAPI;
dashboardContainer: DashboardApi;
onLoad?: (mode: ViewMode) => void;
}) => {
const { setHeaderActionMenu } = useKibana().services;
const viewMode =
dashboardContainer?.select((state) => state.explicitInput.viewMode) ?? ViewMode.VIEW;
const viewMode = useStateFromPublishingSubject(dashboardContainer.viewMode);
const { navigateTo, getAppUrl } = useNavigation();
const redirectTo = useCallback(
@ -56,7 +56,7 @@ const DashboardToolBarComponent = ({
);
useEffect(() => {
onLoad?.(viewMode);
onLoad?.((viewMode as ViewMode) ?? 'view');
}, [onLoad, viewMode]);
const embedSettings = useMemo(
@ -73,7 +73,7 @@ const DashboardToolBarComponent = ({
return showWriteControls ? (
<DashboardTopNav
customLeadingBreadCrumbs={landingBreadcrumb}
dashboardContainer={dashboardContainer}
dashboardApi={dashboardContainer}
forceHideUnifiedSearch={true}
embedSettings={embedSettings}
redirectTo={redirectTo}

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
import { useDashboardRenderer } from './use_dashboard_renderer';
jest.mock('../../common/lib/kibana');
const mockDashboardContainer = { getExplicitInput: () => ({ tags: ['tagId'] }) } as DashboardAPI;
const mockDashboardContainer = {} as DashboardApi;
describe('useDashboardRenderer', () => {
it('should set dashboard container correctly when dashboard is loaded', async () => {

View file

@ -6,12 +6,12 @@
*/
import { useCallback, useMemo, useState } from 'react';
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
export const useDashboardRenderer = () => {
const [dashboardContainer, setDashboardContainer] = useState<DashboardAPI>();
const [dashboardContainer, setDashboardContainer] = useState<DashboardApi>();
const handleDashboardLoaded = useCallback((container: DashboardAPI) => {
const handleDashboardLoaded = useCallback((container: DashboardApi) => {
setDashboardContainer(container);
}, []);

View file

@ -223,6 +223,7 @@
"@kbn/cloud-security-posture",
"@kbn/security-solution-distribution-bar",
"@kbn/cloud-security-posture-common",
"@kbn/presentation-publishing",
"@kbn/entityManager-plugin",
"@kbn/entities-schema",
]