mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[8.4] [Kubernetes Security] - adding Collapse / Expand button to Tree View Navigation (#136782)
This commit is contained in:
parent
19aa51e5a8
commit
711d337820
8 changed files with 142 additions and 72 deletions
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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']>;
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -23,6 +23,7 @@ export const useStyles = () => {
|
|||
|
||||
const treeViewContainer: CSSObject = {
|
||||
height: '600px',
|
||||
width: '288px',
|
||||
overflowY: 'auto',
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue