[8.4] [Kubernetes Security] - adding Collapse / Expand button to Tree View Navigation (#136782)

This commit is contained in:
Paulo Henrique 2022-07-25 10:29:59 -07:00 committed by GitHub
parent 19aa51e5a8
commit 711d337820
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 72 deletions

View file

@ -55,6 +55,20 @@ export const TREE_NAVIGATION_SHOW_MORE = (name: string) =>
defaultMessage: 'Show more {name}',
});
export const TREE_NAVIGATION_COLLAPSE = i18n.translate(
'xpack.kubernetesSecurity.treeNavigation.collapse',
{
defaultMessage: 'Collapse Tree Navigation',
}
);
export const TREE_NAVIGATION_EXPAND = i18n.translate(
'xpack.kubernetesSecurity.treeNavigation.expand',
{
defaultMessage: 'Expand Tree Navigation',
}
);
export const CHART_TOGGLE_SHOW = i18n.translate('xpack.kubernetesSecurity.chartsToggle.show', {
defaultMessage: 'Show charts',
});

View file

@ -9,7 +9,7 @@ import React from 'react';
import { waitFor } from '@testing-library/react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../test';
import { DynamicTreeView } from '.';
import { clusterResponseMock, nodeResponseMock } from './mocks';
import { clusterResponseMock, nodeResponseMock } from '../mocks';
describe('DynamicTreeView component', () => {
let render: (props?: any) => ReturnType<AppContextTestRender['render']>;

View file

@ -6,7 +6,7 @@
*/
import React, { useCallback, useMemo, useState } from 'react';
import { EuiSplitPanel, EuiText } from '@elastic/eui';
import { EuiSplitPanel } from '@elastic/eui';
import { useStyles } from './styles';
import { IndexPattern, GlobalFilter, TreeNavSelection, KubernetesCollection } from '../../types';
import { TreeNav } from './tree_nav';
@ -39,14 +39,12 @@ export const TreeViewContainer = ({
return (
<EuiSplitPanel.Outer direction="row" hasBorder borderRadius="m" css={styles.outerPanel}>
<EuiSplitPanel.Inner color="subdued" grow={false} css={styles.navPanel}>
<EuiText css={styles.treeViewNav}>
<TreeNav
indexPattern={indexPattern}
globalFilter={globalFilter}
onSelect={onTreeNavSelect}
hasSelection={hasSelection}
/>
</EuiText>
<TreeNav
indexPattern={indexPattern}
globalFilter={globalFilter}
onSelect={onTreeNavSelect}
hasSelection={hasSelection}
/>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner css={styles.sessionsPanel}>
<Breadcrumb treeNavSelection={treeNavSelection} onSelect={onTreeNavSelect} />

View file

@ -23,10 +23,6 @@ export const useStyles = () => {
borderRight: border.thin,
};
const treeViewNav: CSSObject = {
width: '316px',
};
const sessionsPanel: CSSObject = {
overflowX: 'auto',
};
@ -34,7 +30,6 @@ export const useStyles = () => {
return {
outerPanel,
navPanel,
treeViewNav,
sessionsPanel,
};
}, [euiTheme]);

View file

@ -7,12 +7,14 @@
import React from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../test';
import { clusterResponseMock } from '../mocks';
import { TreeNav } from '.';
describe('TreeNav component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
let mockedApi: AppContextTestRender['coreStart']['http']['get'];
const defaultProps = {
globalFilter: {
@ -25,6 +27,8 @@ describe('TreeNav component', () => {
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
mockedApi = mockedContext.coreStart.http.get;
mockedApi.mockResolvedValue(clusterResponseMock);
});
it('mount with Logical View selected by default', async () => {
@ -49,4 +53,18 @@ describe('TreeNav component', () => {
logicViewRadio.click();
expect(renderResult.getByText(logicalViewPath)).toBeInTheDocument();
});
it('collapses / expands the tree nav when clicking on collapse button', async () => {
renderResult = mockedContext.render(<TreeNav {...defaultProps} />);
expect(renderResult.getByText(/cluster/i)).toBeVisible();
const collapseButton = await renderResult.getByLabelText(/collapse/i);
collapseButton.click();
expect(renderResult.getByText(/cluster/i)).not.toBeVisible();
const expandButton = await renderResult.getByLabelText(/expand/i);
expandButton.click();
expect(renderResult.getByText(/cluster/i)).toBeVisible();
});
});

View file

@ -5,11 +5,22 @@
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import { EuiButtonGroup, useGeneratedHtmlId, EuiText, EuiSpacer } from '@elastic/eui';
import {
EuiButtonGroup,
useGeneratedHtmlId,
EuiText,
EuiSpacer,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import {
TREE_VIEW_INFRASTRUCTURE_VIEW,
TREE_VIEW_LOGICAL_VIEW,
TREE_VIEW_SWITCHER_LEGEND,
TREE_NAVIGATION_COLLAPSE,
TREE_NAVIGATION_EXPAND,
} from '../../../../common/translations';
import { useStyles } from './styles';
import { IndexPattern, GlobalFilter, TreeNavSelection } from '../../../types';
@ -29,6 +40,16 @@ export const TreeNav = ({ indexPattern, globalFilter, onSelect, hasSelection }:
const styles = useStyles();
const [tree, setTree] = useState(TREE_VIEW.logical);
const [selected, setSelected] = useState('');
const [isCollapsed, setIsCollapsed] = useState(false);
const treeNavTypePrefix = useGeneratedHtmlId({
prefix: 'treeNavType',
});
const logicalTreeViewPrefix = `${treeNavTypePrefix}${LOGICAL}`;
const [toggleIdSelected, setToggleIdSelected] = useState(logicalTreeViewPrefix);
const handleToggleCollapse = () => {
setIsCollapsed(!isCollapsed);
};
const filterQueryWithTimeRange = useMemo(() => {
return addTimerangeAndDefaultFilterToQuery(
@ -38,26 +59,25 @@ export const TreeNav = ({ indexPattern, globalFilter, onSelect, hasSelection }:
);
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]);
const treeNavTypePrefix = useGeneratedHtmlId({
prefix: 'treeNavType',
});
const options: TreeViewOptionsGroup[] = useMemo(
() => [
{
id: logicalTreeViewPrefix,
label: TREE_VIEW_LOGICAL_VIEW,
value: LOGICAL,
},
{
id: `${treeNavTypePrefix}${INFRASTRUCTURE}`,
label: TREE_VIEW_INFRASTRUCTURE_VIEW,
value: INFRASTRUCTURE,
},
],
[logicalTreeViewPrefix, treeNavTypePrefix]
);
const logicalTreeViewPrefix = `${treeNavTypePrefix}${LOGICAL}`;
const [toggleIdSelected, setToggleIdSelected] = useState(logicalTreeViewPrefix);
const options: TreeViewOptionsGroup[] = [
{
id: logicalTreeViewPrefix,
label: TREE_VIEW_LOGICAL_VIEW,
value: LOGICAL,
},
{
id: `${treeNavTypePrefix}${INFRASTRUCTURE}`,
label: TREE_VIEW_INFRASTRUCTURE_VIEW,
value: INFRASTRUCTURE,
},
];
const selectedLabel = useMemo(() => {
return options.find((opt) => opt.id === toggleIdSelected)!.label;
}, [options, toggleIdSelected]);
const handleTreeViewSwitch = (id: string, value: TreeViewKind) => {
setToggleIdSelected(id);
@ -66,43 +86,67 @@ export const TreeNav = ({ indexPattern, globalFilter, onSelect, hasSelection }:
return (
<>
<EuiButtonGroup
name="coarsness"
legend={TREE_VIEW_SWITCHER_LEGEND}
options={options}
idSelected={toggleIdSelected}
onChange={handleTreeViewSwitch}
buttonSize="compressed"
isFullWidth
color="primary"
css={styles.treeViewSwitcher}
/>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="xs" css={styles.treeViewLegend}>
{tree.map((t) => t.name).join(' / ')}
</EuiText>
<EuiSpacer size="s" />
<div css={styles.treeViewContainer} className="eui-scrollBar">
<DynamicTreeView
query={JSON.parse(filterQueryWithTimeRange)}
indexPattern={indexPattern?.title}
tree={tree}
aria-label="Logical Tree View"
selected={selected}
onSelect={(selectionDepth, key, type) => {
const newSelectionDepth = {
...selectionDepth,
[type]: key,
};
setSelected(
Object.entries(newSelectionDepth)
.map(([k, v]) => `${k}.${v}`)
.join()
);
onSelect(newSelectionDepth);
}}
hasSelection={hasSelection}
/>
{isCollapsed && (
<EuiToolTip content={TREE_NAVIGATION_EXPAND}>
<EuiButtonIcon
onClick={handleToggleCollapse}
iconType="menuRight"
aria-label={TREE_NAVIGATION_EXPAND}
/>
</EuiToolTip>
)}
<div style={{ display: isCollapsed ? 'none' : 'inherit' }}>
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiButtonGroup
name="coarsness"
legend={TREE_VIEW_SWITCHER_LEGEND}
options={options}
idSelected={toggleIdSelected}
onChange={handleTreeViewSwitch}
buttonSize="compressed"
isFullWidth
color="primary"
css={styles.treeViewSwitcher}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={TREE_NAVIGATION_COLLAPSE}>
<EuiButtonIcon
onClick={handleToggleCollapse}
iconType="menuLeft"
aria-label={TREE_NAVIGATION_COLLAPSE}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="xs" css={styles.treeViewLegend}>
{tree.map((t) => t.name).join(' / ')}
</EuiText>
<EuiSpacer size="s" />
<div css={styles.treeViewContainer} className="eui-scrollBar">
<DynamicTreeView
query={JSON.parse(filterQueryWithTimeRange)}
indexPattern={indexPattern?.title}
tree={tree}
aria-label={selectedLabel}
selected={selected}
onSelect={(selectionDepth, key, type) => {
const newSelectionDepth = {
...selectionDepth,
[type]: key,
};
setSelected(
Object.entries(newSelectionDepth)
.map(([k, v]) => `${k}.${v}`)
.join()
);
onSelect(newSelectionDepth);
}}
hasSelection={hasSelection}
/>
</div>
</div>
</>
);

View file

@ -23,6 +23,7 @@ export const useStyles = () => {
const treeViewContainer: CSSObject = {
height: '600px',
width: '288px',
overflowY: 'auto',
};