mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution][Flyout] Use kbn url storage provider, housekeeping (#175882)
## Summary **This does not change any of the api's defined previously.** Changes: - replace custom hook for url serialization with off the shelf solution from public plugins - cleanup the codebase futher - remove internal context based hook ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
585630d060
commit
1fe7833a23
29 changed files with 265 additions and 418 deletions
|
@ -27,7 +27,11 @@ The expandable-flyout is making some strict UI design decisions:
|
||||||
|
|
||||||
The ExpandableFlyout [React component](https://github.com/elastic/kibana/tree/main/packages/kbn-expandable-flyout/src/index.tsx) renders the UI, leveraging an [EuiFlyout](https://eui.elastic.co/#/layout/flyout).
|
The ExpandableFlyout [React component](https://github.com/elastic/kibana/tree/main/packages/kbn-expandable-flyout/src/index.tsx) renders the UI, leveraging an [EuiFlyout](https://eui.elastic.co/#/layout/flyout).
|
||||||
|
|
||||||
The ExpandableFlyout [hooks](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/context.tsx) expose the state and the following api:
|
To retrieve the flyout's layout (left, right and preview panels), you can utilize [useExpandableFlyoutState](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts).
|
||||||
|
|
||||||
|
To control (or mutate) flyout's layout, you can utilize [useExpandableFlyoutApi](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_api.ts).
|
||||||
|
|
||||||
|
**Expandable Flyout API** exposes the following methods:
|
||||||
- **openFlyout**: open the flyout with a set of panels
|
- **openFlyout**: open the flyout with a set of panels
|
||||||
- **openRightPanel**: open a right panel
|
- **openRightPanel**: open a right panel
|
||||||
- **openLeftPanel**: open a left panel
|
- **openLeftPanel**: open a left panel
|
||||||
|
@ -38,10 +42,6 @@ The ExpandableFlyout [hooks](https://github.com/elastic/kibana/blob/main/package
|
||||||
- **previousPreviewPanel**: navigate to the previous preview panel
|
- **previousPreviewPanel**: navigate to the previous preview panel
|
||||||
- **closeFlyout**: close the flyout
|
- **closeFlyout**: close the flyout
|
||||||
|
|
||||||
To retrieve the flyout's layout (left, right and preview panels), you can use the **useExpandableFlyoutState** from the same [React context](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/context.tsx).
|
|
||||||
|
|
||||||
To control (or mutate) flyout's layout, you can use the **useExpandableFlyoutApi** from the same [React context](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/context.tsx).
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To use the expandable flyout in your plugin, first you need wrap your code with the [context provider](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/context.tsx) at a high enough level as follows:
|
To use the expandable flyout in your plugin, first you need wrap your code with the [context provider](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/context.tsx) at a high enough level as follows:
|
||||||
|
|
|
@ -8,11 +8,8 @@
|
||||||
|
|
||||||
export { ExpandableFlyout } from './src';
|
export { ExpandableFlyout } from './src';
|
||||||
|
|
||||||
export {
|
export { useExpandableFlyoutApi } from './src/hooks/use_expandable_flyout_api';
|
||||||
type ExpandableFlyoutContext,
|
export { useExpandableFlyoutState } from './src/hooks/use_expandable_flyout_state';
|
||||||
useExpandableFlyoutState,
|
|
||||||
useExpandableFlyoutApi,
|
|
||||||
} from './src/context';
|
|
||||||
|
|
||||||
export { type State as ExpandableFlyoutState } from './src/state';
|
export { type State as ExpandableFlyoutState } from './src/state';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"type": "shared-common",
|
"type": "shared-browser",
|
||||||
"id": "@kbn/expandable-flyout",
|
"id": "@kbn/expandable-flyout",
|
||||||
"owner": "@elastic/security-threat-hunting-investigations"
|
"owner": "@elastic/security-threat-hunting-investigations"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ export enum ActionType {
|
||||||
closePreviewPanel = 'close_preview_panel',
|
closePreviewPanel = 'close_preview_panel',
|
||||||
previousPreviewPanel = 'previous_preview_panel',
|
previousPreviewPanel = 'previous_preview_panel',
|
||||||
closeFlyout = 'close_flyout',
|
closeFlyout = 'close_flyout',
|
||||||
|
urlChanged = 'urlChanged',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openPanelsAction = createAction<{
|
export const openPanelsAction = createAction<{
|
||||||
|
@ -37,3 +38,9 @@ export const closeLeftPanelAction = createAction(ActionType.closeLeftPanel);
|
||||||
export const closePreviewPanelAction = createAction(ActionType.closePreviewPanel);
|
export const closePreviewPanelAction = createAction(ActionType.closePreviewPanel);
|
||||||
|
|
||||||
export const previousPreviewPanelAction = createAction(ActionType.previousPreviewPanel);
|
export const previousPreviewPanelAction = createAction(ActionType.previousPreviewPanel);
|
||||||
|
|
||||||
|
export const urlChangedAction = createAction<{
|
||||||
|
right?: FlyoutPanelProps;
|
||||||
|
left?: FlyoutPanelProps;
|
||||||
|
preview?: FlyoutPanelProps;
|
||||||
|
}>(ActionType.urlChanged);
|
||||||
|
|
|
@ -14,21 +14,19 @@ import {
|
||||||
PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID,
|
PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID,
|
||||||
PREVIEW_SECTION_TEST_ID,
|
PREVIEW_SECTION_TEST_ID,
|
||||||
} from './test_ids';
|
} from './test_ids';
|
||||||
import { ExpandableFlyoutContextValue } from '../context';
|
|
||||||
import { TestProvider } from '../test/provider';
|
import { TestProvider } from '../test/provider';
|
||||||
|
import { State } from '../state';
|
||||||
|
|
||||||
describe('PreviewSection', () => {
|
describe('PreviewSection', () => {
|
||||||
const context = {
|
const context = {
|
||||||
panels: {
|
right: {},
|
||||||
right: {},
|
left: {},
|
||||||
left: {},
|
preview: [
|
||||||
preview: [
|
{
|
||||||
{
|
id: 'key',
|
||||||
id: 'key',
|
},
|
||||||
},
|
],
|
||||||
],
|
} as unknown as State;
|
||||||
},
|
|
||||||
} as unknown as ExpandableFlyoutContextValue;
|
|
||||||
|
|
||||||
const component = <div>{'component'}</div>;
|
const component = <div>{'component'}</div>;
|
||||||
const left = 500;
|
const left = 500;
|
||||||
|
@ -37,7 +35,7 @@ describe('PreviewSection', () => {
|
||||||
const showBackButton = false;
|
const showBackButton = false;
|
||||||
|
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={context}>
|
||||||
<PreviewSection component={component} leftPosition={left} showBackButton={showBackButton} />
|
<PreviewSection component={component} leftPosition={left} showBackButton={showBackButton} />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +47,7 @@ describe('PreviewSection', () => {
|
||||||
const showBackButton = true;
|
const showBackButton = true;
|
||||||
|
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={context}>
|
||||||
<PreviewSection component={component} leftPosition={left} showBackButton={showBackButton} />
|
<PreviewSection component={component} leftPosition={left} showBackButton={showBackButton} />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
|
@ -67,7 +65,7 @@ describe('PreviewSection', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { getByTestId, getByText } = render(
|
const { getByTestId, getByText } = render(
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={context}>
|
||||||
<PreviewSection
|
<PreviewSection
|
||||||
component={component}
|
component={component}
|
||||||
leftPosition={left}
|
leftPosition={left}
|
||||||
|
|
|
@ -1,57 +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 { createContext, useContext } from 'react';
|
|
||||||
import { useFlyoutMemoryState } from './context/memory_state_provider';
|
|
||||||
import { useFlyoutUrlState } from './context/url_state_provider';
|
|
||||||
import { type ExpandableFlyoutApi } from './types';
|
|
||||||
|
|
||||||
export type { ExpandableFlyoutApi as ExpandableFlyoutContextValue };
|
|
||||||
|
|
||||||
type ExpandableFlyoutContextValue = 'memory' | 'url';
|
|
||||||
|
|
||||||
export const ExpandableFlyoutContext = createContext<ExpandableFlyoutContextValue | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve Flyout's api and state
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export const useExpandableFlyoutContext = (): ExpandableFlyoutApi => {
|
|
||||||
const contextValue = useContext(ExpandableFlyoutContext);
|
|
||||||
|
|
||||||
if (!contextValue) {
|
|
||||||
throw new Error(
|
|
||||||
'ExpandableFlyoutContext can only be used within ExpandableFlyoutContext provider'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const memoryState = useFlyoutMemoryState();
|
|
||||||
const urlState = useFlyoutUrlState();
|
|
||||||
|
|
||||||
return contextValue === 'memory' ? memoryState : urlState;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This hook allows you to interact with the flyout, open panels and previews etc.
|
|
||||||
*/
|
|
||||||
export const useExpandableFlyoutApi = () => {
|
|
||||||
const { panels, ...api } = useExpandableFlyoutContext();
|
|
||||||
|
|
||||||
return api;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This hook allows you to access the flyout state, read open panels and previews.
|
|
||||||
*/
|
|
||||||
export const useExpandableFlyoutState = () => {
|
|
||||||
const expandableFlyoutApiAndState = useExpandableFlyoutContext();
|
|
||||||
|
|
||||||
return expandableFlyoutApiAndState.panels;
|
|
||||||
};
|
|
|
@ -1,109 +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, useMemo } from 'react';
|
|
||||||
import { FlyoutPanelProps, ExpandableFlyoutApi } from '../types';
|
|
||||||
import { useRightPanel } from '../hooks/use_right_panel';
|
|
||||||
import { useLeftPanel } from '../hooks/use_left_panel';
|
|
||||||
import { usePreviewPanel } from '../hooks/use_preview_panel';
|
|
||||||
import { State } from '../state';
|
|
||||||
|
|
||||||
export const useFlyoutUrlState = (): ExpandableFlyoutApi => {
|
|
||||||
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: ExpandableFlyoutApi = useMemo(
|
|
||||||
() => ({
|
|
||||||
panels,
|
|
||||||
openFlyout: openPanels,
|
|
||||||
openRightPanel,
|
|
||||||
openLeftPanel,
|
|
||||||
openPreviewPanel,
|
|
||||||
closeRightPanel,
|
|
||||||
closeLeftPanel,
|
|
||||||
closePreviewPanel,
|
|
||||||
closeFlyout: closePanels,
|
|
||||||
previousPreviewPanel,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
panels,
|
|
||||||
openPanels,
|
|
||||||
openRightPanel,
|
|
||||||
openLeftPanel,
|
|
||||||
openPreviewPanel,
|
|
||||||
closeRightPanel,
|
|
||||||
closeLeftPanel,
|
|
||||||
closePreviewPanel,
|
|
||||||
closePanels,
|
|
||||||
previousPreviewPanel,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return contextValue;
|
|
||||||
};
|
|
|
@ -6,18 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, FC, useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import {
|
|
||||||
createDispatchHook,
|
|
||||||
createSelectorHook,
|
|
||||||
Provider as ReduxProvider,
|
|
||||||
ReactReduxContextValue,
|
|
||||||
} from 'react-redux';
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import { reducer } from '../reducer';
|
|
||||||
import { initialState, State } from '../state';
|
|
||||||
import type { ExpandableFlyoutApi, FlyoutPanelProps } from '../types';
|
|
||||||
import {
|
import {
|
||||||
closeLeftPanelAction,
|
closeLeftPanelAction,
|
||||||
closePanelsAction,
|
closePanelsAction,
|
||||||
|
@ -29,24 +18,15 @@ import {
|
||||||
openRightPanelAction,
|
openRightPanelAction,
|
||||||
previousPreviewPanelAction,
|
previousPreviewPanelAction,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
|
import { useDispatch } from '../redux';
|
||||||
|
import { FlyoutPanelProps, type ExpandableFlyoutApi } from '../types';
|
||||||
|
|
||||||
export const store = configureStore({
|
export type { ExpandableFlyoutApi };
|
||||||
reducer,
|
|
||||||
devTools: process.env.NODE_ENV !== 'production',
|
|
||||||
preloadedState: {},
|
|
||||||
enhancers: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Context = createContext<ReactReduxContextValue<State>>({
|
/**
|
||||||
store,
|
* This hook allows you to interact with the flyout, open panels and previews etc.
|
||||||
storeState: initialState,
|
*/
|
||||||
});
|
export const useExpandableFlyoutApi = () => {
|
||||||
|
|
||||||
const useDispatch = createDispatchHook(Context);
|
|
||||||
const useSelector = createSelectorHook(Context);
|
|
||||||
|
|
||||||
export const useFlyoutMemoryState = (): ExpandableFlyoutApi => {
|
|
||||||
const state = useSelector((s) => s);
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const openPanels = useCallback(
|
const openPanels = useCallback(
|
||||||
|
@ -94,7 +74,6 @@ export const useFlyoutMemoryState = (): ExpandableFlyoutApi => {
|
||||||
|
|
||||||
const api: ExpandableFlyoutApi = useMemo(
|
const api: ExpandableFlyoutApi = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
panels: state,
|
|
||||||
openFlyout: openPanels,
|
openFlyout: openPanels,
|
||||||
openRightPanel,
|
openRightPanel,
|
||||||
openLeftPanel,
|
openLeftPanel,
|
||||||
|
@ -106,7 +85,6 @@ export const useFlyoutMemoryState = (): ExpandableFlyoutApi => {
|
||||||
previousPreviewPanel,
|
previousPreviewPanel,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
state,
|
|
||||||
openPanels,
|
openPanels,
|
||||||
openRightPanel,
|
openRightPanel,
|
||||||
openLeftPanel,
|
openLeftPanel,
|
||||||
|
@ -121,15 +99,3 @@ export const useFlyoutMemoryState = (): ExpandableFlyoutApi => {
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 }) => {
|
|
||||||
return (
|
|
||||||
<ReduxProvider context={Context} store={store}>
|
|
||||||
{children}
|
|
||||||
</ReduxProvider>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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 { stateSelector, useSelector } from '../redux';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hook allows you to access the flyout state, read open panels and previews.
|
||||||
|
*/
|
||||||
|
export const useExpandableFlyoutState = () => {
|
||||||
|
return useSelector(stateSelector);
|
||||||
|
};
|
|
@ -1,23 +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 { 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<FlyoutPanelProps>(
|
|
||||||
EXPANDABLE_FLYOUT_URL_KEY,
|
|
||||||
'leftPanel'
|
|
||||||
);
|
|
||||||
|
|
||||||
return { leftPanelState, setLeftPanelState } as const;
|
|
||||||
};
|
|
|
@ -1,23 +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 { 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<FlyoutPanelProps[]>(
|
|
||||||
EXPANDABLE_FLYOUT_URL_KEY,
|
|
||||||
'preview'
|
|
||||||
);
|
|
||||||
|
|
||||||
return { previewState, setPreviewState } as const;
|
|
||||||
};
|
|
|
@ -1,23 +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 { 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<FlyoutPanelProps>(
|
|
||||||
EXPANDABLE_FLYOUT_URL_KEY,
|
|
||||||
'rightPanel'
|
|
||||||
);
|
|
||||||
|
|
||||||
return { rightPanelState, setRightPanelState } as const;
|
|
||||||
};
|
|
|
@ -19,8 +19,8 @@ import {
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { ExpandableFlyout } from '.';
|
import { ExpandableFlyout } from '.';
|
||||||
import { ExpandableFlyoutContextValue } from './context';
|
|
||||||
import { TestProvider } from './test/provider';
|
import { TestProvider } from './test/provider';
|
||||||
|
import { State } from './state';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
component: ExpandableFlyout,
|
component: ExpandableFlyout,
|
||||||
|
@ -101,89 +101,81 @@ const registeredPanels = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Right: Story<void> = () => {
|
export const Right: Story<void> = () => {
|
||||||
const context = {
|
const state = {
|
||||||
panels: {
|
right: {
|
||||||
right: {
|
id: 'right',
|
||||||
id: 'right',
|
|
||||||
},
|
|
||||||
left: {},
|
|
||||||
preview: [],
|
|
||||||
},
|
},
|
||||||
} as unknown as ExpandableFlyoutContextValue;
|
left: {},
|
||||||
|
preview: [],
|
||||||
|
} as unknown as State;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={state}>
|
||||||
<ExpandableFlyout registeredPanels={registeredPanels} />
|
<ExpandableFlyout registeredPanels={registeredPanels} />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Left: Story<void> = () => {
|
export const Left: Story<void> = () => {
|
||||||
const context = {
|
const state = {
|
||||||
panels: {
|
right: {
|
||||||
right: {
|
id: 'right',
|
||||||
id: 'right',
|
|
||||||
},
|
|
||||||
left: {
|
|
||||||
id: 'left',
|
|
||||||
},
|
|
||||||
preview: [],
|
|
||||||
},
|
},
|
||||||
} as unknown as ExpandableFlyoutContextValue;
|
left: {
|
||||||
|
id: 'left',
|
||||||
|
},
|
||||||
|
preview: [],
|
||||||
|
} as unknown as State;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={state}>
|
||||||
<ExpandableFlyout registeredPanels={registeredPanels} />
|
<ExpandableFlyout registeredPanels={registeredPanels} />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Preview: Story<void> = () => {
|
export const Preview: Story<void> = () => {
|
||||||
const context = {
|
const state = {
|
||||||
panels: {
|
right: {
|
||||||
right: {
|
id: 'right',
|
||||||
id: 'right',
|
|
||||||
},
|
|
||||||
left: {
|
|
||||||
id: 'left',
|
|
||||||
},
|
|
||||||
preview: [
|
|
||||||
{
|
|
||||||
id: 'preview1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
} as unknown as ExpandableFlyoutContextValue;
|
left: {
|
||||||
|
id: 'left',
|
||||||
|
},
|
||||||
|
preview: [
|
||||||
|
{
|
||||||
|
id: 'preview1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as State;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={state}>
|
||||||
<ExpandableFlyout registeredPanels={registeredPanels} />
|
<ExpandableFlyout registeredPanels={registeredPanels} />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultiplePreviews: Story<void> = () => {
|
export const MultiplePreviews: Story<void> = () => {
|
||||||
const context = {
|
const state = {
|
||||||
panels: {
|
right: {
|
||||||
right: {
|
id: 'right',
|
||||||
id: 'right',
|
|
||||||
},
|
|
||||||
left: {
|
|
||||||
id: 'left',
|
|
||||||
},
|
|
||||||
preview: [
|
|
||||||
{
|
|
||||||
id: 'preview1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'preview2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
} as unknown as ExpandableFlyoutContextValue;
|
left: {
|
||||||
|
id: 'left',
|
||||||
|
},
|
||||||
|
preview: [
|
||||||
|
{
|
||||||
|
id: 'preview1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'preview2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as State;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestProvider state={context.panels}>
|
<TestProvider state={state}>
|
||||||
<ExpandableFlyout registeredPanels={registeredPanels} />
|
<ExpandableFlyout registeredPanels={registeredPanels} />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
} from './components/test_ids';
|
} from './components/test_ids';
|
||||||
import { type State } from './state';
|
import { type State } from './state';
|
||||||
import { TestProvider } from './test/provider';
|
import { TestProvider } from './test/provider';
|
||||||
jest.mock('./context/url_state_provider');
|
|
||||||
|
|
||||||
const registeredPanels: Panel[] = [
|
const registeredPanels: Panel[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,7 +11,8 @@ import { EuiFlyoutProps } from '@elastic/eui';
|
||||||
import { EuiFlexGroup, EuiFlyout } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlyout } from '@elastic/eui';
|
||||||
import { useSectionSizes } from './hooks/use_sections_sizes';
|
import { useSectionSizes } from './hooks/use_sections_sizes';
|
||||||
import { useWindowSize } from './hooks/use_window_size';
|
import { useWindowSize } from './hooks/use_window_size';
|
||||||
import { useExpandableFlyoutContext } from './context';
|
import { useExpandableFlyoutState } from './hooks/use_expandable_flyout_state';
|
||||||
|
import { useExpandableFlyoutApi } from './hooks/use_expandable_flyout_api';
|
||||||
import { PreviewSection } from './components/preview_section';
|
import { PreviewSection } from './components/preview_section';
|
||||||
import { RightSection } from './components/right_section';
|
import { RightSection } from './components/right_section';
|
||||||
import type { FlyoutPanelProps, Panel } from './types';
|
import type { FlyoutPanelProps, Panel } from './types';
|
||||||
|
@ -40,9 +41,8 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const windowWidth = useWindowSize();
|
const windowWidth = useWindowSize();
|
||||||
|
|
||||||
const { closeFlyout, panels } = useExpandableFlyoutContext();
|
const { left, right, preview } = useExpandableFlyoutState();
|
||||||
|
const { closeFlyout } = useExpandableFlyoutApi();
|
||||||
const { left, right, preview } = panels;
|
|
||||||
|
|
||||||
const leftSection = useMemo(
|
const leftSection = useMemo(
|
||||||
() => registeredPanels.find((panel) => panel.key === left?.id),
|
() => registeredPanels.find((panel) => panel.key === left?.id),
|
||||||
|
|
|
@ -6,16 +6,70 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, PropsWithChildren } from 'react';
|
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||||
import { ExpandableFlyoutContext } from './context';
|
import React, { FC, PropsWithChildren, useEffect, useMemo } from 'react';
|
||||||
import { MemoryStateProvider } from './context/memory_state_provider';
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { State } from './state';
|
||||||
|
import { useExpandableFlyoutState } from './hooks/use_expandable_flyout_state';
|
||||||
|
import { EXPANDABLE_FLYOUT_URL_KEY } from './constants';
|
||||||
|
import { Context, store, useDispatch } from './redux';
|
||||||
|
import { urlChangedAction } from './actions';
|
||||||
|
|
||||||
|
export type ExpandableFlyoutStorageMode = 'memory' | 'url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches actions when url state changes and initializes the state when the app is loaded with flyout url parameters
|
||||||
|
*/
|
||||||
|
const UrlSynchronizer = () => {
|
||||||
|
const state = useExpandableFlyoutState();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const urlStorage = useMemo(
|
||||||
|
() =>
|
||||||
|
createKbnUrlStateStorage({
|
||||||
|
history,
|
||||||
|
useHash: false,
|
||||||
|
useHashQuery: false,
|
||||||
|
}),
|
||||||
|
[history]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentValue = urlStorage.get<State>(EXPANDABLE_FLYOUT_URL_KEY);
|
||||||
|
|
||||||
|
// Dispatch current value to redux store as it does not happen automatically
|
||||||
|
if (currentValue) {
|
||||||
|
dispatch(urlChangedAction({ ...currentValue, preview: currentValue?.preview[0] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = urlStorage.change$<State>(EXPANDABLE_FLYOUT_URL_KEY).subscribe((value) => {
|
||||||
|
dispatch(urlChangedAction({ ...value, preview: value?.preview?.[0] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [dispatch, urlStorage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { needsSync, ...stateToSync } = state;
|
||||||
|
|
||||||
|
if (needsSync) {
|
||||||
|
urlStorage.set(EXPANDABLE_FLYOUT_URL_KEY, stateToSync);
|
||||||
|
}
|
||||||
|
}, [urlStorage, state]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
interface ExpandableFlyoutProviderProps {
|
interface ExpandableFlyoutProviderProps {
|
||||||
/**
|
/**
|
||||||
* This allows the user to choose how the flyout storage is handled.
|
* This allows the user to choose how the flyout storage is handled.
|
||||||
* Url storage syncs current values straight to the browser query string.
|
* Url storage syncs current values straight to the browser query string.
|
||||||
*/
|
*/
|
||||||
storage?: 'url' | 'memory';
|
storage?: ExpandableFlyoutStorageMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,8 +84,11 @@ export const ExpandableFlyoutProvider: FC<PropsWithChildren<ExpandableFlyoutProv
|
||||||
storage = 'url',
|
storage = 'url',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ExpandableFlyoutContext.Provider value={storage}>
|
<ReduxProvider context={Context} store={store}>
|
||||||
<MemoryStateProvider>{children}</MemoryStateProvider>
|
<>
|
||||||
</ExpandableFlyoutContext.Provider>
|
{storage === 'url' ? <UrlSynchronizer /> : null}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
</ReduxProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,6 +61,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,6 +82,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel2,
|
left: leftPanel2,
|
||||||
right: rightPanel2,
|
right: rightPanel2,
|
||||||
preview: [previewPanel2],
|
preview: [previewPanel2],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,6 +101,7 @@ describe('reducer', () => {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: rightPanel2,
|
right: rightPanel2,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -113,6 +116,7 @@ describe('reducer', () => {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -129,6 +133,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: rightPanel2,
|
right: rightPanel2,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -143,6 +148,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,6 +165,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel2,
|
left: leftPanel2,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -173,6 +180,7 @@ describe('reducer', () => {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -189,6 +197,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [previewPanel1, previewPanel2],
|
preview: [previewPanel1, previewPanel2],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -199,7 +208,7 @@ describe('reducer', () => {
|
||||||
const action = closeRightPanelAction();
|
const action = closeRightPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual({ ...state, needsSync: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return unmodified state when removing right panel when no right panel exist`, () => {
|
it(`should return unmodified state when removing right panel when no right panel exist`, () => {
|
||||||
|
@ -207,6 +216,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
};
|
};
|
||||||
const action = closeRightPanelAction();
|
const action = closeRightPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
@ -228,6 +238,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -238,7 +249,7 @@ describe('reducer', () => {
|
||||||
const action = closeLeftPanelAction();
|
const action = closeLeftPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual({ ...state, needsSync: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return unmodified state when removing left panel when no left panel exist`, () => {
|
it(`should return unmodified state when removing left panel when no left panel exist`, () => {
|
||||||
|
@ -246,6 +257,7 @@ describe('reducer', () => {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
};
|
};
|
||||||
const action = closeLeftPanelAction();
|
const action = closeLeftPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
@ -266,13 +278,14 @@ describe('reducer', () => {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should handle closePreviewPanel action', () => {
|
describe('should handle closePreviewPanel action', () => {
|
||||||
it('should return empty state when removing preview panel on empty state', () => {
|
it('should return empty state when removing preview panel on empty state', () => {
|
||||||
const state: State = initialState;
|
const state: State = { ...initialState, needsSync: true };
|
||||||
const action = closePreviewPanelAction();
|
const action = closePreviewPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
|
||||||
|
@ -288,7 +301,7 @@ describe('reducer', () => {
|
||||||
const action = closePreviewPanelAction();
|
const action = closePreviewPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual({ ...state, needsSync: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove all preview panels', () => {
|
it('should remove all preview panels', () => {
|
||||||
|
@ -304,6 +317,7 @@ describe('reducer', () => {
|
||||||
left: rightPanel1,
|
left: rightPanel1,
|
||||||
right: leftPanel1,
|
right: leftPanel1,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -314,7 +328,7 @@ describe('reducer', () => {
|
||||||
const action = previousPreviewPanelAction();
|
const action = previousPreviewPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual({ ...initialState, needsSync: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return unmodified state when previous preview panel when no preview panel exist`, () => {
|
it(`should return unmodified state when previous preview panel when no preview panel exist`, () => {
|
||||||
|
@ -322,6 +336,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
};
|
};
|
||||||
const action = previousPreviewPanelAction();
|
const action = previousPreviewPanelAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
@ -342,6 +357,7 @@ describe('reducer', () => {
|
||||||
left: leftPanel1,
|
left: leftPanel1,
|
||||||
right: rightPanel1,
|
right: rightPanel1,
|
||||||
preview: [previewPanel1],
|
preview: [previewPanel1],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -352,7 +368,7 @@ describe('reducer', () => {
|
||||||
const action = closePanelsAction();
|
const action = closePanelsAction();
|
||||||
const newState: State = reducer(state, action);
|
const newState: State = reducer(state, action);
|
||||||
|
|
||||||
expect(newState).toEqual(initialState);
|
expect(newState).toEqual({ ...initialState, needsSync: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove all panels', () => {
|
it('should remove all panels', () => {
|
||||||
|
@ -368,6 +384,7 @@ describe('reducer', () => {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
closeRightPanelAction,
|
closeRightPanelAction,
|
||||||
previousPreviewPanelAction,
|
previousPreviewPanelAction,
|
||||||
openPreviewPanelAction,
|
openPreviewPanelAction,
|
||||||
|
urlChangedAction,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { initialState } from './state';
|
import { initialState } from './state';
|
||||||
|
|
||||||
|
@ -25,39 +26,57 @@ export const reducer = createReducer(initialState, (builder) => {
|
||||||
state.preview = preview ? [preview] : [];
|
state.preview = preview ? [preview] : [];
|
||||||
state.right = right;
|
state.right = right;
|
||||||
state.left = left;
|
state.left = left;
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(openLeftPanelAction, (state, { payload }) => {
|
builder.addCase(openLeftPanelAction, (state, { payload }) => {
|
||||||
state.left = payload;
|
state.left = payload;
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(openRightPanelAction, (state, { payload }) => {
|
builder.addCase(openRightPanelAction, (state, { payload }) => {
|
||||||
state.right = payload;
|
state.right = payload;
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(openPreviewPanelAction, (state, { payload }) => {
|
builder.addCase(openPreviewPanelAction, (state, { payload }) => {
|
||||||
state.preview.push(payload);
|
state.preview.push(payload);
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(previousPreviewPanelAction, (state) => {
|
builder.addCase(previousPreviewPanelAction, (state) => {
|
||||||
state.preview.pop();
|
state.preview.pop();
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(closePanelsAction, (state) => {
|
builder.addCase(closePanelsAction, (state) => {
|
||||||
state.preview = [];
|
state.preview = [];
|
||||||
state.right = undefined;
|
state.right = undefined;
|
||||||
state.left = undefined;
|
state.left = undefined;
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(closeLeftPanelAction, (state) => {
|
builder.addCase(closeLeftPanelAction, (state) => {
|
||||||
state.left = undefined;
|
state.left = undefined;
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(closeRightPanelAction, (state) => {
|
builder.addCase(closeRightPanelAction, (state) => {
|
||||||
state.right = undefined;
|
state.right = undefined;
|
||||||
|
state.needsSync = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(closePreviewPanelAction, (state) => {
|
builder.addCase(closePreviewPanelAction, (state) => {
|
||||||
state.preview = [];
|
state.preview = [];
|
||||||
|
state.needsSync = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(urlChangedAction, (state, { payload: { preview, left, right } }) => {
|
||||||
|
state.needsSync = false;
|
||||||
|
state.preview = preview ? [preview] : [];
|
||||||
|
state.left = left;
|
||||||
|
state.right = right;
|
||||||
|
|
||||||
|
return state;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
29
packages/kbn-expandable-flyout/src/redux.ts
Normal file
29
packages/kbn-expandable-flyout/src/redux.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||||
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
|
* Side Public License, v 1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext } from 'react';
|
||||||
|
import { createDispatchHook, createSelectorHook, ReactReduxContextValue } from 'react-redux';
|
||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { reducer } from './reducer';
|
||||||
|
import { initialState, State } from './state';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer,
|
||||||
|
devTools: process.env.NODE_ENV !== 'production',
|
||||||
|
enhancers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Context = createContext<ReactReduxContextValue<State>>({
|
||||||
|
store,
|
||||||
|
storeState: initialState,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useDispatch = createDispatchHook(Context);
|
||||||
|
export const useSelector = createSelectorHook(Context);
|
||||||
|
|
||||||
|
export const stateSelector = (state: State) => state;
|
|
@ -21,10 +21,18 @@ export interface State {
|
||||||
* Panels to render in the preview section
|
* Panels to render in the preview section
|
||||||
*/
|
*/
|
||||||
preview: FlyoutPanelProps[];
|
preview: FlyoutPanelProps[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the flyout in sync with external storage (eg. url)?
|
||||||
|
* This value can be used in useEffect for example, to control whether we should
|
||||||
|
* call an external state sync method.
|
||||||
|
*/
|
||||||
|
needsSync?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState: State = {
|
export const initialState: State = {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
preview: [],
|
preview: [],
|
||||||
|
needsSync: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,8 +10,7 @@ import { Provider as ReduxProvider } from 'react-redux';
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import React, { FC, PropsWithChildren } from 'react';
|
import React, { FC, PropsWithChildren } from 'react';
|
||||||
import { reducer } from '../reducer';
|
import { reducer } from '../reducer';
|
||||||
import { Context } from '../context/memory_state_provider';
|
import { Context } from '../redux';
|
||||||
import { ExpandableFlyoutContext } from '../context';
|
|
||||||
import { initialState, State } from '../state';
|
import { initialState, State } from '../state';
|
||||||
|
|
||||||
interface TestProviderProps {
|
interface TestProviderProps {
|
||||||
|
@ -30,10 +29,8 @@ export const TestProvider: FC<PropsWithChildren<TestProviderProps>> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableFlyoutContext.Provider value="memory">
|
<ReduxProvider store={store} context={Context}>
|
||||||
<ReduxProvider store={store} context={Context}>
|
{children}
|
||||||
{children}
|
</ReduxProvider>
|
||||||
</ReduxProvider>
|
|
||||||
</ExpandableFlyoutContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,14 +7,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { State } from './state';
|
|
||||||
|
|
||||||
export interface ExpandableFlyoutApi {
|
export interface ExpandableFlyoutApi {
|
||||||
/**
|
|
||||||
* Right, left and preview panels
|
|
||||||
*/
|
|
||||||
panels: State;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the flyout with left, right and/or preview panels
|
* Open the flyout with left, right and/or preview panels
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -20,6 +20,6 @@
|
||||||
],
|
],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/i18n",
|
"@kbn/i18n",
|
||||||
"@kbn/url-state"
|
"@kbn/kibana-utils-plugin"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_f
|
||||||
import { TestProvidersComponent } from '../../../../common/mock';
|
import { TestProvidersComponent } from '../../../../common/mock';
|
||||||
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
|
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
|
||||||
import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
|
import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
|
||||||
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
|
|
||||||
|
|
||||||
jest.mock('../../../../common/lib/kibana');
|
jest.mock('../../../../common/lib/kibana');
|
||||||
jest.mock('../hooks/use_assistant');
|
jest.mock('../hooks/use_assistant');
|
||||||
|
@ -40,11 +39,9 @@ const mockContextValue = {
|
||||||
const renderHeaderActions = (contextValue: RightPanelContext) =>
|
const renderHeaderActions = (contextValue: RightPanelContext) =>
|
||||||
render(
|
render(
|
||||||
<TestProvidersComponent>
|
<TestProvidersComponent>
|
||||||
<ExpandableFlyoutProvider>
|
<RightPanelContext.Provider value={contextValue}>
|
||||||
<RightPanelContext.Provider value={contextValue}>
|
<HeaderActions />
|
||||||
<HeaderActions />
|
</RightPanelContext.Provider>
|
||||||
</RightPanelContext.Provider>
|
|
||||||
</ExpandableFlyoutProvider>
|
|
||||||
</TestProvidersComponent>
|
</TestProvidersComponent>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
INVESTIGATION_GUIDE_TEST_ID,
|
INVESTIGATION_GUIDE_TEST_ID,
|
||||||
} from './test_ids';
|
} from './test_ids';
|
||||||
import { mockContextValue } from '../mocks/mock_context';
|
import { mockContextValue } from '../mocks/mock_context';
|
||||||
import { mockFlyoutContextValue } from '../../shared/mocks/mock_flyout_context';
|
|
||||||
import type { ExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
import type { ExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||||
import { useInvestigationGuide } from '../../shared/hooks/use_investigation_guide';
|
import { useInvestigationGuide } from '../../shared/hooks/use_investigation_guide';
|
||||||
|
@ -25,6 +24,8 @@ import { LeftPanelInvestigationTab, DocumentDetailsLeftPanelKey } from '../../le
|
||||||
jest.mock('../../shared/hooks/use_investigation_guide');
|
jest.mock('../../shared/hooks/use_investigation_guide');
|
||||||
jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn() }));
|
jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn() }));
|
||||||
|
|
||||||
|
const mockFlyoutContextValue = { openLeftPanel: jest.fn() };
|
||||||
|
|
||||||
const NO_DATA_MESSAGE = 'Investigation guideThere’s no investigation guide for this rule.';
|
const NO_DATA_MESSAGE = 'Investigation guideThere’s no investigation guide for this rule.';
|
||||||
const PREVIEW_MESSAGE = 'Investigation guide is not available in alert preview.';
|
const PREVIEW_MESSAGE = 'Investigation guide is not available in alert preview.';
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context';
|
import type { ExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock flyout context
|
* Mock flyout api
|
||||||
*/
|
*/
|
||||||
export const mockFlyoutContextValue: ExpandableFlyoutContextValue = {
|
export const mockFlyoutApi: ExpandableFlyoutApi = {
|
||||||
openFlyout: jest.fn(),
|
openFlyout: jest.fn(),
|
||||||
openRightPanel: jest.fn(),
|
openRightPanel: jest.fn(),
|
||||||
openLeftPanel: jest.fn(),
|
openLeftPanel: jest.fn(),
|
||||||
|
@ -20,9 +20,4 @@ export const mockFlyoutContextValue: ExpandableFlyoutContextValue = {
|
||||||
closePreviewPanel: jest.fn(),
|
closePreviewPanel: jest.fn(),
|
||||||
previousPreviewPanel: jest.fn(),
|
previousPreviewPanel: jest.fn(),
|
||||||
closeFlyout: jest.fn(),
|
closeFlyout: jest.fn(),
|
||||||
panels: {
|
|
||||||
left: undefined,
|
|
||||||
right: undefined,
|
|
||||||
preview: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,18 +19,13 @@ import type { ExpandableFlyoutState } from '@kbn/expandable-flyout';
|
||||||
import {
|
import {
|
||||||
useExpandableFlyoutApi,
|
useExpandableFlyoutApi,
|
||||||
type ExpandableFlyoutApi,
|
type ExpandableFlyoutApi,
|
||||||
ExpandableFlyoutProvider,
|
|
||||||
useExpandableFlyoutState,
|
useExpandableFlyoutState,
|
||||||
} from '@kbn/expandable-flyout';
|
} from '@kbn/expandable-flyout';
|
||||||
|
|
||||||
const expandDetails = jest.fn();
|
const expandDetails = jest.fn();
|
||||||
|
|
||||||
const ExpandableFlyoutTestProviders: FC<PropsWithChildren<{}>> = ({ children }) => {
|
const ExpandableFlyoutTestProviders: FC<PropsWithChildren<{}>> = ({ children }) => {
|
||||||
return (
|
return <TestProviders>{children}</TestProviders>;
|
||||||
<TestProviders>
|
|
||||||
<ExpandableFlyoutProvider>{children}</ExpandableFlyoutProvider>
|
|
||||||
</TestProviders>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('@kbn/expandable-flyout', () => ({
|
jest.mock('@kbn/expandable-flyout', () => ({
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { type PropsWithChildren } from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
@ -23,14 +23,12 @@ jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
|
||||||
useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled,
|
useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@kbn/expandable-flyout/src/context', () => {
|
jest.mock('@kbn/expandable-flyout', () => {
|
||||||
const original = jest.requireActual('@kbn/expandable-flyout/src/context');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...original,
|
|
||||||
useExpandableFlyoutApi: () => ({
|
useExpandableFlyoutApi: () => ({
|
||||||
openRightPanel: mockOpenRightPanel,
|
openRightPanel: mockOpenRightPanel,
|
||||||
}),
|
}),
|
||||||
|
TestProvider: ({ children }: PropsWithChildren<{}>) => <>{children}</>,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -26,13 +26,13 @@ describe('Expandable flyout state sync', { tags: ['@ess', '@serverless'] }, () =
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should test flyout url sync', () => {
|
it('should test flyout url sync', () => {
|
||||||
cy.url().should('not.include', 'rightPanel');
|
cy.url().should('not.include', 'right');
|
||||||
|
|
||||||
expandFirstAlertExpandableFlyout();
|
expandFirstAlertExpandableFlyout();
|
||||||
|
|
||||||
cy.log('should serialize its state to url');
|
cy.log('should serialize its state to url');
|
||||||
|
|
||||||
cy.url().should('include', 'rightPanel');
|
cy.url().should('include', 'right');
|
||||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('have.text', rule.name);
|
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('have.text', rule.name);
|
||||||
|
|
||||||
cy.log('should reopen the flyout after browser refresh');
|
cy.log('should reopen the flyout after browser refresh');
|
||||||
|
@ -40,13 +40,13 @@ describe('Expandable flyout state sync', { tags: ['@ess', '@serverless'] }, () =
|
||||||
cy.reload();
|
cy.reload();
|
||||||
waitForAlertsToPopulate();
|
waitForAlertsToPopulate();
|
||||||
|
|
||||||
cy.url().should('include', 'rightPanel');
|
cy.url().should('include', 'right');
|
||||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('have.text', rule.name);
|
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('have.text', rule.name);
|
||||||
|
|
||||||
cy.log('should clear the url state when flyout is closed');
|
cy.log('should clear the url state when flyout is closed');
|
||||||
|
|
||||||
closeFlyout();
|
closeFlyout();
|
||||||
|
|
||||||
cy.url().should('not.include', 'rightPanel');
|
cy.url().should('not.include', 'right');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue