mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 02:31:06 -04:00
[SecuritySolution] Security Solution Dashboard edit mode (#159486)
## Summary issue: https://github.com/elastic/kibana/issues/152955 Test environment: https://p.elstc.co/paste/9-b8FqRA#EkVP6KV1UAFOu1cWFwX1laj63P9wc5eQtnT7bCPyEuX Known issues: https://github.com/elastic/kibana/pull/159486#issuecomment-174060465123d30613
-2dc3-423c-ada1-b52cd2f409ee 1. Reuse Kibana Dashboard's tool bar <img width="2543" alt="Screenshot 2023-09-26 at 15 51 30" src="b0279665
-578a-45f9-b416-675e152b7dbd"> 2. Dashboard with a `Managed` tag does `not` have the edit tool bar under the title. <img width="2558" alt="Screenshot 2023-09-29 at 10 11 34" src="7c0774c1
-2bf2-478b-a30e-59ff8609b584"> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Massaneda <sergi.massaneda@gmail.com> Co-authored-by: Devon Thomson <devon.thomson@elastic.co>
This commit is contained in:
parent
4c07f9c999
commit
8fd6dbed55
56 changed files with 854 additions and 490 deletions
|
@ -31,15 +31,15 @@ import {
|
||||||
} from './url/search_sessions_integration';
|
} from './url/search_sessions_integration';
|
||||||
import { DashboardAPI, DashboardRenderer } from '..';
|
import { DashboardAPI, DashboardRenderer } from '..';
|
||||||
import { type DashboardEmbedSettings } from './types';
|
import { type DashboardEmbedSettings } from './types';
|
||||||
import { DASHBOARD_APP_ID } from '../dashboard_constants';
|
|
||||||
import { pluginServices } from '../services/plugin_services';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
|
|
||||||
import { AwaitingDashboardAPI } from '../dashboard_container';
|
import { AwaitingDashboardAPI } from '../dashboard_container';
|
||||||
import { DashboardRedirect } from '../dashboard_container/types';
|
import { DashboardRedirect } from '../dashboard_container/types';
|
||||||
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
||||||
|
import { createDashboardEditUrl, DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||||
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
|
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
|
||||||
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
|
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
|
||||||
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
|
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
|
||||||
|
import { DashboardTopNav } from '../dashboard_top_nav';
|
||||||
|
|
||||||
export interface DashboardAppProps {
|
export interface DashboardAppProps {
|
||||||
history: History;
|
history: History;
|
||||||
|
@ -160,6 +160,10 @@ export function DashboardApp({
|
||||||
getInitialInput,
|
getInitialInput,
|
||||||
validateLoadedSavedObject: validateOutcome,
|
validateLoadedSavedObject: validateOutcome,
|
||||||
isEmbeddedExternally: Boolean(embedSettings), // embed settings are only sent if the dashboard URL has `embed=true`
|
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,
|
history,
|
||||||
|
@ -192,9 +196,11 @@ export function DashboardApp({
|
||||||
{!showNoDataPage && (
|
{!showNoDataPage && (
|
||||||
<>
|
<>
|
||||||
{dashboardAPI && (
|
{dashboardAPI && (
|
||||||
<DashboardAPIContext.Provider value={dashboardAPI}>
|
<DashboardTopNav
|
||||||
<DashboardTopNav redirectTo={redirectTo} embedSettings={embedSettings} />
|
redirectTo={redirectTo}
|
||||||
</DashboardAPIContext.Provider>
|
embedSettings={embedSettings}
|
||||||
|
dashboardContainer={dashboardAPI}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{getLegacyConflictWarning?.()}
|
{getLegacyConflictWarning?.()}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { METRIC_TYPE } from '@kbn/analytics';
|
import { METRIC_TYPE } from '@kbn/analytics';
|
||||||
|
@ -21,7 +20,7 @@ import { EditorMenu } from './editor_menu';
|
||||||
import { useDashboardAPI } from '../dashboard_app';
|
import { useDashboardAPI } from '../dashboard_app';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { ControlsToolbarButton } from './controls_toolbar_button';
|
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';
|
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
|
||||||
|
|
||||||
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
|
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
|
||||||
|
@ -70,12 +69,13 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
||||||
stateTransferService.navigateToEditor(appId, {
|
stateTransferService.navigateToEditor(appId, {
|
||||||
path,
|
path,
|
||||||
state: {
|
state: {
|
||||||
originatingApp: DASHBOARD_APP_ID,
|
originatingApp: dashboard.getAppContext()?.currentAppId,
|
||||||
|
originatingPath: dashboard.getAppContext()?.getCurrentPath?.(),
|
||||||
searchSessionId: search.session.getSessionId(),
|
searchSessionId: search.session.getSessionId(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[stateTransferService, search.session, trackUiMetric]
|
[stateTransferService, dashboard, search.session, trackUiMetric]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createNewEmbeddable = useCallback(
|
const createNewEmbeddable = useCallback(
|
||||||
|
|
|
@ -26,10 +26,12 @@ export const useDashboardMenuItems = ({
|
||||||
redirectTo,
|
redirectTo,
|
||||||
isLabsShown,
|
isLabsShown,
|
||||||
setIsLabsShown,
|
setIsLabsShown,
|
||||||
|
showResetChange,
|
||||||
}: {
|
}: {
|
||||||
redirectTo: DashboardRedirect;
|
redirectTo: DashboardRedirect;
|
||||||
isLabsShown: boolean;
|
isLabsShown: boolean;
|
||||||
setIsLabsShown: Dispatch<SetStateAction<boolean>>;
|
setIsLabsShown: Dispatch<SetStateAction<boolean>>;
|
||||||
|
showResetChange?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
|
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
|
||||||
|
|
||||||
|
@ -276,32 +278,56 @@ export const useDashboardMenuItems = ({
|
||||||
const shareMenuItem = share ? [menuItems.share] : [];
|
const shareMenuItem = share ? [menuItems.share] : [];
|
||||||
const cloneMenuItem = showWriteControls ? [menuItems.clone] : [];
|
const cloneMenuItem = showWriteControls ? [menuItems.clone] : [];
|
||||||
const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : [];
|
const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : [];
|
||||||
|
const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...labsMenuItem,
|
...labsMenuItem,
|
||||||
menuItems.fullScreen,
|
menuItems.fullScreen,
|
||||||
...shareMenuItem,
|
...shareMenuItem,
|
||||||
...cloneMenuItem,
|
...cloneMenuItem,
|
||||||
resetChangesMenuItem,
|
...mayberesetChangesMenuItem,
|
||||||
...editMenuItem,
|
...editMenuItem,
|
||||||
];
|
];
|
||||||
}, [isLabsEnabled, menuItems, share, showWriteControls, managed, resetChangesMenuItem]);
|
}, [
|
||||||
|
isLabsEnabled,
|
||||||
|
menuItems,
|
||||||
|
share,
|
||||||
|
showWriteControls,
|
||||||
|
managed,
|
||||||
|
showResetChange,
|
||||||
|
resetChangesMenuItem,
|
||||||
|
]);
|
||||||
|
|
||||||
const editModeTopNavConfig = useMemo(() => {
|
const editModeTopNavConfig = useMemo(() => {
|
||||||
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
|
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
|
||||||
const shareMenuItem = share ? [menuItems.share] : [];
|
const shareMenuItem = share ? [menuItems.share] : [];
|
||||||
const editModeItems: TopNavMenuData[] = [];
|
const editModeItems: TopNavMenuData[] = [];
|
||||||
|
|
||||||
if (lastSavedId) {
|
if (lastSavedId) {
|
||||||
editModeItems.push(
|
editModeItems.push(menuItems.saveAs, menuItems.switchToViewMode);
|
||||||
menuItems.saveAs,
|
|
||||||
menuItems.switchToViewMode,
|
if (showResetChange) {
|
||||||
resetChangesMenuItem,
|
editModeItems.push(resetChangesMenuItem);
|
||||||
menuItems.quickSave
|
}
|
||||||
);
|
|
||||||
|
editModeItems.push(menuItems.quickSave);
|
||||||
} else {
|
} else {
|
||||||
editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs);
|
editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs);
|
||||||
}
|
}
|
||||||
return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems];
|
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 };
|
return { viewModeTopNavConfig, editModeTopNavConfig };
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
import { pluginServices } from '../../../services/plugin_services';
|
||||||
import { emptyScreenStrings } from '../../_dashboard_container_strings';
|
import { emptyScreenStrings } from '../../_dashboard_container_strings';
|
||||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
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() {
|
export function DashboardEmptyScreen() {
|
||||||
const {
|
const {
|
||||||
|
@ -44,6 +44,14 @@ export function DashboardEmptyScreen() {
|
||||||
[getVisTypeAliases]
|
[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(() => {
|
const goToLens = useCallback(() => {
|
||||||
if (!lensAlias || !lensAlias.aliasPath) return;
|
if (!lensAlias || !lensAlias.aliasPath) return;
|
||||||
const trackUiMetric = usageCollection.reportUiCounter?.bind(
|
const trackUiMetric = usageCollection.reportUiCounter?.bind(
|
||||||
|
@ -57,16 +65,19 @@ export function DashboardEmptyScreen() {
|
||||||
getStateTransfer().navigateToEditor(lensAlias.aliasApp, {
|
getStateTransfer().navigateToEditor(lensAlias.aliasApp, {
|
||||||
path: lensAlias.aliasPath,
|
path: lensAlias.aliasPath,
|
||||||
state: {
|
state: {
|
||||||
originatingApp: DASHBOARD_APP_ID,
|
originatingApp,
|
||||||
|
originatingPath,
|
||||||
searchSessionId: search.session.getSessionId(),
|
searchSessionId: search.session.getSessionId(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [getStateTransfer, lensAlias, search.session, usageCollection]);
|
}, [
|
||||||
|
getStateTransfer,
|
||||||
const dashboardContainer = useDashboardContainer();
|
lensAlias,
|
||||||
const isDarkTheme = useObservable(theme$)?.darkMode;
|
originatingApp,
|
||||||
const isEditMode =
|
originatingPath,
|
||||||
dashboardContainer.select((state) => state.explicitInput.viewMode) === ViewMode.EDIT;
|
search.session,
|
||||||
|
usageCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
// TODO replace these SVGs with versions from EuiIllustration as soon as it becomes available.
|
// TODO replace these SVGs with versions from EuiIllustration as soon as it becomes available.
|
||||||
const imageUrl = basePath.prepend(
|
const imageUrl = basePath.prepend(
|
||||||
|
|
|
@ -53,7 +53,7 @@ import { DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||||
import { placePanel } from '../component/panel_placement';
|
import { placePanel } from '../component/panel_placement';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { pluginServices } from '../../services/plugin_services';
|
||||||
import { initializeDashboard } from './create/create_dashboard';
|
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 { DashboardCreationOptions } from './dashboard_container_factory';
|
||||||
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
||||||
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
|
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
|
||||||
|
@ -107,7 +107,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
public controlGroup?: ControlGroupContainer;
|
public controlGroup?: ControlGroupContainer;
|
||||||
|
|
||||||
public searchSessionId?: string;
|
public searchSessionId?: string;
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
public stopSyncingWithUnifiedSearch?: () => void;
|
public stopSyncingWithUnifiedSearch?: () => void;
|
||||||
private cleanupStateTools: () => void;
|
private cleanupStateTools: () => void;
|
||||||
|
@ -185,6 +184,16 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
||||||
this.select = reduxTools.select;
|
this.select = reduxTools.select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAppContext() {
|
||||||
|
const embeddableAppContext = this.creationOptions?.getEmbeddableAppContext?.(
|
||||||
|
this.getDashboardSavedObjectId()
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...embeddableAppContext,
|
||||||
|
currentAppId: embeddableAppContext?.currentAppId ?? DASHBOARD_APP_ID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getDashboardSavedObjectId() {
|
public getDashboardSavedObjectId() {
|
||||||
return this.getState().componentState.lastSavedId;
|
return this.getState().componentState.lastSavedId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
EmbeddableFactory,
|
EmbeddableFactory,
|
||||||
EmbeddableFactoryDefinition,
|
EmbeddableFactoryDefinition,
|
||||||
EmbeddablePackageState,
|
EmbeddablePackageState,
|
||||||
|
EmbeddableAppContext,
|
||||||
} from '@kbn/embeddable-plugin/public';
|
} from '@kbn/embeddable-plugin/public';
|
||||||
import { SearchSessionInfoProvider } from '@kbn/data-plugin/public';
|
import { SearchSessionInfoProvider } from '@kbn/data-plugin/public';
|
||||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||||
|
@ -58,6 +59,8 @@ export interface DashboardCreationOptions {
|
||||||
validateLoadedSavedObject?: (result: LoadDashboardReturn) => 'valid' | 'invalid' | 'redirected';
|
validateLoadedSavedObject?: (result: LoadDashboardReturn) => 'valid' | 'invalid' | 'redirected';
|
||||||
|
|
||||||
isEmbeddedExternally?: boolean;
|
isEmbeddedExternally?: boolean;
|
||||||
|
|
||||||
|
getEmbeddableAppContext?: (dashboardId?: string) => EmbeddableAppContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardContainerFactoryDefinition
|
export class DashboardContainerFactoryDefinition
|
||||||
|
|
|
@ -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) => (
|
||||||
|
<DashboardAPIContext.Provider value={props.dashboardContainer}>
|
||||||
|
<InternalDashboardTopNav {...props} />
|
||||||
|
</DashboardAPIContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default DashboardTopNavWithContext;
|
29
src/plugins/dashboard/public/dashboard_top_nav/index.tsx
Normal file
29
src/plugins/dashboard/public/dashboard_top_nav/index.tsx
Normal file
|
@ -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 (
|
||||||
|
<Suspense fallback={<div />}>
|
||||||
|
<LazyDashboardTopNav {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
|
@ -18,36 +18,49 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||||
import { EuiHorizontalRule, EuiIcon, EuiToolTipProps } from '@elastic/eui';
|
import { EuiHorizontalRule, EuiIcon, EuiToolTipProps } from '@elastic/eui';
|
||||||
|
import { EuiBreadcrumbProps } from '@elastic/eui/src/components/breadcrumbs/breadcrumb';
|
||||||
|
import { MountPoint } from '@kbn/core/public';
|
||||||
import {
|
import {
|
||||||
getDashboardTitle,
|
getDashboardTitle,
|
||||||
leaveConfirmStrings,
|
leaveConfirmStrings,
|
||||||
getDashboardBreadcrumb,
|
getDashboardBreadcrumb,
|
||||||
unsavedChangesBadgeStrings,
|
unsavedChangesBadgeStrings,
|
||||||
dashboardManagedBadge,
|
dashboardManagedBadge,
|
||||||
} from '../_dashboard_app_strings';
|
} from '../dashboard_app/_dashboard_app_strings';
|
||||||
import { UI_SETTINGS } from '../../../common';
|
import { UI_SETTINGS } from '../../common';
|
||||||
import { useDashboardAPI } from '../dashboard_app';
|
import { useDashboardAPI } from '../dashboard_app/dashboard_app';
|
||||||
import { DashboardEmbedSettings } from '../types';
|
import { pluginServices } from '../services/plugin_services';
|
||||||
import { pluginServices } from '../../services/plugin_services';
|
import { useDashboardMenuItems } from '../dashboard_app/top_nav/use_dashboard_menu_items';
|
||||||
import { useDashboardMenuItems } from './use_dashboard_menu_items';
|
import { DashboardEmbedSettings } from '../dashboard_app/types';
|
||||||
import { DashboardRedirect } from '../../dashboard_container/types';
|
import { DashboardEditingToolbar } from '../dashboard_app/top_nav/dashboard_editing_toolbar';
|
||||||
import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
|
import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount_context';
|
||||||
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
|
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||||
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
|
|
||||||
|
|
||||||
import './_dashboard_top_nav.scss';
|
import './_dashboard_top_nav.scss';
|
||||||
export interface DashboardTopNavProps {
|
import { DashboardRedirect } from '../dashboard_container/types';
|
||||||
|
|
||||||
|
export interface InternalDashboardTopNavProps {
|
||||||
|
customLeadingBreadCrumbs?: EuiBreadcrumbProps[];
|
||||||
embedSettings?: DashboardEmbedSettings;
|
embedSettings?: DashboardEmbedSettings;
|
||||||
|
forceHideUnifiedSearch?: boolean;
|
||||||
redirectTo: DashboardRedirect;
|
redirectTo: DashboardRedirect;
|
||||||
|
setCustomHeaderActionMenu?: (menuMount: MountPoint<HTMLElement> | undefined) => void;
|
||||||
|
showBorderBottom?: boolean;
|
||||||
|
showResetChange?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LabsFlyout = withSuspense(LazyLabsFlyout, null);
|
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 [isChromeVisible, setIsChromeVisible] = useState(false);
|
||||||
const [isLabsShown, setIsLabsShown] = useState(false);
|
const [isLabsShown, setIsLabsShown] = useState(false);
|
||||||
|
|
||||||
const dashboardTitleRef = useRef<HTMLHeadingElement>(null);
|
const dashboardTitleRef = useRef<HTMLHeadingElement>(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
|
// set only the dashboardTitleBreadcrumbs because the main breadcrumbs automatically come as part of the navigation config
|
||||||
serverless.setBreadcrumbs(dashboardTitleBreadcrumbs);
|
serverless.setBreadcrumbs(dashboardTitleBreadcrumbs);
|
||||||
} else {
|
} else {
|
||||||
// non-serverless regular breadcrumbs
|
/**
|
||||||
setBreadcrumbs([
|
* non-serverless regular breadcrumbs
|
||||||
{
|
* Dashboard embedded in other plugins (e.g. SecuritySolution)
|
||||||
text: getDashboardBreadcrumb(),
|
* will have custom leading breadcrumbs for back to their app.
|
||||||
'data-test-subj': 'dashboardListingBreadcrumb',
|
**/
|
||||||
onClick: () => {
|
setBreadcrumbs(
|
||||||
redirectTo({ destination: 'listing' });
|
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
|
* Build app leave handler whenever hasUnsavedChanges changes
|
||||||
|
@ -205,12 +232,6 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
};
|
};
|
||||||
}, [onAppLeave, getStateTransfer, hasUnsavedChanges, viewMode]);
|
}, [onAppLeave, getStateTransfer, hasUnsavedChanges, viewMode]);
|
||||||
|
|
||||||
const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
|
|
||||||
redirectTo,
|
|
||||||
isLabsShown,
|
|
||||||
setIsLabsShown,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibilityProps = useMemo(() => {
|
const visibilityProps = useMemo(() => {
|
||||||
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
|
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
|
||||||
(forceShow || isChromeVisible) && !fullScreenMode;
|
(forceShow || isChromeVisible) && !fullScreenMode;
|
||||||
|
@ -218,14 +239,17 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
!forceHide && (filterManager.getFilters().length > 0 || !fullScreenMode);
|
!forceHide && (filterManager.getFilters().length > 0 || !fullScreenMode);
|
||||||
|
|
||||||
const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
|
const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
|
||||||
const showQueryInput = shouldShowNavBarComponent(
|
const showQueryInput = Boolean(forceHideUnifiedSearch)
|
||||||
Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
|
? false
|
||||||
);
|
: shouldShowNavBarComponent(
|
||||||
const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
|
Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
|
||||||
|
);
|
||||||
|
const showDatePicker = Boolean(forceHideUnifiedSearch)
|
||||||
|
? false
|
||||||
|
: shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
|
||||||
const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar));
|
const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar));
|
||||||
const showQueryBar = showQueryInput || showDatePicker || showFilterBar;
|
const showQueryBar = showQueryInput || showDatePicker || showFilterBar;
|
||||||
const showSearchBar = showQueryBar || showFilterBar;
|
const showSearchBar = showQueryBar || showFilterBar;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showTopNavMenu,
|
showTopNavMenu,
|
||||||
showSearchBar,
|
showSearchBar,
|
||||||
|
@ -233,7 +257,21 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
showQueryInput,
|
showQueryInput,
|
||||||
showDatePicker,
|
showDatePicker,
|
||||||
};
|
};
|
||||||
}, [embedSettings, filterManager, fullScreenMode, isChromeVisible, viewMode]);
|
}, [
|
||||||
|
embedSettings,
|
||||||
|
filterManager,
|
||||||
|
forceHideUnifiedSearch,
|
||||||
|
fullScreenMode,
|
||||||
|
isChromeVisible,
|
||||||
|
viewMode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
|
||||||
|
redirectTo,
|
||||||
|
isLabsShown,
|
||||||
|
setIsLabsShown,
|
||||||
|
showResetChange,
|
||||||
|
});
|
||||||
|
|
||||||
UseUnmount(() => {
|
UseUnmount(() => {
|
||||||
dashboard.clearOverlays();
|
dashboard.clearOverlays();
|
||||||
|
@ -301,7 +339,11 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
|
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
|
||||||
appName={LEGACY_DASHBOARD_APP_ID}
|
appName={LEGACY_DASHBOARD_APP_ID}
|
||||||
visible={viewMode !== ViewMode.PRINT}
|
visible={viewMode !== ViewMode.PRINT}
|
||||||
setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
|
setMenuMountPoint={
|
||||||
|
embedSettings || fullScreenMode
|
||||||
|
? setCustomHeaderActionMenu ?? undefined
|
||||||
|
: setHeaderActionMenu
|
||||||
|
}
|
||||||
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
|
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
|
||||||
config={
|
config={
|
||||||
visibilityProps.showTopNavMenu
|
visibilityProps.showTopNavMenu
|
||||||
|
@ -327,7 +369,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
{viewMode === ViewMode.EDIT ? (
|
{viewMode === ViewMode.EDIT ? (
|
||||||
<DashboardEditingToolbar isDisabled={!!focusedPanelId} />
|
<DashboardEditingToolbar isDisabled={!!focusedPanelId} />
|
||||||
) : null}
|
) : null}
|
||||||
<EuiHorizontalRule margin="none" />
|
{showBorderBottom && <EuiHorizontalRule margin="none" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -25,7 +25,7 @@ export {
|
||||||
export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin';
|
export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin';
|
||||||
|
|
||||||
export { DashboardListingTable } from './dashboard_listing';
|
export { DashboardListingTable } from './dashboard_listing';
|
||||||
|
export { DashboardTopNav } from './dashboard_top_nav';
|
||||||
export {
|
export {
|
||||||
type DashboardAppLocator,
|
type DashboardAppLocator,
|
||||||
type DashboardAppLocatorParams,
|
type DashboardAppLocatorParams,
|
||||||
|
|
|
@ -49,14 +49,7 @@ const getEventStatus = (output: EmbeddableOutput): EmbeddablePhase => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
|
export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
|
||||||
const {
|
const { hideHeader, showShadow, embeddable, hideInspector, onPanelStatusChange } = panelProps;
|
||||||
hideHeader,
|
|
||||||
showShadow,
|
|
||||||
embeddable,
|
|
||||||
hideInspector,
|
|
||||||
containerContext,
|
|
||||||
onPanelStatusChange,
|
|
||||||
} = panelProps;
|
|
||||||
const [node, setNode] = useState<ReactNode | undefined>();
|
const [node, setNode] = useState<ReactNode | undefined>();
|
||||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||||
|
|
||||||
|
@ -74,8 +67,7 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
|
||||||
const editPanel = new EditPanelAction(
|
const editPanel = new EditPanelAction(
|
||||||
embeddableStart.getEmbeddableFactory,
|
embeddableStart.getEmbeddableFactory,
|
||||||
core.application,
|
core.application,
|
||||||
stateTransfer,
|
stateTransfer
|
||||||
containerContext?.getCurrentPath
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions: PanelUniversalActions = {
|
const actions: PanelUniversalActions = {
|
||||||
|
@ -91,7 +83,7 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
|
||||||
};
|
};
|
||||||
if (!hideInspector) actions.inspectPanel = new InspectPanelAction(inspector);
|
if (!hideInspector) actions.inspectPanel = new InspectPanelAction(inspector);
|
||||||
return actions;
|
return actions;
|
||||||
}, [containerContext?.getCurrentPath, hideInspector]);
|
}, [hideInspector]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track panel status changes
|
* Track panel status changes
|
||||||
|
|
|
@ -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 () => {
|
test('redirects to app using state transfer', async () => {
|
||||||
applicationMock.currentAppId$ = of('superCoolCurrentApp');
|
applicationMock.currentAppId$ = of('superCoolCurrentApp');
|
||||||
const testPath = '/test-path';
|
const testPath = '/test-path';
|
||||||
const action = new EditPanelAction(
|
const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
|
||||||
getFactory,
|
|
||||||
applicationMock,
|
|
||||||
stateTransferMock,
|
|
||||||
() => testPath
|
|
||||||
);
|
|
||||||
const embeddable = new EditableEmbeddable(
|
const embeddable = new EditableEmbeddable(
|
||||||
{
|
{
|
||||||
id: '123',
|
id: '123',
|
||||||
|
@ -62,6 +57,9 @@ test('redirects to app using state transfer', async () => {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' }));
|
embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' }));
|
||||||
|
embeddable.getAppContext = jest.fn().mockReturnValue({
|
||||||
|
getCurrentPath: () => testPath,
|
||||||
|
});
|
||||||
await action.execute({ embeddable });
|
await action.execute({ embeddable });
|
||||||
expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', {
|
expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', {
|
||||||
path: '/123',
|
path: '/123',
|
||||||
|
|
|
@ -45,8 +45,7 @@ export class EditPanelAction implements Action<ActionContext> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'],
|
private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'],
|
||||||
private readonly application: ApplicationStart,
|
private readonly application: ApplicationStart,
|
||||||
private readonly stateTransfer?: EmbeddableStateTransfer,
|
private readonly stateTransfer?: EmbeddableStateTransfer
|
||||||
private readonly getOriginatingPath?: () => string
|
|
||||||
) {
|
) {
|
||||||
if (this.application?.currentAppId$) {
|
if (this.application?.currentAppId$) {
|
||||||
this.application.currentAppId$
|
this.application.currentAppId$
|
||||||
|
@ -139,7 +138,7 @@ export class EditPanelAction implements Action<ActionContext> {
|
||||||
|
|
||||||
if (app && path) {
|
if (app && path) {
|
||||||
if (this.currentAppId) {
|
if (this.currentAppId) {
|
||||||
const originatingPath = this.getOriginatingPath?.();
|
const originatingPath = embeddable.getAppContext()?.getCurrentPath?.();
|
||||||
|
|
||||||
const state: EmbeddableEditorState = {
|
const state: EmbeddableEditorState = {
|
||||||
originatingApp: this.currentAppId,
|
originatingApp: this.currentAppId,
|
||||||
|
|
|
@ -19,11 +19,12 @@ import {
|
||||||
import { EmbeddableError } from '../lib/embeddables/i_embeddable';
|
import { EmbeddableError } from '../lib/embeddables/i_embeddable';
|
||||||
import { EmbeddableContext, EmbeddableInput, EmbeddableOutput, IEmbeddable } from '..';
|
import { EmbeddableContext, EmbeddableInput, EmbeddableOutput, IEmbeddable } from '..';
|
||||||
|
|
||||||
export interface EmbeddableContainerContext {
|
export interface EmbeddableAppContext {
|
||||||
/**
|
/**
|
||||||
* Current app's path including query and hash starting from {appId}
|
* Current app's path including query and hash starting from {appId}
|
||||||
*/
|
*/
|
||||||
getCurrentPath?: () => string;
|
getCurrentPath?: () => string;
|
||||||
|
currentAppId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,7 +54,6 @@ export interface EmbeddablePanelProps {
|
||||||
hideHeader?: boolean;
|
hideHeader?: boolean;
|
||||||
hideInspector?: boolean;
|
hideInspector?: boolean;
|
||||||
showNotifications?: boolean;
|
showNotifications?: boolean;
|
||||||
containerContext?: EmbeddableContainerContext;
|
|
||||||
actionPredicate?: (actionId: string) => boolean;
|
actionPredicate?: (actionId: string) => boolean;
|
||||||
onPanelStatusChange?: (info: EmbeddablePhaseEvent) => void;
|
onPanelStatusChange?: (info: EmbeddablePhaseEvent) => void;
|
||||||
getActions?: UiActionsService['getTriggerCompatibleActions'];
|
getActions?: UiActionsService['getTriggerCompatibleActions'];
|
||||||
|
|
|
@ -100,7 +100,7 @@ export {
|
||||||
export type {
|
export type {
|
||||||
EmbeddablePhase,
|
EmbeddablePhase,
|
||||||
EmbeddablePhaseEvent,
|
EmbeddablePhaseEvent,
|
||||||
EmbeddableContainerContext,
|
EmbeddableAppContext,
|
||||||
} from './embeddable_panel/types';
|
} from './embeddable_panel/types';
|
||||||
|
|
||||||
export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
|
export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { IContainer } from '../containers';
|
||||||
import { EmbeddableError, EmbeddableOutput, IEmbeddable } from './i_embeddable';
|
import { EmbeddableError, EmbeddableOutput, IEmbeddable } from './i_embeddable';
|
||||||
import { EmbeddableInput, ViewMode } from '../../../common/types';
|
import { EmbeddableInput, ViewMode } from '../../../common/types';
|
||||||
import { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
|
import { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
|
||||||
|
import { EmbeddableAppContext } from '../../embeddable_panel/types';
|
||||||
|
|
||||||
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
|
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
|
||||||
if (input.hidePanelTitles) return '';
|
if (input.hidePanelTitles) return '';
|
||||||
|
@ -102,6 +103,10 @@ export abstract class Embeddable<
|
||||||
.subscribe((title) => this.renderComplete.setTitle(title));
|
.subscribe((title) => this.renderComplete.setTitle(title));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAppContext(): EmbeddableAppContext | undefined {
|
||||||
|
return this.parent?.getAppContext();
|
||||||
|
}
|
||||||
|
|
||||||
public reportsEmbeddableLoad() {
|
public reportsEmbeddableLoad() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ErrorLike } from '@kbn/expressions-plugin/common';
|
||||||
import { Adapters } from '../types';
|
import { Adapters } from '../types';
|
||||||
import { IContainer } from '../containers/i_container';
|
import { IContainer } from '../containers/i_container';
|
||||||
import { EmbeddableInput } from '../../../common/types';
|
import { EmbeddableInput } from '../../../common/types';
|
||||||
|
import { EmbeddableAppContext } from '../../embeddable_panel/types';
|
||||||
|
|
||||||
export type EmbeddableError = ErrorLike;
|
export type EmbeddableError = ErrorLike;
|
||||||
export type { EmbeddableInput };
|
export type { EmbeddableInput };
|
||||||
|
@ -181,6 +182,11 @@ export interface IEmbeddable<
|
||||||
*/
|
*/
|
||||||
getRoot(): IEmbeddable | IContainer;
|
getRoot(): IEmbeddable | IContainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the context of this embeddable's container, or undefined.
|
||||||
|
*/
|
||||||
|
getAppContext(): EmbeddableAppContext | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the embeddable at the given node.
|
* Renders the embeddable at the given node.
|
||||||
* @param domNode
|
* @param domNode
|
||||||
|
|
|
@ -17,13 +17,13 @@ import {
|
||||||
isErrorEmbeddable,
|
isErrorEmbeddable,
|
||||||
EmbeddablePanel,
|
EmbeddablePanel,
|
||||||
} from '@kbn/embeddable-plugin/public';
|
} 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 { StartDeps } from '../../plugin';
|
||||||
import { EmbeddableExpression } from '../../expression_types/embeddable';
|
import { EmbeddableExpression } from '../../expression_types/embeddable';
|
||||||
import { RendererStrings } from '../../../i18n';
|
import { RendererStrings } from '../../../i18n';
|
||||||
import { embeddableInputToExpression } from './embeddable_input_to_expression';
|
import { embeddableInputToExpression } from './embeddable_input_to_expression';
|
||||||
import { RendererFactory, EmbeddableInput } from '../../../types';
|
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;
|
const { embeddable: strings } = RendererStrings;
|
||||||
|
|
||||||
|
@ -41,18 +41,19 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const embeddableContainerContext: EmbeddableContainerContext = {
|
const canvasAppContext: EmbeddableAppContext = {
|
||||||
getCurrentPath: () => {
|
getCurrentPath: () => {
|
||||||
const urlToApp = core.application.getUrlForApp(currentAppId);
|
const urlToApp = core.application.getUrlForApp(currentAppId);
|
||||||
const inAppPath = window.location.pathname.replace(urlToApp, '');
|
const inAppPath = window.location.pathname.replace(urlToApp, '');
|
||||||
|
|
||||||
return inAppPath + window.location.search + window.location.hash;
|
return inAppPath + window.location.search + window.location.hash;
|
||||||
},
|
},
|
||||||
|
currentAppId: CANVAS_APP,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
embeddable.getAppContext = () => canvasAppContext;
|
||||||
<EmbeddablePanel embeddable={embeddable} containerContext={embeddableContainerContext} />
|
|
||||||
);
|
return <EmbeddablePanel embeddable={embeddable} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (embeddableObject: IEmbeddable) => {
|
return (embeddableObject: IEmbeddable) => {
|
||||||
|
|
|
@ -794,18 +794,14 @@ export class Embeddable
|
||||||
* Used for the Edit in Lens link inside the inline editing flyout.
|
* Used for the Edit in Lens link inside the inline editing flyout.
|
||||||
*/
|
*/
|
||||||
private async navigateToLensEditor() {
|
private async navigateToLensEditor() {
|
||||||
const executionContext = this.getExecutionContext();
|
const appContext = this.getAppContext();
|
||||||
/**
|
/**
|
||||||
* The origininating app variable is very important for the Save and Return button
|
* The origininating app variable is very important for the Save and Return button
|
||||||
* of the editor to work properly.
|
* 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 = {
|
const transferState = {
|
||||||
originatingApp:
|
originatingApp: appContext?.currentAppId ?? 'dashboards',
|
||||||
executionContext?.type === 'dashboard'
|
originatingPath: appContext?.getCurrentPath?.(),
|
||||||
? 'dashboards'
|
|
||||||
: executionContext?.type ?? 'dashboards',
|
|
||||||
valueInput: this.getExplicitInput(),
|
valueInput: this.getExplicitInput(),
|
||||||
embeddableId: this.id,
|
embeddableId: this.id,
|
||||||
searchSessionId: this.getInput().searchSessionId,
|
searchSessionId: this.getInput().searchSessionId,
|
||||||
|
@ -818,6 +814,7 @@ export class Embeddable
|
||||||
await transfer.navigateToEditor(APP_ID, {
|
await transfer.navigateToEditor(APP_ID, {
|
||||||
path: this.output.editPath,
|
path: this.output.editPath,
|
||||||
state: transferState,
|
state: transferState,
|
||||||
|
skipAppLeave: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ interface StartAppComponent {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
history: History;
|
history: History;
|
||||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
|
||||||
store: Store<State, Action>;
|
store: Store<State, Action>;
|
||||||
theme$: AppMountParameters['theme$'];
|
theme$: AppMountParameters['theme$'];
|
||||||
}
|
}
|
||||||
|
@ -46,7 +45,6 @@ interface StartAppComponent {
|
||||||
const StartAppComponent: FC<StartAppComponent> = ({
|
const StartAppComponent: FC<StartAppComponent> = ({
|
||||||
children,
|
children,
|
||||||
history,
|
history,
|
||||||
setHeaderActionMenu,
|
|
||||||
onAppLeave,
|
onAppLeave,
|
||||||
store,
|
store,
|
||||||
theme$,
|
theme$,
|
||||||
|
@ -79,11 +77,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
||||||
>
|
>
|
||||||
<UpsellingProvider upsellingService={upselling}>
|
<UpsellingProvider upsellingService={upselling}>
|
||||||
<DiscoverInTimelineContextProvider>
|
<DiscoverInTimelineContextProvider>
|
||||||
<PageRouter
|
<PageRouter history={history} onAppLeave={onAppLeave}>
|
||||||
history={history}
|
|
||||||
onAppLeave={onAppLeave}
|
|
||||||
setHeaderActionMenu={setHeaderActionMenu}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</PageRouter>
|
</PageRouter>
|
||||||
</DiscoverInTimelineContextProvider>
|
</DiscoverInTimelineContextProvider>
|
||||||
|
@ -113,7 +107,6 @@ interface SecurityAppComponentProps {
|
||||||
history: History;
|
history: History;
|
||||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||||
services: StartServices;
|
services: StartServices;
|
||||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
|
||||||
store: Store<State, Action>;
|
store: Store<State, Action>;
|
||||||
theme$: AppMountParameters['theme$'];
|
theme$: AppMountParameters['theme$'];
|
||||||
}
|
}
|
||||||
|
@ -123,7 +116,6 @@ const SecurityAppComponent: React.FC<SecurityAppComponentProps> = ({
|
||||||
history,
|
history,
|
||||||
onAppLeave,
|
onAppLeave,
|
||||||
services,
|
services,
|
||||||
setHeaderActionMenu,
|
|
||||||
store,
|
store,
|
||||||
theme$,
|
theme$,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -137,13 +129,7 @@ const SecurityAppComponent: React.FC<SecurityAppComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloudProvider>
|
<CloudProvider>
|
||||||
<StartApp
|
<StartApp history={history} onAppLeave={onAppLeave} store={store} theme$={theme$}>
|
||||||
history={history}
|
|
||||||
onAppLeave={onAppLeave}
|
|
||||||
setHeaderActionMenu={setHeaderActionMenu}
|
|
||||||
store={store}
|
|
||||||
theme$={theme$}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</StartApp>
|
</StartApp>
|
||||||
</CloudProvider>
|
</CloudProvider>
|
||||||
|
|
|
@ -49,7 +49,6 @@ jest.mock('react-reverse-portal', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('global header', () => {
|
describe('global header', () => {
|
||||||
const mockSetHeaderActionMenu = jest.fn();
|
|
||||||
const state = {
|
const state = {
|
||||||
...mockGlobalState,
|
...mockGlobalState,
|
||||||
timeline: {
|
timeline: {
|
||||||
|
@ -75,7 +74,7 @@ describe('global header', () => {
|
||||||
]);
|
]);
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
expect(getByText('Add integrations')).toBeInTheDocument();
|
expect(getByText('Add integrations')).toBeInTheDocument();
|
||||||
|
@ -87,7 +86,7 @@ describe('global header', () => {
|
||||||
]);
|
]);
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
const link = queryByTestId('add-data');
|
const link = queryByTestId('add-data');
|
||||||
|
@ -98,7 +97,7 @@ describe('global header', () => {
|
||||||
(useLocation as jest.Mock).mockReturnValue({ pathname: THREAT_INTELLIGENCE_PATH });
|
(useLocation as jest.Mock).mockReturnValue({ pathname: THREAT_INTELLIGENCE_PATH });
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
const link = queryByTestId('add-data');
|
const link = queryByTestId('add-data');
|
||||||
|
@ -118,7 +117,7 @@ describe('global header', () => {
|
||||||
);
|
);
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
const link = queryByTestId('add-data');
|
const link = queryByTestId('add-data');
|
||||||
|
@ -130,7 +129,7 @@ describe('global header', () => {
|
||||||
|
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
|
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
|
||||||
|
@ -141,7 +140,7 @@ describe('global header', () => {
|
||||||
|
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
|
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
|
||||||
|
@ -166,7 +165,7 @@ describe('global header', () => {
|
||||||
|
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<TestProviders store={mockStore}>
|
<TestProviders store={mockStore}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -180,7 +179,7 @@ describe('global header', () => {
|
||||||
|
|
||||||
const { findByTestId } = render(
|
const { findByTestId } = render(
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,10 @@ import { useLocation } from 'react-router-dom';
|
||||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import type { AppMountParameters } from '@kbn/core/public';
|
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
|
||||||
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
|
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
|
||||||
import { useKibana } from '../../../common/lib/kibana';
|
import { useKibana } from '../../../common/lib/kibana';
|
||||||
import { isDetectionsPath } from '../../../helpers';
|
import { isDetectionsPath, isDashboardViewPath } from '../../../helpers';
|
||||||
import { Sourcerer } from '../../../common/components/sourcerer';
|
import { Sourcerer } from '../../../common/components/sourcerer';
|
||||||
import { TimelineId } from '../../../../common/types/timeline';
|
import { TimelineId } from '../../../../common/types/timeline';
|
||||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
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
|
* 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
|
* right hand side of the Kibana global header
|
||||||
*/
|
*/
|
||||||
export const GlobalHeader = React.memo(
|
export const GlobalHeader = React.memo(() => {
|
||||||
({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
|
const portalNode = useMemo(() => createHtmlPortalNode(), []);
|
||||||
const portalNode = useMemo(() => createHtmlPortalNode(), []);
|
const { theme, setHeaderActionMenu, i18n: kibanaServiceI18n } = useKibana().services;
|
||||||
const { theme } = useKibana().services;
|
const { pathname } = useLocation();
|
||||||
const { pathname } = useLocation();
|
|
||||||
|
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const showTimeline = useShallowEqualSelector(
|
const showTimeline = useShallowEqualSelector(
|
||||||
(state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
|
(state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
|
||||||
);
|
);
|
||||||
|
|
||||||
const sourcererScope = getScopeFromPath(pathname);
|
const sourcererScope = getScopeFromPath(pathname);
|
||||||
const showSourcerer = showSourcererByPath(pathname);
|
const showSourcerer = showSourcererByPath(pathname);
|
||||||
|
const dashboardViewPath = isDashboardViewPath(pathname);
|
||||||
|
|
||||||
const { href, onClick } = useAddIntegrationsUrl();
|
const { href, onClick } = useAddIntegrationsUrl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeaderActionMenu((element) => {
|
setHeaderActionMenu((element) => {
|
||||||
const mount = toMountPoint(<OutPortal node={portalNode} />, { theme$: theme.theme$ });
|
const mount = toMountPoint(<OutPortal node={portalNode} />, {
|
||||||
return mount(element);
|
theme,
|
||||||
|
i18n: kibanaServiceI18n,
|
||||||
});
|
});
|
||||||
|
return mount(element);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
portalNode.unmount();
|
/* Dashboard mounts an edit toolbar, it should be restored when leaving dashboard editing page */
|
||||||
setHeaderActionMenu(undefined);
|
if (dashboardViewPath) {
|
||||||
};
|
return;
|
||||||
}, [portalNode, setHeaderActionMenu, theme.theme$]);
|
}
|
||||||
|
portalNode.unmount();
|
||||||
return (
|
setHeaderActionMenu(undefined);
|
||||||
<InPortal node={portalNode}>
|
};
|
||||||
<EuiHeaderSection side="right">
|
}, [portalNode, setHeaderActionMenu, theme, kibanaServiceI18n, dashboardViewPath]);
|
||||||
{isDetectionsPath(pathname) && (
|
|
||||||
<EuiHeaderSectionItem>
|
|
||||||
<MlPopover />
|
|
||||||
</EuiHeaderSectionItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InPortal node={portalNode}>
|
||||||
|
<EuiHeaderSection side="right">
|
||||||
|
{isDetectionsPath(pathname) && (
|
||||||
<EuiHeaderSectionItem>
|
<EuiHeaderSectionItem>
|
||||||
<EuiHeaderLinks>
|
<MlPopover />
|
||||||
<EuiHeaderLink
|
|
||||||
color="primary"
|
|
||||||
data-test-subj="add-data"
|
|
||||||
href={href}
|
|
||||||
iconType="indexOpen"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{BUTTON_ADD_DATA}
|
|
||||||
</EuiHeaderLink>
|
|
||||||
{showSourcerer && !showTimeline && (
|
|
||||||
<Sourcerer scope={sourcererScope} data-test-subj="sourcerer" />
|
|
||||||
)}
|
|
||||||
<AssistantHeaderLink />
|
|
||||||
</EuiHeaderLinks>
|
|
||||||
</EuiHeaderSectionItem>
|
</EuiHeaderSectionItem>
|
||||||
</EuiHeaderSection>
|
)}
|
||||||
</InPortal>
|
|
||||||
);
|
<EuiHeaderSectionItem>
|
||||||
}
|
<EuiHeaderLinks>
|
||||||
);
|
<EuiHeaderLink
|
||||||
|
color="primary"
|
||||||
|
data-test-subj="add-data"
|
||||||
|
href={href}
|
||||||
|
iconType="indexOpen"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{BUTTON_ADD_DATA}
|
||||||
|
</EuiHeaderLink>
|
||||||
|
{showSourcerer && !showTimeline && (
|
||||||
|
<Sourcerer scope={sourcererScope} data-test-subj="sourcerer" />
|
||||||
|
)}
|
||||||
|
<AssistantHeaderLink />
|
||||||
|
</EuiHeaderLinks>
|
||||||
|
</EuiHeaderSectionItem>
|
||||||
|
</EuiHeaderSection>
|
||||||
|
</InPortal>
|
||||||
|
);
|
||||||
|
});
|
||||||
GlobalHeader.displayName = 'GlobalHeader';
|
GlobalHeader.displayName = 'GlobalHeader';
|
||||||
|
|
|
@ -106,6 +106,7 @@ jest.mock('../../timelines/store/timeline', () => ({
|
||||||
|
|
||||||
const mockedFilterManager = new FilterManager(coreMock.createStart().uiSettings);
|
const mockedFilterManager = new FilterManager(coreMock.createStart().uiSettings);
|
||||||
const mockGetSavedQuery = jest.fn();
|
const mockGetSavedQuery = jest.fn();
|
||||||
|
const mockSetHeaderActionMenu = jest.fn();
|
||||||
|
|
||||||
const dummyFilter: Filter = {
|
const dummyFilter: Filter = {
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -198,6 +199,7 @@ jest.mock('../../common/lib/kibana', () => {
|
||||||
savedQueries: { getSavedQuery: mockGetSavedQuery },
|
savedQueries: { getSavedQuery: mockGetSavedQuery },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setHeaderActionMenu: mockSetHeaderActionMenu,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
KibanaServices: {
|
KibanaServices: {
|
||||||
|
@ -226,7 +228,7 @@ describe('HomePage', () => {
|
||||||
it('calls useInitializeUrlParam for appQuery, filters and savedQuery', () => {
|
it('calls useInitializeUrlParam for appQuery, filters and savedQuery', () => {
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -252,7 +254,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -294,7 +296,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -326,7 +328,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -361,7 +363,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders store={mockStore}>
|
<TestProviders store={mockStore}>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -378,7 +380,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -420,7 +422,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -465,7 +467,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -515,7 +517,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
const TestComponent = () => (
|
const TestComponent = () => (
|
||||||
<TestProviders store={mockStore}>
|
<TestProviders store={mockStore}>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -572,7 +574,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
const TestComponent = () => (
|
const TestComponent = () => (
|
||||||
<TestProviders store={mockStore}>
|
<TestProviders store={mockStore}>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -612,7 +614,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -637,7 +639,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
const TestComponent = () => (
|
const TestComponent = () => (
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -669,7 +671,7 @@ describe('HomePage', () => {
|
||||||
|
|
||||||
const TestComponent = () => (
|
const TestComponent = () => (
|
||||||
<TestProviders store={store}>
|
<TestProviders store={store}>
|
||||||
<HomePage setHeaderActionMenu={jest.fn()}>
|
<HomePage>
|
||||||
<span />
|
<span />
|
||||||
</HomePage>
|
</HomePage>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
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 { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper';
|
||||||
import { SecuritySolutionAppWrapper } from '../../common/components/page';
|
import { SecuritySolutionAppWrapper } from '../../common/components/page';
|
||||||
|
|
||||||
|
@ -33,10 +32,9 @@ import { AssistantOverlay } from '../../assistant/overlay';
|
||||||
|
|
||||||
interface HomePageProps {
|
interface HomePageProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const HomePageComponent: React.FC<HomePageProps> = ({ children, setHeaderActionMenu }) => {
|
const HomePageComponent: React.FC<HomePageProps> = ({ children }) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
useInitSourcerer(getScopeFromPath(pathname));
|
useInitSourcerer(getScopeFromPath(pathname));
|
||||||
useUrlState();
|
useUrlState();
|
||||||
|
@ -58,7 +56,7 @@ const HomePageComponent: React.FC<HomePageProps> = ({ children, setHeaderActionM
|
||||||
<ConsoleManager>
|
<ConsoleManager>
|
||||||
<TourContextProvider>
|
<TourContextProvider>
|
||||||
<>
|
<>
|
||||||
<GlobalHeader setHeaderActionMenu={setHeaderActionMenu} />
|
<GlobalHeader />
|
||||||
<DragDropContextWrapper browserFields={browserFields}>
|
<DragDropContextWrapper browserFields={browserFields}>
|
||||||
{children}
|
{children}
|
||||||
</DragDropContextWrapper>
|
</DragDropContextWrapper>
|
||||||
|
|
|
@ -16,7 +16,6 @@ export const renderApp = ({
|
||||||
element,
|
element,
|
||||||
history,
|
history,
|
||||||
onAppLeave,
|
onAppLeave,
|
||||||
setHeaderActionMenu,
|
|
||||||
services,
|
services,
|
||||||
store,
|
store,
|
||||||
usageCollection,
|
usageCollection,
|
||||||
|
@ -31,7 +30,6 @@ export const renderApp = ({
|
||||||
history={history}
|
history={history}
|
||||||
onAppLeave={onAppLeave}
|
onAppLeave={onAppLeave}
|
||||||
services={services}
|
services={services}
|
||||||
setHeaderActionMenu={setHeaderActionMenu}
|
|
||||||
store={store}
|
store={store}
|
||||||
theme$={theme$}
|
theme$={theme$}
|
||||||
>
|
>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import type { FC } from 'react';
|
||||||
import React, { memo, useEffect } from 'react';
|
import React, { memo, useEffect } from 'react';
|
||||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||||
import { useDispatch } from 'react-redux';
|
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 { APP_ID } from '../../common/constants';
|
||||||
import { RouteCapture } from '../common/components/endpoint/route_capture';
|
import { RouteCapture } from '../common/components/endpoint/route_capture';
|
||||||
|
@ -24,15 +24,9 @@ interface RouterProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
history: History;
|
history: History;
|
||||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageRouterComponent: FC<RouterProps> = ({
|
const PageRouterComponent: FC<RouterProps> = ({ children, history, onAppLeave }) => {
|
||||||
children,
|
|
||||||
history,
|
|
||||||
onAppLeave,
|
|
||||||
setHeaderActionMenu,
|
|
||||||
}) => {
|
|
||||||
const { cases } = useKibana().services;
|
const { cases } = useKibana().services;
|
||||||
const CasesContext = cases.ui.getCasesContext();
|
const CasesContext = cases.ui.getCasesContext();
|
||||||
const userCasesPermissions = useGetUserCasesPermissions();
|
const userCasesPermissions = useGetUserCasesPermissions();
|
||||||
|
@ -55,7 +49,7 @@ const PageRouterComponent: FC<RouterProps> = ({
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
|
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
|
||||||
<HomePage setHeaderActionMenu={setHeaderActionMenu}>{children}</HomePage>
|
<HomePage>{children}</HomePage>
|
||||||
</CasesContext>
|
</CasesContext>
|
||||||
</Route>
|
</Route>
|
||||||
<Route>
|
<Route>
|
||||||
|
|
|
@ -34,3 +34,14 @@ export const getTagsByName = jest
|
||||||
export const createTag = jest
|
export const createTag = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(() => Promise.resolve(DEFAULT_CREATE_TAGS_RESPONSE[0]));
|
.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',
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
|
@ -121,6 +121,7 @@ export const createStartServicesMock = (
|
||||||
const cloudExperiments = cloudExperimentsMock.createStartMock();
|
const cloudExperiments = cloudExperimentsMock.createStartMock();
|
||||||
const guidedOnboarding = guidedOnboardingMock.createStart();
|
const guidedOnboarding = guidedOnboardingMock.createStart();
|
||||||
const cloud = cloudMock.createStart();
|
const cloud = cloudMock.createStart();
|
||||||
|
const mockSetHeaderActionMenu = jest.fn();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...core,
|
...core,
|
||||||
|
@ -220,6 +221,7 @@ export const createStartServicesMock = (
|
||||||
customDataService,
|
customDataService,
|
||||||
uiActions: uiActionsPluginMock.createStartContract(),
|
uiActions: uiActionsPluginMock.createStartContract(),
|
||||||
savedSearch: savedSearchPluginMock.createStartContract(),
|
savedSearch: savedSearchPluginMock.createStartContract(),
|
||||||
|
setHeaderActionMenu: mockSetHeaderActionMenu,
|
||||||
} as unknown as StartServices;
|
} as unknown as StartServices;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,10 @@ import { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-
|
||||||
import { TestProviders } from '../../common/mock';
|
import { TestProviders } from '../../common/mock';
|
||||||
import { DashboardRenderer } from './dashboard_renderer';
|
import { DashboardRenderer } from './dashboard_renderer';
|
||||||
|
|
||||||
jest.mock('@kbn/dashboard-plugin/public', () => {
|
jest.mock('@kbn/dashboard-plugin/public', () => ({
|
||||||
const actual = jest.requireActual('@kbn/dashboard-plugin/public');
|
DashboardRenderer: jest.fn().mockReturnValue(<div data-test-subj="dashboardRenderer" />),
|
||||||
return {
|
DashboardTopNav: jest.fn().mockReturnValue(<span data-test-subj="dashboardTopNav" />),
|
||||||
...actual,
|
}));
|
||||||
DashboardRenderer: jest.fn().mockReturnValue(<div data-test-subj="dashboardRenderer" />),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => {
|
jest.mock('react-router-dom', () => {
|
||||||
const actual = jest.requireActual('react-router-dom');
|
const actual = jest.requireActual('react-router-dom');
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
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 { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import type { Filter, Query } from '@kbn/es-query';
|
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 { useDispatch } from 'react-redux';
|
||||||
import { InputsModelId } from '../../common/store/inputs/constants';
|
import { InputsModelId } from '../../common/store/inputs/constants';
|
||||||
import { inputsActions } from '../../common/store/inputs';
|
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 = ({
|
const DashboardRendererComponent = ({
|
||||||
canReadDashboard,
|
canReadDashboard,
|
||||||
|
dashboardContainer,
|
||||||
filters,
|
filters,
|
||||||
id,
|
id,
|
||||||
inputId = InputsModelId.global,
|
inputId = InputsModelId.global,
|
||||||
|
@ -23,8 +28,10 @@ const DashboardRendererComponent = ({
|
||||||
query,
|
query,
|
||||||
savedObjectId,
|
savedObjectId,
|
||||||
timeRange,
|
timeRange,
|
||||||
|
viewMode = ViewMode.VIEW,
|
||||||
}: {
|
}: {
|
||||||
canReadDashboard: boolean;
|
canReadDashboard: boolean;
|
||||||
|
dashboardContainer?: DashboardAPI;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
id: string;
|
id: string;
|
||||||
inputId?: InputsModelId.global | InputsModelId.timeline;
|
inputId?: InputsModelId.global | InputsModelId.timeline;
|
||||||
|
@ -37,17 +44,36 @@ const DashboardRendererComponent = ({
|
||||||
to: string;
|
to: string;
|
||||||
toStr?: string | undefined;
|
toStr?: string | undefined;
|
||||||
};
|
};
|
||||||
|
viewMode?: ViewMode;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { embeddable } = useKibana().services;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardAPI>();
|
|
||||||
|
|
||||||
const getCreationOptions = useCallback(
|
const securityTags = useSecurityTags();
|
||||||
|
const firstSecurityTagId = securityTags?.[0]?.id;
|
||||||
|
|
||||||
|
const isCreateDashboard = !savedObjectId;
|
||||||
|
|
||||||
|
const getCreationOptions: () => Promise<DashboardCreationOptions> = useCallback(
|
||||||
() =>
|
() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
getInitialInput: () => ({ timeRange, viewMode: ViewMode.VIEW, query, filters }),
|
useSessionStorageIntegration: true,
|
||||||
useControlGroupIntegration: 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(() => {
|
const refetchByForceRefresh = useCallback(() => {
|
||||||
|
@ -73,20 +99,33 @@ const DashboardRendererComponent = ({
|
||||||
dashboardContainer?.updateInput({ timeRange, query, filters });
|
dashboardContainer?.updateInput({ timeRange, query, filters });
|
||||||
}, [dashboardContainer, filters, query, timeRange]);
|
}, [dashboardContainer, filters, query, timeRange]);
|
||||||
|
|
||||||
const handleDashboardLoaded = useCallback(
|
useEffect(() => {
|
||||||
(container: DashboardAPI) => {
|
if (isCreateDashboard && firstSecurityTagId)
|
||||||
setDashboardContainer(container);
|
dashboardContainer?.updateInput({ tags: [firstSecurityTagId] });
|
||||||
onDashboardContainerLoaded?.(container);
|
}, [dashboardContainer, firstSecurityTagId, isCreateDashboard]);
|
||||||
},
|
|
||||||
[onDashboardContainerLoaded]
|
/** Dashboard renderer is stored in the state as it's a temporary solution for
|
||||||
);
|
* https://github.com/elastic/kibana/issues/167751
|
||||||
return savedObjectId && canReadDashboard ? (
|
**/
|
||||||
<DashboardContainerRenderer
|
const [dashboardContainerRenderer, setDashboardContainerRenderer] = useState<
|
||||||
ref={handleDashboardLoaded}
|
React.ReactElement | undefined
|
||||||
savedObjectId={savedObjectId}
|
>(undefined);
|
||||||
getCreationOptions={getCreationOptions}
|
|
||||||
/>
|
useEffect(() => {
|
||||||
) : null;
|
setDashboardContainerRenderer(
|
||||||
|
<DashboardContainerRenderer
|
||||||
|
ref={onDashboardContainerLoaded}
|
||||||
|
savedObjectId={savedObjectId}
|
||||||
|
getCreationOptions={getCreationOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setDashboardContainerRenderer(undefined);
|
||||||
|
};
|
||||||
|
}, [getCreationOptions, onDashboardContainerLoaded, refetchByForceRefresh, savedObjectId]);
|
||||||
|
|
||||||
|
return canReadDashboard ? <>{dashboardContainerRenderer}</> : null;
|
||||||
};
|
};
|
||||||
DashboardRendererComponent.displayName = 'DashboardRendererComponent';
|
DashboardRendererComponent.displayName = 'DashboardRendererComponent';
|
||||||
export const DashboardRenderer = React.memo(DashboardRendererComponent);
|
export const DashboardRenderer = React.memo(DashboardRendererComponent);
|
||||||
|
|
|
@ -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 ? <span>{title}</span> : <EuiLoadingSpinner size="m" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DashboardTitle = React.memo(DashboardTitleComponent);
|
|
@ -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(() => <div data-test-subj="dashboard-top-nav" />),
|
||||||
|
}));
|
||||||
|
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 }) => (
|
||||||
|
<TestProviders>
|
||||||
|
<NavigationProvider core={mockCore}>{children}</NavigationProvider>
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('DashboardToolBar', () => {
|
||||||
|
const mockOnLoad = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(useNavigation as jest.Mock).mockReturnValue({
|
||||||
|
navigateTo: mockNavigateTo,
|
||||||
|
getAppUrl: mockGetAppUrl,
|
||||||
|
});
|
||||||
|
render(<DashboardToolBar onLoad={mockOnLoad} dashboardContainer={mockDashboardContainer} />, {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<DashboardCapabilities>(LEGACY_DASHBOARD_APP_ID);
|
||||||
|
|
||||||
|
return showWriteControls ? (
|
||||||
|
<DashboardTopNav
|
||||||
|
customLeadingBreadCrumbs={landingBreadcrumb}
|
||||||
|
dashboardContainer={dashboardContainer}
|
||||||
|
forceHideUnifiedSearch={true}
|
||||||
|
embedSettings={embedSettings}
|
||||||
|
redirectTo={redirectTo}
|
||||||
|
showBorderBottom={false}
|
||||||
|
setCustomHeaderActionMenu={setHeaderActionMenu}
|
||||||
|
showResetChange={false}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DashboardToolBar = React.memo(DashboardToolBarComponent);
|
|
@ -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(
|
|
||||||
<TestProviders>
|
|
||||||
<EditDashboardButton {...testProps} />
|
|
||||||
</TestProviders>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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<EditDashboardButtonComponentProps> = ({
|
|
||||||
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 (
|
|
||||||
<EuiButton
|
|
||||||
color="primary"
|
|
||||||
data-test-subj="dashboardEditButton"
|
|
||||||
fill
|
|
||||||
iconType="pencil"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{EDIT_DASHBOARD_BUTTON_TITLE}
|
|
||||||
</EuiButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EditDashboardButtonComponent.displayName = 'EditDashboardComponent';
|
|
||||||
export const EditDashboardButton = React.memo(EditDashboardButtonComponent);
|
|
|
@ -20,7 +20,6 @@ const DashboardContext = React.createContext<DashboardContextType | null>({ secu
|
||||||
|
|
||||||
export const DashboardContextProvider: React.FC = ({ children }) => {
|
export const DashboardContextProvider: React.FC = ({ children }) => {
|
||||||
const { tags, isLoading } = useFetchSecurityTags();
|
const { tags, isLoading } = useFetchSecurityTags();
|
||||||
|
|
||||||
const securityTags = isLoading || !tags ? null : tags;
|
const securityTags = isLoading || !tags ? null : tags;
|
||||||
|
|
||||||
return <DashboardContext.Provider value={{ securityTags }}>{children}</DashboardContext.Provider>;
|
return <DashboardContext.Provider value={{ securityTags }}>{children}</DashboardContext.Provider>;
|
||||||
|
|
|
@ -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',
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -6,23 +6,37 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { renderHook, act } from '@testing-library/react-hooks';
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
|
|
||||||
import { useKibana } from '../../common/lib/kibana';
|
import { useKibana } from '../../common/lib/kibana';
|
||||||
import { useCreateSecurityDashboardLink } from './use_create_security_dashboard_link';
|
import { useCreateSecurityDashboardLink } from './use_create_security_dashboard_link';
|
||||||
import { DashboardContextProvider } from '../context/dashboard_context';
|
import { DashboardContextProvider } from '../context/dashboard_context';
|
||||||
import { getTagsByName } from '../../common/containers/tags/api';
|
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');
|
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 = () =>
|
const renderUseCreateSecurityDashboardLink = () =>
|
||||||
renderHook(() => useCreateSecurityDashboardLink(), {
|
renderHook(() => useCreateSecurityDashboardLink(), {
|
||||||
wrapper: DashboardContextProvider,
|
wrapper: ({ children }) => (
|
||||||
|
<TestProviders>
|
||||||
|
<DashboardContextProvider>{children}</DashboardContextProvider>
|
||||||
|
</TestProviders>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const asyncRenderUseCreateSecurityDashboard = async () => {
|
const asyncRenderUseCreateSecurityDashboard = async () => {
|
||||||
const renderedHook = renderUseCreateSecurityDashboardLink();
|
const renderedHook = renderUseCreateSecurityDashboardLink();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await renderedHook.waitForNextUpdate();
|
await renderedHook.waitForNextUpdate();
|
||||||
});
|
});
|
||||||
|
@ -30,12 +44,15 @@ const asyncRenderUseCreateSecurityDashboard = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('useCreateSecurityDashboardLink', () => {
|
describe('useCreateSecurityDashboardLink', () => {
|
||||||
const mockGetRedirectUrl = jest.fn(() => URL);
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
useKibana().services.dashboard = {
|
(useKibana as jest.Mock).mockReturnValue({
|
||||||
locator: { getRedirectUrl: mockGetRedirectUrl },
|
services: {
|
||||||
} as unknown as DashboardStart;
|
savedObjectsTagging: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
http: { get: jest.fn() },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -55,8 +72,7 @@ describe('useCreateSecurityDashboardLink', () => {
|
||||||
const result1 = result.current;
|
const result1 = result.current;
|
||||||
act(() => rerender());
|
act(() => rerender());
|
||||||
const result2 = result.current;
|
const result2 = result.current;
|
||||||
|
expect(result1).toEqual(result2);
|
||||||
expect(result1).toBe(result2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not re-request tag id when re-rendered', async () => {
|
it('should not re-request tag id when re-rendered', async () => {
|
||||||
|
@ -71,14 +87,14 @@ describe('useCreateSecurityDashboardLink', () => {
|
||||||
const { result, waitForNextUpdate } = renderUseCreateSecurityDashboardLink();
|
const { result, waitForNextUpdate } = renderUseCreateSecurityDashboardLink();
|
||||||
|
|
||||||
expect(result.current.isLoading).toEqual(true);
|
expect(result.current.isLoading).toEqual(true);
|
||||||
expect(result.current.url).toEqual('');
|
expect(result.current.url).toEqual('/app/security/dashboards/create');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.isLoading).toEqual(false);
|
expect(result.current.isLoading).toEqual(false);
|
||||||
expect(result.current.url).toEqual(URL);
|
expect(result.current.url).toEqual('/app/security/dashboards/create');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -7,24 +7,28 @@
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSecurityTags } from '../context/dashboard_context';
|
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 };
|
type UseCreateDashboard = () => { isLoading: boolean; url: string };
|
||||||
|
|
||||||
export const useCreateSecurityDashboardLink: UseCreateDashboard = () => {
|
export const useCreateSecurityDashboardLink: UseCreateDashboard = () => {
|
||||||
const { dashboard } = useKibana().services;
|
const getSecuritySolutionUrl = useGetSecuritySolutionUrl();
|
||||||
const securityTags = useSecurityTags();
|
const securityTags = useSecurityTags();
|
||||||
|
const url = getSecuritySolutionUrl({
|
||||||
|
deepLinkId: SecurityPageName.dashboards,
|
||||||
|
path: 'create',
|
||||||
|
});
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
const firstSecurityTagId = securityTags?.[0]?.id;
|
const firstSecurityTagId = securityTags?.[0]?.id;
|
||||||
if (!firstSecurityTagId) {
|
if (!firstSecurityTagId) {
|
||||||
return { isLoading: true, url: '' };
|
return { isLoading: true, url };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
url: dashboard?.locator?.getRedirectUrl({ tags: [firstSecurityTagId] }) ?? '',
|
url,
|
||||||
};
|
};
|
||||||
}, [securityTags, dashboard?.locator]);
|
}, [securityTags, url]);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<DashboardAPI>();
|
||||||
|
|
||||||
|
const handleDashboardLoaded = useCallback((container: DashboardAPI) => {
|
||||||
|
setDashboardContainer(container);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
dashboardContainer,
|
||||||
|
handleDashboardLoaded,
|
||||||
|
}),
|
||||||
|
[dashboardContainer, handleDashboardLoaded]
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,9 +8,9 @@
|
||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback } from 'react';
|
||||||
import type { MouseEventHandler } from 'react';
|
import type { MouseEventHandler } from 'react';
|
||||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
import { LinkAnchor } from '../../common/components/links';
|
import { LinkAnchor } from '../../common/components/links';
|
||||||
import { useKibana, useNavigateTo } from '../../common/lib/kibana';
|
import { useKibana, useNavigateTo } from '../../common/lib/kibana';
|
||||||
import * as i18n from './translations';
|
|
||||||
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../common/lib/telemetry';
|
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../common/lib/telemetry';
|
||||||
import { SecurityPageName } from '../../../common/constants';
|
import { SecurityPageName } from '../../../common/constants';
|
||||||
import { useGetSecuritySolutionUrl } from '../../common/components/link_to';
|
import { useGetSecuritySolutionUrl } from '../../common/components/link_to';
|
||||||
|
@ -56,7 +56,9 @@ export const useSecurityDashboardsTableColumns = (): Array<
|
||||||
(): Array<EuiBasicTableColumn<DashboardTableItem>> => [
|
(): Array<EuiBasicTableColumn<DashboardTableItem>> => [
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'title',
|
||||||
name: i18n.DASHBOARD_TITLE,
|
name: i18n.translate('xpack.securitySolution.dashboards.title', {
|
||||||
|
defaultMessage: 'Title',
|
||||||
|
}),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (title: string, { id }) => {
|
render: (title: string, { id }) => {
|
||||||
const href = `${getSecuritySolutionUrl({
|
const href = `${getSecuritySolutionUrl({
|
||||||
|
@ -75,7 +77,9 @@ export const useSecurityDashboardsTableColumns = (): Array<
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
name: i18n.DASHBOARDS_DESCRIPTION,
|
name: i18n.translate('xpack.securitySolution.dashboards.description', {
|
||||||
|
defaultMessage: 'Description',
|
||||||
|
}),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (description: string) => description || getEmptyValue(),
|
render: (description: string) => description || getEmptyValue(),
|
||||||
'data-test-subj': 'dashboardTableDescriptionCell',
|
'data-test-subj': 'dashboardTableDescriptionCell',
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { matchPath } from 'react-router-dom';
|
||||||
import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/breadcrumbs/types';
|
import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/breadcrumbs/types';
|
||||||
|
import { CREATE_DASHBOARD_TITLE } from './translations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module should only export this function.
|
* 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.
|
* 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) => {
|
export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => {
|
||||||
|
if (matchPath(params.pathName, { path: '/create' })) {
|
||||||
|
return [{ text: CREATE_DASHBOARD_TITLE }];
|
||||||
|
}
|
||||||
|
|
||||||
const breadcrumbName = params?.state?.dashboardName;
|
const breadcrumbName = params?.state?.dashboardName;
|
||||||
if (breadcrumbName) {
|
if (breadcrumbName) {
|
||||||
return [{ text: breadcrumbName }];
|
return [{ text: breadcrumbName }];
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { Router } from '@kbn/shared-ux-router';
|
||||||
import { DashboardView } from '.';
|
import { DashboardView } from '.';
|
||||||
import { useCapabilities } from '../../../common/lib/kibana';
|
import { useCapabilities } from '../../../common/lib/kibana';
|
||||||
import { TestProviders } from '../../../common/mock';
|
import { TestProviders } from '../../../common/mock';
|
||||||
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => {
|
jest.mock('react-router-dom', () => {
|
||||||
const actual = jest.requireActual('react-router-dom');
|
const actual = jest.requireActual('react-router-dom');
|
||||||
|
@ -68,7 +69,7 @@ describe('DashboardView', () => {
|
||||||
test('render when no error state', () => {
|
test('render when no error state', () => {
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<Router history={mockHistory}>
|
<Router history={mockHistory}>
|
||||||
<DashboardView />
|
<DashboardView initialViewMode={ViewMode.VIEW} />
|
||||||
</Router>,
|
</Router>,
|
||||||
{ wrapper: TestProviders }
|
{ wrapper: TestProviders }
|
||||||
);
|
);
|
||||||
|
@ -83,7 +84,7 @@ describe('DashboardView', () => {
|
||||||
});
|
});
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<Router history={mockHistory}>
|
<Router history={mockHistory}>
|
||||||
<DashboardView />
|
<DashboardView initialViewMode={ViewMode.VIEW} />
|
||||||
</Router>,
|
</Router>,
|
||||||
{ wrapper: TestProviders }
|
{ wrapper: TestProviders }
|
||||||
);
|
);
|
||||||
|
@ -95,7 +96,7 @@ describe('DashboardView', () => {
|
||||||
test('render dashboard view with height', () => {
|
test('render dashboard view with height', () => {
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<Router history={mockHistory}>
|
<Router history={mockHistory}>
|
||||||
<DashboardView />
|
<DashboardView initialViewMode={ViewMode.VIEW} />
|
||||||
</Router>,
|
</Router>,
|
||||||
{ wrapper: TestProviders }
|
{ wrapper: TestProviders }
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,13 +7,12 @@
|
||||||
|
|
||||||
import React, { useState, useCallback, useMemo } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import { LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
|
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 type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { pick } from 'lodash/fp';
|
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 { SecurityPageName } from '../../../../common/constants';
|
||||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||||
import { useCapabilities } from '../../../common/lib/kibana';
|
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 { InputsModelId } from '../../../common/store/inputs/constants';
|
||||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||||
import { HeaderPage } from '../../../common/components/header_page';
|
import { HeaderPage } from '../../../common/components/header_page';
|
||||||
import { DASHBOARD_NOT_FOUND_TITLE } from './translations';
|
|
||||||
import { inputsSelectors } from '../../../common/store';
|
import { inputsSelectors } from '../../../common/store';
|
||||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||||
import { EditDashboardButton } from '../../components/edit_dashboard_button';
|
import { DashboardToolBar } from '../../components/dashboard_tool_bar';
|
||||||
|
|
||||||
type DashboardDetails = Record<string, string>;
|
import { useDashboardRenderer } from '../../hooks/use_dashboard_renderer';
|
||||||
|
import { DashboardTitle } from '../../components/dashboard_title';
|
||||||
|
|
||||||
|
interface DashboardViewProps {
|
||||||
|
initialViewMode: ViewMode;
|
||||||
|
}
|
||||||
|
|
||||||
const dashboardViewFlexGroupStyle = { minHeight: `calc(100vh - 140px)` };
|
const dashboardViewFlexGroupStyle = { minHeight: `calc(100vh - 140px)` };
|
||||||
|
|
||||||
const DashboardViewComponent: React.FC = () => {
|
const DashboardViewComponent: React.FC<DashboardViewProps> = ({
|
||||||
|
initialViewMode,
|
||||||
|
}: DashboardViewProps) => {
|
||||||
const { fromStr, toStr, from, to } = useDeepEqualSelector((state) =>
|
const { fromStr, toStr, from, to } = useDeepEqualSelector((state) =>
|
||||||
pick(['fromStr', 'toStr', 'from', 'to'], inputsSelectors.globalTimeRangeSelector(state))
|
pick(['fromStr', 'toStr', 'from', 'to'], inputsSelectors.globalTimeRangeSelector(state))
|
||||||
);
|
);
|
||||||
|
@ -47,36 +52,28 @@ const DashboardViewComponent: React.FC = () => {
|
||||||
);
|
);
|
||||||
const query = useDeepEqualSelector(getGlobalQuerySelector);
|
const query = useDeepEqualSelector(getGlobalQuerySelector);
|
||||||
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
|
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
|
||||||
const { indexPattern, indicesExist } = useSourcererDataView();
|
const { indexPattern } = useSourcererDataView();
|
||||||
|
|
||||||
const { show: canReadDashboard, showWriteControls } =
|
const { show: canReadDashboard } =
|
||||||
useCapabilities<DashboardCapabilities>(LEGACY_DASHBOARD_APP_ID);
|
useCapabilities<DashboardCapabilities>(LEGACY_DASHBOARD_APP_ID);
|
||||||
const errorState = useMemo(
|
const errorState = useMemo(
|
||||||
() => (canReadDashboard ? null : DashboardViewPromptState.NoReadPermission),
|
() => (canReadDashboard ? null : DashboardViewPromptState.NoReadPermission),
|
||||||
[canReadDashboard]
|
[canReadDashboard]
|
||||||
);
|
);
|
||||||
const [dashboardDetails, setDashboardDetails] = useState<DashboardDetails | undefined>();
|
const [viewMode, setViewMode] = useState<ViewMode>(initialViewMode);
|
||||||
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 { detailName: savedObjectId } = useParams<{ detailName?: string }>();
|
const { detailName: savedObjectId } = useParams<{ detailName?: string }>();
|
||||||
|
const [dashboardTitle, setDashboardTitle] = useState<string>();
|
||||||
|
|
||||||
|
const { dashboardContainer, handleDashboardLoaded } = useDashboardRenderer();
|
||||||
|
const onDashboardToolBarLoad = useCallback((mode: ViewMode) => {
|
||||||
|
setViewMode(mode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{indicesExist && (
|
<FiltersGlobal>
|
||||||
<FiltersGlobal>
|
<SiemSearchBar id={InputsModelId.global} indexPattern={indexPattern} />
|
||||||
<SiemSearchBar id={InputsModelId.global} indexPattern={indexPattern} />
|
</FiltersGlobal>
|
||||||
</FiltersGlobal>
|
|
||||||
)}
|
|
||||||
<SecuritySolutionPageWrapper>
|
<SecuritySolutionPageWrapper>
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
direction="column"
|
direction="column"
|
||||||
|
@ -85,16 +82,23 @@ const DashboardViewComponent: React.FC = () => {
|
||||||
data-test-subj="dashboard-view-wrapper"
|
data-test-subj="dashboard-view-wrapper"
|
||||||
>
|
>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<HeaderPage border title={dashboardDetails?.title ?? <EuiLoadingSpinner size="m" />}>
|
{dashboardContainer && (
|
||||||
{showWriteControls && dashboardExists && (
|
<HeaderPage
|
||||||
<EditDashboardButton
|
border
|
||||||
filters={filters}
|
title={
|
||||||
query={query}
|
<DashboardTitle
|
||||||
savedObjectId={savedObjectId}
|
dashboardContainer={dashboardContainer}
|
||||||
timeRange={timeRange}
|
onTitleLoaded={setDashboardTitle}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
</HeaderPage>
|
subtitle={
|
||||||
|
<DashboardToolBar
|
||||||
|
dashboardContainer={dashboardContainer}
|
||||||
|
onLoad={onDashboardToolBarLoad}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{!errorState && (
|
{!errorState && (
|
||||||
<EuiFlexItem grow>
|
<EuiFlexItem grow>
|
||||||
|
@ -102,10 +106,12 @@ const DashboardViewComponent: React.FC = () => {
|
||||||
query={query}
|
query={query}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
canReadDashboard={canReadDashboard}
|
canReadDashboard={canReadDashboard}
|
||||||
|
dashboardContainer={dashboardContainer}
|
||||||
id={`dashboard-view-${savedObjectId}`}
|
id={`dashboard-view-${savedObjectId}`}
|
||||||
onDashboardContainerLoaded={onDashboardContainerLoaded}
|
onDashboardContainerLoaded={handleDashboardLoaded}
|
||||||
savedObjectId={savedObjectId}
|
savedObjectId={savedObjectId}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
)}
|
)}
|
||||||
|
@ -116,7 +122,7 @@ const DashboardViewComponent: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
<SpyRoute
|
<SpyRoute
|
||||||
pageName={SecurityPageName.dashboards}
|
pageName={SecurityPageName.dashboards}
|
||||||
state={{ dashboardName: dashboardDetails?.title }}
|
state={{ dashboardName: dashboardTitle }}
|
||||||
/>
|
/>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</SecuritySolutionPageWrapper>
|
</SecuritySolutionPageWrapper>
|
||||||
|
|
|
@ -40,3 +40,24 @@ export const EDIT_DASHBOARD_BUTTON_TITLE = i18n.translate(
|
||||||
defaultMessage: `Edit`,
|
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`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Routes, Route } from '@kbn/shared-ux-router';
|
import { Routes, Route } from '@kbn/shared-ux-router';
|
||||||
|
|
||||||
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import { DashboardsLandingPage } from './landing_page';
|
import { DashboardsLandingPage } from './landing_page';
|
||||||
import { DashboardView } from './details';
|
import { DashboardView } from './details';
|
||||||
import { DASHBOARDS_PATH } from '../../../common/constants';
|
import { DASHBOARDS_PATH } from '../../../common/constants';
|
||||||
|
@ -16,8 +17,14 @@ const DashboardsContainerComponent = () => {
|
||||||
return (
|
return (
|
||||||
<DashboardContextProvider>
|
<DashboardContextProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route strict path={`${DASHBOARDS_PATH}/create`}>
|
||||||
|
<DashboardView initialViewMode={ViewMode.EDIT} />
|
||||||
|
</Route>
|
||||||
|
<Route strict path={`${DASHBOARDS_PATH}/:detailName/edit`}>
|
||||||
|
<DashboardView initialViewMode={ViewMode.EDIT} />
|
||||||
|
</Route>
|
||||||
<Route strict path={`${DASHBOARDS_PATH}/:detailName`}>
|
<Route strict path={`${DASHBOARDS_PATH}/:detailName`}>
|
||||||
<DashboardView />
|
<DashboardView initialViewMode={ViewMode.VIEW} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${DASHBOARDS_PATH}`}>
|
<Route path={`${DASHBOARDS_PATH}`}>
|
||||||
<DashboardsLandingPage />
|
<DashboardsLandingPage />
|
||||||
|
|
|
@ -23,13 +23,10 @@ import { DASHBOARDS_PAGE_SECTION_CUSTOM } from './translations';
|
||||||
jest.mock('../../../common/containers/tags/api');
|
jest.mock('../../../common/containers/tags/api');
|
||||||
jest.mock('../../../common/lib/kibana');
|
jest.mock('../../../common/lib/kibana');
|
||||||
jest.mock('../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null }));
|
jest.mock('../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null }));
|
||||||
jest.mock('@kbn/dashboard-plugin/public', () => {
|
jest.mock('@kbn/dashboard-plugin/public', () => ({
|
||||||
const actual = jest.requireActual('@kbn/dashboard-plugin/public');
|
DashboardListingTable: jest.fn().mockReturnValue(<span data-test-subj="dashboardsTable" />),
|
||||||
return {
|
DashboardTopNav: jest.fn().mockReturnValue(<span data-test-subj="dashboardTopNav" />),
|
||||||
...actual,
|
}));
|
||||||
DashboardListingTable: jest.fn().mockReturnValue(<span data-test-subj="dashboardsTable" />),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockUseObservable = jest.fn();
|
const mockUseObservable = jest.fn();
|
||||||
|
|
||||||
|
|
|
@ -99,20 +99,18 @@ export const DashboardsLandingPage = () => {
|
||||||
})}`,
|
})}`,
|
||||||
[getSecuritySolutionUrl]
|
[getSecuritySolutionUrl]
|
||||||
);
|
);
|
||||||
const { isLoading: loadingCreateDashboardUrl, url: createDashboardUrl } =
|
|
||||||
useCreateSecurityDashboardLink();
|
|
||||||
|
|
||||||
const getHref = useCallback(
|
|
||||||
(id: string | undefined) => (id ? getSecuritySolutionDashboardUrl(id) : createDashboardUrl),
|
|
||||||
[createDashboardUrl, getSecuritySolutionDashboardUrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const goToDashboard = useCallback(
|
const goToDashboard = useCallback(
|
||||||
(dashboardId: string | undefined) => {
|
(dashboardId: string | undefined) => {
|
||||||
track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.DASHBOARD);
|
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();
|
const securityTags = useSecurityTags();
|
||||||
|
@ -151,7 +149,7 @@ export const DashboardsLandingPage = () => {
|
||||||
<EuiHorizontalRule margin="s" />
|
<EuiHorizontalRule margin="s" />
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<DashboardListingTable
|
<DashboardListingTable
|
||||||
disableCreateDashboardButton={loadingCreateDashboardUrl}
|
disableCreateDashboardButton={!canCreateDashboard}
|
||||||
getDashboardUrl={getSecuritySolutionDashboardUrl}
|
getDashboardUrl={getSecuritySolutionDashboardUrl}
|
||||||
goToDashboard={goToDashboard}
|
goToDashboard={goToDashboard}
|
||||||
initialFilter={initialFilter}
|
initialFilter={initialFilter}
|
||||||
|
|
|
@ -9,3 +9,10 @@ import { i18n } from '@kbn/i18n';
|
||||||
export const DASHBOARDS_PAGE_TITLE = i18n.translate('xpack.securitySolution.dashboards.pageTitle', {
|
export const DASHBOARDS_PAGE_TITLE = i18n.translate('xpack.securitySolution.dashboards.pageTitle', {
|
||||||
defaultMessage: 'Dashboards',
|
defaultMessage: 'Dashboards',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const CREATE_DASHBOARD_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolution.dashboards.dashboard.createDashboardTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: `Editing New Dashboard`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
APP_UI_ID,
|
APP_UI_ID,
|
||||||
CASES_FEATURE_ID,
|
CASES_FEATURE_ID,
|
||||||
CASES_PATH,
|
CASES_PATH,
|
||||||
|
DASHBOARDS_PATH,
|
||||||
EXCEPTIONS_PATH,
|
EXCEPTIONS_PATH,
|
||||||
RULES_PATH,
|
RULES_PATH,
|
||||||
SERVER_APP_ID,
|
SERVER_APP_ID,
|
||||||
|
@ -173,6 +174,13 @@ export const isDetectionsPath = (pathname: string): boolean => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isDashboardViewPath = (pathname: string): boolean =>
|
||||||
|
matchPath(pathname, {
|
||||||
|
path: `/${DASHBOARDS_PATH}/:id`,
|
||||||
|
exact: false,
|
||||||
|
strict: false,
|
||||||
|
}) != null;
|
||||||
|
|
||||||
const isAlertsPath = (pathname: string): boolean => {
|
const isAlertsPath = (pathname: string): boolean => {
|
||||||
return !!matchPath(pathname, {
|
return !!matchPath(pathname, {
|
||||||
path: `${ALERTS_PATH}`,
|
path: `${ALERTS_PATH}`,
|
||||||
|
|
|
@ -187,6 +187,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
||||||
...this.contract.getStartServices(),
|
...this.contract.getStartServices(),
|
||||||
apm,
|
apm,
|
||||||
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
|
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
|
||||||
|
setHeaderActionMenu: params.setHeaderActionMenu,
|
||||||
storage: this.storage,
|
storage: this.storage,
|
||||||
sessionStorage: this.sessionStorage,
|
sessionStorage: this.sessionStorage,
|
||||||
security: startPluginsDeps.security,
|
security: startPluginsDeps.security,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
|
|
||||||
import type { AppLeaveHandler, CoreStart } from '@kbn/core/public';
|
import type { CoreStart, AppMountParameters, AppLeaveHandler } from '@kbn/core/public';
|
||||||
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
||||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
|
import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
|
||||||
|
@ -158,6 +158,7 @@ export type StartServices = CoreStart &
|
||||||
sessionStorage: Storage;
|
sessionStorage: Storage;
|
||||||
apm: ApmBase;
|
apm: ApmBase;
|
||||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||||
|
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
||||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -176,6 +176,7 @@
|
||||||
"@kbn/subscription-tracking",
|
"@kbn/subscription-tracking",
|
||||||
"@kbn/core-application-common",
|
"@kbn/core-application-common",
|
||||||
"@kbn/openapi-generator",
|
"@kbn/openapi-generator",
|
||||||
"@kbn/es"
|
"@kbn/es",
|
||||||
|
"@kbn/react-kibana-mount"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue