From 79cb9c56dc11fcf0792ce99d0caac2381b6ef803 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 18 Sep 2024 08:22:54 -0600 Subject: [PATCH] [8.x] [dashboard] Lazy DashboardRenderer (#192754) (#193219) # Backport This will backport the following commits from `main` to `8.x`: - [[dashboard] Lazy DashboardRenderer (#192754)](https://github.com/elastic/kibana/pull/192754) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/app.tsx | 48 +-- .../dashboard_with_controls_example.tsx | 28 +- ...xample.tsx => dual_dashboards_example.tsx} | 33 +- .../public/dynamically_add_panels_example.tsx | 151 ---------- .../public/plugin.tsx | 13 +- .../portable_dashboards_example/tsconfig.json | 4 +- .../dashboard/public/dashboard_api/types.ts | 62 ++++ .../public/dashboard_api/use_dashboard_api.ts | 21 ++ .../dashboard_app/_dashboard_app_strings.ts | 12 +- .../public/dashboard_app/dashboard_app.tsx | 97 +++--- ...use_observability_ai_assistant_context.tsx | 15 +- .../dashboard_tab_title_setter.tsx | 22 +- .../add_data_control_button.tsx | 6 +- .../add_time_slider_control_button.tsx | 6 +- .../top_nav/dashboard_editing_toolbar.tsx | 16 +- .../top_nav/editor_menu.test.tsx | 19 +- .../dashboard_app/top_nav/editor_menu.tsx | 24 +- .../top_nav/share/show_share_modal.test.tsx | 77 +++-- .../top_nav/share/show_share_modal.tsx | 12 +- .../top_nav/use_dashboard_menu_items.tsx | 61 ++-- ...nc_dashboard_url_state.ts => url_utils.ts} | 22 -- .../embeddable/dashboard_container.tsx | 75 ++++- .../external_api/dashboard_api.ts | 15 +- .../external_api/dashboard_renderer.test.tsx | 2 + .../external_api/dashboard_renderer.tsx | 285 ++++++++---------- .../external_api/lazy_dashboard_renderer.tsx | 23 ++ .../public/dashboard_container/index.ts | 3 +- .../dashboard_top_nav_with_context.tsx | 10 +- .../internal_dashboard_top_nav.test.tsx | 11 +- .../internal_dashboard_top_nav.tsx | 93 +++--- src/plugins/dashboard/public/index.ts | 5 +- .../compatibility/legacy_embeddable_to_api.ts | 1 + .../quick_create_job_base.ts | 8 +- .../jobs/new_job/job_from_lens/utils.ts | 4 +- .../jobs/new_job/job_from_map/utils.ts | 4 +- .../app/metrics/static_dashboard/index.tsx | 16 +- .../app/service_dashboards/index.tsx | 21 +- .../tabs/dashboards/dashboards.tsx | 22 +- .../components/dashboard_renderer.tsx | 23 +- .../dashboards/components/dashboard_title.tsx | 14 +- .../components/dashboard_tool_bar.test.tsx | 6 +- .../components/dashboard_tool_bar.tsx | 14 +- .../hooks/use_dashboard_renderer.test.tsx | 4 +- .../hooks/use_dashboard_renderer.tsx | 6 +- .../plugins/security_solution/tsconfig.json | 1 + 45 files changed, 673 insertions(+), 742 deletions(-) rename examples/portable_dashboards_example/public/{dual_redux_example.tsx => dual_dashboards_example.tsx} (62%) delete mode 100644 examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx create mode 100644 src/plugins/dashboard/public/dashboard_api/types.ts create mode 100644 src/plugins/dashboard/public/dashboard_api/use_dashboard_api.ts rename src/plugins/dashboard/public/dashboard_app/url/{sync_dashboard_url_state.ts => url_utils.ts} (79%) create mode 100644 src/plugins/dashboard/public/dashboard_container/external_api/lazy_dashboard_renderer.tsx diff --git a/examples/portable_dashboards_example/public/app.tsx b/examples/portable_dashboards_example/public/app.tsx index fd24fc607d06d..c68b612c31193 100644 --- a/examples/portable_dashboards_example/public/app.tsx +++ b/examples/portable_dashboards_example/public/app.tsx @@ -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( - , + , element ); return () => ReactDOM.unmountComponentAtNode(element); }; const PortableDashboardsDemos = ({ + coreStart, data, dashboard, history, }: { + coreStart: CoreStart; data: StartDeps['data']; dashboard: StartDeps['dashboard']; history: AppMountParameters['history']; }) => { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); }; @@ -91,9 +101,7 @@ const DashboardsDemo = ({ <> - - - + diff --git a/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx b/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx index 205e160f2c81d..316ed8e47fb28 100644 --- a/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx +++ b/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx @@ -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(); + const [dashboard, setDashboard] = useState(); // 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} /> diff --git a/examples/portable_dashboards_example/public/dual_redux_example.tsx b/examples/portable_dashboards_example/public/dual_dashboards_example.tsx similarity index 62% rename from examples/portable_dashboards_example/public/dual_redux_example.tsx rename to examples/portable_dashboards_example/public/dual_dashboards_example.tsx index 66544067ca831..2e4fcbd130e23 100644 --- a/examples/portable_dashboards_example/public/dual_redux_example.tsx +++ b/examples/portable_dashboards_example/public/dual_dashboards_example.tsx @@ -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(); - const [secondDashboardContainer, setSecondDashboardContainer] = useState(); +export const DualDashboardsExample = () => { + const [firstDashboardApi, setFirstDashboardApi] = useState(); + const [secondDashboardApi, setSecondDashboardApi] = useState(); - const ButtonControls = ({ dashboard }: { dashboard: DashboardAPI }) => { - const viewMode = dashboard.select((state) => state.explicitInput.viewMode); + const ButtonControls = ({ dashboardApi }: { dashboardApi: DashboardApi }) => { + const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode); return ( { }, ]} 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 ( <> -

Dual redux example

+

Dual dashboards example

- 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.

@@ -73,18 +70,18 @@ export const DualReduxExample = () => {

Dashboard #1

- {firstDashboardContainer && } + {firstDashboardApi && } - +

Dashboard #2

- {secondDashboardContainer && } + {secondDashboardApi && } - +
diff --git a/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx b/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx deleted file mode 100644 index 3816beea96341..0000000000000 --- a/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx +++ /dev/null @@ -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(); - - 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( - '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 ( - <> - -

Edit and save example

-
- -

Customize the dashboard and persist the state to local storage.

-
- - - - - - - - Add visualization by value - - - - dashboard?.addFromLibrary()} isDisabled={disableButtons}> - Add visualization from library - - - - - - - - - Save to local storage - - - - - Empty dashboard and reset local storage - - - - - - - - { - 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} - /> - - - ); -}; diff --git a/examples/portable_dashboards_example/public/plugin.tsx b/examples/portable_dashboards_example/public/plugin.tsx index b356944393548..2974de1c028ab 100644 --- a/examples/portable_dashboards_example/public/plugin.tsx +++ b/examples/portable_dashboards_example/public/plugin.tsx @@ -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 ({ + width: 48, + height: 12, + })); + } public stop() {} } diff --git a/examples/portable_dashboards_example/tsconfig.json b/examples/portable_dashboards_example/tsconfig.json index e15af91765497..19f3d4690884b 100644 --- a/examples/portable_dashboards_example/tsconfig.json +++ b/examples/portable_dashboards_example/tsconfig.json @@ -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" ] } diff --git a/src/plugins/dashboard/public/dashboard_api/types.ts b/src/plugins/dashboard/public/dashboard_api/types.ts new file mode 100644 index 0000000000000..216081efbd8c8 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/types.ts @@ -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 & + PublishesSavedObjectId & + PublishesUnifiedSearch & + PublishesViewMode & + TracksOverlays & { + addFromLibrary: () => void; + asyncResetToLastSavedState: () => Promise; + controlGroupApi$: PublishingSubject; + fullScreenMode$: PublishingSubject; + focusedPanelId$: PublishingSubject; + forceRefresh: () => void; + getPanelsState: () => DashboardPanelMap; + hasOverlays$: PublishingSubject; + hasRunMigrations$: PublishingSubject; + hasUnsavedChanges$: PublishingSubject; + managed$: PublishingSubject; + runInteractiveSave: (interactionMode: ViewMode) => Promise; + runQuickSave: () => Promise; + 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; + }; diff --git a/src/plugins/dashboard/public/dashboard_api/use_dashboard_api.ts b/src/plugins/dashboard/public/dashboard_api/use_dashboard_api.ts new file mode 100644 index 0000000000000..982ba6869dfa1 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/use_dashboard_api.ts @@ -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(undefined); + +export const useDashboardApi = (): DashboardApi => { + const api = useContext(DashboardContext); + if (!api) { + throw new Error('useDashboardApi must be used inside DashboardContext'); + } + return api; +}; diff --git a/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts b/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts index 561da68e8db82..8a6e24a183f3a 100644 --- a/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts +++ b/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts @@ -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}', diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx index 07043178d4d94..fa1b505879090 100644 --- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx +++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx @@ -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(null); - -export const useDashboardAPI = (): DashboardAPI => { - const api = useContext(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(false); + const [regenerateId, setRegenerateId] = useState(uuidv4()); useMount(() => { (async () => setShowNoDataPage(await isDashboardAppInNoDataState()))(); }); - const [dashboardAPI, setDashboardAPI] = useState(null); + const [dashboardApi, setDashboardApi] = useState(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( + 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 ? ( + setShowNoDataPage(false)} /> + ) : ( <> - {showNoDataPage && ( - setShowNoDataPage(false)} /> - )} - {!showNoDataPage && ( + {dashboardApi && ( <> - {dashboardAPI && ( - <> - - - - )} - - {getLegacyConflictWarning?.()} - + )} + + {getLegacyConflictWarning?.()} + ); } diff --git a/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx b/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx index d1f27e5cd487b..0348e68f77418 100644 --- a/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx +++ b/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx @@ -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({ + return dashboardApi + .addNewPanel({ panelType: 'lens', initialState: embeddableInput, }) @@ -383,5 +382,5 @@ export function useObservabilityAIAssistantContext({ ] : [], }); - }, [observabilityAIAssistant, dashboardAPI, search, dataViews]); + }, [observabilityAIAssistant, dashboardApi, search, dataViews]); } diff --git a/src/plugins/dashboard/public/dashboard_app/tab_title_setter/dashboard_tab_title_setter.tsx b/src/plugins/dashboard/public/dashboard_app/tab_title_setter/dashboard_tab_title_setter.tsx index 8b13e12a95ba0..fab0a3cb91c37 100644 --- a/src/plugins/dashboard/public/dashboard_app/tab_title_setter/dashboard_tab_title_setter.tsx +++ b/src/plugins/dashboard/public/dashboard_app/tab_title_setter/dashboard_tab_title_setter.tsx @@ -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; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx index 4c6bf79d9f651..d836ac85aebfa 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx @@ -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 ( diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx index 2ec743b3354a0..47b4b8b06127b 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx @@ -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" diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 38892191cdb04..ae91af4891bf2 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -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 = [ - , + , dashboard.addFromLibrary()} + onClick={() => dashboardApi.addFromLibrary()} size="s" data-test-subj="dashboardAddFromLibraryButton" isDisabled={isDisabled} diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx index c0aa2d6e4363f..a867d0133b46d 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx @@ -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; - describe('editor menu', () => { - const defaultProps: ComponentProps = { - api: mockApi, - createNewVisType: jest.fn(), - }; - it('renders without crashing', async () => { - render(, { + render(, { wrapper: ({ children }) => { return ( - + {children} - + ); }, }); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index e7514ce998a9f..a79024fe8b9dc 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -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[0], 'api' | 'createNewVisType'> { + extends Pick[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 ( ); @@ -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 ( diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index 6ace26535f1c4..ff91eaf896481 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx @@ -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 = ( diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index 95db839b5fc45..81226fbb5b298 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -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]) => { diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index aaad8f8598ded..57bf640a59e20 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -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, diff --git a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/dashboard_app/url/url_utils.ts similarity index 79% rename from src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts rename to src/plugins/dashboard/public/dashboard_app/url/url_utils.ts index 667e4eaaeb80f..7fc7d17c55053 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/url_utils.ts @@ -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 }; -}; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 1c9def025fb8b..b836b3a53a726 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -301,23 +301,42 @@ export class DashboardContainer this.select = reduxTools.select; this.savedObjectId = new BehaviorSubject(this.getDashboardSavedObjectId()); - this.publishingSubscription.add( - this.onStateChange(() => { - if (this.savedObjectId.value === this.getDashboardSavedObjectId()) return; - this.savedObjectId.next(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.publishingSubscription.add( - this.savedObjectId.subscribe(() => { - this.hadContentfulRender = false; - }) - ); - - this.expandedPanelId = new BehaviorSubject(this.getDashboardSavedObjectId()); + this.hasUnsavedChanges$ = new BehaviorSubject(this.getState().componentState.hasUnsavedChanges); + this.hasOverlays$ = new BehaviorSubject(this.getState().componentState.hasOverlays); 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; public expandedPanelId: BehaviorSubject; + public focusedPanelId$: BehaviorSubject; + public managed$: BehaviorSubject; + public fullScreenMode$: BehaviorSubject; + public hasRunMigrations$: BehaviorSubject; + public hasUnsavedChanges$: BehaviorSubject; + public hasOverlays$: BehaviorSubject; 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); diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_api.ts b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_api.ts index 54008f6732f64..16f4347351fa7 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_api.ts +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_api.ts @@ -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 & diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx index bcfa6385a1d7c..6248800cf3740 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx @@ -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), diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx index 3bf6ad3284250..1222b3433877a 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx @@ -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, 'navigate' | 'getRedirectUrl'>; } -export const DashboardRenderer = forwardRef( - ({ 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(); - const [fatalError, setFatalError] = useState(); - const [dashboardMissing, setDashboardMissing] = useState(false); - - useImperativeHandle( - ref, - () => buildApiFromDashboardContainer(dashboardContainer) as DashboardAPI, - [dashboardContainer] - ); - - 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(), []); - - 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); - - 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); - } - }); +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(); + const [fatalError, setFatalError] = useState(); + const [dashboardMissing, setDashboardMissing] = useState(false); + + const { embeddable, screenshotMode } = pluginServices.getServices(); + + 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(() => { + /** + * 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; + } + + 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 ? ( - - ) : ( - - ); - - const renderDashboardContents = () => { - if (dashboardMissing) return ; - if (fatalError) return fatalError.render(); - if (loading) return loadingSpinner; - return
; + setDashboardContainer(container); + onApiAvailable?.(container as DashboardApi); + })(); + return () => { + canceled = true; }; - - return ( -
- {dashboardViewport?.current && - dashboardContainer && - !isErrorEmbeddable(dashboardContainer) && ( - - )} - {renderDashboardContents()} -
- ); - } -); + // 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 ? ( + + ) : ( + + ); + + const renderDashboardContents = () => { + if (dashboardMissing) return ; + if (fatalError) return fatalError.render(); + if (loading) return loadingSpinner; + return
; + }; + + return ( +
+ {dashboardViewport?.current && + dashboardContainer && + !isErrorEmbeddable(dashboardContainer) && ( + + )} + {renderDashboardContents()} +
+ ); +} /** * Maximizing a panel in Dashboard only works if the parent div has a certain class. This @@ -202,13 +179,13 @@ export const DashboardRenderer = forwardRef { - const maximizedPanelId = dashboard.select((state) => state.componentState.expandedPanelId); + const maximizedPanelId = useStateFromPublishingSubject(dashboardApi.expandedPanelId); useLayoutEffect(() => { const parentDiv = viewportRef.parentElement; diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/lazy_dashboard_renderer.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/lazy_dashboard_renderer.tsx new file mode 100644 index 0000000000000..56c6e5f82ac15 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/external_api/lazy_dashboard_renderer.tsx @@ -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 ; +} diff --git a/src/plugins/dashboard/public/dashboard_container/index.ts b/src/plugins/dashboard/public/dashboard_container/index.ts index 0debcd561d3d6..815823f74164d 100644 --- a/src/plugins/dashboard/public/dashboard_container/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/index.ts @@ -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'; diff --git a/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx b/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx index b323368411eda..751f7f1d5e0ef 100644 --- a/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx +++ b/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx @@ -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) => ( - + - + ); // eslint-disable-next-line import/no-default-export diff --git a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx index 67dc06562d09a..4114b7ab06d06 100644 --- a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx +++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx @@ -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( - + - + ); expect(component.queryByText('Managed')).toBeNull(); @@ -54,9 +55,9 @@ describe('Internal dashboard top nav', () => { const container = buildMockDashboard(); container.dispatch.setManaged(true); const component = render( - + - + ); expect(component.getByText('Managed')).toBeInTheDocument(); diff --git a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index e2b93e2c9764e..a166a01ba327d 100644 --- a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -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 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 [ + 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 [savedQueryId, setSavedQueryId] = useState(); 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} 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({ { - 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}`} { 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 ? ( setIsLabsShown(false)} /> ) : null} - {viewMode === ViewMode.EDIT ? ( - - ) : null} + {viewMode === 'edit' ? : null} {showBorderBottom && }
); diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index b0d31d40a6e64..535834b964372 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -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, diff --git a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts index 7a64df25f35dc..dab0968af0056 100644 --- a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts +++ b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts @@ -200,6 +200,7 @@ export const legacyEmbeddableToApi = ( const filters$: BehaviorSubject = new BehaviorSubject( undefined ); + const query$: BehaviorSubject = new BehaviorSubject< Query | AggregateQuery | undefined >(undefined); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts index bb1ee52335864..8ec738af55d02 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts @@ -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] } : {}; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts index 27490eff19991..9950d1dff9ad5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts @@ -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; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/utils.ts index 307306734d142..fc9e1f09c556a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/utils.ts @@ -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) { diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/index.tsx index 6f2c91dec9bfe..15a4b62a7efe7 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/index.tsx @@ -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(); + const [dashboard, setDashboard] = useState(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 ( getCreationOptions(dashboardProps, notifications, dataView)} - ref={setDashboard} + onApiAvailable={setDashboard} /> ); } diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx index 1b299182debab..bf864e134a698 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx @@ -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(); + const [dashboard, setDashboard] = useState(); const [serviceDashboards, setServiceDashboards] = useState([]); const [currentDashboard, setCurrentDashboard] = useState(); 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} /> )} diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx index 6ce9ff1f48835..ae5251a5c17b9 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/dashboards/dashboards.tsx @@ -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(); + const [dashboard, setDashboard] = useState(); const [customDashboards, setCustomDashboards] = useState([]); const [currentDashboard, setCurrentDashboard] = useState(); 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() { )} diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx index 83f48d76246e5..cad24fe3f494c 100644 --- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx @@ -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( diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx index 67d8e73bacdb6..dacc99176475a 100644 --- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx @@ -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); diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx index da0bf3e3dbcea..c6d38870e73ce 100644 --- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx @@ -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 }) => ( diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx index 3a5e3d43eae63..c3783f359031b 100644 --- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx @@ -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 ? ( ({ tags: ['tagId'] }) } as DashboardAPI; +const mockDashboardContainer = {} as DashboardApi; describe('useDashboardRenderer', () => { it('should set dashboard container correctly when dashboard is loaded', async () => { diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx index 104692e62f2bb..65adf31b4a1bb 100644 --- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx @@ -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(); + const [dashboardContainer, setDashboardContainer] = useState(); - const handleDashboardLoaded = useCallback((container: DashboardAPI) => { + const handleDashboardLoaded = useCallback((container: DashboardApi) => { setDashboardContainer(container); }, []); diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 6ccd61fd34394..1841d96351b32 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -223,5 +223,6 @@ "@kbn/cloud-security-posture", "@kbn/security-solution-distribution-bar", "@kbn/cloud-security-posture-common", + "@kbn/presentation-publishing", ] }