diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
index f9f3efdd299f3..00b68c2abc547 100644
--- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
@@ -31,15 +31,15 @@ import {
} from './url/search_sessions_integration';
import { DashboardAPI, DashboardRenderer } from '..';
import { type DashboardEmbedSettings } from './types';
-import { DASHBOARD_APP_ID } from '../dashboard_constants';
import { pluginServices } from '../services/plugin_services';
-import { DashboardTopNav } from './top_nav/dashboard_top_nav';
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 { 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';
export interface DashboardAppProps {
history: History;
@@ -160,6 +160,10 @@ export function DashboardApp({
getInitialInput,
validateLoadedSavedObject: validateOutcome,
isEmbeddedExternally: Boolean(embedSettings), // embed settings are only sent if the dashboard URL has `embed=true`
+ getEmbeddableAppContext: (dashboardId) => ({
+ currentAppId: DASHBOARD_APP_ID,
+ getCurrentPath: () => `#${createDashboardEditUrl(dashboardId)}`,
+ }),
});
}, [
history,
@@ -192,9 +196,11 @@ export function DashboardApp({
{!showNoDataPage && (
<>
{dashboardAPI && (
-
-
-
+
)}
{getLegacyConflictWarning?.()}
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 63dd1d96d1169..0190bbaefa00b 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
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
@@ -21,7 +20,7 @@ import { EditorMenu } from './editor_menu';
import { useDashboardAPI } from '../dashboard_app';
import { pluginServices } from '../../services/plugin_services';
import { ControlsToolbarButton } from './controls_toolbar_button';
-import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
+import { DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
@@ -70,12 +69,13 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
stateTransferService.navigateToEditor(appId, {
path,
state: {
- originatingApp: DASHBOARD_APP_ID,
+ originatingApp: dashboard.getAppContext()?.currentAppId,
+ originatingPath: dashboard.getAppContext()?.getCurrentPath?.(),
searchSessionId: search.session.getSessionId(),
},
});
},
- [stateTransferService, search.session, trackUiMetric]
+ [stateTransferService, dashboard, search.session, trackUiMetric]
);
const createNewEmbeddable = useCallback(
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 643765bdfbab6..9c5aedb76c147 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
@@ -26,10 +26,12 @@ export const useDashboardMenuItems = ({
redirectTo,
isLabsShown,
setIsLabsShown,
+ showResetChange,
}: {
redirectTo: DashboardRedirect;
isLabsShown: boolean;
setIsLabsShown: Dispatch>;
+ showResetChange?: boolean;
}) => {
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
@@ -276,32 +278,56 @@ export const useDashboardMenuItems = ({
const shareMenuItem = share ? [menuItems.share] : [];
const cloneMenuItem = showWriteControls ? [menuItems.clone] : [];
const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : [];
+ const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : [];
+
return [
...labsMenuItem,
menuItems.fullScreen,
...shareMenuItem,
...cloneMenuItem,
- resetChangesMenuItem,
+ ...mayberesetChangesMenuItem,
...editMenuItem,
];
- }, [isLabsEnabled, menuItems, share, showWriteControls, managed, resetChangesMenuItem]);
+ }, [
+ isLabsEnabled,
+ menuItems,
+ share,
+ showWriteControls,
+ managed,
+ showResetChange,
+ resetChangesMenuItem,
+ ]);
const editModeTopNavConfig = useMemo(() => {
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
const shareMenuItem = share ? [menuItems.share] : [];
const editModeItems: TopNavMenuData[] = [];
+
if (lastSavedId) {
- editModeItems.push(
- menuItems.saveAs,
- menuItems.switchToViewMode,
- resetChangesMenuItem,
- menuItems.quickSave
- );
+ editModeItems.push(menuItems.saveAs, menuItems.switchToViewMode);
+
+ if (showResetChange) {
+ editModeItems.push(resetChangesMenuItem);
+ }
+
+ editModeItems.push(menuItems.quickSave);
} else {
editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs);
}
return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems];
- }, [lastSavedId, menuItems, share, resetChangesMenuItem, isLabsEnabled]);
+ }, [
+ isLabsEnabled,
+ menuItems.labs,
+ menuItems.share,
+ menuItems.settings,
+ menuItems.saveAs,
+ menuItems.switchToViewMode,
+ menuItems.quickSave,
+ share,
+ lastSavedId,
+ showResetChange,
+ resetChangesMenuItem,
+ ]);
return { viewModeTopNavConfig, editModeTopNavConfig };
};
diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx
index 050de4189c279..8767b5abe3567 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx
@@ -24,7 +24,7 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import { pluginServices } from '../../../services/plugin_services';
import { emptyScreenStrings } from '../../_dashboard_container_strings';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
-import { DASHBOARD_UI_METRIC_ID, DASHBOARD_APP_ID } from '../../../dashboard_constants';
+import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants';
export function DashboardEmptyScreen() {
const {
@@ -44,6 +44,14 @@ export function DashboardEmptyScreen() {
[getVisTypeAliases]
);
+ const dashboardContainer = useDashboardContainer();
+ const isDarkTheme = useObservable(theme$)?.darkMode;
+ const isEditMode =
+ dashboardContainer.select((state) => state.explicitInput.viewMode) === ViewMode.EDIT;
+ const embeddableAppContext = dashboardContainer.getAppContext();
+ const originatingPath = embeddableAppContext?.getCurrentPath?.() ?? '';
+ const originatingApp = embeddableAppContext?.currentAppId;
+
const goToLens = useCallback(() => {
if (!lensAlias || !lensAlias.aliasPath) return;
const trackUiMetric = usageCollection.reportUiCounter?.bind(
@@ -57,16 +65,19 @@ export function DashboardEmptyScreen() {
getStateTransfer().navigateToEditor(lensAlias.aliasApp, {
path: lensAlias.aliasPath,
state: {
- originatingApp: DASHBOARD_APP_ID,
+ originatingApp,
+ originatingPath,
searchSessionId: search.session.getSessionId(),
},
});
- }, [getStateTransfer, lensAlias, search.session, usageCollection]);
-
- const dashboardContainer = useDashboardContainer();
- const isDarkTheme = useObservable(theme$)?.darkMode;
- const isEditMode =
- dashboardContainer.select((state) => state.explicitInput.viewMode) === ViewMode.EDIT;
+ }, [
+ getStateTransfer,
+ lensAlias,
+ originatingApp,
+ originatingPath,
+ search.session,
+ usageCollection,
+ ]);
// TODO replace these SVGs with versions from EuiIllustration as soon as it becomes available.
const imageUrl = basePath.prepend(
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 4c88d246f6ca3..0899fa0ebc97e 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -53,7 +53,7 @@ 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_LOADED_EVENT } from '../../dashboard_constants';
+import { DASHBOARD_APP_ID, DASHBOARD_LOADED_EVENT } from '../../dashboard_constants';
import { DashboardCreationOptions } from './dashboard_container_factory';
import { DashboardAnalyticsService } from '../../services/analytics/types';
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
@@ -107,7 +107,6 @@ export class DashboardContainer extends Container void;
private cleanupStateTools: () => void;
@@ -185,6 +184,16 @@ export class DashboardContainer extends Container 'valid' | 'invalid' | 'redirected';
isEmbeddedExternally?: boolean;
+
+ getEmbeddableAppContext?: (dashboardId?: string) => EmbeddableAppContext;
}
export class DashboardContainerFactoryDefinition
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/_dashboard_top_nav.scss b/src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
similarity index 100%
rename from src/plugins/dashboard/public/dashboard_app/top_nav/_dashboard_top_nav.scss
rename to src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
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
new file mode 100644
index 0000000000000..ed2a7426697c1
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { DashboardAPIContext } from '../dashboard_app/dashboard_app';
+import { DashboardContainer } from '../dashboard_container';
+import {
+ InternalDashboardTopNav,
+ InternalDashboardTopNavProps,
+} from './internal_dashboard_top_nav';
+export interface DashboardTopNavProps extends InternalDashboardTopNavProps {
+ dashboardContainer: DashboardContainer;
+}
+
+export const DashboardTopNavWithContext = (props: DashboardTopNavProps) => (
+
+
+
+);
+
+// eslint-disable-next-line import/no-default-export
+export default DashboardTopNavWithContext;
diff --git a/src/plugins/dashboard/public/dashboard_top_nav/index.tsx b/src/plugins/dashboard/public/dashboard_top_nav/index.tsx
new file mode 100644
index 0000000000000..d0cfc496fcc3f
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_top_nav/index.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+import React, { Suspense } from 'react';
+import { servicesReady } from '../plugin';
+import { DashboardTopNavProps } from './dashboard_top_nav_with_context';
+
+const LazyDashboardTopNav = React.lazy(() =>
+ (async () => {
+ const modulePromise = import('./dashboard_top_nav_with_context');
+ const [module] = await Promise.all([modulePromise, servicesReady]);
+
+ return {
+ default: module.DashboardTopNavWithContext,
+ };
+ })().then((module) => module)
+);
+
+export const DashboardTopNav = (props: DashboardTopNavProps) => {
+ return (
+ }>
+
+
+ );
+};
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
similarity index 78%
rename from src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx
rename to src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
index 67c51a19052d9..c2e0e273a572e 100644
--- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx
+++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
@@ -18,36 +18,49 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
import { EuiHorizontalRule, EuiIcon, EuiToolTipProps } from '@elastic/eui';
-
+import { EuiBreadcrumbProps } from '@elastic/eui/src/components/breadcrumbs/breadcrumb';
+import { MountPoint } from '@kbn/core/public';
import {
getDashboardTitle,
leaveConfirmStrings,
getDashboardBreadcrumb,
unsavedChangesBadgeStrings,
dashboardManagedBadge,
-} from '../_dashboard_app_strings';
-import { UI_SETTINGS } from '../../../common';
-import { useDashboardAPI } from '../dashboard_app';
-import { DashboardEmbedSettings } from '../types';
-import { pluginServices } from '../../services/plugin_services';
-import { useDashboardMenuItems } from './use_dashboard_menu_items';
-import { DashboardRedirect } from '../../dashboard_container/types';
-import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
-import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
-import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
-
+} 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';
+import { DashboardEditingToolbar } from '../dashboard_app/top_nav/dashboard_editing_toolbar';
+import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount_context';
+import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants';
import './_dashboard_top_nav.scss';
-export interface DashboardTopNavProps {
+import { DashboardRedirect } from '../dashboard_container/types';
+
+export interface InternalDashboardTopNavProps {
+ customLeadingBreadCrumbs?: EuiBreadcrumbProps[];
embedSettings?: DashboardEmbedSettings;
+ forceHideUnifiedSearch?: boolean;
redirectTo: DashboardRedirect;
+ setCustomHeaderActionMenu?: (menuMount: MountPoint | undefined) => void;
+ showBorderBottom?: boolean;
+ showResetChange?: boolean;
}
const LabsFlyout = withSuspense(LazyLabsFlyout, null);
-export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavProps) {
+export function InternalDashboardTopNav({
+ customLeadingBreadCrumbs = [],
+ embedSettings,
+ forceHideUnifiedSearch,
+ redirectTo,
+ setCustomHeaderActionMenu,
+ showBorderBottom = true,
+ showResetChange = true,
+}: InternalDashboardTopNavProps) {
const [isChromeVisible, setIsChromeVisible] = useState(false);
const [isLabsShown, setIsLabsShown] = useState(false);
-
const dashboardTitleRef = useRef(null);
/**
@@ -168,19 +181,33 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
// set only the dashboardTitleBreadcrumbs because the main breadcrumbs automatically come as part of the navigation config
serverless.setBreadcrumbs(dashboardTitleBreadcrumbs);
} else {
- // non-serverless regular breadcrumbs
- setBreadcrumbs([
- {
- text: getDashboardBreadcrumb(),
- 'data-test-subj': 'dashboardListingBreadcrumb',
- onClick: () => {
- redirectTo({ destination: 'listing' });
+ /**
+ * non-serverless regular breadcrumbs
+ * Dashboard embedded in other plugins (e.g. SecuritySolution)
+ * will have custom leading breadcrumbs for back to their app.
+ **/
+ setBreadcrumbs(
+ customLeadingBreadCrumbs.concat([
+ {
+ text: getDashboardBreadcrumb(),
+ 'data-test-subj': 'dashboardListingBreadcrumb',
+ onClick: () => {
+ redirectTo({ destination: 'listing' });
+ },
},
- },
- ...dashboardTitleBreadcrumbs,
- ]);
+ ...dashboardTitleBreadcrumbs,
+ ])
+ );
}
- }, [setBreadcrumbs, redirectTo, dashboardTitle, dashboard, viewMode, serverless]);
+ }, [
+ setBreadcrumbs,
+ redirectTo,
+ dashboardTitle,
+ dashboard,
+ viewMode,
+ serverless,
+ customLeadingBreadCrumbs,
+ ]);
/**
* Build app leave handler whenever hasUnsavedChanges changes
@@ -205,12 +232,6 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
};
}, [onAppLeave, getStateTransfer, hasUnsavedChanges, viewMode]);
- const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
- redirectTo,
- isLabsShown,
- setIsLabsShown,
- });
-
const visibilityProps = useMemo(() => {
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
(forceShow || isChromeVisible) && !fullScreenMode;
@@ -218,14 +239,17 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
!forceHide && (filterManager.getFilters().length > 0 || !fullScreenMode);
const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
- const showQueryInput = shouldShowNavBarComponent(
- Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
- );
- const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
+ const showQueryInput = Boolean(forceHideUnifiedSearch)
+ ? false
+ : shouldShowNavBarComponent(
+ Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
+ );
+ const showDatePicker = Boolean(forceHideUnifiedSearch)
+ ? false
+ : shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar));
const showQueryBar = showQueryInput || showDatePicker || showFilterBar;
const showSearchBar = showQueryBar || showFilterBar;
-
return {
showTopNavMenu,
showSearchBar,
@@ -233,7 +257,21 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
showQueryInput,
showDatePicker,
};
- }, [embedSettings, filterManager, fullScreenMode, isChromeVisible, viewMode]);
+ }, [
+ embedSettings,
+ filterManager,
+ forceHideUnifiedSearch,
+ fullScreenMode,
+ isChromeVisible,
+ viewMode,
+ ]);
+
+ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
+ redirectTo,
+ isLabsShown,
+ setIsLabsShown,
+ showResetChange,
+ });
UseUnmount(() => {
dashboard.clearOverlays();
@@ -301,7 +339,11 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
appName={LEGACY_DASHBOARD_APP_ID}
visible={viewMode !== ViewMode.PRINT}
- setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
+ setMenuMountPoint={
+ embedSettings || fullScreenMode
+ ? setCustomHeaderActionMenu ?? undefined
+ : setHeaderActionMenu
+ }
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
config={
visibilityProps.showTopNavMenu
@@ -327,7 +369,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
{viewMode === ViewMode.EDIT ? (
) : null}
-
+ {showBorderBottom && }
);
}
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index 6882090df441a..89cc7b1aabed8 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -25,7 +25,7 @@ export {
export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin';
export { DashboardListingTable } from './dashboard_listing';
-
+export { DashboardTopNav } from './dashboard_top_nav';
export {
type DashboardAppLocator,
type DashboardAppLocatorParams,
diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
index 3d823c215498b..93279e311b065 100644
--- a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
@@ -49,14 +49,7 @@ const getEventStatus = (output: EmbeddableOutput): EmbeddablePhase => {
};
export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
- const {
- hideHeader,
- showShadow,
- embeddable,
- hideInspector,
- containerContext,
- onPanelStatusChange,
- } = panelProps;
+ const { hideHeader, showShadow, embeddable, hideInspector, onPanelStatusChange } = panelProps;
const [node, setNode] = useState();
const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []);
@@ -74,8 +67,7 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
const editPanel = new EditPanelAction(
embeddableStart.getEmbeddableFactory,
core.application,
- stateTransfer,
- containerContext?.getCurrentPath
+ stateTransfer
);
const actions: PanelUniversalActions = {
@@ -91,7 +83,7 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
};
if (!hideInspector) actions.inspectPanel = new InspectPanelAction(inspector);
return actions;
- }, [containerContext?.getCurrentPath, hideInspector]);
+ }, [hideInspector]);
/**
* Track panel status changes
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
index a1b1bf4df5ec4..33b1cc15a55bc 100644
--- a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
@@ -46,12 +46,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn
test('redirects to app using state transfer', async () => {
applicationMock.currentAppId$ = of('superCoolCurrentApp');
const testPath = '/test-path';
- const action = new EditPanelAction(
- getFactory,
- applicationMock,
- stateTransferMock,
- () => testPath
- );
+ const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
const embeddable = new EditableEmbeddable(
{
id: '123',
@@ -62,6 +57,9 @@ test('redirects to app using state transfer', async () => {
true
);
embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' }));
+ embeddable.getAppContext = jest.fn().mockReturnValue({
+ getCurrentPath: () => testPath,
+ });
await action.execute({ embeddable });
expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', {
path: '/123',
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
index fe55b9a39158b..32e9fbac493aa 100644
--- a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
@@ -45,8 +45,7 @@ export class EditPanelAction implements Action {
constructor(
private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'],
private readonly application: ApplicationStart,
- private readonly stateTransfer?: EmbeddableStateTransfer,
- private readonly getOriginatingPath?: () => string
+ private readonly stateTransfer?: EmbeddableStateTransfer
) {
if (this.application?.currentAppId$) {
this.application.currentAppId$
@@ -139,7 +138,7 @@ export class EditPanelAction implements Action {
if (app && path) {
if (this.currentAppId) {
- const originatingPath = this.getOriginatingPath?.();
+ const originatingPath = embeddable.getAppContext()?.getCurrentPath?.();
const state: EmbeddableEditorState = {
originatingApp: this.currentAppId,
diff --git a/src/plugins/embeddable/public/embeddable_panel/types.ts b/src/plugins/embeddable/public/embeddable_panel/types.ts
index 9a1b17d4a3a4d..03e29810d4056 100644
--- a/src/plugins/embeddable/public/embeddable_panel/types.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/types.ts
@@ -19,11 +19,12 @@ import {
import { EmbeddableError } from '../lib/embeddables/i_embeddable';
import { EmbeddableContext, EmbeddableInput, EmbeddableOutput, IEmbeddable } from '..';
-export interface EmbeddableContainerContext {
+export interface EmbeddableAppContext {
/**
* Current app's path including query and hash starting from {appId}
*/
getCurrentPath?: () => string;
+ currentAppId?: string;
}
/**
@@ -53,7 +54,6 @@ export interface EmbeddablePanelProps {
hideHeader?: boolean;
hideInspector?: boolean;
showNotifications?: boolean;
- containerContext?: EmbeddableContainerContext;
actionPredicate?: (actionId: string) => boolean;
onPanelStatusChange?: (info: EmbeddablePhaseEvent) => void;
getActions?: UiActionsService['getTriggerCompatibleActions'];
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index 0e3650ea8a8a4..ebe3b1a11af03 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -100,7 +100,7 @@ export {
export type {
EmbeddablePhase,
EmbeddablePhaseEvent,
- EmbeddableContainerContext,
+ EmbeddableAppContext,
} from './embeddable_panel/types';
export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index d145bfb3c1ae0..ac1c8462b5bf3 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -17,6 +17,7 @@ import { IContainer } from '../containers';
import { EmbeddableError, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { EmbeddableInput, ViewMode } from '../../../common/types';
import { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
+import { EmbeddableAppContext } from '../../embeddable_panel/types';
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
if (input.hidePanelTitles) return '';
@@ -102,6 +103,10 @@ export abstract class Embeddable<
.subscribe((title) => this.renderComplete.setTitle(title));
}
+ public getAppContext(): EmbeddableAppContext | undefined {
+ return this.parent?.getAppContext();
+ }
+
public reportsEmbeddableLoad() {
return false;
}
diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
index f371208271623..92d0309688e76 100644
--- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
@@ -11,6 +11,7 @@ import { ErrorLike } from '@kbn/expressions-plugin/common';
import { Adapters } from '../types';
import { IContainer } from '../containers/i_container';
import { EmbeddableInput } from '../../../common/types';
+import { EmbeddableAppContext } from '../../embeddable_panel/types';
export type EmbeddableError = ErrorLike;
export type { EmbeddableInput };
@@ -181,6 +182,11 @@ export interface IEmbeddable<
*/
getRoot(): IEmbeddable | IContainer;
+ /**
+ * Returns the context of this embeddable's container, or undefined.
+ */
+ getAppContext(): EmbeddableAppContext | undefined;
+
/**
* Renders the embeddable at the given node.
* @param domNode
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
index 51c6cdc54131d..ec602510fd9f0 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
@@ -17,13 +17,13 @@ import {
isErrorEmbeddable,
EmbeddablePanel,
} from '@kbn/embeddable-plugin/public';
-import type { EmbeddableContainerContext } from '@kbn/embeddable-plugin/public';
+import type { EmbeddableAppContext } from '@kbn/embeddable-plugin/public';
import { StartDeps } from '../../plugin';
import { EmbeddableExpression } from '../../expression_types/embeddable';
import { RendererStrings } from '../../../i18n';
import { embeddableInputToExpression } from './embeddable_input_to_expression';
import { RendererFactory, EmbeddableInput } from '../../../types';
-import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
+import { CANVAS_APP, CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
const { embeddable: strings } = RendererStrings;
@@ -41,18 +41,19 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
return null;
}
- const embeddableContainerContext: EmbeddableContainerContext = {
+ const canvasAppContext: EmbeddableAppContext = {
getCurrentPath: () => {
const urlToApp = core.application.getUrlForApp(currentAppId);
const inAppPath = window.location.pathname.replace(urlToApp, '');
return inAppPath + window.location.search + window.location.hash;
},
+ currentAppId: CANVAS_APP,
};
- return (
-
- );
+ embeddable.getAppContext = () => canvasAppContext;
+
+ return ;
};
return (embeddableObject: IEmbeddable) => {
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index 0150d922c481f..1817a3eaa8175 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -794,18 +794,14 @@ export class Embeddable
* Used for the Edit in Lens link inside the inline editing flyout.
*/
private async navigateToLensEditor() {
- const executionContext = this.getExecutionContext();
+ const appContext = this.getAppContext();
/**
* The origininating app variable is very important for the Save and Return button
* of the editor to work properly.
- * The best way to get it dynamically is from the execution context but for the dashboard
- * it needs to be pluralized
*/
const transferState = {
- originatingApp:
- executionContext?.type === 'dashboard'
- ? 'dashboards'
- : executionContext?.type ?? 'dashboards',
+ originatingApp: appContext?.currentAppId ?? 'dashboards',
+ originatingPath: appContext?.getCurrentPath?.(),
valueInput: this.getExplicitInput(),
embeddableId: this.id,
searchSessionId: this.getInput().searchSessionId,
@@ -818,6 +814,7 @@ export class Embeddable
await transfer.navigateToEditor(APP_ID, {
path: this.output.editPath,
state: transferState,
+ skipAppLeave: true,
});
}
}
diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx
index 4b9941b1cbe5d..fcf4fd666a58a 100644
--- a/x-pack/plugins/security_solution/public/app/app.tsx
+++ b/x-pack/plugins/security_solution/public/app/app.tsx
@@ -38,7 +38,6 @@ interface StartAppComponent {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
theme$: AppMountParameters['theme$'];
}
@@ -46,7 +45,6 @@ interface StartAppComponent {
const StartAppComponent: FC = ({
children,
history,
- setHeaderActionMenu,
onAppLeave,
store,
theme$,
@@ -79,11 +77,7 @@ const StartAppComponent: FC = ({
>
-
+
{children}
@@ -113,7 +107,6 @@ interface SecurityAppComponentProps {
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
services: StartServices;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
theme$: AppMountParameters['theme$'];
}
@@ -123,7 +116,6 @@ const SecurityAppComponent: React.FC = ({
history,
onAppLeave,
services,
- setHeaderActionMenu,
store,
theme$,
}) => {
@@ -137,13 +129,7 @@ const SecurityAppComponent: React.FC = ({
}}
>
-
+
{children}
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
index fe0b5bd500dc8..bfce21c47867c 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
@@ -49,7 +49,6 @@ jest.mock('react-reverse-portal', () => ({
}));
describe('global header', () => {
- const mockSetHeaderActionMenu = jest.fn();
const state = {
...mockGlobalState,
timeline: {
@@ -75,7 +74,7 @@ describe('global header', () => {
]);
const { getByText } = render(
-
+
);
expect(getByText('Add integrations')).toBeInTheDocument();
@@ -87,7 +86,7 @@ describe('global header', () => {
]);
const { queryByTestId } = render(
-
+
);
const link = queryByTestId('add-data');
@@ -98,7 +97,7 @@ describe('global header', () => {
(useLocation as jest.Mock).mockReturnValue({ pathname: THREAT_INTELLIGENCE_PATH });
const { queryByTestId } = render(
-
+
);
const link = queryByTestId('add-data');
@@ -118,7 +117,7 @@ describe('global header', () => {
);
const { queryByTestId } = render(
-
+
);
const link = queryByTestId('add-data');
@@ -130,7 +129,7 @@ describe('global header', () => {
const { getByTestId } = render(
-
+
);
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
@@ -141,7 +140,7 @@ describe('global header', () => {
const { getByTestId } = render(
-
+
);
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
@@ -166,7 +165,7 @@ describe('global header', () => {
const { queryByTestId } = render(
-
+
);
@@ -180,7 +179,7 @@ describe('global header', () => {
const { findByTestId } = render(
-
+
);
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
index bde0b71a43270..e5a12721a6292 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
@@ -15,11 +15,10 @@ import { useLocation } from 'react-router-dom';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { i18n } from '@kbn/i18n';
-import type { AppMountParameters } from '@kbn/core/public';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { toMountPoint } from '@kbn/react-kibana-mount';
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
import { useKibana } from '../../../common/lib/kibana';
-import { isDetectionsPath } from '../../../helpers';
+import { isDetectionsPath, isDashboardViewPath } from '../../../helpers';
import { Sourcerer } from '../../../common/components/sourcerer';
import { TimelineId } from '../../../../common/types/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
@@ -37,63 +36,69 @@ const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.butt
* This component uses the reverse portal to add the Add Data, ML job settings, and AI Assistant buttons on the
* right hand side of the Kibana global header
*/
-export const GlobalHeader = React.memo(
- ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
- const portalNode = useMemo(() => createHtmlPortalNode(), []);
- const { theme } = useKibana().services;
- const { pathname } = useLocation();
+export const GlobalHeader = React.memo(() => {
+ const portalNode = useMemo(() => createHtmlPortalNode(), []);
+ const { theme, setHeaderActionMenu, i18n: kibanaServiceI18n } = useKibana().services;
+ const { pathname } = useLocation();
- const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
- const showTimeline = useShallowEqualSelector(
- (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
- );
+ const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+ const showTimeline = useShallowEqualSelector(
+ (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
+ );
- const sourcererScope = getScopeFromPath(pathname);
- const showSourcerer = showSourcererByPath(pathname);
+ const sourcererScope = getScopeFromPath(pathname);
+ const showSourcerer = showSourcererByPath(pathname);
+ const dashboardViewPath = isDashboardViewPath(pathname);
- const { href, onClick } = useAddIntegrationsUrl();
+ const { href, onClick } = useAddIntegrationsUrl();
- useEffect(() => {
- setHeaderActionMenu((element) => {
- const mount = toMountPoint(, { theme$: theme.theme$ });
- return mount(element);
+ useEffect(() => {
+ setHeaderActionMenu((element) => {
+ const mount = toMountPoint(, {
+ theme,
+ i18n: kibanaServiceI18n,
});
+ return mount(element);
+ });
- return () => {
- portalNode.unmount();
- setHeaderActionMenu(undefined);
- };
- }, [portalNode, setHeaderActionMenu, theme.theme$]);
-
- return (
-
-
- {isDetectionsPath(pathname) && (
-
-
-
- )}
+ return () => {
+ /* Dashboard mounts an edit toolbar, it should be restored when leaving dashboard editing page */
+ if (dashboardViewPath) {
+ return;
+ }
+ portalNode.unmount();
+ setHeaderActionMenu(undefined);
+ };
+ }, [portalNode, setHeaderActionMenu, theme, kibanaServiceI18n, dashboardViewPath]);
+ return (
+
+
+ {isDetectionsPath(pathname) && (
-
-
- {BUTTON_ADD_DATA}
-
- {showSourcerer && !showTimeline && (
-
- )}
-
-
+
-
-
- );
- }
-);
+ )}
+
+
+
+
+ {BUTTON_ADD_DATA}
+
+ {showSourcerer && !showTimeline && (
+
+ )}
+
+
+
+
+
+ );
+});
GlobalHeader.displayName = 'GlobalHeader';
diff --git a/x-pack/plugins/security_solution/public/app/home/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/index.test.tsx
index 9bce539a0b311..fc62a8236f9cc 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.test.tsx
@@ -106,6 +106,7 @@ jest.mock('../../timelines/store/timeline', () => ({
const mockedFilterManager = new FilterManager(coreMock.createStart().uiSettings);
const mockGetSavedQuery = jest.fn();
+const mockSetHeaderActionMenu = jest.fn();
const dummyFilter: Filter = {
meta: {
@@ -198,6 +199,7 @@ jest.mock('../../common/lib/kibana', () => {
savedQueries: { getSavedQuery: mockGetSavedQuery },
},
},
+ setHeaderActionMenu: mockSetHeaderActionMenu,
},
}),
KibanaServices: {
@@ -226,7 +228,7 @@ describe('HomePage', () => {
it('calls useInitializeUrlParam for appQuery, filters and savedQuery', () => {
render(
-
+
@@ -252,7 +254,7 @@ describe('HomePage', () => {
render(
-
+
@@ -294,7 +296,7 @@ describe('HomePage', () => {
render(
-
+
@@ -326,7 +328,7 @@ describe('HomePage', () => {
render(
-
+
@@ -361,7 +363,7 @@ describe('HomePage', () => {
render(
-
+
@@ -378,7 +380,7 @@ describe('HomePage', () => {
render(
-
+
@@ -420,7 +422,7 @@ describe('HomePage', () => {
render(
-
+
@@ -465,7 +467,7 @@ describe('HomePage', () => {
render(
-
+
@@ -515,7 +517,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
@@ -572,7 +574,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
@@ -612,7 +614,7 @@ describe('HomePage', () => {
render(
-
+
@@ -637,7 +639,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
@@ -669,7 +671,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index b951501b16cb7..bded1d58c8d84 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
-import type { AppMountParameters } from '@kbn/core/public';
import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { SecuritySolutionAppWrapper } from '../../common/components/page';
@@ -33,10 +32,9 @@ import { AssistantOverlay } from '../../assistant/overlay';
interface HomePageProps {
children: React.ReactNode;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const HomePageComponent: React.FC = ({ children, setHeaderActionMenu }) => {
+const HomePageComponent: React.FC = ({ children }) => {
const { pathname } = useLocation();
useInitSourcerer(getScopeFromPath(pathname));
useUrlState();
@@ -58,7 +56,7 @@ const HomePageComponent: React.FC = ({ children, setHeaderActionM
<>
-
+
{children}
diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx
index e635c2d9fd3d3..6f0fc3eb8d01c 100644
--- a/x-pack/plugins/security_solution/public/app/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/index.tsx
@@ -16,7 +16,6 @@ export const renderApp = ({
element,
history,
onAppLeave,
- setHeaderActionMenu,
services,
store,
usageCollection,
@@ -31,7 +30,6 @@ export const renderApp = ({
history={history}
onAppLeave={onAppLeave}
services={services}
- setHeaderActionMenu={setHeaderActionMenu}
store={store}
theme$={theme$}
>
diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx
index af5aaba76363f..73fe2615b0e5a 100644
--- a/x-pack/plugins/security_solution/public/app/routes.tsx
+++ b/x-pack/plugins/security_solution/public/app/routes.tsx
@@ -10,7 +10,7 @@ import type { FC } from 'react';
import React, { memo, useEffect } from 'react';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { useDispatch } from 'react-redux';
-import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public';
+import type { AppLeaveHandler } from '@kbn/core/public';
import { APP_ID } from '../../common/constants';
import { RouteCapture } from '../common/components/endpoint/route_capture';
@@ -24,15 +24,9 @@ interface RouterProps {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const PageRouterComponent: FC = ({
- children,
- history,
- onAppLeave,
- setHeaderActionMenu,
-}) => {
+const PageRouterComponent: FC = ({ children, history, onAppLeave }) => {
const { cases } = useKibana().services;
const CasesContext = cases.ui.getCasesContext();
const userCasesPermissions = useGetUserCasesPermissions();
@@ -55,7 +49,7 @@ const PageRouterComponent: FC = ({
- {children}
+ {children}
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts
rename to x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx
diff --git a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts
index d7401ff19d916..9d13b857c155e 100644
--- a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts
@@ -34,3 +34,14 @@ export const getTagsByName = jest
export const createTag = jest
.fn()
.mockImplementation(() => Promise.resolve(DEFAULT_CREATE_TAGS_RESPONSE[0]));
+
+export const fetchTags = jest.fn().mockImplementation(({ tagIds }: { tagIds: string[] }) =>
+ Promise.resolve(
+ tagIds.map((id, i) => ({
+ id,
+ name: `${MOCK_TAG_NAME}-${i}`,
+ description: 'test tag description',
+ color: '#2c7b8',
+ }))
+ )
+);
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
index 9c78db17abf37..7c9d7c5f0656c 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
@@ -121,6 +121,7 @@ export const createStartServicesMock = (
const cloudExperiments = cloudExperimentsMock.createStartMock();
const guidedOnboarding = guidedOnboardingMock.createStart();
const cloud = cloudMock.createStart();
+ const mockSetHeaderActionMenu = jest.fn();
return {
...core,
@@ -220,6 +221,7 @@ export const createStartServicesMock = (
customDataService,
uiActions: uiActionsPluginMock.createStartContract(),
savedSearch: savedSearchPluginMock.createStartContract(),
+ setHeaderActionMenu: mockSetHeaderActionMenu,
} as unknown as StartServices;
};
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx
index c01f07fa36653..9c6df7bb6e395 100644
--- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx
@@ -12,13 +12,10 @@ import { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-
import { TestProviders } from '../../common/mock';
import { DashboardRenderer } from './dashboard_renderer';
-jest.mock('@kbn/dashboard-plugin/public', () => {
- const actual = jest.requireActual('@kbn/dashboard-plugin/public');
- return {
- ...actual,
- DashboardRenderer: jest.fn().mockReturnValue(),
- };
-});
+jest.mock('@kbn/dashboard-plugin/public', () => ({
+ DashboardRenderer: jest.fn().mockReturnValue(),
+ DashboardTopNav: jest.fn().mockReturnValue(),
+}));
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
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 aa51842a33c1e..73538439de568 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
@@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
-import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+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';
@@ -13,9 +13,14 @@ 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 { APP_UI_ID } from '../../../common';
+import { useSecurityTags } from '../context/dashboard_context';
+import { DASHBOARDS_PATH } from '../../../common/constants';
const DashboardRendererComponent = ({
canReadDashboard,
+ dashboardContainer,
filters,
id,
inputId = InputsModelId.global,
@@ -23,8 +28,10 @@ const DashboardRendererComponent = ({
query,
savedObjectId,
timeRange,
+ viewMode = ViewMode.VIEW,
}: {
canReadDashboard: boolean;
+ dashboardContainer?: DashboardAPI;
filters?: Filter[];
id: string;
inputId?: InputsModelId.global | InputsModelId.timeline;
@@ -37,17 +44,36 @@ const DashboardRendererComponent = ({
to: string;
toStr?: string | undefined;
};
+ viewMode?: ViewMode;
}) => {
+ const { embeddable } = useKibana().services;
const dispatch = useDispatch();
- const [dashboardContainer, setDashboardContainer] = useState();
- const getCreationOptions = useCallback(
+ const securityTags = useSecurityTags();
+ const firstSecurityTagId = securityTags?.[0]?.id;
+
+ const isCreateDashboard = !savedObjectId;
+
+ const getCreationOptions: () => Promise = useCallback(
() =>
Promise.resolve({
- getInitialInput: () => ({ timeRange, viewMode: ViewMode.VIEW, query, filters }),
+ 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,
+ }),
}),
- [filters, query, timeRange]
+ [embeddable, filters, query, timeRange, viewMode]
);
const refetchByForceRefresh = useCallback(() => {
@@ -73,20 +99,33 @@ const DashboardRendererComponent = ({
dashboardContainer?.updateInput({ timeRange, query, filters });
}, [dashboardContainer, filters, query, timeRange]);
- const handleDashboardLoaded = useCallback(
- (container: DashboardAPI) => {
- setDashboardContainer(container);
- onDashboardContainerLoaded?.(container);
- },
- [onDashboardContainerLoaded]
- );
- return savedObjectId && canReadDashboard ? (
-
- ) : null;
+ useEffect(() => {
+ if (isCreateDashboard && firstSecurityTagId)
+ dashboardContainer?.updateInput({ tags: [firstSecurityTagId] });
+ }, [dashboardContainer, firstSecurityTagId, isCreateDashboard]);
+
+ /** Dashboard renderer is stored in the state as it's a temporary solution for
+ * https://github.com/elastic/kibana/issues/167751
+ **/
+ const [dashboardContainerRenderer, setDashboardContainerRenderer] = useState<
+ React.ReactElement | undefined
+ >(undefined);
+
+ useEffect(() => {
+ setDashboardContainerRenderer(
+
+ );
+
+ return () => {
+ setDashboardContainerRenderer(undefined);
+ };
+ }, [getCreationOptions, onDashboardContainerLoaded, refetchByForceRefresh, savedObjectId]);
+
+ return canReadDashboard ? <>{dashboardContainerRenderer}> : null;
};
DashboardRendererComponent.displayName = 'DashboardRendererComponent';
export const DashboardRenderer = React.memo(DashboardRendererComponent);
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
new file mode 100644
index 0000000000000..67d8e73bacdb6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+import { EDIT_DASHBOARD_TITLE } from '../pages/details/translations';
+
+const DashboardTitleComponent = ({
+ dashboardContainer,
+ onTitleLoaded,
+}: {
+ 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;
+
+ useEffect(() => {
+ onTitleLoaded(title);
+ }, [dashboardContainer, title, onTitleLoaded]);
+
+ return dashboardTitle != null ? {title} : ;
+};
+
+export const DashboardTitle = React.memo(DashboardTitleComponent);
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
new file mode 100644
index 0000000000000..da0bf3e3dbcea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+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 { coreMock } from '@kbn/core/public/mocks';
+import { DashboardTopNav } from '@kbn/dashboard-plugin/public';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
+import { APP_NAME } from '../../../common/constants';
+import { NavigationProvider, SecurityPageName } from '@kbn/security-solution-navigation';
+import { TestProviders } from '../../common/mock';
+import { useNavigation } from '../../common/lib/kibana';
+
+const mockDashboardTopNav = DashboardTopNav as jest.Mock;
+
+jest.mock('../../common/lib/kibana', () => {
+ const actual = jest.requireActual('../../common/lib/kibana');
+ return {
+ ...actual,
+ useNavigation: jest.fn(),
+ useCapabilities: jest.fn(() => ({ showWriteControls: true })),
+ };
+});
+jest.mock('../../common/components/link_to', () => ({ useGetSecuritySolutionUrl: jest.fn() }));
+jest.mock('@kbn/dashboard-plugin/public', () => ({
+ DashboardTopNav: jest.fn(() => ),
+}));
+const mockCore = coreMock.createStart();
+const mockNavigateTo = jest.fn();
+const mockGetAppUrl = jest.fn();
+const mockDashboardContainer = {
+ select: jest.fn(),
+} as unknown as DashboardAPI;
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+describe('DashboardToolBar', () => {
+ const mockOnLoad = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useNavigation as jest.Mock).mockReturnValue({
+ navigateTo: mockNavigateTo,
+ getAppUrl: mockGetAppUrl,
+ });
+ render(, {
+ wrapper,
+ });
+ });
+ it('should render the DashboardToolBar component', () => {
+ expect(screen.getByTestId('dashboard-top-nav')).toBeInTheDocument();
+ });
+
+ it('should render the DashboardToolBar component with the correct props for view mode', () => {
+ expect(mockOnLoad).toHaveBeenCalledWith(ViewMode.VIEW);
+ });
+
+ it('should render the DashboardTopNav component with the correct redirect to listing url', () => {
+ mockDashboardTopNav.mock.calls[0][0].redirectTo({ destination: 'listing' });
+ });
+
+ it('should render the DashboardTopNav component with the correct breadcrumb', () => {
+ expect(mockGetAppUrl.mock.calls[0][0].deepLinkId).toEqual(SecurityPageName.landing);
+ expect(mockDashboardTopNav.mock.calls[0][0].customLeadingBreadCrumbs[0].text).toEqual(APP_NAME);
+ });
+
+ it('should render the DashboardTopNav component with the correct redirect to create dashboard url', () => {
+ mockDashboardTopNav.mock.calls[0][0].redirectTo({ destination: 'dashboard' });
+
+ expect(mockNavigateTo.mock.calls[0][0].deepLinkId).toEqual(SecurityPageName.dashboards);
+ expect(mockNavigateTo.mock.calls[0][0].path).toEqual(`/create`);
+ });
+
+ it('should render the DashboardTopNav component with the correct redirect to edit dashboard url', () => {
+ const mockDashboardId = 'dashboard123';
+
+ mockDashboardTopNav.mock.calls[0][0].redirectTo({
+ destination: 'dashboard',
+ id: mockDashboardId,
+ });
+ expect(mockNavigateTo.mock.calls[0][0].deepLinkId).toEqual(SecurityPageName.dashboards);
+ expect(mockNavigateTo.mock.calls[0][0].path).toEqual(`${mockDashboardId}/edit`);
+ });
+
+ it('should render the DashboardTopNav component with the correct props', () => {
+ expect(mockDashboardTopNav.mock.calls[0][0].embedSettings).toEqual(
+ expect.objectContaining({
+ forceHideFilterBar: true,
+ forceShowTopNavMenu: true,
+ forceShowDatePicker: false,
+ forceShowQueryInput: false,
+ })
+ );
+ });
+});
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
new file mode 100644
index 0000000000000..eb74f7c563500
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useEffect, useMemo } from 'react';
+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 { ChromeBreadcrumb } from '@kbn/core/public';
+import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common';
+import { SecurityPageName } from '../../../common';
+import { useCapabilities, useKibana, useNavigation } from '../../common/lib/kibana';
+import { APP_NAME } from '../../../common/constants';
+
+const DashboardToolBarComponent = ({
+ dashboardContainer,
+ onLoad,
+}: {
+ dashboardContainer: DashboardAPI;
+ onLoad?: (mode: ViewMode) => void;
+}) => {
+ const { setHeaderActionMenu } = useKibana().services;
+
+ const viewMode =
+ dashboardContainer?.select((state) => state.explicitInput.viewMode) ?? ViewMode.VIEW;
+
+ const { navigateTo, getAppUrl } = useNavigation();
+ const redirectTo = useCallback(
+ ({ destination, id }) => {
+ if (destination === 'listing') {
+ navigateTo({ deepLinkId: SecurityPageName.dashboards });
+ }
+ if (destination === 'dashboard') {
+ navigateTo({
+ deepLinkId: SecurityPageName.dashboards,
+ path: id ? `${id}/edit` : `/create`,
+ });
+ }
+ },
+ [navigateTo]
+ );
+
+ const landingBreadcrumb: ChromeBreadcrumb[] = useMemo(
+ () => [
+ {
+ text: APP_NAME,
+ href: getAppUrl({ deepLinkId: SecurityPageName.landing }),
+ },
+ ],
+ [getAppUrl]
+ );
+
+ useEffect(() => {
+ onLoad?.(viewMode);
+ }, [onLoad, viewMode]);
+
+ const embedSettings = useMemo(
+ () => ({
+ forceHideFilterBar: true,
+ forceShowTopNavMenu: true,
+ forceShowQueryInput: false,
+ forceShowDatePicker: false,
+ }),
+ []
+ );
+ const { showWriteControls } = useCapabilities(LEGACY_DASHBOARD_APP_ID);
+
+ return showWriteControls ? (
+
+ ) : null;
+};
+
+export const DashboardToolBar = React.memo(DashboardToolBarComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.test.tsx
deleted file mode 100644
index 43afa552d50fd..0000000000000
--- a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.test.tsx
+++ /dev/null
@@ -1,81 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { RenderResult } from '@testing-library/react';
-import { fireEvent, render } from '@testing-library/react';
-import React from 'react';
-import type { Query } from '@kbn/es-query';
-
-import { useKibana } from '../../common/lib/kibana';
-import { TestProviders } from '../../common/mock/test_providers';
-import type { EditDashboardButtonComponentProps } from './edit_dashboard_button';
-import { EditDashboardButton } from './edit_dashboard_button';
-import { ViewMode } from '@kbn/embeddable-plugin/public';
-
-jest.mock('../../common/lib/kibana/kibana_react', () => {
- return {
- useKibana: jest.fn(),
- };
-});
-
-describe('EditDashboardButton', () => {
- const timeRange = {
- from: '2023-03-24T00:00:00.000Z',
- to: '2023-03-24T23:59:59.999Z',
- };
-
- const props = {
- filters: [],
- query: { query: '', language: '' } as Query,
- savedObjectId: 'mockSavedObjectId',
- timeRange,
- };
- const servicesMock = {
- dashboard: { locator: { getRedirectUrl: jest.fn() } },
- application: {
- navigateToApp: jest.fn(),
- navigateToUrl: jest.fn(),
- },
- };
-
- const renderButton = (testProps: EditDashboardButtonComponentProps) => {
- return render(
-
-
-
- );
- };
-
- let renderResult: RenderResult;
- beforeEach(() => {
- (useKibana as jest.Mock).mockReturnValue({
- services: servicesMock,
- });
- renderResult = renderButton(props);
- });
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should render', () => {
- expect(renderResult.queryByTestId('dashboardEditButton')).toBeInTheDocument();
- });
-
- it('should render dashboard edit url', () => {
- fireEvent.click(renderResult.getByTestId('dashboardEditButton'));
- expect(servicesMock.dashboard?.locator?.getRedirectUrl).toHaveBeenCalledWith(
- expect.objectContaining({
- query: props.query,
- filters: props.filters,
- timeRange: props.timeRange,
- dashboardId: props.savedObjectId,
- viewMode: ViewMode.EDIT,
- })
- );
- });
-});
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.tsx b/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.tsx
deleted file mode 100644
index bd360229c7e1f..0000000000000
--- a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.tsx
+++ /dev/null
@@ -1,68 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useCallback } from 'react';
-import type { Query, Filter } from '@kbn/es-query';
-import { EuiButton } from '@elastic/eui';
-import { ViewMode } from '@kbn/embeddable-plugin/public';
-import { EDIT_DASHBOARD_BUTTON_TITLE } from '../pages/details/translations';
-import { useKibana, useNavigation } from '../../common/lib/kibana';
-
-export interface EditDashboardButtonComponentProps {
- filters?: Filter[];
- query?: Query;
- savedObjectId: string | undefined;
- timeRange: {
- from: string;
- to: string;
- fromStr?: string | undefined;
- toStr?: string | undefined;
- };
-}
-
-const EditDashboardButtonComponent: React.FC = ({
- filters,
- query,
- savedObjectId,
- timeRange,
-}) => {
- const {
- services: { dashboard },
- } = useKibana();
- const { navigateTo } = useNavigation();
-
- const onClick = useCallback(
- (e) => {
- e.preventDefault();
- const url = dashboard?.locator?.getRedirectUrl({
- query,
- filters,
- timeRange,
- dashboardId: savedObjectId,
- viewMode: ViewMode.EDIT,
- });
- if (url) {
- navigateTo({ url });
- }
- },
- [dashboard?.locator, query, filters, timeRange, savedObjectId, navigateTo]
- );
- return (
-
- {EDIT_DASHBOARD_BUTTON_TITLE}
-
- );
-};
-
-EditDashboardButtonComponent.displayName = 'EditDashboardComponent';
-export const EditDashboardButton = React.memo(EditDashboardButtonComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx
index cb8b40b1c0907..02bccd69eb253 100644
--- a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx
@@ -20,7 +20,6 @@ const DashboardContext = React.createContext({ secu
export const DashboardContextProvider: React.FC = ({ children }) => {
const { tags, isLoading } = useFetchSecurityTags();
-
const securityTags = isLoading || !tags ? null : tags;
return {children};
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/translations.ts b/x-pack/plugins/security_solution/public/dashboards/hooks/translations.ts
deleted file mode 100644
index 58254aa8fe9f6..0000000000000
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/translations.ts
+++ /dev/null
@@ -1,19 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export const DASHBOARD_TITLE = i18n.translate('xpack.securitySolution.dashboards.title', {
- defaultMessage: 'Title',
-});
-
-export const DASHBOARDS_DESCRIPTION = i18n.translate(
- 'xpack.securitySolution.dashboards.description',
- {
- defaultMessage: 'Description',
- }
-);
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.ts b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.tsx
similarity index 69%
rename from x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.ts
rename to x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.tsx
index 47509521e5574..52540aff6aa7e 100644
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.tsx
@@ -6,23 +6,37 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
-import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { useKibana } from '../../common/lib/kibana';
import { useCreateSecurityDashboardLink } from './use_create_security_dashboard_link';
import { DashboardContextProvider } from '../context/dashboard_context';
import { getTagsByName } from '../../common/containers/tags/api';
+import React from 'react';
+import { TestProviders } from '../../common/mock';
-jest.mock('../../common/lib/kibana');
+jest.mock('@kbn/security-solution-navigation/src/context');
+jest.mock('../../common/lib/kibana', () => ({
+ useKibana: jest.fn(),
+}));
jest.mock('../../common/containers/tags/api');
-const URL = '/path';
+jest.mock('../../common/lib/apm/use_track_http_request');
+jest.mock('../../common/components/link_to', () => ({
+ useGetSecuritySolutionUrl: jest
+ .fn()
+ .mockReturnValue(jest.fn().mockReturnValue('/app/security/dashboards/create')),
+}));
const renderUseCreateSecurityDashboardLink = () =>
renderHook(() => useCreateSecurityDashboardLink(), {
- wrapper: DashboardContextProvider,
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
});
const asyncRenderUseCreateSecurityDashboard = async () => {
const renderedHook = renderUseCreateSecurityDashboardLink();
+
await act(async () => {
await renderedHook.waitForNextUpdate();
});
@@ -30,12 +44,15 @@ const asyncRenderUseCreateSecurityDashboard = async () => {
};
describe('useCreateSecurityDashboardLink', () => {
- const mockGetRedirectUrl = jest.fn(() => URL);
-
beforeAll(() => {
- useKibana().services.dashboard = {
- locator: { getRedirectUrl: mockGetRedirectUrl },
- } as unknown as DashboardStart;
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ savedObjectsTagging: {
+ create: jest.fn(),
+ },
+ http: { get: jest.fn() },
+ },
+ });
});
afterEach(() => {
@@ -55,8 +72,7 @@ describe('useCreateSecurityDashboardLink', () => {
const result1 = result.current;
act(() => rerender());
const result2 = result.current;
-
- expect(result1).toBe(result2);
+ expect(result1).toEqual(result2);
});
it('should not re-request tag id when re-rendered', async () => {
@@ -71,14 +87,14 @@ describe('useCreateSecurityDashboardLink', () => {
const { result, waitForNextUpdate } = renderUseCreateSecurityDashboardLink();
expect(result.current.isLoading).toEqual(true);
- expect(result.current.url).toEqual('');
+ expect(result.current.url).toEqual('/app/security/dashboards/create');
await act(async () => {
await waitForNextUpdate();
});
expect(result.current.isLoading).toEqual(false);
- expect(result.current.url).toEqual(URL);
+ expect(result.current.url).toEqual('/app/security/dashboards/create');
});
});
});
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts
index 633d9d01efe17..24f91eec5211c 100644
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts
@@ -7,24 +7,28 @@
import { useMemo } from 'react';
import { useSecurityTags } from '../context/dashboard_context';
-import { useKibana } from '../../common/lib/kibana';
+import { useGetSecuritySolutionUrl } from '../../common/components/link_to';
+import { SecurityPageName } from '../../../common';
type UseCreateDashboard = () => { isLoading: boolean; url: string };
export const useCreateSecurityDashboardLink: UseCreateDashboard = () => {
- const { dashboard } = useKibana().services;
+ const getSecuritySolutionUrl = useGetSecuritySolutionUrl();
const securityTags = useSecurityTags();
-
+ const url = getSecuritySolutionUrl({
+ deepLinkId: SecurityPageName.dashboards,
+ path: 'create',
+ });
const result = useMemo(() => {
const firstSecurityTagId = securityTags?.[0]?.id;
if (!firstSecurityTagId) {
- return { isLoading: true, url: '' };
+ return { isLoading: true, url };
}
return {
isLoading: false,
- url: dashboard?.locator?.getRedirectUrl({ tags: [firstSecurityTagId] }) ?? '',
+ url,
};
- }, [securityTags, dashboard?.locator]);
+ }, [securityTags, url]);
return result;
};
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.test.tsx b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.test.tsx
new file mode 100644
index 0000000000000..d8f5bae2361c8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.test.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { renderHook, act } from '@testing-library/react-hooks';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+
+import { useDashboardRenderer } from './use_dashboard_renderer';
+
+jest.mock('../../common/lib/kibana');
+
+const mockDashboardContainer = { getExplicitInput: () => ({ tags: ['tagId'] }) } as DashboardAPI;
+
+describe('useDashboardRenderer', () => {
+ it('should set dashboard container correctly when dashboard is loaded', async () => {
+ const { result } = renderHook(() => useDashboardRenderer());
+
+ await act(async () => {
+ await result.current.handleDashboardLoaded(mockDashboardContainer);
+ });
+
+ expect(result.current.dashboardContainer).toEqual(mockDashboardContainer);
+ });
+});
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
new file mode 100644
index 0000000000000..104692e62f2bb
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useMemo, useState } from 'react';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+
+export const useDashboardRenderer = () => {
+ const [dashboardContainer, setDashboardContainer] = useState();
+
+ const handleDashboardLoaded = useCallback((container: DashboardAPI) => {
+ setDashboardContainer(container);
+ }, []);
+
+ return useMemo(
+ () => ({
+ dashboardContainer,
+ handleDashboardLoaded,
+ }),
+ [dashboardContainer, handleDashboardLoaded]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx b/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx
index 7f58f804d488c..07446294ab203 100644
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx
@@ -8,9 +8,9 @@
import React, { useMemo, useCallback } from 'react';
import type { MouseEventHandler } from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { LinkAnchor } from '../../common/components/links';
import { useKibana, useNavigateTo } from '../../common/lib/kibana';
-import * as i18n from './translations';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../common/lib/telemetry';
import { SecurityPageName } from '../../../common/constants';
import { useGetSecuritySolutionUrl } from '../../common/components/link_to';
@@ -56,7 +56,9 @@ export const useSecurityDashboardsTableColumns = (): Array<
(): Array> => [
{
field: 'title',
- name: i18n.DASHBOARD_TITLE,
+ name: i18n.translate('xpack.securitySolution.dashboards.title', {
+ defaultMessage: 'Title',
+ }),
sortable: true,
render: (title: string, { id }) => {
const href = `${getSecuritySolutionUrl({
@@ -75,7 +77,9 @@ export const useSecurityDashboardsTableColumns = (): Array<
},
{
field: 'description',
- name: i18n.DASHBOARDS_DESCRIPTION,
+ name: i18n.translate('xpack.securitySolution.dashboards.description', {
+ defaultMessage: 'Description',
+ }),
sortable: true,
render: (description: string) => description || getEmptyValue(),
'data-test-subj': 'dashboardTableDescriptionCell',
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts b/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts
index 01e663e8abb7e..f59369cb4e755 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
+import { matchPath } from 'react-router-dom';
import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/breadcrumbs/types';
+import { CREATE_DASHBOARD_TITLE } from './translations';
/**
* This module should only export this function.
@@ -13,6 +15,10 @@ import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/
* We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size.
*/
export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => {
+ if (matchPath(params.pathName, { path: '/create' })) {
+ return [{ text: CREATE_DASHBOARD_TITLE }];
+ }
+
const breadcrumbName = params?.state?.dashboardName;
if (breadcrumbName) {
return [{ text: breadcrumbName }];
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
index 9411bda35a632..3c85a18f2d3aa 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
@@ -11,6 +11,7 @@ import { Router } from '@kbn/shared-ux-router';
import { DashboardView } from '.';
import { useCapabilities } from '../../../common/lib/kibana';
import { TestProviders } from '../../../common/mock';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@@ -68,7 +69,7 @@ describe('DashboardView', () => {
test('render when no error state', () => {
const { queryByTestId } = render(
-
+
,
{ wrapper: TestProviders }
);
@@ -83,7 +84,7 @@ describe('DashboardView', () => {
});
const { queryByTestId } = render(
-
+
,
{ wrapper: TestProviders }
);
@@ -95,7 +96,7 @@ describe('DashboardView', () => {
test('render dashboard view with height', () => {
const { queryByTestId } = render(
-
+
,
{ wrapper: TestProviders }
);
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
index 8bbfec9f99218..6f07b377a22d0 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
@@ -7,13 +7,12 @@
import React, { useState, useCallback, useMemo } from 'react';
import { LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
-import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types';
import { useParams } from 'react-router-dom';
-
import { pick } from 'lodash/fp';
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import type { ViewMode } from '@kbn/embeddable-plugin/common';
import { SecurityPageName } from '../../../../common/constants';
import { SpyRoute } from '../../../common/utils/route/spy_routes';
import { useCapabilities } from '../../../common/lib/kibana';
@@ -26,16 +25,22 @@ import { FiltersGlobal } from '../../../common/components/filters_global';
import { InputsModelId } from '../../../common/store/inputs/constants';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { HeaderPage } from '../../../common/components/header_page';
-import { DASHBOARD_NOT_FOUND_TITLE } from './translations';
import { inputsSelectors } from '../../../common/store';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
-import { EditDashboardButton } from '../../components/edit_dashboard_button';
+import { DashboardToolBar } from '../../components/dashboard_tool_bar';
+
+import { useDashboardRenderer } from '../../hooks/use_dashboard_renderer';
+import { DashboardTitle } from '../../components/dashboard_title';
-type DashboardDetails = Record;
+interface DashboardViewProps {
+ initialViewMode: ViewMode;
+}
const dashboardViewFlexGroupStyle = { minHeight: `calc(100vh - 140px)` };
-const DashboardViewComponent: React.FC = () => {
+const DashboardViewComponent: React.FC = ({
+ initialViewMode,
+}: DashboardViewProps) => {
const { fromStr, toStr, from, to } = useDeepEqualSelector((state) =>
pick(['fromStr', 'toStr', 'from', 'to'], inputsSelectors.globalTimeRangeSelector(state))
);
@@ -47,36 +52,28 @@ const DashboardViewComponent: React.FC = () => {
);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
- const { indexPattern, indicesExist } = useSourcererDataView();
+ const { indexPattern } = useSourcererDataView();
- const { show: canReadDashboard, showWriteControls } =
+ const { show: canReadDashboard } =
useCapabilities(LEGACY_DASHBOARD_APP_ID);
const errorState = useMemo(
() => (canReadDashboard ? null : DashboardViewPromptState.NoReadPermission),
[canReadDashboard]
);
- const [dashboardDetails, setDashboardDetails] = useState();
- const onDashboardContainerLoaded = useCallback((dashboard: DashboardAPI) => {
- if (dashboard) {
- const title = dashboard.getTitle().trim();
- if (title) {
- setDashboardDetails({ title });
- } else {
- setDashboardDetails({ title: DASHBOARD_NOT_FOUND_TITLE });
- }
- }
- }, []);
-
- const dashboardExists = useMemo(() => dashboardDetails != null, [dashboardDetails]);
+ const [viewMode, setViewMode] = useState(initialViewMode);
const { detailName: savedObjectId } = useParams<{ detailName?: string }>();
+ const [dashboardTitle, setDashboardTitle] = useState();
+
+ const { dashboardContainer, handleDashboardLoaded } = useDashboardRenderer();
+ const onDashboardToolBarLoad = useCallback((mode: ViewMode) => {
+ setViewMode(mode);
+ }, []);
return (
<>
- {indicesExist && (
-
-
-
- )}
+
+
+
{
data-test-subj="dashboard-view-wrapper"
>
- }>
- {showWriteControls && dashboardExists && (
-
- )}
-
+ {dashboardContainer && (
+
+ }
+ subtitle={
+
+ }
+ />
+ )}
{!errorState && (
@@ -102,10 +106,12 @@ const DashboardViewComponent: React.FC = () => {
query={query}
filters={filters}
canReadDashboard={canReadDashboard}
+ dashboardContainer={dashboardContainer}
id={`dashboard-view-${savedObjectId}`}
- onDashboardContainerLoaded={onDashboardContainerLoaded}
+ onDashboardContainerLoaded={handleDashboardLoaded}
savedObjectId={savedObjectId}
timeRange={timeRange}
+ viewMode={viewMode}
/>
)}
@@ -116,7 +122,7 @@ const DashboardViewComponent: React.FC = () => {
)}
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts b/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts
index ddfa94bd75584..a760d79a8e30d 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts
@@ -40,3 +40,24 @@ export const EDIT_DASHBOARD_BUTTON_TITLE = i18n.translate(
defaultMessage: `Edit`,
}
);
+
+export const EDIT_DASHBOARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.dashboards.dashboard.editDashboardTitle',
+ {
+ defaultMessage: `Editing new dashboard`,
+ }
+);
+
+export const VIEW_DASHBOARD_BUTTON_TITLE = i18n.translate(
+ 'xpack.securitySolution.dashboards.dashboard.viewDashboardButtonTitle',
+ {
+ defaultMessage: `Switch to view mode`,
+ }
+);
+
+export const SAVE_DASHBOARD_BUTTON_TITLE = i18n.translate(
+ 'xpack.securitySolution.dashboards.dashboard.saveDashboardButtonTitle',
+ {
+ defaultMessage: `Save`,
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx
index 0bd521f47a69c..993a4b37a1ec7 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardsLandingPage } from './landing_page';
import { DashboardView } from './details';
import { DASHBOARDS_PATH } from '../../../common/constants';
@@ -16,8 +17,14 @@ const DashboardsContainerComponent = () => {
return (
+
+
+
+
+
+
-
+
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx
index ac26f9038d5d4..8723bfb69f326 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx
@@ -23,13 +23,10 @@ import { DASHBOARDS_PAGE_SECTION_CUSTOM } from './translations';
jest.mock('../../../common/containers/tags/api');
jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null }));
-jest.mock('@kbn/dashboard-plugin/public', () => {
- const actual = jest.requireActual('@kbn/dashboard-plugin/public');
- return {
- ...actual,
- DashboardListingTable: jest.fn().mockReturnValue(),
- };
-});
+jest.mock('@kbn/dashboard-plugin/public', () => ({
+ DashboardListingTable: jest.fn().mockReturnValue(),
+ DashboardTopNav: jest.fn().mockReturnValue(),
+}));
const mockUseObservable = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
index 2af7ef2f9902b..10fb3c060548f 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
@@ -99,20 +99,18 @@ export const DashboardsLandingPage = () => {
})}`,
[getSecuritySolutionUrl]
);
- const { isLoading: loadingCreateDashboardUrl, url: createDashboardUrl } =
- useCreateSecurityDashboardLink();
-
- const getHref = useCallback(
- (id: string | undefined) => (id ? getSecuritySolutionDashboardUrl(id) : createDashboardUrl),
- [createDashboardUrl, getSecuritySolutionDashboardUrl]
- );
const goToDashboard = useCallback(
(dashboardId: string | undefined) => {
track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.DASHBOARD);
- navigateTo({ url: getHref(dashboardId) });
+ navigateTo({
+ url: getSecuritySolutionUrl({
+ deepLinkId: SecurityPageName.dashboards,
+ path: dashboardId ?? 'create',
+ }),
+ });
},
- [getHref, navigateTo]
+ [getSecuritySolutionUrl, navigateTo]
);
const securityTags = useSecurityTags();
@@ -151,7 +149,7 @@ export const DashboardsLandingPage = () => {
{
});
};
+export const isDashboardViewPath = (pathname: string): boolean =>
+ matchPath(pathname, {
+ path: `/${DASHBOARDS_PATH}/:id`,
+ exact: false,
+ strict: false,
+ }) != null;
+
const isAlertsPath = (pathname: string): boolean => {
return !!matchPath(pathname, {
path: `${ALERTS_PATH}`,
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 904ec870e9a2c..65453e37d686b 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -187,6 +187,7 @@ export class Plugin implements IPlugin void;
/**
diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json
index 19ced3697e2c9..de11312c2f60e 100644
--- a/x-pack/plugins/security_solution/tsconfig.json
+++ b/x-pack/plugins/security_solution/tsconfig.json
@@ -176,6 +176,7 @@
"@kbn/subscription-tracking",
"@kbn/core-application-common",
"@kbn/openapi-generator",
- "@kbn/es"
+ "@kbn/es",
+ "@kbn/react-kibana-mount"
]
}