[Security Solution] Simplify shared values management / passing across the flyout (#160779)

## Summary

The idea here is to simplify shared values management in the expandable
flyout. This PR is a start for a series of changes aimed at this goal.

Here, we are moving the flyout init to a dedicated Flyout and Provider
components, composing Expandable Flyout and respective providers into
something specific to Security Solution. Also, changes to how the flyout
is composed will no longer trigger codeowner review for 3 teams, just
one.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Luke G 2023-07-24 11:15:00 +02:00 committed by GitHub
parent 59450f0a22
commit 20a9f586b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 152 additions and 65 deletions

View file

@ -16,4 +16,4 @@ export {
export type { ExpandableFlyoutApi } from './src/context';
export type { ExpandableFlyoutProps } from './src';
export type { FlyoutPanel } from './src/types';
export type { FlyoutPanelProps } from './src/types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { FlyoutPanel } from './types';
import { FlyoutPanelProps } from './types';
export enum ActionType {
openFlyout = 'open_flyout',
@ -24,22 +24,22 @@ export type Action =
| {
type: ActionType.openFlyout;
payload: {
right?: FlyoutPanel;
left?: FlyoutPanel;
preview?: FlyoutPanel;
right?: FlyoutPanelProps;
left?: FlyoutPanelProps;
preview?: FlyoutPanelProps;
};
}
| {
type: ActionType.openRightPanel;
payload: FlyoutPanel;
payload: FlyoutPanelProps;
}
| {
type: ActionType.openLeftPanel;
payload: FlyoutPanel;
payload: FlyoutPanelProps;
}
| {
type: ActionType.openPreviewPanel;
payload: FlyoutPanel;
payload: FlyoutPanelProps;
}
| {
type: ActionType.closeRightPanel;

View file

@ -17,7 +17,7 @@ import React, {
} from 'react';
import { ActionType } from './actions';
import { reducer, State } from './reducer';
import type { FlyoutPanel } from './types';
import type { FlyoutPanelProps } from './types';
import { initialState } from './reducer';
export interface ExpandableFlyoutContext {
@ -28,19 +28,23 @@ export interface ExpandableFlyoutContext {
/**
* Open the flyout with left, right and/or preview panels
*/
openFlyout: (panels: { left?: FlyoutPanel; right?: FlyoutPanel; preview?: FlyoutPanel }) => void;
openFlyout: (panels: {
left?: FlyoutPanelProps;
right?: FlyoutPanelProps;
preview?: FlyoutPanelProps;
}) => void;
/**
* Replaces the current right panel with a new one
*/
openRightPanel: (panel: FlyoutPanel) => void;
openRightPanel: (panel: FlyoutPanelProps) => void;
/**
* Replaces the current left panel with a new one
*/
openLeftPanel: (panel: FlyoutPanel) => void;
openLeftPanel: (panel: FlyoutPanelProps) => void;
/**
* Add a new preview panel to the list of current preview panels
*/
openPreviewPanel: (panel: FlyoutPanel) => void;
openPreviewPanel: (panel: FlyoutPanelProps) => void;
/**
* Closes right panel
*/
@ -111,25 +115,25 @@ export const ExpandableFlyoutProvider = React.forwardRef<
left,
preview,
}: {
right?: FlyoutPanel;
left?: FlyoutPanel;
preview?: FlyoutPanel;
right?: FlyoutPanelProps;
left?: FlyoutPanelProps;
preview?: FlyoutPanelProps;
}) => dispatch({ type: ActionType.openFlyout, payload: { left, right, preview } }),
[dispatch]
);
const openRightPanel = useCallback(
(panel: FlyoutPanel) => dispatch({ type: ActionType.openRightPanel, payload: panel }),
(panel: FlyoutPanelProps) => dispatch({ type: ActionType.openRightPanel, payload: panel }),
[]
);
const openLeftPanel = useCallback(
(panel: FlyoutPanel) => dispatch({ type: ActionType.openLeftPanel, payload: panel }),
(panel: FlyoutPanelProps) => dispatch({ type: ActionType.openLeftPanel, payload: panel }),
[]
);
const openPreviewPanel = useCallback(
(panel: FlyoutPanel) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }),
(panel: FlyoutPanelProps) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }),
[]
);

View file

