Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] fix issue related to local storage on expandable flyout non-alert document #180391

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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 { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import type { UseTabsParams, UseTabsResult } from './use_tabs';
import { allThreeTabs, twoTabs, useTabs } from './use_tabs';

const mockStorageGet = jest.fn();
jest.mock('../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../common/lib/kibana');
return {
...originalModule,
useKibana: jest.fn().mockReturnValue({
services: {
storage: {
get: () => mockStorageGet(),
},
},
}),
};
});

describe('useTabs', () => {
let hookResult: RenderHookResult<UseTabsParams, UseTabsResult>;

it('should return 3 tabs to render and the one from path as selected', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: true,
path: { tab: 'table' },
};

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.tabsDisplayed).toEqual(allThreeTabs);
expect(hookResult.result.current.selectedTabId).toEqual('table');
});

it('should return 2 tabs to render and the one from path as selected', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: false,
path: { tab: 'json' },
};

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.tabsDisplayed).toEqual(twoTabs);
expect(hookResult.result.current.selectedTabId).toEqual('json');
});

it('should ignore the value from path if it is not in the list of tabs to display', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: true,
path: { tab: 'wrong' },
};

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.selectedTabId).not.toEqual('wrong');
});

it('should return selected tab from local storage if it is in the list of tabs to display', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: true,
path: undefined,
};
mockStorageGet.mockReturnValue('overview');

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.tabsDisplayed).toEqual(allThreeTabs);
expect(hookResult.result.current.selectedTabId).toEqual('overview');
});

it('should ignore the local storage value if it is not in the list of tabs to display', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: true,
path: undefined,
};
mockStorageGet.mockReturnValue('wrong');

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.selectedTabId).not.toEqual('wrong');
});

it('should return 3 tabs to render and and the first from the list as selected tab', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: true,
path: undefined,
};
mockStorageGet.mockReturnValue(undefined);

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.tabsDisplayed).toEqual(allThreeTabs);
expect(hookResult.result.current.selectedTabId).toEqual('overview');
});

it('should return 2 tabs to render and and the first from the list as selected tab', () => {
const initialProps: UseTabsParams = {
flyoutIsExpandable: false,
path: undefined,
};
mockStorageGet.mockReturnValue(undefined);

hookResult = renderHook((props: UseTabsParams) => useTabs(props), { initialProps });

expect(hookResult.result.current.tabsDisplayed).toEqual(twoTabs);
expect(hookResult.result.current.selectedTabId).toEqual('table');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 { useMemo } from 'react';
import type { PanelPath } from '@kbn/expandable-flyout';
import type { RightPanelPaths } from '..';
import { useKibana } from '../../../../common/lib/kibana';
import { FLYOUT_STORAGE_KEYS } from '../../shared/constants/local_storage';
import * as tabs from '../tabs';
import type { RightPanelTabType } from '../tabs';

export const allThreeTabs = [tabs.overviewTab, tabs.tableTab, tabs.jsonTab];
export const twoTabs = [tabs.tableTab, tabs.jsonTab];

export interface UseTabsParams {
/**
* Whether the flyout is expandable or not. This will drive how many tabs we display.
*/
flyoutIsExpandable: boolean;
/**
* The path passed in when using the expandable flyout API to open a panel.
*/
path: PanelPath | undefined;
}

export interface UseTabsResult {
/**
* The tabs to display in the right panel.
*/
tabsDisplayed: RightPanelTabType[];
/**
* The tab id to selected in the right panel.
*/
selectedTabId: RightPanelPaths;
}

/**
* Hook to get the tabs to display in the right panel and the selected tab.
*/
export const useTabs = ({ flyoutIsExpandable, path }: UseTabsParams): UseTabsResult => {
const { storage } = useKibana().services;

// if the flyout is expandable we render all 3 tabs (overview, table and json)
// if the flyout is not, we render only table and json
const tabsDisplayed = useMemo(
() => (flyoutIsExpandable ? allThreeTabs : twoTabs),
[flyoutIsExpandable]
);

const selectedTabId = useMemo(() => {
// we use the value passed from the url and use it if it exists in the list of tabs to display
if (path) {
const selectedTab = tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab);
if (selectedTab) {
return selectedTab;
}
}

// we check the tab saved in local storage and use it if it exists in the list of tabs to display
const tabSavedInlocalStorage = storage.get(FLYOUT_STORAGE_KEYS.RIGHT_PANEL_SELECTED_TABS);
if (
tabSavedInlocalStorage &&
tabsDisplayed.map((tab) => tab.id).includes(tabSavedInlocalStorage)
) {
return tabSavedInlocalStorage;
}

// we default back to the first tab of the list of tabs to display in case everything else has failed
const defaultTab = tabsDisplayed[0].id;
return defaultTab;
}, [path, storage, tabsDisplayed]);

return {
tabsDisplayed,
selectedTabId,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
*/

import type { FC } from 'react';
import React, { memo, useMemo, useEffect } from 'react';
import React, { memo, useEffect } from 'react';
import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useTabs } from './hooks/use_tabs';
import { FLYOUT_STORAGE_KEYS } from '../shared/constants/local_storage';
import { useKibana } from '../../../common/lib/kibana';
import { useRightPanelContext } from './context';
import { PanelNavigation } from './navigation';
import { PanelHeader } from './header';
import { PanelContent } from './content';
import type { RightPanelTabType } from './tabs';
import * as tabs from './tabs';
import { PanelFooter } from './footer';
import { useFlyoutIsExpandable } from './hooks/use_flyout_is_expandable';

Expand Down Expand Up @@ -45,25 +45,7 @@ export const RightPanel: FC<Partial<RightPanelProps>> = memo(({ path }) => {
// if the flyout is expandable we render all 3 tabs (overview, table and json)
// if the flyout is not, we render only table and json
const flyoutIsExpandable = useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject });
const tabsDisplayed = useMemo(() => {
return flyoutIsExpandable
? [tabs.overviewTab, tabs.tableTab, tabs.jsonTab]
: [tabs.tableTab, tabs.jsonTab];
}, [flyoutIsExpandable]);

// we give priority to the url, meaning if a value is saved in the url then we load this one
// if not we check the local storage and use that value if it exists
// if not we use the default tab
const selectedTabId = useMemo(() => {
const defaultTab = tabsDisplayed[0].id;
const tabSavedInlocalStorage = storage.get(FLYOUT_STORAGE_KEYS.RIGHT_PANEL_SELECTED_TABS);

if (!path) return tabSavedInlocalStorage || defaultTab;
return (
tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab) ??
(tabSavedInlocalStorage || defaultTab)
);
}, [path, storage, tabsDisplayed]);
const { tabsDisplayed, selectedTabId } = useTabs({ flyoutIsExpandable, path });

const setSelectedTabId = (tabId: RightPanelTabType['id']) => {
openRightPanel({
Expand Down