Skip to content

Commit

Permalink
[Security Solution][Alert Details] - push and overlay expandable flyo…
Browse files Browse the repository at this point in the history
…ut (elastic#182615)

## Summary

This PR adds a new functionality to the `kbn-expandable-flyout` package
and its usage in the Security Solution application.

The package's flyout now support to be rendered in `overlay` or `push`
mode, following [EUI's
recommendation](https://eui.elastic.co/#/layout/flyout#push-versus-overlay).
A gear icon button is rendered in the top right corner, next to the
close button. When clicked, a menu appears where users can select `push`
or `overlay` values. `overlay` is the default value. If `push` is
selected, a `Reset to default` empty button can be used to reset to
`overlay`.

Overlay option selected (by default)
![Screenshot 2024-09-04 at 12 10
34 PM](https://github.com/user-attachments/assets/87f57238-9b44-4d29-9516-9eb329c49bb2)

Push option selected
![Screenshot 2024-09-04 at 12 10
42 PM](https://github.com/user-attachments/assets/80e7879a-b238-46ba-9c13-2c8e236e138f)

The flyout should be toggled between `overlay` and `push` mode in all
the pages it's been currently used in:
- alerts page
- rule creation page
- explore pages (host, users...)
- case detail page


https://github.com/user-attachments/assets/b4cec138-802c-430d-8f37-01258e6afef3

But the flyout cannot be set to `push` mode when opened from Timeline.
Timeline is a modal (an EUI Portal to be precise), and getting the
portal as well as the overlay mask to correctly resize according to the
flyout's width (which is dynamic, changes with the screen size and also
changes if the flyout is in collapsed or expanded mode) is very
difficult.
A future PR might add this functionality to TImeline. At this time, the
flyout offers the option to disable the `push/overlay` toggle as well as
an icon to show a tooltip to explain why.


https://github.com/user-attachments/assets/e00961c8-cc75-4eb9-b34d-544bc4391d5c

#### Notes

The package also offers a way to hide the gear icon entirely. In the
future, we might need a bit more flexibility if we want to be able to
show the gear icon with options others than the `push/overlay` entry.

Finally the state of the flyout type (`overlay` vs `push`) is saved in
local storage so that users don't have to set the value over and over
again. This state is persisted within the `kbn-expandable-flyout`
package so that developers don't have to worry about setting it up. The
package uses its internal `urlKey` to guarantee that the key used to
save in localStorage is unique. This means that `memory` flyouts cannot
persist the `push` or `overlay` states, this is expected.


https://github.com/elastic/kibana/assets/17276605/500315b5-07d4-4498-aab9-ee2e2be0253b

### Notes

The package's README has been updated.
New Storybook stories have been added to reflect the new push/overlay
functionality.

elastic#182593
  • Loading branch information
PhilippeOberti authored Sep 6, 2024
1 parent 832bc99 commit d968cc0
Show file tree
Hide file tree
Showing 20 changed files with 835 additions and 111 deletions.
15 changes: 15 additions & 0 deletions packages/kbn-expandable-flyout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ 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_

## Optional properties

The expandable flyout now offers a way for users to change some of the flyout's UI properties. These are done via a gear icon in the top right corner of the flyout, to the left of the close icon.

The gear icon can be hidden by setting the `hideSettings` property to `true` in the flyout's custom props.
The `typeDisabled` property allows to disable the push/overlay toggle.
```typescript
flyoutCustomProps?: {
hideSettings?: boolean;
typeDisabled?: boolean,
};
```

At the moment, clicking on the gear icon opens a popover that allows you to toggle the flyout between `overlay` and `push` modes (see [EUI](https://eui.elastic.co/#/layout/flyout#push-versus-overlay)). The default value is `overlay`. The package remembers the selected value in local storage, only for expandable flyout that have a urlKey. The state of memory flyouts is not persisted.

## Terminology

### Section
Expand Down
20 changes: 20 additions & 0 deletions packages/kbn-expandable-flyout/__mocks__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import React from 'react';
import type { IStorage } from '@kbn/kibana-utils-plugin/public';

export const useExpandableFlyoutApi = jest.fn(() => ({
openFlyout: jest.fn(),
Expand Down Expand Up @@ -37,3 +38,22 @@ export const withExpandableFlyoutProvider = <T extends object>(
};

export const ExpandableFlyout = jest.fn();

export const localStorageMock = (): IStorage => {
let store: Record<string, unknown> = {};

return {
getItem: (key: string) => {
return store[key] || null;
},
setItem: (key: string, value: unknown) => {
store[key] = value;
},
clear() {
store = {};
},
removeItem(key: string) {
delete store[key];
},
};
};
26 changes: 14 additions & 12 deletions packages/kbn-expandable-flyout/src/components/left_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { LEFT_SECTION_TEST_ID } from './test_ids';

interface LeftSectionProps {
Expand All @@ -24,16 +24,18 @@ interface LeftSectionProps {
/**
* Left section of the expanded flyout rendering a panel
*/
export const LeftSection: React.FC<LeftSectionProps> = ({ component, width }: LeftSectionProps) => {
const style = useMemo<React.CSSProperties>(
() => ({ height: '100%', width: `${width}px` }),
[width]
);
return (
<EuiFlexItem grow data-test-subj={LEFT_SECTION_TEST_ID} style={style}>
{component}
</EuiFlexItem>
);
};
export const LeftSection: React.FC<LeftSectionProps> = memo(
({ component, width }: LeftSectionProps) => {
const style = useMemo<React.CSSProperties>(
() => ({ height: '100%', width: `${width}px` }),
[width]
);
return (
<EuiFlexItem grow data-test-subj={LEFT_SECTION_TEST_ID} style={style}>
{component}
</EuiFlexItem>
);
}
);

LeftSection.displayName = 'LeftSection';
148 changes: 73 additions & 75 deletions packages/kbn-expandable-flyout/src/components/preview_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
EuiSplitPanel,
transparentize,
} from '@elastic/eui';
import React from 'react';
import React, { memo } from 'react';
import { css } from '@emotion/react';
import { has } from 'lodash';
import {
Expand Down Expand Up @@ -79,91 +79,89 @@ interface PreviewSectionProps {
* Preview section of the expanded flyout rendering one or multiple panels.
* Will display a back and close button in the header for the previous and close feature respectively.
*/
export const PreviewSection: React.FC<PreviewSectionProps> = ({
component,
leftPosition,
banner,
}: PreviewSectionProps) => {
const { euiTheme } = useEuiTheme();
const { closePreviewPanel, previousPreviewPanel } = useExpandableFlyoutApi();
export const PreviewSection: React.FC<PreviewSectionProps> = memo(
({ component, leftPosition, banner }: PreviewSectionProps) => {
const { euiTheme } = useEuiTheme();
const { closePreviewPanel, previousPreviewPanel } = useExpandableFlyoutApi();

const left = leftPosition + 4;
const left = leftPosition + 4;

const closeButton = (
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
onClick={() => closePreviewPanel()}
data-test-subj={PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID}
aria-label={CLOSE_BUTTON}
/>
</EuiFlexItem>
);
const header = (
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
const closeButton = (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType="arrowLeft"
iconSide="left"
onClick={() => previousPreviewPanel()}
data-test-subj={PREVIEW_SECTION_BACK_BUTTON_TEST_ID}
aria-label={BACK_BUTTON}
>
{BACK_BUTTON}
</EuiButtonEmpty>
<EuiButtonIcon
iconType="cross"
onClick={() => closePreviewPanel()}
data-test-subj={PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID}
aria-label={CLOSE_BUTTON}
/>
</EuiFlexItem>
{closeButton}
</EuiFlexGroup>
);
);
const header = (
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType="arrowLeft"
iconSide="left"
onClick={() => previousPreviewPanel()}
data-test-subj={PREVIEW_SECTION_BACK_BUTTON_TEST_ID}
aria-label={BACK_BUTTON}
>
{BACK_BUTTON}
</EuiButtonEmpty>
</EuiFlexItem>
{closeButton}
</EuiFlexGroup>
);

return (
<div
css={css`
position: absolute;
top: 8px;
bottom: 8px;
right: 4px;
left: ${left}px;
z-index: 1000;
`}
>
<EuiSplitPanel.Outer
return (
<div
css={css`
margin: ${euiTheme.size.xs};
box-shadow: 0 0 16px 0px ${transparentize(euiTheme.colors.mediumShade, 0.5)};
position: absolute;
top: 8px;
bottom: 8px;
right: 4px;
left: ${left}px;
z-index: 1000;
`}
data-test-subj={PREVIEW_SECTION_TEST_ID}
className="eui-fullHeight"
>
{isPreviewBanner(banner) && (
<EuiSplitPanel.Outer
css={css`
margin: ${euiTheme.size.xs};
box-shadow: 0 0 16px 0px ${transparentize(euiTheme.colors.mediumShade, 0.5)};
`}
data-test-subj={PREVIEW_SECTION_TEST_ID}
className="eui-fullHeight"
>
{isPreviewBanner(banner) && (
<EuiSplitPanel.Inner
grow={false}
color={banner.backgroundColor}
paddingSize="xs"
data-test-subj={`${PREVIEW_SECTION_TEST_ID}BannerPanel`}
>
<EuiText
textAlign="center"
color={banner.textColor}
size="xs"
data-test-subj={`${PREVIEW_SECTION_TEST_ID}BannerText`}
>
{banner.title}
</EuiText>
</EuiSplitPanel.Inner>
)}
<EuiSplitPanel.Inner
grow={false}
color={banner.backgroundColor}
paddingSize="xs"
data-test-subj={`${PREVIEW_SECTION_TEST_ID}BannerPanel`}
paddingSize="s"
data-test-subj={PREVIEW_SECTION_HEADER_TEST_ID}
>
<EuiText
textAlign="center"
color={banner.textColor}
size="xs"
data-test-subj={`${PREVIEW_SECTION_TEST_ID}BannerText`}
>
{banner.title}
</EuiText>
{header}
</EuiSplitPanel.Inner>
)}
<EuiSplitPanel.Inner
grow={false}
paddingSize="s"
data-test-subj={PREVIEW_SECTION_HEADER_TEST_ID}
>
{header}
</EuiSplitPanel.Inner>
{component}
</EuiSplitPanel.Outer>
</div>
);
};
{component}
</EuiSplitPanel.Outer>
</div>
);
}
);

PreviewSection.displayName = 'PreviewSection';
29 changes: 14 additions & 15 deletions packages/kbn-expandable-flyout/src/components/right_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { RIGHT_SECTION_TEST_ID } from './test_ids';

interface RightSectionProps {
Expand All @@ -24,20 +24,19 @@ interface RightSectionProps {
/**
* Right section of the expanded flyout rendering a panel
*/
export const RightSection: React.FC<RightSectionProps> = ({
component,
width,
}: RightSectionProps) => {
const style = useMemo<React.CSSProperties>(
() => ({ height: '100%', width: `${width}px` }),
[width]
);
export const RightSection: React.FC<RightSectionProps> = memo(
({ component, width }: RightSectionProps) => {
const style = useMemo<React.CSSProperties>(
() => ({ height: '100%', width: `${width}px` }),
[width]
);

return (
<EuiFlexItem grow={false} style={style} data-test-subj={RIGHT_SECTION_TEST_ID}>
{component}
</EuiFlexItem>
);
};
return (
<EuiFlexItem grow={false} style={style} data-test-subj={RIGHT_SECTION_TEST_ID}>
{component}
</EuiFlexItem>
);
}
);

RightSection.displayName = 'RightSection';
Loading

0 comments on commit d968cc0

Please sign in to comment.