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