From a2a6cd2a83246ae910219b92f7929f09f1276394 Mon Sep 17 00:00:00 2001
From: Luke G <11671118+lgestc@users.noreply.github.com>
Date: Thu, 30 Nov 2023 14:51:55 +0100
Subject: [PATCH] [Security Solution] [Flyout] drive flyout state with url or
memory + support back button navigation from timelines (#169661)
## Summary
This is a PoC for flyout state (left, right, preview panels) stored
entirely in the url without separate syncing mechanism. It is also
possible to opt in for in-memory storage.
### This vs current solution:
- **browser navigation is supported**
- we dont need to sync anything with in-memory state
- we can remove useImperativeHandle from expandable flyout package
- flyout state can be updated on the individual widget level, without
prop drilling
- when clicking between alerts, current flyout arrangement is retained -
so the tabs you have open etc are still there (no custom code required)
- **it is now possible to investigate something in timeline using the
flyout action & go back to the flyout view**
https://github.com/elastic/security-team/issues/8135
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
packages/kbn-expandable-flyout/README.md | 6 +
packages/kbn-expandable-flyout/index.ts | 11 +-
.../src/components/preview_section.test.tsx | 6 +-
.../kbn-expandable-flyout/src/constants.ts | 9 +
.../kbn-expandable-flyout/src/context.tsx | 193 +-----------------
.../src/context/memory_state_provider.tsx | 98 +++++++++
.../src/context/url_state_provider.tsx | 117 +++++++++++
.../src/hooks/use_left_panel.ts | 23 +++
.../src/hooks/use_preview_panel.ts | 23 +++
.../src/hooks/use_right_panel.ts | 23 +++
.../src/index.stories.tsx | 18 +-
.../kbn-expandable-flyout/src/index.test.tsx | 18 +-
packages/kbn-expandable-flyout/src/index.tsx | 21 +-
.../kbn-expandable-flyout/src/provider.tsx | 37 ++++
packages/kbn-expandable-flyout/src/types.ts | 49 +++++
packages/kbn-expandable-flyout/tsconfig.json | 3 +-
packages/kbn-url-state/README.md | 36 ++--
packages/kbn-url-state/index.test.ts | 88 +++-----
packages/kbn-url-state/index.ts | 116 ++++++++++-
packages/kbn-url-state/use_sync_to_url.ts | 92 ---------
.../timeline/use_init_timeline_url_param.ts | 21 +-
.../public/common/hooks/use_url_state.ts | 26 ++-
.../utils/global_query_string/helpers.ts | 7 +
.../use_investigate_in_timeline.tsx | 3 +
.../pages/alerts/alert_details_redirect.tsx | 3 +-
.../left/tabs/insights_tab.tsx | 17 +-
.../components/correlations_overview.test.tsx | 3 +-
.../right/components/description.test.tsx | 14 +-
.../right/components/description.tsx | 8 +-
.../right/components/header_actions.test.tsx | 9 +-
.../right/components/header_actions.tsx | 4 +-
.../right/components/header_title.test.tsx | 3 +-
.../highlighted_fields_cell.test.tsx | 3 +-
.../components/host_entity_overview.test.tsx | 3 +-
.../components/investigation_section.test.tsx | 3 +-
.../components/prevalence_overview.test.tsx | 3 +-
.../right/components/reason.test.tsx | 16 +-
.../right/components/reason.tsx | 10 +-
.../components/response_section.test.tsx | 3 +-
.../right/components/session_preview.test.tsx | 3 +-
.../right/components/status.test.tsx | 3 +-
.../threat_intelligence_overview.test.tsx | 3 +-
.../components/user_entity_overview.test.tsx | 3 +-
.../visualizations_section.test.tsx | 3 +-
.../shared/context/url_sync.tsx | 47 -----
...expandable_flyout_state_from_event_meta.ts | 7 +-
.../use_sync_flyout_state_with_url.test.tsx | 77 -------
.../url/use_sync_flyout_state_with_url.tsx | 55 -----
.../shared/mocks/mock_flyout_context.ts | 11 +-
.../components/risk_summary.stories.tsx | 3 +-
.../user_right/content.stories.tsx | 3 +-
.../security_solution/public/flyout/index.tsx | 42 +---
.../components/flyout_navigation.stories.tsx | 5 +-
.../components/flyout_navigation.test.tsx | 5 +-
.../shared/components/flyout_navigation.tsx | 2 +-
.../components/flyout/header/index.tsx | 6 +-
.../timeline/query_tab_content/index.tsx | 7 +-
.../plugins/security_solution/tsconfig.json | 1 -
.../alert_details_url_sync.cy.ts | 8 +-
59 files changed, 722 insertions(+), 718 deletions(-)
create mode 100644 packages/kbn-expandable-flyout/src/constants.ts
create mode 100644 packages/kbn-expandable-flyout/src/context/memory_state_provider.tsx
create mode 100644 packages/kbn-expandable-flyout/src/context/url_state_provider.tsx
create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_left_panel.ts
create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_preview_panel.ts
create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_right_panel.ts
create mode 100644 packages/kbn-expandable-flyout/src/provider.tsx
delete mode 100644 packages/kbn-url-state/use_sync_to_url.ts
delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/context/url_sync.tsx
delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.test.tsx
delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.tsx
diff --git a/packages/kbn-expandable-flyout/README.md b/packages/kbn-expandable-flyout/README.md
index 3e3b620a06b53..d5428bd7e4fcb 100644
--- a/packages/kbn-expandable-flyout/README.md
+++ b/packages/kbn-expandable-flyout/README.md
@@ -58,6 +58,12 @@ Then use the [React UI component](https://github.com/elastic/kibana/tree/main/pa
```
_where `myPanels` is a list of all the panels that can be rendered in the flyout_
+## State persistence
+
+The expandable flyout offers 2 ways of managing its state:
+- the default behavior saves the state of the flyout in the url. This allows the flyout to be automatically reopened when users refresh the browser page, or when users share a url
+- the second way (done by setting the `storage` prop to `memory`) stores the state of the flyout in memory. This means that the flyout will not be reopened when users refresh the browser page, or when users share a url
+
## Terminology
diff --git a/packages/kbn-expandable-flyout/index.ts b/packages/kbn-expandable-flyout/index.ts
index 5a9094d6dbac9..6de82033af04c 100644
--- a/packages/kbn-expandable-flyout/index.ts
+++ b/packages/kbn-expandable-flyout/index.ts
@@ -7,13 +7,12 @@
*/
export { ExpandableFlyout } from './src';
-export {
- ExpandableFlyoutProvider,
- useExpandableFlyoutContext,
- type ExpandableFlyoutContext,
-} from './src/context';
-export type { ExpandableFlyoutApi } from './src/context';
+export { useExpandableFlyoutContext, type ExpandableFlyoutContext } from './src/context';
+
+export { ExpandableFlyoutProvider } from './src/provider';
export type { ExpandableFlyoutProps } from './src';
export type { FlyoutPanelProps, PanelPath } from './src/types';
+
+export { EXPANDABLE_FLYOUT_URL_KEY } from './src/constants';
diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx
index 950d406893082..69bdd7050e64a 100644
--- a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx
+++ b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx
@@ -14,10 +14,10 @@ import {
PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID,
PREVIEW_SECTION_TEST_ID,
} from './test_ids';
-import { ExpandableFlyoutContext } from '../context';
+import { ExpandableFlyoutContext, ExpandableFlyoutContextValue } from '../context';
describe('PreviewSection', () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {},
left: {},
@@ -27,7 +27,7 @@ describe('PreviewSection', () => {
},
],
},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const component =
{'component'}
;
const left = 500;
diff --git a/packages/kbn-expandable-flyout/src/constants.ts b/packages/kbn-expandable-flyout/src/constants.ts
new file mode 100644
index 0000000000000..4ee20ebb8e8f4
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/constants.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+export const EXPANDABLE_FLYOUT_URL_KEY = 'eventFlyout' as const;
diff --git a/packages/kbn-expandable-flyout/src/context.tsx b/packages/kbn-expandable-flyout/src/context.tsx
index 9738c2a4c867d..6deb5d2bede30 100644
--- a/packages/kbn-expandable-flyout/src/context.tsx
+++ b/packages/kbn-expandable-flyout/src/context.tsx
@@ -6,202 +6,19 @@
* Side Public License, v 1.
*/
-import React, {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useImperativeHandle,
- useMemo,
- useReducer,
-} from 'react';
-import { ActionType } from './actions';
-import { reducer, State } from './reducer';
-import type { FlyoutPanelProps } from './types';
-import { initialState } from './reducer';
+import { createContext, useContext } from 'react';
+import { type ExpandableFlyoutContextValue } from './types';
-export interface ExpandableFlyoutContext {
- /**
- * Right, left and preview panels
- */
- panels: State;
- /**
- * Open the flyout with left, right and/or preview panels
- */
- openFlyout: (panels: {
- left?: FlyoutPanelProps;
- right?: FlyoutPanelProps;
- preview?: FlyoutPanelProps;
- }) => void;
- /**
- * Replaces the current right panel with a new one
- */
- openRightPanel: (panel: FlyoutPanelProps) => void;
- /**
- * Replaces the current left panel with a new one
- */
- openLeftPanel: (panel: FlyoutPanelProps) => void;
- /**
- * Add a new preview panel to the list of current preview panels
- */
- openPreviewPanel: (panel: FlyoutPanelProps) => void;
- /**
- * Closes right panel
- */
- closeRightPanel: () => void;
- /**
- * Closes left panel
- */
- closeLeftPanel: () => void;
- /**
- * Closes all preview panels
- */
- closePreviewPanel: () => void;
- /**
- * Go back to previous preview panel
- */
- previousPreviewPanel: () => void;
- /**
- * Close all panels and closes flyout
- */
- closeFlyout: () => void;
-}
+export type { ExpandableFlyoutContextValue };
-export const ExpandableFlyoutContext = createContext(
+export const ExpandableFlyoutContext = createContext(
undefined
);
-export type ExpandableFlyoutApi = Pick & {
- getState: () => State;
-};
-
-export interface ExpandableFlyoutProviderProps {
- /**
- * React children
- */
- children: React.ReactNode;
- /**
- * Triggered whenever flyout state changes. You can use it to store it's state somewhere for instance.
- */
- onChanges?: (state: State) => void;
- /**
- * Triggered whenever flyout is closed. This is independent from the onChanges above.
- */
- onClosePanels?: () => void;
-}
-
-/**
- * Wrap your plugin with this context for the ExpandableFlyout React component.
- */
-export const ExpandableFlyoutProvider = React.forwardRef<
- ExpandableFlyoutApi,
- ExpandableFlyoutProviderProps
->(({ children, onChanges = () => {}, onClosePanels = () => {} }, ref) => {
- const [state, dispatch] = useReducer(reducer, initialState);
-
- useEffect(() => {
- const closed = !state.right;
- if (closed) {
- // manual close is singalled via separate callback
- return;
- }
-
- onChanges(state);
- }, [state, onChanges]);
-
- const openPanels = useCallback(
- ({
- right,
- left,
- preview,
- }: {
- right?: FlyoutPanelProps;
- left?: FlyoutPanelProps;
- preview?: FlyoutPanelProps;
- }) => dispatch({ type: ActionType.openFlyout, payload: { left, right, preview } }),
- [dispatch]
- );
-
- const openRightPanel = useCallback(
- (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openRightPanel, payload: panel }),
- []
- );
-
- const openLeftPanel = useCallback(
- (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openLeftPanel, payload: panel }),
- []
- );
-
- const openPreviewPanel = useCallback(
- (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }),
- []
- );
-
- const closeRightPanel = useCallback(() => dispatch({ type: ActionType.closeRightPanel }), []);
-
- const closeLeftPanel = useCallback(() => dispatch({ type: ActionType.closeLeftPanel }), []);
-
- const closePreviewPanel = useCallback(() => dispatch({ type: ActionType.closePreviewPanel }), []);
-
- const previousPreviewPanel = useCallback(
- () => dispatch({ type: ActionType.previousPreviewPanel }),
- []
- );
-
- const closePanels = useCallback(() => {
- dispatch({ type: ActionType.closeFlyout });
- onClosePanels();
- }, [onClosePanels]);
-
- useImperativeHandle(
- ref,
- () => {
- return {
- openFlyout: openPanels,
- getState: () => state,
- };
- },
- [openPanels, state]
- );
-
- const contextValue = useMemo(
- () => ({
- panels: state,
- openFlyout: openPanels,
- openRightPanel,
- openLeftPanel,
- openPreviewPanel,
- closeRightPanel,
- closeLeftPanel,
- closePreviewPanel,
- closeFlyout: closePanels,
- previousPreviewPanel,
- }),
- [
- state,
- openPanels,
- openRightPanel,
- openLeftPanel,
- openPreviewPanel,
- closeRightPanel,
- closeLeftPanel,
- closePreviewPanel,
- closePanels,
- previousPreviewPanel,
- ]
- );
-
- return (
-
- {children}
-
- );
-});
-
/**
* Retrieve context's properties
*/
-export const useExpandableFlyoutContext = (): ExpandableFlyoutContext => {
+export const useExpandableFlyoutContext = (): ExpandableFlyoutContextValue => {
const contextValue = useContext(ExpandableFlyoutContext);
if (!contextValue) {
diff --git a/packages/kbn-expandable-flyout/src/context/memory_state_provider.tsx b/packages/kbn-expandable-flyout/src/context/memory_state_provider.tsx
new file mode 100644
index 0000000000000..f70ae9dbbe8de
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/context/memory_state_provider.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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, { FC, PropsWithChildren, useCallback, useMemo, useReducer } from 'react';
+import { ActionType } from '../actions';
+import { reducer } from '../reducer';
+import type { ExpandableFlyoutContextValue, FlyoutPanelProps } from '../types';
+import { initialState } from '../reducer';
+import { ExpandableFlyoutContext } from '../context';
+
+/**
+ * In-memory state provider for the expandable flyout, for cases when we don't want changes to be persisted
+ * in the url.
+ */
+export const MemoryStateProvider: FC> = ({ children }) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ const openPanels = useCallback(
+ ({
+ right,
+ left,
+ preview,
+ }: {
+ right?: FlyoutPanelProps;
+ left?: FlyoutPanelProps;
+ preview?: FlyoutPanelProps;
+ }) => dispatch({ type: ActionType.openFlyout, payload: { left, right, preview } }),
+ [dispatch]
+ );
+
+ const openRightPanel = useCallback(
+ (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openRightPanel, payload: panel }),
+ []
+ );
+
+ const openLeftPanel = useCallback(
+ (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openLeftPanel, payload: panel }),
+ []
+ );
+
+ const openPreviewPanel = useCallback(
+ (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }),
+ []
+ );
+
+ const closeRightPanel = useCallback(() => dispatch({ type: ActionType.closeRightPanel }), []);
+
+ const closeLeftPanel = useCallback(() => dispatch({ type: ActionType.closeLeftPanel }), []);
+
+ const closePreviewPanel = useCallback(() => dispatch({ type: ActionType.closePreviewPanel }), []);
+
+ const previousPreviewPanel = useCallback(
+ () => dispatch({ type: ActionType.previousPreviewPanel }),
+ []
+ );
+
+ const closePanels = useCallback(() => {
+ dispatch({ type: ActionType.closeFlyout });
+ }, []);
+
+ const contextValue: ExpandableFlyoutContextValue = useMemo(
+ () => ({
+ panels: state,
+ openFlyout: openPanels,
+ openRightPanel,
+ openLeftPanel,
+ openPreviewPanel,
+ closeRightPanel,
+ closeLeftPanel,
+ closePreviewPanel,
+ closeFlyout: closePanels,
+ previousPreviewPanel,
+ }),
+ [
+ state,
+ openPanels,
+ openRightPanel,
+ openLeftPanel,
+ openPreviewPanel,
+ closeRightPanel,
+ closeLeftPanel,
+ closePreviewPanel,
+ closePanels,
+ previousPreviewPanel,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/kbn-expandable-flyout/src/context/url_state_provider.tsx b/packages/kbn-expandable-flyout/src/context/url_state_provider.tsx
new file mode 100644
index 0000000000000..e6f81e23d2fb1
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/context/url_state_provider.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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, { FC, PropsWithChildren, useCallback, useMemo } from 'react';
+import { FlyoutPanelProps, ExpandableFlyoutContextValue } from '../types';
+import { useRightPanel } from '../hooks/use_right_panel';
+import { useLeftPanel } from '../hooks/use_left_panel';
+import { usePreviewPanel } from '../hooks/use_preview_panel';
+import { ExpandableFlyoutContext } from '../context';
+import { State } from '../reducer';
+
+/**
+ * Private component that manages flyout state with url query params
+ */
+export const UrlStateProvider: FC> = ({ children }) => {
+ const { setRightPanelState, rightPanelState } = useRightPanel();
+ const { setLeftPanelState, leftPanelState } = useLeftPanel();
+ const { previewState, setPreviewState } = usePreviewPanel();
+
+ const panels: State = useMemo(
+ () => ({
+ left: leftPanelState,
+ right: rightPanelState,
+ preview: previewState || [],
+ }),
+ [leftPanelState, previewState, rightPanelState]
+ );
+
+ const openPanels = useCallback(
+ ({
+ right,
+ left,
+ preview,
+ }: {
+ right?: FlyoutPanelProps;
+ left?: FlyoutPanelProps;
+ preview?: FlyoutPanelProps;
+ }) => {
+ setRightPanelState(right);
+ setLeftPanelState(left);
+ setPreviewState(preview ? [preview] : []);
+ },
+ [setRightPanelState, setLeftPanelState, setPreviewState]
+ );
+
+ const openRightPanel = useCallback(
+ (panel: FlyoutPanelProps) => {
+ setRightPanelState(panel);
+ },
+ [setRightPanelState]
+ );
+
+ const openLeftPanel = useCallback(
+ (panel: FlyoutPanelProps) => setLeftPanelState(panel),
+ [setLeftPanelState]
+ );
+
+ const openPreviewPanel = useCallback(
+ (panel: FlyoutPanelProps) => setPreviewState([...(previewState ?? []), panel]),
+ [previewState, setPreviewState]
+ );
+
+ const closeRightPanel = useCallback(() => setRightPanelState(undefined), [setRightPanelState]);
+
+ const closeLeftPanel = useCallback(() => setLeftPanelState(undefined), [setLeftPanelState]);
+
+ const closePreviewPanel = useCallback(() => setPreviewState([]), [setPreviewState]);
+
+ const previousPreviewPanel = useCallback(
+ () => setPreviewState(previewState?.slice(0, previewState.length - 1)),
+ [previewState, setPreviewState]
+ );
+
+ const closePanels = useCallback(() => {
+ setRightPanelState(undefined);
+ setLeftPanelState(undefined);
+ setPreviewState([]);
+ }, [setRightPanelState, setLeftPanelState, setPreviewState]);
+
+ const contextValue: ExpandableFlyoutContextValue = useMemo(
+ () => ({
+ panels,
+ openFlyout: openPanels,
+ openRightPanel,
+ openLeftPanel,
+ openPreviewPanel,
+ closeRightPanel,
+ closeLeftPanel,
+ closePreviewPanel,
+ closeFlyout: closePanels,
+ previousPreviewPanel,
+ }),
+ [
+ panels,
+ openPanels,
+ openRightPanel,
+ openLeftPanel,
+ openPreviewPanel,
+ closeRightPanel,
+ closeLeftPanel,
+ closePreviewPanel,
+ closePanels,
+ previousPreviewPanel,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/kbn-expandable-flyout/src/hooks/use_left_panel.ts b/packages/kbn-expandable-flyout/src/hooks/use_left_panel.ts
new file mode 100644
index 0000000000000..2a3e4212a06fe
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/hooks/use_left_panel.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { useUrlState } from '@kbn/url-state';
+import { EXPANDABLE_FLYOUT_URL_KEY } from '../constants';
+import { FlyoutPanelProps } from '../types';
+
+/**
+ * This hook stores state in the URL
+ */
+export const useLeftPanel = () => {
+ const [leftPanelState, setLeftPanelState] = useUrlState(
+ EXPANDABLE_FLYOUT_URL_KEY,
+ 'leftPanel'
+ );
+
+ return { leftPanelState, setLeftPanelState } as const;
+};
diff --git a/packages/kbn-expandable-flyout/src/hooks/use_preview_panel.ts b/packages/kbn-expandable-flyout/src/hooks/use_preview_panel.ts
new file mode 100644
index 0000000000000..5e9cfddb93ba4
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/hooks/use_preview_panel.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { useUrlState } from '@kbn/url-state';
+import { EXPANDABLE_FLYOUT_URL_KEY } from '../constants';
+import { FlyoutPanelProps } from '../types';
+
+/**
+ * This hook stores state in the URL
+ */
+export const usePreviewPanel = () => {
+ const [previewState, setPreviewState] = useUrlState(
+ EXPANDABLE_FLYOUT_URL_KEY,
+ 'preview'
+ );
+
+ return { previewState, setPreviewState } as const;
+};
diff --git a/packages/kbn-expandable-flyout/src/hooks/use_right_panel.ts b/packages/kbn-expandable-flyout/src/hooks/use_right_panel.ts
new file mode 100644
index 0000000000000..2bce75d65f23e
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/hooks/use_right_panel.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { useUrlState } from '@kbn/url-state';
+import { EXPANDABLE_FLYOUT_URL_KEY } from '../constants';
+import { FlyoutPanelProps } from '../types';
+
+/**
+ * This hook stores state in the URL
+ */
+export const useRightPanel = () => {
+ const [rightPanelState, setRightPanelState] = useUrlState(
+ EXPANDABLE_FLYOUT_URL_KEY,
+ 'rightPanel'
+ );
+
+ return { rightPanelState, setRightPanelState } as const;
+};
diff --git a/packages/kbn-expandable-flyout/src/index.stories.tsx b/packages/kbn-expandable-flyout/src/index.stories.tsx
index 2bbc26c3363f3..615621bf9a571 100644
--- a/packages/kbn-expandable-flyout/src/index.stories.tsx
+++ b/packages/kbn-expandable-flyout/src/index.stories.tsx
@@ -19,7 +19,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { ExpandableFlyout } from '.';
-import { ExpandableFlyoutContext } from './context';
+import { ExpandableFlyoutContext, ExpandableFlyoutContextValue } from './context';
export default {
component: ExpandableFlyout,
@@ -100,7 +100,7 @@ const registeredPanels = [
];
export const Right: Story = () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {
id: 'right',
@@ -109,7 +109,7 @@ export const Right: Story = () => {
preview: [],
},
closeFlyout: () => window.alert('closeFlyout api'),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
return (
@@ -119,7 +119,7 @@ export const Right: Story = () => {
};
export const Left: Story = () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {
id: 'right',
@@ -130,7 +130,7 @@ export const Left: Story = () => {
preview: [],
},
closeFlyout: () => window.alert('closeFlyout api'),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
return (
@@ -140,7 +140,7 @@ export const Left: Story = () => {
};
export const Preview: Story = () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {
id: 'right',
@@ -156,7 +156,7 @@ export const Preview: Story = () => {
},
closePreviewPanel: () => window.alert('closePreviewPanel api'),
closeFlyout: () => window.alert('closeFlyout api'),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
return (
@@ -166,7 +166,7 @@ export const Preview: Story = () => {
};
export const MultiplePreviews: Story = () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {
id: 'right',
@@ -186,7 +186,7 @@ export const MultiplePreviews: Story = () => {
closePreviewPanel: () => window.alert('closePreviewPanel api'),
previousPreviewPanel: () => window.alert('previousPreviewPanel api'),
closeFlyout: () => window.alert('closeFlyout api'),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
return (
diff --git a/packages/kbn-expandable-flyout/src/index.test.tsx b/packages/kbn-expandable-flyout/src/index.test.tsx
index c6da99ad01777..46ae2dce508f2 100644
--- a/packages/kbn-expandable-flyout/src/index.test.tsx
+++ b/packages/kbn-expandable-flyout/src/index.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { Panel } from './types';
+import { ExpandableFlyoutContextValue, Panel } from './types';
import { ExpandableFlyout } from '.';
import {
LEFT_SECTION_TEST_ID,
@@ -26,13 +26,13 @@ describe('ExpandableFlyout', () => {
];
it(`shouldn't render flyout if no panels`, () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: undefined,
left: undefined,
preview: [],
},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const result = render(
@@ -44,7 +44,7 @@ describe('ExpandableFlyout', () => {
});
it('should render right section', () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {
id: 'key',
@@ -52,7 +52,7 @@ describe('ExpandableFlyout', () => {
left: {},
preview: [],
},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId } = render(
@@ -64,7 +64,7 @@ describe('ExpandableFlyout', () => {
});
it('should render left section', () => {
- const context: ExpandableFlyoutContext = {
+ const context = {
panels: {
right: {},
left: {
@@ -72,7 +72,7 @@ describe('ExpandableFlyout', () => {
},
preview: [],
},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId } = render(
@@ -84,7 +84,7 @@ describe('ExpandableFlyout', () => {
});
it('should render preview section', () => {
- const context: ExpandableFlyoutContext = {
+ const context: ExpandableFlyoutContextValue = {
panels: {
right: {},
left: {},
@@ -94,7 +94,7 @@ describe('ExpandableFlyout', () => {
},
],
},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId } = render(
diff --git a/packages/kbn-expandable-flyout/src/index.tsx b/packages/kbn-expandable-flyout/src/index.tsx
index 17613be6859b7..e7f6c3fcafb23 100644
--- a/packages/kbn-expandable-flyout/src/index.tsx
+++ b/packages/kbn-expandable-flyout/src/index.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { useCallback, useMemo } from 'react';
+import React, { useMemo } from 'react';
import { EuiFlyoutProps } from '@elastic/eui';
import { EuiFlexGroup, EuiFlyout } from '@elastic/eui';
import { useSectionSizes } from './hooks/use_sections_sizes';
@@ -25,10 +25,6 @@ export interface ExpandableFlyoutProps extends Omit {
* List of all registered panels available for render
*/
registeredPanels: Panel[];
- /**
- * Propagate out EuiFlyout onClose event
- */
- handleOnFlyoutClosed?: () => void;
}
/**
@@ -40,18 +36,13 @@ export interface ExpandableFlyoutProps extends Omit {
*/
export const ExpandableFlyout: React.FC = ({
registeredPanels,
- handleOnFlyoutClosed,
...flyoutProps
}) => {
const windowWidth = useWindowSize();
- const { panels, closeFlyout } = useExpandableFlyoutContext();
- const { left, right, preview } = panels;
+ const { closeFlyout, panels } = useExpandableFlyoutContext();
- const onClose = useCallback(() => {
- if (handleOnFlyoutClosed) handleOnFlyoutClosed();
- closeFlyout();
- }, [closeFlyout, handleOnFlyoutClosed]);
+ const { left, right, preview } = panels;
const leftSection = useMemo(
() => registeredPanels.find((panel) => panel.key === left?.id),
@@ -69,7 +60,7 @@ export const ExpandableFlyout: React.FC = ({
? mostRecentPreview?.params?.banner
: undefined;
- const showBackButton = preview && preview.length > 1;
+ const showBackButton = !!preview && preview.length > 1;
const previewSection = useMemo(
() => registeredPanels.find((panel) => panel.key === mostRecentPreview?.id),
[mostRecentPreview, registeredPanels]
@@ -86,13 +77,13 @@ export const ExpandableFlyout: React.FC = ({
showPreview,
});
- const hideFlyout = !left && !right && !preview.length;
+ const hideFlyout = !left && !right && !preview?.length;
if (hideFlyout) {
return null;
}
return (
-
+
> = ({
+ children,
+ storage = 'url',
+}) => {
+ if (storage === 'memory') {
+ return {children};
+ }
+
+ return {children};
+};
diff --git a/packages/kbn-expandable-flyout/src/types.ts b/packages/kbn-expandable-flyout/src/types.ts
index bfe64c4599085..92c4af79024b8 100644
--- a/packages/kbn-expandable-flyout/src/types.ts
+++ b/packages/kbn-expandable-flyout/src/types.ts
@@ -7,6 +7,55 @@
*/
import React from 'react';
+import { State } from './reducer';
+
+export interface ExpandableFlyoutContextValue {
+ /**
+ * Right, left and preview panels
+ */
+ panels: State;
+
+ /**
+ * Open the flyout with left, right and/or preview panels
+ */
+ openFlyout: (panels: {
+ left?: FlyoutPanelProps;
+ right?: FlyoutPanelProps;
+ preview?: FlyoutPanelProps;
+ }) => void;
+ /**
+ * Replaces the current right panel with a new one
+ */
+ openRightPanel: (panel: FlyoutPanelProps) => void;
+ /**
+ * Replaces the current left panel with a new one
+ */
+ openLeftPanel: (panel: FlyoutPanelProps) => void;
+ /**
+ * Add a new preview panel to the list of current preview panels
+ */
+ openPreviewPanel: (panel: FlyoutPanelProps) => void;
+ /**
+ * Closes right panel
+ */
+ closeRightPanel: () => void;
+ /**
+ * Closes left panel
+ */
+ closeLeftPanel: () => void;
+ /**
+ * Closes all preview panels
+ */
+ closePreviewPanel: () => void;
+ /**
+ * Go back to previous preview panel
+ */
+ previousPreviewPanel: () => void;
+ /**
+ * Close all panels and closes flyout
+ */
+ closeFlyout: () => void;
+}
export interface PanelPath {
/**
diff --git a/packages/kbn-expandable-flyout/tsconfig.json b/packages/kbn-expandable-flyout/tsconfig.json
index d1755389bcddc..9a5dcbaf03048 100644
--- a/packages/kbn-expandable-flyout/tsconfig.json
+++ b/packages/kbn-expandable-flyout/tsconfig.json
@@ -19,6 +19,7 @@
"target/**/*"
],
"kbn_references": [
- "@kbn/i18n"
+ "@kbn/i18n",
+ "@kbn/url-state"
]
}
diff --git a/packages/kbn-url-state/README.md b/packages/kbn-url-state/README.md
index e7b131e3743d5..3549936888f9f 100644
--- a/packages/kbn-url-state/README.md
+++ b/packages/kbn-url-state/README.md
@@ -1,45 +1,33 @@
# @kbn/url-state - utils for syncing state to URL
-This package provides:
+This package provides a React hook called `useUrlState` that can be used to synchronize state to the URL. This can be useful when you want to make a portion of state shareable.
-- a React hook called `useSyncToUrl` that can be used to synchronize state to the URL. This can be useful when you want to make a portion of state shareable.
-
-## useSyncToUrl
-
-The `useSyncToUrl` hook takes three arguments:
-
-```
-key (string): The key to use in the URL to store the state.
-restore (function): A function that is called with the deserialized value from the URL. You should use this function to update your state based on the value from the URL.
-cleanupOnHistoryNavigation (optional boolean, default: true): If true, the hook will clear the URL state when the user navigates using the browser's history API.
-```
+The state is grouped under a namespace, to avoid collisions. See the example url below for how it would look like.
### Example usage:
```
import React, { useState } from 'react';
-import { useSyncToUrl } from '@kbn/url-state';
+import { useUrlState } from '@kbn/url-state';
function MyComponent() {
- const [count, setCount] = useState(0);
-
- useSyncToUrl('count', (value) => {
- setCount(value);
- });
+ const [name, setName] = useUrlState('namespace','name');
const handleClick = () => {
- setCount((prevCount) => prevCount + 1);
+ setName('John Doe')
};
return (
-
Count: {count}
-
+
Name: {name}
+
);
}
```
-In this example, the count state is synced to the URL using the `useSyncToUrl` hook.
-Whenever the count state changes, the hook will update the URL with the new value.
-When the user copies the updated url or refreshes the page, `restore` function will be called to update the count state.
\ No newline at end of file
+The resulting URL will look like this:
+
+```
+http://localhost:5601/?namespace=(name:John%20Doe)
+```
diff --git a/packages/kbn-url-state/index.test.ts b/packages/kbn-url-state/index.test.ts
index e2a85e58902f1..7e646a387d245 100644
--- a/packages/kbn-url-state/index.test.ts
+++ b/packages/kbn-url-state/index.test.ts
@@ -7,8 +7,7 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
-import { useSyncToUrl } from '.';
-import { encode } from '@kbn/rison';
+import { useUrlState } from '.';
describe('useSyncToUrl', () => {
let originalLocation: Location;
@@ -28,95 +27,70 @@ describe('useSyncToUrl', () => {
window.history = {
...originalHistory,
replaceState: jest.fn(),
+ pushState: jest.fn(),
};
+
+ jest.useFakeTimers();
});
afterEach(() => {
window.location = originalLocation;
window.history = originalHistory;
+ jest.useRealTimers();
});
- it('should restore the value from the query string on mount', () => {
- const key = 'testKey';
- const restoredValue = { test: 'value' };
- const encodedValue = encode(restoredValue);
- const restore = jest.fn();
-
- window.location.search = `?${key}=${encodedValue}`;
-
- renderHook(() => useSyncToUrl(key, restore));
-
- expect(restore).toHaveBeenCalledWith(restoredValue);
- });
-
- it('should sync the value to the query string', () => {
- const key = 'testKey';
- const valueToSerialize = { test: 'value' };
+ it('should update the URL when the state changes', () => {
+ window.location.hash = '#should_be_there';
- const { result } = renderHook(() => useSyncToUrl(key, jest.fn()));
+ const { result } = renderHook(() => useUrlState('namespace', 'test'));
act(() => {
- result.current(valueToSerialize);
+ result.current[1]('foo');
+ jest.runAllTimers();
});
- expect(window.history.replaceState).toHaveBeenCalledWith(
- { path: expect.any(String) },
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ {},
'',
- '/?testKey=%28test%3Avalue%29'
+ '#should_be_there?namespace=(test:foo)'
);
});
- it('should should not alter the location hash', () => {
- const key = 'testKey';
- const valueToSerialize = { test: 'value' };
+ it('should remove the key from the namespace after undefined is passed (state clear mechanism)', () => {
window.location.hash = '#should_be_there';
- const { result } = renderHook(() => useSyncToUrl(key, jest.fn()));
+ const { result } = renderHook(() => useUrlState('namespace', 'test'));
act(() => {
- result.current(valueToSerialize);
+ result.current[1](undefined);
+ jest.runAllTimers();
});
- expect(window.history.replaceState).toHaveBeenCalledWith(
- { path: expect.any(String) },
- '',
- '/#should_be_there?testKey=%28test%3Avalue%29'
- );
+ expect(window.history.pushState).toHaveBeenCalledWith({}, '', '#should_be_there?namespace=()');
});
- it('should clear the value from the query string on unmount', () => {
- const key = 'testKey';
-
- // Location should have a key to clear
- window.location.search = `?${key}=${encode({ test: 'value' })}`;
-
- const { unmount } = renderHook(() => useSyncToUrl(key, jest.fn()));
+ it('should restore the value from the query string on mount', () => {
+ window.location.search = `?namespace=(test:foo)`;
- act(() => {
- unmount();
- });
+ const {
+ result: { current: state },
+ } = renderHook(() => useUrlState('namespace', 'test'));
- expect(window.history.replaceState).toHaveBeenCalledWith(
- { path: expect.any(String) },
- '',
- expect.any(String)
- );
+ expect(state[0]).toEqual('foo');
});
- it('should clear the value from the query string when history back or forward is pressed', () => {
- const key = 'testKey';
- const restore = jest.fn();
+ it('should return updated state on browser navigation', () => {
+ window.location.search = '?namespace=(test:foo)';
- // Location should have a key to clear
- window.location.search = `?${key}=${encode({ test: 'value' })}`;
+ const { result } = renderHook(() => useUrlState('namespace', 'test'));
- renderHook(() => useSyncToUrl(key, restore, true));
+ expect(result.current[0]).toEqual('foo');
act(() => {
- window.dispatchEvent(new Event('popstate'));
+ window.location.search = '?namespace=(test:bar)';
+ window.dispatchEvent(new CustomEvent('popstate'));
});
- expect(window.history.replaceState).toHaveBeenCalledTimes(1);
- expect(window.history.replaceState).toHaveBeenCalledWith({ path: expect.any(String) }, '', '/');
+ expect(result.current[0]).toEqual('bar');
});
});
diff --git a/packages/kbn-url-state/index.ts b/packages/kbn-url-state/index.ts
index 73568222fb4c0..d165d75e1f956 100644
--- a/packages/kbn-url-state/index.ts
+++ b/packages/kbn-url-state/index.ts
@@ -6,4 +6,118 @@
* Side Public License, v 1.
*/
-export { useSyncToUrl } from './use_sync_to_url';
+import { useCallback, useEffect, useState } from 'react';
+import { encode, decode, RisonValue } from '@kbn/rison';
+import { stringify, parse } from 'query-string';
+
+interface StateCache {
+ namespaces: Record>;
+ timeoutHandle: number;
+}
+
+/**
+ * Temporary cache for state stored in the URL. This will be serialized to the URL
+ * in a single batched update to avoid excessive history entries.
+ */
+const cache: StateCache = {
+ namespaces: {},
+ timeoutHandle: 0,
+};
+
+const CUSTOM_URL_EVENT = 'url:update' as const;
+
+// This is a list of events that can trigger a render.
+const URL_CHANGE_EVENTS: string[] = ['popstate', CUSTOM_URL_EVENT];
+
+/**
+ * This hook stores state in the URL, but with a namespace to avoid collisions with other values in the URL.
+ * It also batches updates to the URL to avoid excessive history entries.
+ * With it, you can store state in the URL and have it persist across page refreshes.
+ * The state is stored in the URL as a Rison encoded object.
+ *
+ * Example: when called like this `const [value, setValue] = useUrlState('myNamespace', 'myKey');`
+ * the state will be stored in the URL like this: `?myNamespace=(myKey:!n)`
+ *
+ * State is not cleared from the URL when the hook is unmounted and this is by design.
+ * If you want it to be cleared, you can do it manually by calling `setValue(undefined)`.
+ *
+ * @param urlNamespace actual top level query param key
+ * @param key sub key of the query param
+ */
+export const useUrlState = (urlNamespace: string, key: string) => {
+ if (!cache.namespaces[urlNamespace]) {
+ cache.namespaces[urlNamespace] = {};
+ }
+
+ const [internalValue, setInternalValue] = useState(undefined);
+
+ useEffect(() => {
+ // This listener is called on browser navigation or on custom event.
+ // It updates the LOCAL state, allowing dependent components to re-render.
+ const listener = () => {
+ const searchParams = new URLSearchParams(window.location.search);
+ const param = searchParams.get(urlNamespace);
+
+ const decodedState = param ? decode(param) : ({} as Record);
+ const decodedValue = (decodedState as Record | undefined)?.[key];
+ cache.namespaces[urlNamespace][key] = decodedValue;
+ setInternalValue(decodedValue as unknown as T);
+ };
+
+ listener();
+
+ URL_CHANGE_EVENTS.forEach((event) => window.addEventListener(event, listener));
+
+ return () => URL_CHANGE_EVENTS.forEach((event) => window.removeEventListener(event, listener));
+ }, [key, urlNamespace]);
+
+ const setValue = useCallback(
+ (updatedValue: T | undefined) => {
+ const currentValue = cache.namespaces[urlNamespace][key];
+
+ const canSpread =
+ typeof updatedValue === 'object' &&
+ typeof currentValue === 'object' &&
+ !Array.isArray(updatedValue) &&
+ !Array.isArray(currentValue);
+
+ cache.namespaces[urlNamespace][key] = canSpread
+ ? ({ ...currentValue, ...updatedValue } as unknown as T)
+ : (updatedValue as unknown as T);
+
+ // This batches updates to the URL state to avoid excessive history entries
+ if (cache.timeoutHandle) {
+ window.clearTimeout(cache.timeoutHandle);
+ }
+
+ // The push state call is delayed to make sure that multiple calls to setValue
+ // within a short period of time are batched together.
+ cache.timeoutHandle = window.setTimeout(() => {
+ const searchParams = parse(location.search);
+
+ for (const ns in cache.namespaces) {
+ if (!Object.prototype.hasOwnProperty.call(cache.namespaces, ns)) {
+ continue;
+ }
+ searchParams[ns] = encode(cache.namespaces[ns]);
+ }
+
+ const newSearch = stringify(searchParams, { encode: false });
+
+ if (window.location.search === newSearch) {
+ return;
+ }
+
+ const newUrl = `${window.location.hash}?${newSearch}`;
+
+ window.history.pushState({}, '', newUrl);
+ // This custom event is used to notify other instances
+ // of this hook that the URL has changed.
+ window.dispatchEvent(new Event(CUSTOM_URL_EVENT));
+ }, 0);
+ },
+ [key, urlNamespace]
+ );
+
+ return [internalValue, setValue] as const;
+};
diff --git a/packages/kbn-url-state/use_sync_to_url.ts b/packages/kbn-url-state/use_sync_to_url.ts
deleted file mode 100644
index e6f1531980f75..0000000000000
--- a/packages/kbn-url-state/use_sync_to_url.ts
+++ /dev/null
@@ -1,92 +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 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 { useCallback, useEffect } from 'react';
-import { encode, decode } from '@kbn/rison';
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event
-const POPSTATE_EVENT = 'popstate' as const;
-
-/**
- * Sync any object with browser query string using @knb/rison
- * @param key query string param to use
- * @param restore use this to handle restored state
- * @param cleanupOnHistoryNavigation use history events to cleanup state on back / forward naviation. true by default
- */
-export const useSyncToUrl = (
- key: string,
- restore: (data: TValueToSerialize) => void,
- cleanupOnHistoryNavigation = true
-) => {
- useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const param = params.get(key);
-
- if (!param) {
- return;
- }
-
- const decodedQuery = decode(param);
-
- if (!decodedQuery) {
- return;
- }
-
- // Only restore the value if it is not falsy
- restore(decodedQuery as unknown as TValueToSerialize);
- }, [key, restore]);
-
- /**
- * Synces value with the url state, under specified key. If payload is undefined, the value will be removed from the query string althogether.
- */
- const syncValueToQueryString = useCallback(
- (valueToSerialize?: TValueToSerialize) => {
- const searchParams = new URLSearchParams(window.location.search);
-
- if (valueToSerialize) {
- const serializedPayload = encode(valueToSerialize);
- searchParams.set(key, serializedPayload);
- } else {
- searchParams.delete(key);
- }
-
- const stringifiedSearchParams = searchParams.toString();
- const newSearch = stringifiedSearchParams.length > 0 ? `?${stringifiedSearchParams}` : '';
-
- if (window.location.search === newSearch) {
- return;
- }
-
- // Update query string without unnecessary re-render
- const newUrl = `${window.location.pathname}${window.location.hash}${newSearch}`;
- window.history.replaceState({ path: newUrl }, '', newUrl);
- },
- [key]
- );
-
- // Clear remove state from the url on unmount / when history back or forward is pressed
- useEffect(() => {
- const clearState = () => {
- syncValueToQueryString(undefined);
- };
-
- if (cleanupOnHistoryNavigation) {
- window.addEventListener(POPSTATE_EVENT, clearState);
- }
-
- return () => {
- clearState();
-
- if (cleanupOnHistoryNavigation) {
- window.removeEventListener(POPSTATE_EVENT, clearState);
- }
- };
- }, [cleanupOnHistoryNavigation, syncValueToQueryString]);
-
- return syncValueToQueryString;
-};
diff --git a/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts b/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts
index c16f5ebb4bae5..4d6ef73c643de 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { useCallback } from 'react';
+import { useCallback, useEffect } from 'react';
+import { safeDecode } from '@kbn/rison';
import { useDispatch } from 'react-redux';
@@ -40,5 +41,23 @@ export const useInitTimelineFromUrlParam = () => {
[dispatch]
);
+ useEffect(() => {
+ const listener = () => {
+ const timelineState = new URLSearchParams(window.location.search).get(URL_PARAM_KEY.timeline);
+
+ if (!timelineState) {
+ return;
+ }
+
+ const parsedState = safeDecode(timelineState) as TimelineUrl | null;
+
+ onInitialize(parsedState);
+ };
+
+ // This is needed to initialize the timeline from the URL when the user clicks the back / forward buttons
+ window.addEventListener('popstate', listener);
+ return () => window.removeEventListener('popstate', listener);
+ }, [onInitialize]);
+
useInitializeUrlParam(URL_PARAM_KEY.timeline, onInitialize);
};
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts
index 30d914f5ccc83..cf5616e0c33fe 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { EXPANDABLE_FLYOUT_URL_KEY } from '@kbn/expandable-flyout';
import { useSyncGlobalQueryString } from '../utils/global_query_string';
import { useInitSearchBarFromUrlParams } from './search_bar/use_init_search_bar_url_params';
import { useInitTimerangeFromUrlParam } from './search_bar/use_init_timerange_url_params';
@@ -15,9 +16,6 @@ import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_b
import { useInitFlyoutFromUrlParam } from './flyout/use_init_flyout_url_param';
import { useSyncFlyoutUrlParam } from './flyout/use_sync_flyout_url_param';
-// NOTE: the expandable flyout package url state is handled here:
-// x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx
-
export const useUrlState = () => {
useSyncGlobalQueryString();
useInitSearchBarFromUrlParams();
@@ -30,14 +28,14 @@ export const useUrlState = () => {
useSyncFlyoutUrlParam();
};
-export enum URL_PARAM_KEY {
- appQuery = 'query',
- eventFlyout = 'eventFlyout',
- filters = 'filters',
- savedQuery = 'savedQuery',
- sourcerer = 'sourcerer',
- timeline = 'timeline',
- timerange = 'timerange',
- pageFilter = 'pageFilters',
- rulesTable = 'rulesTable',
-}
+export const URL_PARAM_KEY = {
+ appQuery: 'query',
+ eventFlyout: EXPANDABLE_FLYOUT_URL_KEY,
+ filters: 'filters',
+ savedQuery: 'savedQuery',
+ sourcerer: 'sourcerer',
+ timeline: 'timeline',
+ timerange: 'timerange',
+ pageFilter: 'pageFilters',
+ rulesTable: 'rulesTable',
+} as const;
diff --git a/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts b/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts
index a5b3bb1c8d31f..1be01959e2d0a 100644
--- a/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.ts
@@ -97,3 +97,10 @@ export const useReplaceUrlParams = (): ((params: Record {
+ // NOTE: This is a workaround to make sure that new history entry is created as a result of the user action.
+ // This is needed because of the way global url state is handled in the security app.
+ // (it defaults to replace the url params instead of pushing new history entry)
+ window.history.pushState({}, '', window.location.href);
+};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx
index 6fe5693796cbf..02d149860c1b4 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx
@@ -18,6 +18,7 @@ import { useApi } from '@kbn/securitysolution-list-hooks';
import type { Filter } from '@kbn/es-query';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
+import { createHistoryEntry } from '../../../../common/utils/global_query_string/helpers';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
import { useKibana } from '../../../../common/lib/kibana';
import { TimelineId } from '../../../../../common/types/timeline';
@@ -175,6 +176,8 @@ export const useInvestigateInTimeline = ({
);
const investigateInTimelineAlertClick = useCallback(async () => {
+ createHistoryEntry();
+
startTransaction({ name: ALERTS_ACTIONS.INVESTIGATE_IN_TIMELINE });
if (onInvestigateInTimelineAlertClick) {
onInvestigateInTimelineAlertClick();
diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx
index 1f1ce5618abfd..f0ff67c39f0e1 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx
@@ -23,7 +23,6 @@ import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
import { inputsSelectors } from '../../../common/store';
import { formatPageFilterSearchParam } from '../../../../common/utils/format_page_filter_search_param';
import { resolveFlyoutParams } from './utils';
-import { FLYOUT_URL_PARAM } from '../../../flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url';
export const AlertDetailsRedirect = () => {
const { alertId } = useParams<{ alertId: string }>();
@@ -72,7 +71,7 @@ export const AlertDetailsRedirect = () => {
const pageFiltersQuery = encode(formatPageFilterSearchParam([statusPageFilter]));
- const currentFlyoutParams = searchParams.get(FLYOUT_URL_PARAM);
+ const currentFlyoutParams = searchParams.get(URL_PARAM_KEY.eventFlyout);
const [isSecurityFlyoutEnabled] = useUiSetting$(ENABLE_EXPANDABLE_FLYOUT_SETTING);
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx
index f3297b57183f3..3964f87e9bde6 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx
@@ -5,13 +5,13 @@
* 2.0.
*/
-import React, { memo, useCallback, useState, useEffect } from 'react';
+import React, { memo, useCallback } from 'react';
import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group';
-import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
+import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import {
INSIGHTS_TAB_BUTTON_GROUP_TEST_ID,
INSIGHTS_TAB_ENTITIES_BUTTON_TEST_ID,
@@ -77,14 +77,11 @@ const insightsButtons: EuiButtonGroupOptionProps[] = [
*/
export const InsightsTab: React.FC = memo(() => {
const { eventId, indexName, scopeId } = useLeftPanelContext();
- const { panels, openLeftPanel } = useExpandableFlyoutContext();
- const [activeInsightsId, setActiveInsightsId] = useState(
- panels.left?.path?.subTab ?? ENTITIES_TAB_ID
- );
+ const { openLeftPanel, panels } = useExpandableFlyoutContext();
+ const activeInsightsId = panels.left?.path?.subTab ?? ENTITIES_TAB_ID;
const onChangeCompressed = useCallback(
(optionId: string) => {
- setActiveInsightsId(optionId);
openLeftPanel({
id: DocumentDetailsLeftPanelKey,
path: {
@@ -101,12 +98,6 @@ export const InsightsTab: React.FC = memo(() => {
[eventId, indexName, scopeId, openLeftPanel]
);
- useEffect(() => {
- if (panels.left?.path?.subTab) {
- setActiveInsightsId(panels.left?.path?.subTab);
- }
- }, [panels.left?.path?.subTab]);
-
return (
<>
', () => {
it('should navigate to the left section Insights tab when clicking on button', () => {
const flyoutContextValue = {
openLeftPanel: jest.fn(),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId } = render(
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx
index 5f4a1ef3283eb..81d6993e805f5 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { FormattedMessage, __IntlProvider as IntlProvider } from '@kbn/i18n-react';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import {
DESCRIPTION_TITLE_TEST_ID,
@@ -19,6 +19,8 @@ import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { DocumentDetailsPreviewPanelKey } from '../../preview';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/types';
+import { i18n } from '@kbn/i18n';
const ruleUuid = {
category: 'kibana',
@@ -46,7 +48,7 @@ const ruleName = {
const flyoutContextValue = {
openPreviewPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const panelContextValue = (dataFormattedForFieldBrowser: TimelineEventsDetailsItem[]) =>
({
@@ -135,11 +137,9 @@ describe('', () => {
indexName: panelContext.indexName,
scopeId: panelContext.scopeId,
banner: {
- title: (
-
+ title: i18n.translate(
+ 'xpack.securitySolution.flyout.right.about.description.rulePreviewTitle',
+ { defaultMessage: 'Preview rule details' }
),
backgroundColor: 'warning',
textColor: 'warning',
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx
index 585325a1212e1..c612e2a6fb5a6 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx
@@ -47,11 +47,9 @@ export const Description: FC = () => {
indexName,
scopeId,
banner: {
- title: (
-
+ title: i18n.translate(
+ 'xpack.securitySolution.flyout.right.about.description.rulePreviewTitle',
+ { defaultMessage: 'Preview rule details' }
),
backgroundColor: 'warning',
textColor: 'warning',
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx
index ec7f85be212c9..1e052aa170347 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { copyToClipboard } from '@elastic/eui';
import { RightPanelContext } from '../context';
@@ -17,7 +18,7 @@ import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { TestProvidersComponent } from '../../../../common/mock';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
-import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
+import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
jest.mock('../../../../common/lib/kibana');
jest.mock('../hooks/use_assistant');
@@ -32,7 +33,7 @@ jest.mock('@elastic/eui', () => ({
}));
const alertUrl = 'https://example.com/alert';
-const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
+const flyoutContextValue = {} as unknown as ExpandableFlyoutContextValue;
const mockContextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
getFieldsData: jest.fn().mockImplementation(mockGetFieldsData),
@@ -58,7 +59,7 @@ describe('', () => {
describe('Share alert url action', () => {
it('should render share button in the title and copy the the value to clipboard if document is an alert', () => {
const syncedFlyoutState = 'flyoutState';
- const query = `?${FLYOUT_URL_PARAM}=${syncedFlyoutState}`;
+ const query = `?${URL_PARAM_KEY.eventFlyout}=${syncedFlyoutState}`;
Object.defineProperty(window, 'location', {
value: {
@@ -73,7 +74,7 @@ describe('', () => {
fireEvent.click(shareButton);
expect(copyToClipboard).toHaveBeenCalledWith(
- `${alertUrl}&${FLYOUT_URL_PARAM}=${syncedFlyoutState}`
+ `${alertUrl}&${URL_PARAM_KEY.eventFlyout}=${syncedFlyoutState}`
);
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx
index 2862aed53501b..52bcb514e7cab 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx
@@ -10,8 +10,8 @@ import React, { memo } from 'react';
import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { NewChatById } from '@kbn/elastic-assistant';
+import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
import { copyFunction } from '../../../shared/utils/copy_to_clipboard';
-import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { useAssistant } from '../hooks/use_assistant';
@@ -39,7 +39,7 @@ export const HeaderActions: VFC = memo(() => {
const modifier = (value: string) => {
const query = new URLSearchParams(window.location.search);
- return `${value}&${FLYOUT_URL_PARAM}=${query.get(FLYOUT_URL_PARAM)}`;
+ return `${value}&${URL_PARAM_KEY.eventFlyout}=${query.get(URL_PARAM_KEY.eventFlyout)}`;
};
const { showAssistant, promptContextId } = useAssistant({
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx
index 696fb7c032cd4..9c3157f9d5d0a 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import {
@@ -28,7 +29,7 @@ moment.suppressDeprecationWarnings = true;
moment.tz.setDefault('UTC');
const dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
-const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
+const flyoutContextValue = {} as unknown as ExpandableFlyoutContextValue;
const mockContextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
getFieldsData: jest.fn().mockImplementation(mockGetFieldsData),
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx
index c56fef68a8bfa..0112e06cb489f 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx
@@ -13,6 +13,7 @@ import {
HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID,
} from './test_ids';
import { HighlightedFieldsCell } from './highlighted_fields_cell';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import { LeftPanelInsightsTab, DocumentDetailsLeftPanelKey } from '../../left';
@@ -24,7 +25,7 @@ jest.mock('../../../../management/hooks');
const flyoutContextValue = {
openLeftPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const panelContextValue = {
eventId: 'event id',
indexName: 'indexName',
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
index 31a86495d0561..36fb3731943c4 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx
@@ -21,6 +21,7 @@ import {
import { RightPanelContext } from '../context';
import { mockContextValue } from '../mocks/mock_context';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { LeftPanelInsightsTab, DocumentDetailsLeftPanelKey } from '../../left';
import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
@@ -42,7 +43,7 @@ const panelContextValue = {
const flyoutContextValue = {
openLeftPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to });
jest.mock('../../../../common/containers/use_global_time', () => {
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx
index 0d53dcd97a120..6775ba2e00b6a 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx
@@ -14,13 +14,14 @@ import {
} from './test_ids';
import { RightPanelContext } from '../context';
import { InvestigationSection } from './investigation_section';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback');
-const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
+const flyoutContextValue = {} as unknown as ExpandableFlyoutContextValue;
const panelContextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser.filter(
(d) => d.field !== 'kibana.alert.rule.type'
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx
index fe711387dbf17..5bdca0ec3dfa2 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
@@ -35,7 +36,7 @@ const NO_DATA_MESSAGE = 'No prevalence data available.';
const flyoutContextValue = {
openLeftPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const renderPrevalenceOverview = (contextValue: RightPanelContext = mockContextValue) =>
render(
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.test.tsx
index 65a241a00e6bb..9b459acbaf327 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { FormattedMessage, __IntlProvider as IntlProvider } from '@kbn/i18n-react';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, REASON_TITLE_TEST_ID } from './test_ids';
import { Reason } from './reason';
import { RightPanelContext } from '../context';
@@ -15,10 +15,12 @@ import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { DocumentDetailsPreviewPanelKey } from '../../preview';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/types';
+import { i18n } from '@kbn/i18n';
const flyoutContextValue = {
openPreviewPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const panelContextValue = {
eventId: 'event id',
@@ -89,11 +91,11 @@ describe('', () => {
indexName: panelContextValue.indexName,
scopeId: panelContextValue.scopeId,
banner: {
- title: (
-
+ title: i18n.translate(
+ 'xpack.securitySolution.flyout.right.about.reason.alertReasonPreviewTitle',
+ {
+ defaultMessage: 'Preview alert reason',
+ }
),
backgroundColor: 'warning',
textColor: 'warning',
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx
index 392e753213714..96ec5b2b0232c 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx
@@ -41,11 +41,11 @@ export const Reason: FC = () => {
indexName,
scopeId,
banner: {
- title: (
-
+ title: i18n.translate(
+ 'xpack.securitySolution.flyout.right.about.reason.alertReasonPreviewTitle',
+ {
+ defaultMessage: 'Preview alert reason',
+ }
),
backgroundColor: 'warning',
textColor: 'warning',
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx
index 2a2609b4749e0..d521155ea1631 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx
@@ -10,12 +10,13 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import { RESPONSE_SECTION_CONTENT_TEST_ID, RESPONSE_SECTION_HEADER_TEST_ID } from './test_ids';
import { RightPanelContext } from '../context';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { ResponseSection } from './response_section';
const PREVIEW_MESSAGE = 'Response is not available in alert preview.';
-const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
+const flyoutContextValue = {} as unknown as ExpandableFlyoutContextValue;
const panelContextValue = {} as unknown as RightPanelContext;
const renderResponseSection = () =>
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview.test.tsx
index 775e195e764fd..525ca03dbc045 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview.test.tsx
@@ -10,6 +10,7 @@ import { useProcessData } from '../hooks/use_process_data';
import { SessionPreview } from './session_preview';
import { TestProviders } from '../../../../common/mock';
import React from 'react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
@@ -17,7 +18,7 @@ jest.mock('../hooks/use_process_data');
const flyoutContextValue = {
openLeftPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const panelContextValue = {
eventId: 'event id',
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.test.tsx
index 0b52e0ef67665..1c9412deab05a 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/status.test.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import { DocumentStatus } from './status';
@@ -19,7 +20,7 @@ jest.mock('../../../../detections/components/alerts_table/timeline_actions/use_a
const flyoutContextValue = {
closeFlyout: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const renderStatus = (contextValue: RightPanelContext) =>
render(
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx
index f5ae35db72f16..d752df3d9350e 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import { TestProviders } from '../../../../common/mock';
@@ -147,7 +148,7 @@ describe('', () => {
});
const flyoutContextValue = {
openLeftPanel: jest.fn(),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId } = render(
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
index 155f2c127fc3c..c8673f41376f4 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx
@@ -20,6 +20,7 @@ import {
import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
import { mockContextValue } from '../mocks/mock_context';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import { LeftPanelInsightsTab, DocumentDetailsLeftPanelKey } from '../../left';
@@ -42,7 +43,7 @@ const panelContextValue = {
const flyoutContextValue = {
openLeftPanel: jest.fn(),
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to });
jest.mock('../../../../common/containers/use_global_time', () => {
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx
index 14a0136c73ed9..8133dfa528526 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx
@@ -15,6 +15,7 @@ import { mockContextValue } from '../mocks/mock_context';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { RightPanelContext } from '../context';
import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({
@@ -40,7 +41,7 @@ describe('', () => {
it('should render visualizations component', () => {
const flyoutContextValue = {
openLeftPanel: jest.fn(),
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId, getAllByRole } = render(
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context/url_sync.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context/url_sync.tsx
deleted file mode 100644
index 29b7019fa7196..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context/url_sync.tsx
+++ /dev/null
@@ -1,47 +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, { createContext, useContext, useMemo, type FC } from 'react';
-import { useSyncFlyoutStateWithUrl } from '../hooks/url/use_sync_flyout_state_with_url';
-
-export type SecuritySolutionFlyoutCloseContextValue = ReturnType;
-
-export const SecuritySolutionFlyoutCloseContext = createContext<
- SecuritySolutionFlyoutCloseContextValue | undefined
->(undefined);
-
-/**
- * Exposes the flyout close context value (returned from syncUrl) as a hook.
- */
-export const useSecurityFlyoutUrlSync = () => {
- const contextValue = useContext(SecuritySolutionFlyoutCloseContext);
-
- if (!contextValue) {
- throw new Error('useSecurityFlyoutUrlSync can only be used inside respective provider');
- }
-
- return contextValue;
-};
-
-/**
- * Provides urlSync hook return value as a context value, for reuse in other components.
- * Main goal here is to avoid calling useSyncFlyoutStateWithUrl multiple times.
- */
-export const SecuritySolutionFlyoutUrlSyncProvider: FC = ({ children }) => {
- const [flyoutRef, handleFlyoutChangedOrClosed] = useSyncFlyoutStateWithUrl();
-
- const value: SecuritySolutionFlyoutCloseContextValue = useMemo(
- () => [flyoutRef, handleFlyoutChangedOrClosed],
- [flyoutRef, handleFlyoutChangedOrClosed]
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/expandable_flyout_state_from_event_meta.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/expandable_flyout_state_from_event_meta.ts
index e25824ed8b68a..d80d6f5983b89 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/expandable_flyout_state_from_event_meta.ts
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/expandable_flyout_state_from_event_meta.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import type { ExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { DocumentDetailsRightPanelKey } from '../../../right';
interface RedirectParams {
@@ -19,11 +18,7 @@ interface RedirectParams {
* This value can be used to open the flyout either by passing it directly to the flyout api (exposed via ref) or
* by serializing it to the url & performing a redirect
*/
-export const expandableFlyoutStateFromEventMeta = ({
- index,
- eventId,
- scopeId,
-}: RedirectParams): ExpandableFlyoutContext['panels'] => {
+export const expandableFlyoutStateFromEventMeta = ({ index, eventId, scopeId }: RedirectParams) => {
return {
right: {
id: DocumentDetailsRightPanelKey,
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.test.tsx
deleted file mode 100644
index 984b2a2e223dc..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.test.tsx
+++ /dev/null
@@ -1,77 +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 { ExpandableFlyoutApi } from '@kbn/expandable-flyout';
-import { useSyncToUrl } from '@kbn/url-state';
-import { renderHook } from '@testing-library/react-hooks';
-import { useSyncFlyoutStateWithUrl } from './use_sync_flyout_state_with_url';
-
-jest.mock('@kbn/url-state');
-
-describe('useSyncFlyoutStateWithUrl', () => {
- it('should return an array containing flyoutApi ref and handleFlyoutChanges function', () => {
- const { result } = renderHook(() => useSyncFlyoutStateWithUrl());
- const [flyoutApi, handleFlyoutChanges] = result.current;
-
- expect(flyoutApi.current).toBeNull();
- expect(typeof handleFlyoutChanges).toBe('function');
- });
-
- it('should open flyout when relevant url state is detected in the query string', () => {
- jest.useFakeTimers();
-
- jest.mocked(useSyncToUrl).mockImplementation((_urlKey, callback) => {
- setTimeout(() => callback({ mocked: { flyout: 'state' } }), 0);
- return jest.fn();
- });
-
- const { result } = renderHook(() => useSyncFlyoutStateWithUrl());
- const [flyoutApi, handleFlyoutChanges] = result.current;
-
- const flyoutApiMock: ExpandableFlyoutApi = {
- openFlyout: jest.fn(),
- getState: () => ({ left: undefined, right: undefined, preview: [] }),
- };
-
- expect(typeof handleFlyoutChanges).toBe('function');
- expect(flyoutApi.current).toBeNull();
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (flyoutApi as any).current = flyoutApiMock;
-
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
-
- expect(flyoutApiMock.openFlyout).toHaveBeenCalledTimes(1);
- expect(flyoutApiMock.openFlyout).toHaveBeenCalledWith({ mocked: { flyout: 'state' } });
- });
-
- it('should sync flyout state to url whenever handleFlyoutChanges is called by the consumer', () => {
- const syncStateToUrl = jest.fn();
- jest.mocked(useSyncToUrl).mockImplementation((_urlKey, callback) => {
- setTimeout(() => callback({ mocked: { flyout: 'state' } }), 0);
- return syncStateToUrl;
- });
-
- const { result } = renderHook(() => useSyncFlyoutStateWithUrl());
- const [_flyoutApi, handleFlyoutChanges] = result.current;
-
- handleFlyoutChanges();
-
- expect(syncStateToUrl).toHaveBeenCalledTimes(1);
- expect(syncStateToUrl).toHaveBeenLastCalledWith(undefined);
-
- handleFlyoutChanges({ left: undefined, right: undefined, preview: [] });
-
- expect(syncStateToUrl).toHaveBeenLastCalledWith({
- left: undefined,
- right: undefined,
- preview: undefined,
- });
- expect(syncStateToUrl).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.tsx
deleted file mode 100644
index 97e2500f3f948..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/url/use_sync_flyout_state_with_url.tsx
+++ /dev/null
@@ -1,55 +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 { useCallback, useRef } from 'react';
-import type { ExpandableFlyoutApi, ExpandableFlyoutContext } from '@kbn/expandable-flyout';
-import { useSyncToUrl } from '@kbn/url-state';
-import last from 'lodash/last';
-import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state';
-
-export const FLYOUT_URL_PARAM = URL_PARAM_KEY.eventFlyout;
-
-type FlyoutState = Parameters[0];
-
-/**
- * Sync flyout state with the url and open it when relevant url state is detected in the query string
- * @returns [ref, flyoutChangesHandler]
- */
-export const useSyncFlyoutStateWithUrl = () => {
- const flyoutApi = useRef(null);
-
- const handleRestoreFlyout = useCallback(
- (state?: FlyoutState) => {
- if (!state) {
- return;
- }
-
- flyoutApi.current?.openFlyout(state);
- },
- [flyoutApi]
- );
-
- const syncStateToUrl = useSyncToUrl(FLYOUT_URL_PARAM, handleRestoreFlyout);
-
- // This should be bound to flyout changed and closed events.
- // When flyout is closed, url state is cleared
- const handleFlyoutChanges = useCallback(
- (state?: ExpandableFlyoutContext['panels']) => {
- if (!state) {
- return syncStateToUrl(undefined);
- }
-
- return syncStateToUrl({
- ...state,
- preview: last(state.preview),
- });
- },
- [syncStateToUrl]
- );
-
- return [flyoutApi, handleFlyoutChanges] as const;
-};
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_flyout_context.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_flyout_context.ts
index b579a68994604..419047bc30f77 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_flyout_context.ts
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_flyout_context.ts
@@ -5,14 +5,12 @@
* 2.0.
*/
-import type { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
-import type { State } from '@kbn/expandable-flyout/src/reducer';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
/**
* Mock flyout context
*/
-export const mockFlyoutContextValue: ExpandableFlyoutContext = {
- panels: {} as State,
+export const mockFlyoutContextValue: ExpandableFlyoutContextValue = {
openFlyout: jest.fn(),
openRightPanel: jest.fn(),
openLeftPanel: jest.fn(),
@@ -22,4 +20,9 @@ export const mockFlyoutContextValue: ExpandableFlyoutContext = {
closePreviewPanel: jest.fn(),
previousPreviewPanel: jest.fn(),
closeFlyout: jest.fn(),
+ panels: {
+ left: undefined,
+ right: undefined,
+ preview: [],
+ },
};
diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/risk_summary.stories.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/risk_summary.stories.tsx
index e45869115c50f..7c12607919cf7 100644
--- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/risk_summary.stories.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/risk_summary.stories.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import type { Story } from '@storybook/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { StorybookProviders } from '../../../../common/mock/storybook_providers';
import { mockRiskScoreState } from '../../../../timelines/components/side_panel/new_user_detail/__mocks__';
@@ -20,7 +21,7 @@ export default {
const flyoutContextValue = {
openLeftPanel: () => window.alert('openLeftPanel called'),
panels: {},
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
export const Default: Story = () => {
return (
diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx
index 746aa8b25e0f1..b4555969dd41c 100644
--- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { EuiFlyout } from '@elastic/eui';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { StorybookProviders } from '../../../common/mock/storybook_providers';
import {
@@ -20,7 +21,7 @@ import { UserPanelContent } from './content';
const flyoutContextValue = {
openLeftPanel: () => window.alert('openLeftPanel called'),
panels: {},
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
storiesOf('Components/UserPanelContent', module)
.addDecorator((storyFn) => (
diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx
index 1c8ac6c4cc2c9..a9194a3a766c6 100644
--- a/x-pack/plugins/security_solution/public/flyout/index.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/index.tsx
@@ -23,10 +23,6 @@ import { RightPanelProvider } from './document_details/right/context';
import type { LeftPanelProps } from './document_details/left';
import { LeftPanel, DocumentDetailsLeftPanelKey } from './document_details/left';
import { LeftPanelProvider } from './document_details/left/context';
-import {
- SecuritySolutionFlyoutUrlSyncProvider,
- useSecurityFlyoutUrlSync,
-} from './document_details/shared/context/url_sync';
import type { PreviewPanelProps } from './document_details/preview';
import { PreviewPanel, DocumentDetailsPreviewPanelKey } from './document_details/preview';
import { PreviewPanelProvider } from './document_details/preview/context';
@@ -34,7 +30,6 @@ import type { UserPanelExpandableFlyoutProps } from './entity_details/user_right
import { UserPanel, UserPanelKey } from './entity_details/user_right';
import type { RiskInputsExpandableFlyoutProps } from './entity_details/risk_inputs_left';
import { RiskInputsPanel, RiskInputsPanelKey } from './entity_details/risk_inputs_left';
-
/**
* List of all panels that will be used within the document details expandable flyout.
* This needs to be passed to the expandable flyout registeredPanels property.
@@ -84,42 +79,15 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
},
];
-const OuterProviders: FC = ({ children }) => {
- return {children};
-};
-
-const InnerProviders: FC = ({ children }) => {
- const [flyoutRef, handleFlyoutChangedOrClosed] = useSecurityFlyoutUrlSync();
-
- return (
-
- {children}
-
- );
-};
-
+// NOTE: provider below accepts "storage" prop, please take a look into component's JSDoc.
export const SecuritySolutionFlyoutContextProvider: FC = ({ children }) => (
-
- {children}
-
+ {children}
);
SecuritySolutionFlyoutContextProvider.displayName = 'SecuritySolutionFlyoutContextProvider';
-export const SecuritySolutionFlyout = memo(() => {
- const [_flyoutRef, handleFlyoutChangedOrClosed] = useSecurityFlyoutUrlSync();
-
- return (
-
- );
-});
+export const SecuritySolutionFlyout = memo(() => (
+
+));
SecuritySolutionFlyout.displayName = 'SecuritySolutionFlyout';
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.stories.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.stories.tsx
index cf8d82ff251a5..e41bf0c347b45 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.stories.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.stories.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import type { Story } from '@storybook/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { EuiButtonIcon } from '@elastic/eui';
import { FlyoutNavigation } from './flyout_navigation';
@@ -21,7 +22,7 @@ export default {
const flyoutContextValue = {
closeLeftPanel: () => window.alert('close left panel'),
panels: {},
-} as unknown as ExpandableFlyoutContext;
+} as unknown as ExpandableFlyoutContextValue;
export const Expand: Story = () => {
return (
@@ -38,7 +39,7 @@ export const Collapse: Story = () => {
{
...flyoutContextValue,
panels: { left: {} },
- } as unknown as ExpandableFlyoutContext
+ } as unknown as ExpandableFlyoutContextValue
}
>
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx
index 9972f0e90f350..7642dfcfd81e3 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { act, render } from '@testing-library/react';
+import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { TestProviders } from '../../../common/mock';
import { FlyoutNavigation } from './flyout_navigation';
@@ -24,7 +25,7 @@ describe('', () => {
it('should render expand button', () => {
const flyoutContextValue = {
panels: {},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId, queryByTestId } = render(
@@ -47,7 +48,7 @@ describe('', () => {
panels: {
left: {},
},
- } as unknown as ExpandableFlyoutContext;
+ } as unknown as ExpandableFlyoutContextValue;
const { getByTestId, queryByTestId } = render(
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx
index 820b0ac5efcd9..cc969988a16a4 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx
@@ -48,7 +48,7 @@ export const FlyoutNavigation: FC = memo(
const { euiTheme } = useEuiTheme();
const { closeLeftPanel, panels } = useExpandableFlyoutContext();
- const isExpanded: boolean = panels.left != null;
+ const isExpanded: boolean = !!panels.left;
const collapseDetails = useCallback(() => closeLeftPanel(), [closeLeftPanel]);
const collapseButton = useMemo(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index 939a450e7064e..14158e884e1bb 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -13,9 +13,9 @@ import styled from 'styled-components';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
+import { createHistoryEntry } from '../../../../common/utils/global_query_string/helpers';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
-import { timelineDefaults } from '../../../store/timeline/defaults';
import type { State } from '../../../../common/store';
import { useKibana } from '../../../../common/lib/kibana';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
@@ -27,8 +27,9 @@ import * as i18n from './translations';
import { TimelineActionMenu } from '../action_menu';
import { AddToFavoritesButton } from '../../timeline/properties/helpers';
import { TimelineStatusInfo } from './timeline_status_info';
+import { timelineDefaults } from '../../../store/timeline/defaults';
-export interface FlyoutHeaderPanelProps {
+interface FlyoutHeaderPanelProps {
timelineId: string;
}
@@ -117,6 +118,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline
);
const handleClose = useCallback(() => {
+ createHistoryEntry();
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
focusActiveTimelineButton();
}, [dispatch, timelineId]);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
index f20283f2c3c94..390369eaf89ed 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
@@ -324,10 +324,15 @@ export const QueryTabContentComponent: React.FC = ({
[ACTION_BUTTON_COUNT]
);
+ // NOTE: The timeline is blank after browser FORWARD navigation (after using back button to navigate to
+ // the previous page from the timeline), yet we still see total count. This is because the timeline
+ // is not getting refreshed when using browser navigation.
+ const showEventsCountBadge = !isBlankTimeline && totalCount >= 0;
+
return (
<>
- {totalCount >= 0 ? {totalCount} : null}
+ {showEventsCountBadge ? {totalCount} : null}
{
- cy.url().should('not.include', 'eventFlyout');
+ cy.url().should('not.include', 'rightPanel');
expandFirstAlertExpandableFlyout();
cy.log('should serialize its state to url');
- cy.url().should('include', 'eventFlyout');
+ cy.url().should('include', 'rightPanel');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('have.text', rule.name);
cy.log('should reopen the flyout after browser refresh');
@@ -40,13 +40,13 @@ describe('Expandable flyout state sync', { tags: ['@ess', '@serverless'] }, () =
cy.reload();
waitForAlertsToPopulate();
- cy.url().should('include', 'eventFlyout');
+ cy.url().should('include', 'rightPanel');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('have.text', rule.name);
cy.log('should clear the url state when flyout is closed');
closeFlyout();
- cy.url().should('not.include', 'eventFlyout');
+ cy.url().should('not.include', 'rightPanel');
});
});