@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlyout } from '@elastic/eui';
import { useExpandableFlyoutContext } from './context';
import { PreviewSection } from './components/preview_section';
import { RightSection } from './components/right_section';
import type { FlyoutPanel, Panel } from './types';
import type { FlyoutPanelProps, Panel } from './types';
import { LeftSection } from './components/left_section';
export interface ExpandableFlyoutProps extends Omit<EuiFlyoutProps, 'onClose'> {
@ -98,13 +98,13 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
>
{leftSection && left ? (
<LeftSection
component={leftSection.component({ ...(left as FlyoutPanel) })}
component={leftSection.component({ ...(left as FlyoutPanelProps) })}
width={leftSectionWidth}
/>
) : null}
{rightSection && right ? (
<RightSection
component={rightSection.component({ ...(right as FlyoutPanel) })}
component={rightSection.component({ ...(right as FlyoutPanelProps) })}
width={rightSectionWidth}
/>
) : null}
@ -112,7 +112,7 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
{previewSection && preview ? (
<PreviewSection
component={previewSection.component({ ...(mostRecentPreview as FlyoutPanel) })}
component={previewSection.component({ ...(mostRecentPreview as FlyoutPanelProps) })}
showBackButton={showBackButton}
width={previewSectionWidth}
/>

View file

@ -6,32 +6,32 @@
* Side Public License, v 1.
*/
import { FlyoutPanel } from './types';
import { FlyoutPanelProps } from './types';
import { initialState, reducer, State } from './reducer';
import { Action, ActionType } from './actions';
const rightPanel1: FlyoutPanel = {
const rightPanel1: FlyoutPanelProps = {
id: 'right1',
path: ['path'],
};
const leftPanel1: FlyoutPanel = {
const leftPanel1: FlyoutPanelProps = {
id: 'left1',
params: { id: 'id' },
};
const previewPanel1: FlyoutPanel = {
const previewPanel1: FlyoutPanelProps = {
id: 'preview1',
state: { id: 'state' },
};
const rightPanel2: FlyoutPanel = {
const rightPanel2: FlyoutPanelProps = {
id: 'right2',
path: ['path'],
};
const leftPanel2: FlyoutPanel = {
const leftPanel2: FlyoutPanelProps = {
id: 'left2',
params: { id: 'id' },
};
const previewPanel2: FlyoutPanel = {
const previewPanel2: FlyoutPanelProps = {
id: 'preview2',
state: { id: 'state' },
};

View file

@ -6,22 +6,22 @@
* Side Public License, v 1.
*/
import { FlyoutPanel } from './types';
import { FlyoutPanelProps } from './types';
import { Action, ActionType } from './actions';
export interface State {
/**
* Panel to render in the left section
*/
left: FlyoutPanel | undefined;
left: FlyoutPanelProps | undefined;
/**
* Panel to render in the right section
*/
right: FlyoutPanel | undefined;
right: FlyoutPanelProps | undefined;
/**
* Panels to render in the preview section
*/
preview: FlyoutPanel[];
preview: FlyoutPanelProps[];
}
export const initialState: State = {
@ -90,7 +90,7 @@ export function reducer(state: State, action: Action) {
* Navigates to the previous preview panel by removing the last entry in the array of preview panels.
*/
case ActionType.previousPreviewPanel: {
const p: FlyoutPanel[] = [...state.preview];
const p: FlyoutPanelProps[] = [...state.preview];
p.pop();
return { ...state, preview: p };
}

View file

@ -8,7 +8,7 @@
import React from 'react';
export interface FlyoutPanel {
export interface FlyoutPanelProps {
/**
* Unique key to identify the panel
*/
@ -35,5 +35,5 @@ export interface Panel {
/**
* Component to be rendered
*/
component: (props: FlyoutPanel) => React.ReactElement;
component: (props: FlyoutPanelProps) => React.ReactElement;
}

View file

@ -11,8 +11,7 @@ import { EuiThemeProvider, useEuiTheme } from '@elastic/eui';
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import { ExpandableFlyout, ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
import { expandableFlyoutDocumentsPanels } from '../../../flyout';
import { SecuritySolutionFlyout, SecuritySolutionFlyoutContextProvider } from '../../../flyout';
import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation';
import { TimelineId } from '../../../../common/types/timeline';
import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors';
@ -24,7 +23,6 @@ import {
SecuritySolutionBottomBarProps,
} from './bottom_bar';
import { useShowTimeline } from '../../../common/utils/timeline/use_show_timeline';
import { useSyncFlyoutStateWithUrl } from '../../../flyout/url/use_sync_flyout_state_with_url';
/**
* Need to apply the styles via a className to effect the containing bottom bar
@ -69,8 +67,6 @@ export const SecuritySolutionTemplateWrapper: React.FC<Omit<KibanaPageTemplatePr
// To keep the mode in sync, we pass in the globalColorMode to the bottom bar here
const { colorMode: globalColorMode } = useEuiTheme();
const [flyoutRef, handleFlyoutChangedOrClosed] = useSyncFlyoutStateWithUrl();
/*
* StyledKibanaPageTemplate is a styled EuiPageTemplate. Security solution currently passes the header
* and page content as the children of StyledKibanaPageTemplate, as opposed to using the pageHeader prop,
@ -78,11 +74,7 @@ export const SecuritySolutionTemplateWrapper: React.FC<Omit<KibanaPageTemplatePr
* between EuiPageTemplate and the security solution pages.
*/
return (
<ExpandableFlyoutProvider
onChanges={handleFlyoutChangedOrClosed}
onClosePanels={handleFlyoutChangedOrClosed}
ref={flyoutRef}
>
<SecuritySolutionFlyoutContextProvider>
<StyledKibanaPageTemplate
$isShowingTimelineOverlay={isShowingTimelineOverlay}
paddingSize="none"
@ -108,12 +100,9 @@ export const SecuritySolutionTemplateWrapper: React.FC<Omit<KibanaPageTemplatePr
</EuiThemeProvider>
</KibanaPageTemplate.BottomBar>
)}
<ExpandableFlyout
registeredPanels={expandableFlyoutDocumentsPanels}
handleOnFlyoutClosed={handleFlyoutChangedOrClosed}
/>
<SecuritySolutionFlyout />
</StyledKibanaPageTemplate>
</ExpandableFlyoutProvider>
</SecuritySolutionFlyoutContextProvider>
);
});

View file

@ -19,7 +19,7 @@ import { inputsSelectors } from '../../../common/store';
import { formatPageFilterSearchParam } from '../../../../common/utils/format_page_filter_search_param';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { resolveFlyoutParams } from './utils';
import { FLYOUT_URL_PARAM } from '../../../flyout/url/use_sync_flyout_state_with_url';
import { FLYOUT_URL_PARAM } from '../../../flyout/shared/hooks/url/use_sync_flyout_state_with_url';
export const AlertDetailsRedirect = () => {
const { alertId } = useParams<{ alertId: string }>();

View file

@ -6,7 +6,7 @@
*/
import { encode } from '@kbn/rison';
import { expandableFlyoutStateFromEventMeta } from '../../../flyout/url/expandable_flyout_state_from_event_meta';
import { expandableFlyoutStateFromEventMeta } from '../../../flyout/shared/hooks/url/expandable_flyout_state_from_event_meta';
export interface ResolveFlyoutParamsConfig {
index: string;

View file

@ -5,20 +5,28 @@
* 2.0.
*/
import React from 'react';
import type { ExpandableFlyoutProps } from '@kbn/expandable-flyout';
import React, { memo, type FC } from 'react';
import {
ExpandableFlyout,
type ExpandableFlyoutProps,
ExpandableFlyoutProvider,
} from '@kbn/expandable-flyout';
import type { RightPanelProps } from './right';
import { RightPanel, RightPanelKey } from './right';
import { RightPanelProvider } from './right/context';
import type { LeftPanelProps } from './left';
import { LeftPanel, LeftPanelKey } from './left';
import { LeftPanelProvider } from './left/context';
import {
SecuritySolutionFlyoutUrlSyncProvider,
useSecurityFlyoutUrlSync,
} from './shared/context/url_sync';
/**
* 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.
*/
export const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] = [
const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] = [
{
key: RightPanelKey,
component: (props) => (
@ -36,3 +44,42 @@ export const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredP
),
},
];
const OuterProviders: FC = ({ children }) => {
return <SecuritySolutionFlyoutUrlSyncProvider>{children}</SecuritySolutionFlyoutUrlSyncProvider>;
};
const InnerProviders: FC = ({ children }) => {
const [flyoutRef, handleFlyoutChangedOrClosed] = useSecurityFlyoutUrlSync();
return (
<ExpandableFlyoutProvider
onChanges={handleFlyoutChangedOrClosed}
onClosePanels={handleFlyoutChangedOrClosed}
ref={flyoutRef}
>
{children}
</ExpandableFlyoutProvider>
);
};
export const SecuritySolutionFlyoutContextProvider: FC = ({ children }) => (
<OuterProviders>
<InnerProviders>{children}</InnerProviders>
</OuterProviders>
);
SecuritySolutionFlyoutContextProvider.displayName = 'SecuritySolutionFlyoutContextProvider';
export const SecuritySolutionFlyout = memo(() => {
const [_flyoutRef, handleFlyoutChangedOrClosed] = useSecurityFlyoutUrlSync();
return (
<ExpandableFlyout
registeredPanels={expandableFlyoutDocumentsPanels}
handleOnFlyoutClosed={handleFlyoutChangedOrClosed}
/>
);
});
SecuritySolutionFlyout.displayName = 'SecuritySolutionFlyout';

View file

@ -9,7 +9,7 @@ import type { FC } from 'react';
import React, { memo, useMemo } from 'react';
import { useEuiBackgroundColor } from '@elastic/eui';
import { css } from '@emotion/react';
import type { FlyoutPanel } from '@kbn/expandable-flyout';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { PanelHeader } from './header';
import { PanelContent } from './content';
@ -24,7 +24,7 @@ export const LeftPanelVisualizeTabPath: LeftPanelProps['path'] = ['visualize'];
export const LeftPanelInsightsTabPath: LeftPanelProps['path'] = ['insights'];
export const LeftPanelInvestigationTabPath: LeftPanelProps['path'] = ['investigation'];
export interface LeftPanelProps extends FlyoutPanel {
export interface LeftPanelProps extends FlyoutPanelProps {
key: 'document-details-left';
path?: LeftPanelPaths[];
params?: {

View file

@ -9,8 +9,8 @@ import { render, screen, fireEvent } from '@testing-library/react';
import { copyToClipboard } from '@elastic/eui';
import { ShareButton } from './share_button';
import React from 'react';
import { FLYOUT_URL_PARAM } from '../../url/use_sync_flyout_state_with_url';
import { FLYOUT_HEADER_SHARE_BUTTON_TEST_ID } from './test_ids';
import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),

View file

@ -8,7 +8,7 @@
import { copyToClipboard, EuiButtonEmpty, EuiCopy } from '@elastic/eui';
import type { FC } from 'react';
import React from 'react';
import { FLYOUT_URL_PARAM } from '../../url/use_sync_flyout_state_with_url';
import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
import { FLYOUT_HEADER_SHARE_BUTTON_TEST_ID } from './test_ids';
import { SHARE } from './translations';

View file

@ -7,7 +7,7 @@
import type { FC } from 'react';
import React, { memo, useMemo } from 'react';
import type { FlyoutPanel } from '@kbn/expandable-flyout';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { useRightPanelContext } from './context';
import { PanelHeader } from './header';
@ -21,7 +21,7 @@ export type RightPanelPaths = 'overview' | 'table' | 'json';
export const RightPanelKey: RightPanelProps['key'] = 'document-details-right';
export const RightPanelTableTabPath: RightPanelProps['path'] = ['table'];
export interface RightPanelProps extends FlyoutPanel {
export interface RightPanelProps extends FlyoutPanelProps {
key: 'document-details-right';
path?: RightPanelPaths[];
params?: {

View file

@ -0,0 +1,47 @@
/*
* 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<typeof useSyncFlyoutStateWithUrl>;
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 (
<SecuritySolutionFlyoutCloseContext.Provider value={value}>
{children}
</SecuritySolutionFlyoutCloseContext.Provider>
);
};

View file

@ -6,7 +6,7 @@
*/
import type { ExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { RightPanelKey } from '../right';
import { RightPanelKey } from '../../../right';
interface RedirectParams {
index: string;

View file

@ -9,7 +9,7 @@ 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';
import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
export const FLYOUT_URL_PARAM = URL_PARAM_KEY.eventFlyout;