diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx index 00b68c2abc54..ea1bea52c83d 100644 --- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx +++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx @@ -31,6 +31,7 @@ import { } from './url/search_sessions_integration'; import { DashboardAPI, DashboardRenderer } from '..'; import { type DashboardEmbedSettings } from './types'; +import { DASHBOARD_APP_LOCATOR } from './locator/locator'; import { pluginServices } from '../services/plugin_services'; import { AwaitingDashboardAPI } from '../dashboard_container'; import { DashboardRedirect } from '../dashboard_container/types'; @@ -82,6 +83,7 @@ export function DashboardApp({ settings: { uiSettings }, data: { search }, customBranding, + share: { url }, } = pluginServices.getServices(); const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false); const { scopedHistory: getScopedHistory } = useDashboardMountContext(); @@ -188,6 +190,8 @@ export function DashboardApp({ return () => stopWatchingAppStateInUrl(); }, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]); + const locator = useMemo(() => url?.locators.get(DASHBOARD_APP_LOCATOR), [url]); + return ( <> {showNoDataPage && ( @@ -206,6 +210,7 @@ export function DashboardApp({ {getLegacyConflictWarning?.()} , options: DashboardDrilldownOptions -): Partial => { - const params: DashboardAppLocatorParams = {}; +): Partial => { + const params: DashboardLocatorParams = {}; const input = source.getInput(); if (isQuery(input.query) && options.useCurrentFilters) { diff --git a/src/plugins/dashboard/public/dashboard_app/locator/locator.ts b/src/plugins/dashboard/public/dashboard_app/locator/locator.ts index 4b8a93d8ad90..c902bc369e04 100644 --- a/src/plugins/dashboard/public/dashboard_app/locator/locator.ts +++ b/src/plugins/dashboard/public/dashboard_app/locator/locator.ts @@ -11,13 +11,11 @@ import { flow } from 'lodash'; import type { Filter } from '@kbn/es-query'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; -import type { DashboardContainerInput } from '../../../common'; -import { SavedDashboardPanel } from '../../../common/content_management'; import { DASHBOARD_APP_ID, SEARCH_SESSION_ID } from '../../dashboard_constants'; +import { DashboardLocatorParams } from '../..'; /** * Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions). @@ -35,50 +33,7 @@ export const cleanEmptyKeys = (stateObj: Record) => { export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR'; -export type DashboardAppLocatorParams = Partial< - Omit< - DashboardContainerInput, - 'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally' - > -> & { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: Array; // used SerializableRecord here to force the GridData type to be read as serializable - - /** - * Control group input - */ - controlGroupInput?: SerializableControlGroupInput; -}; - -export type DashboardAppLocator = LocatorPublic; +export type DashboardAppLocator = LocatorPublic; export interface DashboardAppLocatorDependencies { useHashedUrl: boolean; @@ -86,16 +41,16 @@ export interface DashboardAppLocatorDependencies { } export type ForwardedDashboardState = Omit< - DashboardAppLocatorParams, + DashboardLocatorParams, 'dashboardId' | 'preserveSavedFilters' | 'useHash' | 'searchSessionId' >; -export class DashboardAppLocatorDefinition implements LocatorDefinition { +export class DashboardAppLocatorDefinition implements LocatorDefinition { public readonly id = DASHBOARD_APP_LOCATOR; constructor(protected readonly deps: DashboardAppLocatorDependencies) {} - public readonly getLocation = async (params: DashboardAppLocatorParams) => { + public readonly getLocation = async (params: DashboardLocatorParams) => { const { filters, useHash: paramsUseHash, 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 0af39e925730..da4cb0b63739 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 @@ -7,9 +7,9 @@ */ import { Capabilities } from '@kbn/core/public'; +import { DashboardLocatorParams } from '../../../dashboard_container'; import { convertPanelMapToSavedPanels, DashboardContainerInput } from '../../../../common'; -import { DashboardAppLocatorParams } from '../../..'; import { pluginServices } from '../../../services/plugin_services'; import { showPublicUrlSwitch, ShowShareModal, ShowShareModalProps } from './show_share_modal'; @@ -56,7 +56,7 @@ describe('showPublicUrlSwitch', () => { describe('ShowShareModal', () => { const unsavedStateKeys = ['query', 'filters', 'options', 'savedQuery', 'panels'] as Array< - keyof DashboardAppLocatorParams + keyof DashboardLocatorParams >; const toggleShareMenuSpy = jest.spyOn( pluginServices.getServices().share, @@ -83,7 +83,7 @@ describe('ShowShareModal', () => { expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1); const shareLocatorParams = ( toggleShareMenuSpy.mock.calls[0][0].sharingData as { - locatorParams: { params: DashboardAppLocatorParams }; + locatorParams: { params: DashboardLocatorParams }; } ).locatorParams.params; unsavedStateKeys.forEach((key) => { @@ -125,7 +125,7 @@ describe('ShowShareModal', () => { expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1); const shareLocatorParams = ( toggleShareMenuSpy.mock.calls[0][0].sharingData as { - locatorParams: { params: DashboardAppLocatorParams }; + locatorParams: { params: DashboardLocatorParams }; } ).locatorParams.params; const rawDashboardState = { @@ -134,7 +134,7 @@ describe('ShowShareModal', () => { }; unsavedStateKeys.forEach((key) => { expect(shareLocatorParams[key]).toStrictEqual( - (rawDashboardState as unknown as Partial)[key] + (rawDashboardState as unknown as Partial)[key] ); }); }); 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 5147982d66e0..b2d5cd9f4fe9 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 @@ -23,7 +23,8 @@ import { dashboardUrlParams } from '../../dashboard_router'; import { shareModalStrings } from '../../_dashboard_app_strings'; import { pluginServices } from '../../../services/plugin_services'; import { convertPanelMapToSavedPanels } from '../../../../common'; -import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator/locator'; +import { DASHBOARD_APP_LOCATOR } from '../../locator/locator'; +import { DashboardLocatorParams } from '../../../dashboard_container'; const showFilterBarId = 'showFilterBar'; @@ -120,7 +121,7 @@ export function ShowShareModal({ ); }; - let unsavedStateForLocator: DashboardAppLocatorParams = {}; + let unsavedStateForLocator: DashboardLocatorParams = {}; const unsavedDashboardState = dashboardBackup.getState(savedObjectId); if (unsavedDashboardState) { @@ -131,7 +132,7 @@ export function ShowShareModal({ panels: unsavedDashboardState.panels ? (convertPanelMapToSavedPanels( unsavedDashboardState.panels - ) as DashboardAppLocatorParams['panels']) + ) as DashboardLocatorParams['panels']) : undefined, // options @@ -143,7 +144,7 @@ export function ShowShareModal({ }; } - const locatorParams: DashboardAppLocatorParams = { + const locatorParams: DashboardLocatorParams = { dashboardId: savedObjectId, preserveSavedFilters: true, refreshInterval: undefined, // We don't share refresh interval externally diff --git a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts index c66042030bc1..91451b225658 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts @@ -19,10 +19,10 @@ import type { Query } from '@kbn/es-query'; import { SearchSessionInfoProvider } from '@kbn/data-plugin/public'; import { SEARCH_SESSION_ID } from '../../dashboard_constants'; -import { DashboardContainer } from '../../dashboard_container'; +import { DashboardContainer, DashboardLocatorParams } from '../../dashboard_container'; import { convertPanelMapToSavedPanels } from '../../../common'; import { pluginServices } from '../../services/plugin_services'; -import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../locator/locator'; +import { DASHBOARD_APP_LOCATOR } from '../locator/locator'; export const removeSearchSessionIdFromURL = (kbnUrlStateStorage: IKbnUrlStateStorage) => { kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => { @@ -46,7 +46,7 @@ export const getSessionURLObservable = (history: History) => export function createSessionRestorationDataProvider( container: DashboardContainer -): SearchSessionInfoProvider { +): SearchSessionInfoProvider { return { getName: async () => container.getTitle(), getLocatorData: async () => ({ @@ -67,7 +67,7 @@ function getLocatorParams({ }: { container: DashboardContainer; shouldRestoreSearchSession: boolean; -}): DashboardAppLocatorParams { +}): DashboardLocatorParams { const { data: { query: { @@ -101,6 +101,6 @@ function getLocatorParams({ : undefined, panels: lastSavedId ? undefined - : (convertPanelMapToSavedPanels(panels) as DashboardAppLocatorParams['panels']), + : (convertPanelMapToSavedPanels(panels) as DashboardLocatorParams['panels']), }; } 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 0899fa0ebc97..ccdebdb6818c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -31,6 +31,7 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { ControlGroupContainer } from '@kbn/controls-plugin/public'; import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen'; import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public'; @@ -49,13 +50,13 @@ import { DashboardReduxState, DashboardRenderPerformanceStats, } from '../types'; -import { DASHBOARD_CONTAINER_TYPE } from '../..'; import { placePanel } from '../component/panel_placement'; import { pluginServices } from '../../services/plugin_services'; import { initializeDashboard } from './create/create_dashboard'; import { DASHBOARD_APP_ID, DASHBOARD_LOADED_EVENT } from '../../dashboard_constants'; import { DashboardCreationOptions } from './dashboard_container_factory'; import { DashboardAnalyticsService } from '../../services/analytics/types'; +import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE } from '../..'; import { DashboardViewport } from '../component/viewport/dashboard_viewport'; import { DashboardPanelState, DashboardContainerInput } from '../../../common'; import { dashboardContainerReducers } from '../state/dashboard_container_reducers'; @@ -107,6 +108,8 @@ export class DashboardContainer extends Container, 'navigate' | 'getRedirectUrl'>; + // cleanup public stopSyncingWithUnifiedSearch?: () => void; private cleanupStateTools: () => void; 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 8bd064d26801..7537d80fd222 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 @@ -8,48 +8,50 @@ import '../_dashboard_container.scss'; +import classNames from 'classnames'; import React, { - useRef, - useMemo, - useState, - useEffect, forwardRef, + useEffect, useImperativeHandle, useLayoutEffect, + useMemo, + useRef, + useState, } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import classNames from 'classnames'; import useUnmount from 'react-use/lib/useUnmount'; +import { v4 as uuidv4 } from 'uuid'; import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui'; -import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { ErrorEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { DASHBOARD_CONTAINER_TYPE } from '..'; +import { DashboardContainerInput } from '../../../common'; +import type { DashboardContainer } from '../embeddable/dashboard_container'; import { - DashboardAPI, - AwaitingDashboardAPI, - buildApiFromDashboardContainer, -} from './dashboard_api'; -import { - DashboardCreationOptions, DashboardContainerFactory, DashboardContainerFactoryDefinition, + DashboardCreationOptions, } from '../embeddable/dashboard_container_factory'; -import { DashboardRedirect } from '../types'; -import { DASHBOARD_CONTAINER_TYPE } from '..'; -import { DashboardContainerInput } from '../../../common'; -import type { DashboardContainer } from '../embeddable/dashboard_container'; +import { DashboardLocatorParams, DashboardRedirect } from '../types'; import { Dashboard404Page } from './dashboard_404'; +import { + AwaitingDashboardAPI, + buildApiFromDashboardContainer, + DashboardAPI, +} from './dashboard_api'; export interface DashboardRendererProps { savedObjectId?: string; showPlainSpinner?: boolean; dashboardRedirect?: DashboardRedirect; getCreationOptions?: () => Promise; + locator?: Pick, 'navigate' | 'getRedirectUrl'>; } export const DashboardRenderer = forwardRef( - ({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner }, ref) => { + ({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner, locator }, ref) => { const dashboardRoot = useRef(null); const dashboardViewport = useRef(null); const [loading, setLoading] = useState(true); @@ -77,6 +79,11 @@ export const DashboardRenderer = forwardRef 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 diff --git a/src/plugins/dashboard/public/dashboard_container/index.ts b/src/plugins/dashboard/public/dashboard_container/index.ts index 471df33c08e3..d09b5014064d 100644 --- a/src/plugins/dashboard/public/dashboard_container/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/index.ts @@ -21,3 +21,4 @@ export { export { DashboardRenderer } from './external_api/dashboard_renderer'; export type { DashboardAPI, AwaitingDashboardAPI } from './external_api/dashboard_api'; +export type { DashboardLocatorParams } from './types'; diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index b71876b1ea72..26f37d7f7d99 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -6,9 +6,13 @@ * Side Public License, v 1. */ +import { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; import type { ContainerOutput } from '@kbn/embeddable-plugin/public'; import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; +import { SerializableRecord } from '@kbn/utility-types'; + import type { DashboardContainerInput, DashboardOptions } from '../../common'; +import { SavedDashboardPanel } from '../../common/content_management'; export type DashboardReduxState = ReduxEmbeddableState< DashboardContainerInput, @@ -73,3 +77,46 @@ export interface DashboardSaveOptions { onTitleDuplicate: () => void; isTitleDuplicateConfirmed: boolean; } + +export type DashboardLocatorParams = Partial< + Omit< + DashboardContainerInput, + 'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally' + > +> & { + /** + * If given, the dashboard saved object with this id will be loaded. If not given, + * a new, unsaved dashboard will be loaded up. + */ + dashboardId?: string; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; + + /** + * When `true` filters from saved filters from destination dashboard as merged with applied filters + * When `false` applied filters take precedence and override saved filters + * + * true is default + */ + preserveSavedFilters?: boolean; + + /** + * Search search session ID to restore. + * (Background search) + */ + searchSessionId?: string; + + /** + * List of dashboard panels + */ + panels?: Array; // used SerializableRecord here to force the GridData type to be read as serializable + + /** + * Control group input + */ + controlGroupInput?: SerializableControlGroupInput; +}; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 89cc7b1aabed..03cd4e03d52a 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -21,17 +21,14 @@ export { DashboardRenderer, DASHBOARD_CONTAINER_TYPE, type DashboardCreationOptions, + type DashboardLocatorParams, } from './dashboard_container'; export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export { DashboardListingTable } from './dashboard_listing'; export { DashboardTopNav } from './dashboard_top_nav'; -export { - type DashboardAppLocator, - type DashboardAppLocatorParams, - cleanEmptyKeys, -} from './dashboard_app/locator/locator'; -export { getEmbeddableParams } from './dashboard_app/locator/get_dashboard_locator_params'; +export { type DashboardAppLocator, cleanEmptyKeys } from './dashboard_app/locator/locator'; +export { getDashboardLocatorParamsFromEmbeddable } from './dashboard_app/locator/get_dashboard_locator_params'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/services/share/share.stub.ts b/src/plugins/dashboard/public/services/share/share.stub.ts index 70c7e374a393..74a9e143ebb9 100644 --- a/src/plugins/dashboard/public/services/share/share.stub.ts +++ b/src/plugins/dashboard/public/services/share/share.stub.ts @@ -15,6 +15,7 @@ export const shareServiceFactory: ShareServiceFactory = () => { const pluginMock = sharePluginMock.createStartContract(); return { + url: pluginMock.url, toggleShareContextMenu: pluginMock.toggleShareContextMenu, }; }; diff --git a/src/plugins/dashboard/public/services/share/share_services.ts b/src/plugins/dashboard/public/services/share/share_services.ts index b96ba0d8f412..5349a1d6d32d 100644 --- a/src/plugins/dashboard/public/services/share/share_services.ts +++ b/src/plugins/dashboard/public/services/share/share_services.ts @@ -19,9 +19,10 @@ export const shareServiceFactory: ShareServiceFactory = ({ startPlugins }) => { const { share } = startPlugins; if (!share) return {}; - const { toggleShareContextMenu } = share; + const { toggleShareContextMenu, url } = share; return { + url, toggleShareContextMenu, }; }; diff --git a/src/plugins/dashboard/public/services/share/types.ts b/src/plugins/dashboard/public/services/share/types.ts index 5920b4b3bcbc..7b1a180c6e5d 100644 --- a/src/plugins/dashboard/public/services/share/types.ts +++ b/src/plugins/dashboard/public/services/share/types.ts @@ -9,5 +9,6 @@ import { SharePluginStart } from '@kbn/share-plugin/public'; export interface DashboardShareService { + url?: SharePluginStart['url']; toggleShareContextMenu?: SharePluginStart['toggleShareContextMenu']; } diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx index cb2479bfb5f5..a63636d40df2 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx @@ -8,19 +8,30 @@ import React from 'react'; -import userEvent from '@testing-library/user-event'; -import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { getDashboardLocatorParamsFromEmbeddable } from '@kbn/dashboard-plugin/public'; +import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; import { DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS } from '@kbn/presentation-util-plugin/public'; -import { DashboardLinkStrings } from './dashboard_link_strings'; -import { LinksEmbeddable, LinksContext } from '../../embeddable/links_embeddable'; -import { mockLinksPanel } from '../../../common/mocks'; +import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + import { LINKS_VERTICAL_LAYOUT } from '../../../common/content_management'; +import { mockLinksPanel } from '../../../common/mocks'; +import { LinksContext, LinksEmbeddable } from '../../embeddable/links_embeddable'; import { DashboardLinkComponent } from './dashboard_link_component'; -import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools'; -import { coreServices } from '../../services/kibana_services'; +import { DashboardLinkStrings } from './dashboard_link_strings'; +import { fetchDashboard } from './dashboard_link_tools'; jest.mock('./dashboard_link_tools'); +jest.mock('@kbn/dashboard-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/dashboard-plugin/public'); + return { + __esModule: true, + ...originalModule, + getDashboardLocatorParamsFromEmbeddable: jest.fn(), + }; +}); + describe('Dashboard link component', () => { const mockDashboards = [ { @@ -58,25 +69,25 @@ describe('Dashboard link component', () => { const onRender = jest.fn(); let linksEmbeddable: LinksEmbeddable; + let dashboardContainer: DashboardContainer; beforeEach(async () => { window.open = jest.fn(); (fetchDashboard as jest.Mock).mockResolvedValue(mockDashboards[0]); - (getDashboardLocator as jest.Mock).mockResolvedValue({ - app: 'dashboard', - path: '/dashboardItem/456', - state: {}, - }); - (getDashboardHref as jest.Mock).mockReturnValue('https://my-kibana.com/dashboard/123'); linksEmbeddable = await mockLinksPanel({ dashboardExplicitInput: mockDashboards[1].attributes, }); + dashboardContainer = linksEmbeddable.parent as DashboardContainer; + dashboardContainer.locator = { + getRedirectUrl: jest.fn().mockReturnValue('https://my-kibana.com/dashboard/123'), + navigate: jest.fn(), + }; }); afterEach(() => { jest.clearAllMocks(); }); - test('by default uses navigateToApp to open in same tab', async () => { + test('by default uses navigate to open in same tab', async () => { render( { await waitFor(() => expect(onLoading).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); expect(fetchDashboard).toHaveBeenCalledWith(defaultLinkInfo.destination); - expect(getDashboardLocator).toHaveBeenCalledTimes(1); - expect(getDashboardLocator).toHaveBeenCalledWith({ - link: { - ...defaultLinkInfo, - options: DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS, - }, - linksEmbeddable, - }); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); expect(link).toHaveTextContent('another dashboard'); - await userEvent.click(link); - expect(coreServices.application.navigateToApp).toBeCalledTimes(1); - expect(coreServices.application.navigateToApp).toBeCalledWith('dashboard', { - path: '/dashboardItem/456', - state: {}, + userEvent.click(link); + expect(dashboardContainer.locator?.getRedirectUrl).toBeCalledWith({ + dashboardId: '456', }); + expect(dashboardContainer.locator?.navigate).toBeCalledTimes(1); }); test('modified click does not trigger event.preventDefault', async () => { @@ -150,16 +152,15 @@ describe('Dashboard link component', () => { await waitFor(() => expect(onLoading).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); expect(fetchDashboard).toHaveBeenCalledWith(linkInfo.destination); - expect(getDashboardLocator).toHaveBeenCalledWith({ link: linkInfo, linksEmbeddable }); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); expect(link).toBeInTheDocument(); - await userEvent.click(link); - expect(coreServices.application.navigateToApp).toBeCalledTimes(0); + userEvent.click(link); + expect(dashboardContainer.locator?.navigate).toBeCalledTimes(0); expect(window.open).toHaveBeenCalledWith('https://my-kibana.com/dashboard/123', '_blank'); }); - test('passes linkOptions to getDashboardLocator', async () => { + test('passes linkOptions to getDashboardLocatorParamsFromEmbeddable', async () => { const linkInfo = { ...defaultLinkInfo, options: { @@ -181,7 +182,10 @@ describe('Dashboard link component', () => { ); await waitFor(() => expect(onLoading).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); - expect(getDashboardLocator).toHaveBeenCalledWith({ link: linkInfo, linksEmbeddable }); + expect(getDashboardLocatorParamsFromEmbeddable).toHaveBeenCalledWith( + linksEmbeddable, + linkInfo.options + ); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); }); @@ -229,8 +233,8 @@ describe('Dashboard link component', () => { await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--bar'); expect(link).toHaveTextContent('current dashboard'); - await userEvent.click(link); - expect(coreServices.application.navigateToApp).toBeCalledTimes(0); + userEvent.click(link); + expect(dashboardContainer.locator?.navigate).toBeCalledTimes(0); expect(window.open).toBeCalledTimes(0); }); @@ -249,7 +253,7 @@ describe('Dashboard link component', () => { await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); - await userEvent.hover(link); + userEvent.hover(link); const tooltip = await screen.findByTestId('dashboardLink--foo--tooltip'); expect(tooltip).toHaveTextContent('another dashboard'); // title expect(tooltip).toHaveTextContent('something awesome'); // description @@ -276,7 +280,7 @@ describe('Dashboard link component', () => { await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); expect(link).toHaveTextContent(label); - await userEvent.hover(link); + userEvent.hover(link); const tooltip = await screen.findByTestId('dashboardLink--foo--tooltip'); expect(tooltip).toHaveTextContent(label); }); diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx index 38097b478509..5ff2bacaf49f 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx @@ -17,12 +17,15 @@ import { } from '@kbn/presentation-util-plugin/public'; import { EuiButtonEmpty, EuiListGroupItem } from '@elastic/eui'; import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; +import { + DashboardLocatorParams, + getDashboardLocatorParamsFromEmbeddable, +} from '@kbn/dashboard-plugin/public'; import { LINKS_VERTICAL_LAYOUT, LinksLayoutType, Link } from '../../../common/content_management'; -import { coreServices } from '../../services/kibana_services'; import { DashboardLinkStrings } from './dashboard_link_strings'; import { useLinks } from '../../embeddable/links_embeddable'; -import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools'; +import { fetchDashboard } from './dashboard_link_tools'; export const DashboardLinkComponent = ({ link, @@ -103,13 +106,15 @@ export const DashboardLinkComponent = ({ ...link.options, } as DashboardDrilldownOptions; - const locator = await getDashboardLocator({ - link: { ...link, options: linkOptions }, - linksEmbeddable, - }); + const params: DashboardLocatorParams = { + dashboardId: link.destination, + ...getDashboardLocatorParamsFromEmbeddable(linksEmbeddable, linkOptions), + }; + + const locator = dashboardContainer.locator; if (!locator) return; - const href = getDashboardHref(locator); + const href = locator.getRedirectUrl(params); return { href, onClick: async (event: React.MouseEvent) => { @@ -127,11 +132,7 @@ export const DashboardLinkComponent = ({ if (linkOptions.openInNewTab) { window.open(href, '_blank'); } else { - const { app, path, state } = locator; - await coreServices.application.navigateToApp(app, { - path, - state, - }); + locator.navigate(params); } }, }; diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts b/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts index eb51758bd9b6..9081b1781532 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts @@ -8,20 +8,8 @@ import { isEmpty, filter } from 'lodash'; -import { - cleanEmptyKeys, - getEmbeddableParams, - DashboardAppLocatorParams, -} from '@kbn/dashboard-plugin/public'; -import { isFilterPinned } from '@kbn/es-query'; -import { KibanaLocation } from '@kbn/share-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public'; - import { DashboardItem } from '../../embeddable/types'; -import type { LinksEmbeddable } from '../../embeddable'; -import { Link } from '../../../common/content_management'; -import { coreServices, dashboardServices } from '../../services/kibana_services'; +import { dashboardServices } from '../../services/kibana_services'; /** * ---------------------------------- @@ -96,56 +84,3 @@ export const fetchDashboards = async ({ return simplifiedDashboardList; }; - -/** - * ---------------------------------- - * Navigate from one dashboard to another - * ---------------------------------- - */ - -interface GetDashboardLocatorProps { - link: Link & { options: DashboardDrilldownOptions }; - linksEmbeddable: LinksEmbeddable; -} - -/** - * Fetch the locator to use for dashboard navigation - * @param props `GetDashboardLocatorProps` - * @returns The locator to use for dashboard navigation - */ -export const getDashboardLocator = async ({ link, linksEmbeddable }: GetDashboardLocatorProps) => { - const params: DashboardAppLocatorParams = { - dashboardId: link.destination, - ...getEmbeddableParams(linksEmbeddable, link.options), - }; - - const locator = dashboardServices.locator; // TODO: Make this generic as part of https://github.com/elastic/kibana/issues/164748 - if (locator) { - const location: KibanaLocation = await locator.getLocation(params); - return location; - } -}; - -/** - * Get URL for dashboard app - should only be used when relying on native `href` functionality - * @param locator Locator that should be used to get the URL - * @returns A full URL to the dashboard, with all state included - */ -export const getDashboardHref = ({ - app, - path, - state, -}: KibanaLocation): string => { - return coreServices.application.getUrlForApp(app, { - path: setStateToKbnUrl( - '_a', - cleanEmptyKeys({ - query: state.query, - filters: state.filters?.filter((f) => !isFilterPinned(f)), - }), - { useHash: false, storeInHashQuery: true }, - path - ), - absolute: true, - }); -}; diff --git a/src/plugins/links/tsconfig.json b/src/plugins/links/tsconfig.json index ba9b5b67d058..e9814f4e107e 100644 --- a/src/plugins/links/tsconfig.json +++ b/src/plugins/links/tsconfig.json @@ -20,8 +20,6 @@ "@kbn/core-saved-objects-server", "@kbn/saved-objects-plugin", "@kbn/ui-actions-enhanced-plugin", - "@kbn/es-query", - "@kbn/share-plugin", "@kbn/kibana-utils-plugin", "@kbn/utility-types", "@kbn/ui-actions-plugin", diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx index 81dc0ba157a0..3083e41c2dac 100644 --- a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { EuiButton, EuiModal, @@ -29,6 +30,7 @@ import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { SERVICE_NAME } from '../../../../../common/es_fields/apm'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { MergedServiceDashboard } from '..'; interface Props { @@ -48,6 +50,7 @@ export function SaveDashboardModal({ core: { notifications }, } = useApmPluginContext(); const { data: allAvailableDashboards, status } = useDashboardFetcher(); + const history = useHistory(); let defaultOption: EuiComboBoxOptionOption | undefined; @@ -87,7 +90,7 @@ export function SaveDashboardModal({ ) ?? false, }) ); - const onSave = useCallback( + const onClickSave = useCallback( async function () { const [newDashboard] = selectedDashboard; try { @@ -110,6 +113,13 @@ export function SaveDashboardModal({ ? getEditSuccessToastLabels(newDashboard.label) : getLinkSuccessToastLabels(newDashboard.label) ); + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(location.search), + dashboardId: newDashboard.value, + }), + }); reloadCustomDashboards(); } } catch (error) { @@ -136,6 +146,7 @@ export function SaveDashboardModal({ isEditMode, serviceName, currentDashboard, + history, ] ); @@ -167,7 +178,7 @@ export function SaveDashboardModal({ placeholder={i18n.translate( 'xpack.apm.serviceDashboards.selectDashboard.placeholder', { - defaultMessage: 'Select dasbboard', + defaultMessage: 'Select dashboard', } )} singleSelection={{ asPlainText: true }} @@ -222,7 +233,7 @@ export function SaveDashboardModal({ {isEditMode diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx index b0dbda84bb6c..c43d3d289b76 100644 --- a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx @@ -7,21 +7,26 @@ import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { MergedServiceDashboard } from '..'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { callApmApi } from '../../../../services/rest/create_call_apm_api'; export function UnlinkDashboard({ currentDashboard, + defaultDashboard, onRefresh, }: { currentDashboard: MergedServiceDashboard; + defaultDashboard: MergedServiceDashboard; onRefresh: () => void; }) { const [isModalVisible, setIsModalVisible] = useState(false); const { core: { notifications }, } = useApmPluginContext(); + const history = useHistory(); const onConfirm = useCallback( async function () { @@ -31,6 +36,14 @@ export function UnlinkDashboard({ signal: null, }); + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(location.search), + dashboardId: defaultDashboard.dashboardSavedObjectId, + }), + }); + notifications.toasts.addSuccess({ title: i18n.translate( 'xpack.apm.serviceDashboards.unlinkSuccess.toast.title', @@ -63,6 +76,8 @@ export function UnlinkDashboard({ setIsModalVisible, onRefresh, isModalVisible, + history, + defaultDashboard, ] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx index 115b97ad41cc..7dbcbe714a42 100644 --- a/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; +import useMount from 'react-use/lib/useMount'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MergedServiceDashboard } from '.'; @@ -14,30 +15,43 @@ import { fromQuery, toQuery } from '../../shared/links/url_helpers'; interface Props { serviceDashboards: MergedServiceDashboard[]; - currentDashboard?: MergedServiceDashboard; - handleOnChange: (selectedId?: string) => void; + currentDashboardId?: string; + setCurrentDashboard: (newDashboard: MergedServiceDashboard) => void; } export function DashboardSelector({ serviceDashboards, - currentDashboard, - handleOnChange, + currentDashboardId, + setCurrentDashboard, }: Props) { const history = useHistory(); - useEffect( - () => + const [selectedDashboard, setSelectedDashboard] = + useState(); + + useMount(() => { + if (!currentDashboardId) { history.push({ ...history.location, search: fromQuery({ ...toQuery(location.search), - dashboardId: currentDashboard?.id, + dashboardId: serviceDashboards[0].dashboardSavedObjectId, }), - }), - // It should only update when loaded - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + }); + } + }); + + useEffect(() => { + const preselectedDashboard = serviceDashboards.find( + ({ dashboardSavedObjectId }) => + dashboardSavedObjectId === currentDashboardId + ); + // preselect dashboard + if (preselectedDashboard) { + setSelectedDashboard(preselectedDashboard); + setCurrentDashboard(preselectedDashboard); + } + }, [serviceDashboards, currentDashboardId, setCurrentDashboard]); function onChange(newDashboardId?: string) { history.push({ @@ -47,8 +61,8 @@ export function DashboardSelector({ dashboardId: newDashboardId, }), }); - handleOnChange(newDashboardId); } + return ( (); const { data: allAvailableDashboards } = useDashboardFetcher(); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { dataView } = useApmDataView(); + const { share } = useApmPluginContext(); const { data, status, refetch } = useFetcher( (callApmApi) => { @@ -95,15 +100,7 @@ export function ServiceDashboards() { ); setServiceDashboards(filteredServiceDashbords); - - const preselectedDashboard = - filteredServiceDashbords.find( - ({ dashboardSavedObjectId }) => dashboardSavedObjectId === dashboardId - ) ?? filteredServiceDashbords[0]; - - // preselect dashboard - setCurrentDashboard(preselectedDashboard); - }, [allAvailableDashboards, data?.serviceDashboards, dashboardId]); + }, [allAvailableDashboards, data?.serviceDashboards]); const getCreationOptions = useCallback((): Promise => { @@ -138,13 +135,34 @@ export function ServiceDashboards() { rangeTo, ]); - const handleOnChange = (selectedId?: string) => { - setCurrentDashboard( - serviceDashboards?.find( - ({ dashboardSavedObjectId }) => dashboardSavedObjectId === selectedId - ) - ); - }; + const getLocatorParams = useCallback( + (params) => { + return { + serviceName, + dashboardId: params.dashboardId, + query: { + environment, + kuery, + rangeFrom, + rangeTo, + }, + }; + }, + [serviceName, environment, kuery, rangeFrom, rangeTo] + ); + + const locator = useMemo(() => { + const baseLocator = share.url.locators.get(APM_APP_LOCATOR_ID); + if (!baseLocator) return; + + return { + ...baseLocator, + getRedirectUrl: (params: SerializableRecord) => + baseLocator.getRedirectUrl(getLocatorParams(params)), + navigate: (params: SerializableRecord) => + baseLocator.navigate(getLocatorParams(params)), + }; + }, [share, getLocatorParams]); return ( @@ -177,9 +195,9 @@ export function ServiceDashboards() { @@ -199,6 +217,7 @@ export function ServiceDashboards() { />, , ]} @@ -208,9 +227,10 @@ export function ServiceDashboards() { - {currentDashboard && ( + {dashboardId && ( diff --git a/x-pack/plugins/apm/public/locator/helpers.ts b/x-pack/plugins/apm/public/locator/helpers.ts index 40fe4dd68250..8cc727dfa11e 100644 --- a/x-pack/plugins/apm/public/locator/helpers.ts +++ b/x-pack/plugins/apm/public/locator/helpers.ts @@ -13,10 +13,16 @@ import type { TimePickerTimeDefaults } from '../components/shared/date_picker/ty export const APMLocatorPayloadValidator = t.union([ t.type({ serviceName: t.undefined }), + t.intersection([ + t.type({ serviceName: t.string }), + t.type({ dashboardId: t.string }), + t.type({ query: environmentRt }), + ]), t.intersection([ t.type({ serviceName: t.string, }), + t.partial({ dashboardId: t.undefined }), t.partial({ serviceOverviewTab: t.keyof({ traces: null, @@ -65,24 +71,40 @@ export function getPathForServiceDetail( }); } - const mapObj = { - logs: '/services/{serviceName}/logs', - metrics: '/services/{serviceName}/metrics', - traces: '/services/{serviceName}/transactions', - errors: '/services/{serviceName}/errors', - default: '/services/{serviceName}/overview', - } as const; - const apmPath = mapObj[payload.serviceOverviewTab || 'default']; + let path; + if (payload.dashboardId !== undefined) { + const apmPath = '/services/{serviceName}/dashboards'; + path = apmRouter.link(apmPath, { + path: { + serviceName: payload.serviceName, + }, + query: { + ...defaultQueryParams, + ...payload.query, + dashboardId: payload.dashboardId, + }, + }); + return path; + } else { + const mapObj = { + logs: '/services/{serviceName}/logs', + metrics: '/services/{serviceName}/metrics', + traces: '/services/{serviceName}/transactions', + errors: '/services/{serviceName}/errors', + default: '/services/{serviceName}/overview', + } as const; + const apmPath = mapObj[payload.serviceOverviewTab || 'default']; - const query = { - ...defaultQueryParams, - ...payload.query, - }; + const query = { + ...defaultQueryParams, + ...payload.query, + }; - const path = apmRouter.link(apmPath, { - path: { serviceName: payload.serviceName }, - query, - }); + path = apmRouter.link(apmPath, { + path: { serviceName: payload.serviceName }, + query, + }); + } return path; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index 27aad8722294..e3bf4a4d468c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -10,7 +10,7 @@ import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilld import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown'; import { savedObjectsServiceMock } from '@kbn/core/public/mocks'; import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; -import { DashboardAppLocatorParams } from '@kbn/dashboard-plugin/public'; +import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { StartDependencies } from '../../../plugin'; import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public/core'; import { EnhancedEmbeddableContext } from '@kbn/embeddable-enhanced-plugin/public'; @@ -95,7 +95,7 @@ describe('.execute() & getHref', () => { uiActionsEnhanced: {}, dashboard: { locator: { - getLocation: async (params: DashboardAppLocatorParams) => { + getLocation: async (params: DashboardLocatorParams) => { return await definition.getLocation(params); }, }, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 9a984af52d21..0838297ee72e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -8,8 +8,8 @@ import { extractTimeRange, isFilterPinned } from '@kbn/es-query'; import type { KibanaLocation } from '@kbn/share-plugin/public'; import { cleanEmptyKeys, - DashboardAppLocatorParams, - getEmbeddableParams, + DashboardLocatorParams, + getDashboardLocatorParamsFromEmbeddable, } from '@kbn/dashboard-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; @@ -44,12 +44,12 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { - let params: DashboardAppLocatorParams = { dashboardId: config.dashboardId }; + let params: DashboardLocatorParams = { dashboardId: config.dashboardId }; if (context.embeddable) { params = { ...params, - ...getEmbeddableParams(context.embeddable, config), + ...getDashboardLocatorParamsFromEmbeddable(context.embeddable, config), }; } @@ -75,7 +75,7 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown) { + private useUrlForState(location: KibanaLocation) { const state = location.state; location.path = setStateToKbnUrl( '_a', 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 3f4e91e925ee..4b4ce7299277 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 @@ -11,7 +11,7 @@ import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimefilterContract } from '@kbn/data-plugin/public'; import { firstValueFrom } from 'rxjs'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { DashboardAppLocatorParams, DashboardStart } from '@kbn/dashboard-plugin/public'; +import type { DashboardLocatorParams, DashboardStart } from '@kbn/dashboard-plugin/public'; import type { Filter, Query, DataViewBase } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { Embeddable } from '@kbn/lens-plugin/public'; @@ -245,7 +245,7 @@ export class QuickJobCreatorBase { return null; } - const params: DashboardAppLocatorParams = { + const params: DashboardLocatorParams = { dashboardId: foundDashboard.id, timeRange: { from: '$earliest$', 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 73538439de56..9fc53f1a4175 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 @@ -4,19 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import type { DashboardContainerInput } from '@kbn/dashboard-plugin/common'; import type { DashboardAPI, DashboardCreationOptions } from '@kbn/dashboard-plugin/public'; import { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; import { useDispatch } from 'react-redux'; -import { InputsModelId } from '../../common/store/inputs/constants'; -import { inputsActions } from '../../common/store/inputs'; -import { useKibana } from '../../common/lib/kibana'; +import { BehaviorSubject } from 'rxjs'; import { APP_UI_ID } from '../../../common'; +import { DASHBOARDS_PATH, SecurityPageName } from '../../../common/constants'; +import { useGetSecuritySolutionUrl } from '../../common/components/link_to'; +import { useKibana, useNavigateTo } from '../../common/lib/kibana'; +import { inputsActions } from '../../common/store/inputs'; +import { InputsModelId } from '../../common/store/inputs/constants'; import { useSecurityTags } from '../context/dashboard_context'; -import { DASHBOARDS_PATH } from '../../../common/constants'; + +const initialInput = new BehaviorSubject>({}); const DashboardRendererComponent = ({ canReadDashboard, @@ -50,32 +57,66 @@ const DashboardRendererComponent = ({ const dispatch = useDispatch(); const securityTags = useSecurityTags(); + const { navigateTo } = useNavigateTo(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); const firstSecurityTagId = securityTags?.[0]?.id; const isCreateDashboard = !savedObjectId; - const getCreationOptions: () => Promise = useCallback( - () => - Promise.resolve({ - useSessionStorageIntegration: true, - useControlGroupIntegration: true, - getInitialInput: () => ({ - timeRange, - viewMode, - query, - filters, - }), - getIncomingEmbeddable: () => - embeddable.getStateTransfer().getIncomingEmbeddablePackage(APP_UI_ID, true), - getEmbeddableAppContext: (dashboardId?: string) => ({ - getCurrentPath: () => - dashboardId ? `${DASHBOARDS_PATH}/${dashboardId}/edit` : `${DASHBOARDS_PATH}/create`, - currentAppId: APP_UI_ID, - }), - }), - [embeddable, filters, query, timeRange, viewMode] + const getSecuritySolutionDashboardUrl = useCallback( + ({ dashboardId }) => { + return getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.dashboards, + path: dashboardId, + }); + }, + [getSecuritySolutionUrl] ); + const goToDashboard = useCallback( + /** + * Note: Due to the query bar being separate from the portable dashboard, the "Use filters and query from origin + * dashboard" and "Use date range from origin dashboard" Link embeddable settings do not make sense in this context. + * Regardless of these settings, navigation to a different dashboard will **always** keep the query state the same. + * I have chosen to keep this consistent **even when** the dashboard is opened in a new tab. + * + * If we want portable dashboard to interact with the query bar in the same way it does in the dashboard app so these + * settings apply, we would need to refactor this portable dashboard. We might also want to make the security app use + * locators in that refactor, as well - not only would this clean up some tech debt, it would also make it so that + * control selections could also be translated to filter pills on navigation. + */ + async (params) => { + navigateTo({ + url: getSecuritySolutionDashboardUrl(params), + }); + }, + [getSecuritySolutionDashboardUrl, navigateTo] + ); + + const locator = useMemo(() => { + return { + navigate: goToDashboard, + getRedirectUrl: getSecuritySolutionDashboardUrl, + }; + }, [goToDashboard, getSecuritySolutionDashboardUrl]); + + const getCreationOptions: () => Promise = useCallback(() => { + return Promise.resolve({ + useSessionStorageIntegration: true, + useControlGroupIntegration: true, + getInitialInput: () => { + return initialInput.value; + }, + getIncomingEmbeddable: () => + embeddable.getStateTransfer().getIncomingEmbeddablePackage(APP_UI_ID, true), + getEmbeddableAppContext: (dashboardId?: string) => ({ + getCurrentPath: () => + dashboardId ? `${DASHBOARDS_PATH}/${dashboardId}/edit` : `${DASHBOARDS_PATH}/create`, + currentAppId: APP_UI_ID, + }), + }); + }, [embeddable]); + const refetchByForceRefresh = useCallback(() => { dashboardContainer?.forceRefresh(); }, [dashboardContainer]); @@ -104,6 +145,11 @@ const DashboardRendererComponent = ({ dashboardContainer?.updateInput({ tags: [firstSecurityTagId] }); }, [dashboardContainer, firstSecurityTagId, isCreateDashboard]); + useEffect(() => { + /** We need to update the initial input on navigation so that changes to filter pills, queries, etc. get applied */ + initialInput.next({ timeRange, viewMode, query, filters }); + }, [timeRange, viewMode, query, filters]); + /** Dashboard renderer is stored in the state as it's a temporary solution for * https://github.com/elastic/kibana/issues/167751 **/ @@ -114,6 +160,7 @@ const DashboardRendererComponent = ({ useEffect(() => { setDashboardContainerRenderer( { setDashboardContainerRenderer(undefined); }; - }, [getCreationOptions, onDashboardContainerLoaded, refetchByForceRefresh, savedObjectId]); + }, [ + getCreationOptions, + onDashboardContainerLoaded, + refetchByForceRefresh, + savedObjectId, + locator, + ]); return canReadDashboard ? <>{dashboardContainerRenderer} : null; };