Skip to content

Commit

Permalink
[8.4] [Kubernetes Security] - Tree Navigation Empty State (#137133)
Browse files Browse the repository at this point in the history
  • Loading branch information
opauloh authored Jul 26, 2022
1 parent 5124d6c commit e0280ea
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 83 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 React, { createContext, useContext } from 'react';

import { useTreeView, UseTreeViewProps } from './hooks';

type TreeViewContextType = ReturnType<typeof useTreeView>;

const TreeViewContext = createContext<TreeViewContextType | null>(null);

export const useTreeViewContext = () => {
const context = useContext(TreeViewContext);
if (!context) {
throw new Error('useTreeViewContext must be called within an TreeViewContextProvider');
}
return context;
};

type TreeViewContextProviderProps = {
children: JSX.Element;
};

export const TreeViewContextProvider = ({
children,
...useTreeViewProps
}: TreeViewContextProviderProps & UseTreeViewProps) => {
return (
<TreeViewContext.Provider value={useTreeView(useTreeViewProps)}>
{children}
</TreeViewContext.Provider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { waitFor } from '@testing-library/react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../test';
import { DynamicTreeView } from '.';
import { clusterResponseMock, nodeResponseMock } from '../mocks';
import { TreeViewContextProvider } from '../contexts';

describe('DynamicTreeView component', () => {
let render: (props?: any) => ReturnType<AppContextTestRender['render']>;
Expand All @@ -19,37 +20,48 @@ describe('DynamicTreeView component', () => {

const waitForApiCall = () => waitFor(() => expect(mockedApi).toHaveBeenCalled());

const defaultProps = {
globalFilter: {
startDate: Date.now().toString(),
endDate: (Date.now() + 1).toString(),
},
indexPattern: {
title: '*-logs',
},
} as any;

beforeEach(() => {
mockedContext = createAppRootMockRenderer();
mockedApi = mockedContext.coreStart.http.get;
mockedApi.mockResolvedValue(clusterResponseMock);
render = (props) =>
(renderResult = mockedContext.render(
<DynamicTreeView
query={{
bool: {
filter: [],
must: [],
must_not: [],
should: [],
},
}}
indexPattern={'*-logs'}
tree={[
{
key: 'cluster',
name: 'cluster',
namePlural: 'clusters',
type: 'cluster',
iconProps: {
<TreeViewContextProvider {...defaultProps}>
<DynamicTreeView
query={{
bool: {
filter: [],
must: [],
must_not: [],
should: [],
},
}}
tree={[
{
key: 'cluster',
name: 'cluster',
namePlural: 'clusters',
type: 'cluster',
iconProps: {
type: 'cluster',
},
},
},
]}
aria-label="Logical Tree View"
onSelect={(selectionDepth, key, type) => {}}
{...props}
/>
]}
aria-label="Logical Tree View"
onSelect={(selectionDepth, key, type) => {}}
{...props}
/>
</TreeViewContextProvider>
));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { useFetchDynamicTreeView } from './hooks';
import { useStyles } from './styles';
import { disableEventDefaults, focusNextElement } from './helpers';
import { useTreeViewContext } from '../contexts';
import type { DynamicTreeViewProps, DynamicTreeViewItemProps } from './types';

const BUTTON_TEST_ID = 'kubernetesSecurity:dynamicTreeViewButton';
Expand Down Expand Up @@ -55,15 +56,15 @@ export const DynamicTreeView = ({
depth = 0,
selectionDepth = {},
query,
indexPattern = '',
onSelect,
hasSelection,
selected = '',
expanded = true,
...props
}: DynamicTreeViewProps) => {
const styles = useStyles(depth);

const { indexPattern, hasSelection, setNoResults } = useTreeViewContext();

const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
useFetchDynamicTreeView(query, tree[depth].key, indexPattern, expanded);

Expand All @@ -86,6 +87,12 @@ export const DynamicTreeView = ({
}
};

useEffect(() => {
if (depth === 0 && data && data.pages?.[0].buckets.length === 0) {
setNoResults(true);
}
}, [data, depth, setNoResults]);

useEffect(() => {
if (expanded) {
fetchNextPage();
Expand Down Expand Up @@ -158,7 +165,6 @@ export const DynamicTreeView = ({
aria-label={ariaLabel}
depth={depth}
expanded={expanded}
indexPattern={indexPattern}
isExpanded={isExpanded}
onSelect={onSelect}
onToggleExpand={onToggleExpand}
Expand Down Expand Up @@ -209,7 +215,6 @@ const DynamicTreeViewItem = ({
selected,
expanded,
query,
indexPattern,
...props
}: DynamicTreeViewItemProps) => {
const isLastNode = depth === tree.length - 1;
Expand Down Expand Up @@ -319,7 +324,6 @@ const DynamicTreeViewItem = ({
[tree[depth].type]: aggData.key,
}}
tree={tree}
indexPattern={indexPattern}
onSelect={onSelect}
selected={selected}
aria-label={`${aggData.key} child of ${props['aria-label']}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export type DynamicTreeViewProps = {
depth?: number;
selectionDepth?: TreeNavSelection;
query: QueryDslQueryContainerBool;
indexPattern?: string;
onSelect: (selectionDepth: TreeNavSelection, key: string | number, type: string) => void;
hasSelection?: boolean;
'aria-label': string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 React from 'react';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CSSObject } from '@emotion/serialize';
import icon from './assets/illustration_product_no_results_magnifying_glass.svg';

export const TREE_EMPTY_STATE = 'kubernetesSecurity:treeEmptyState';

const panelStyle: CSSObject = {
maxWidth: 500,
};

const wrapperStyle: CSSObject = {
height: 262,
};

export const EmptyState: React.FC = () => {
return (
<EuiPanel color="subdued" data-test-subj={TREE_EMPTY_STATE}>
<EuiFlexGroup css={wrapperStyle} alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiPanel hasBorder={true} css={panelStyle}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="s">
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.kubernetesSecurity.treeView.empty.title"
defaultMessage="No results match your search criteria"
/>
</h3>
</EuiTitle>
<p>
<FormattedMessage
id="xpack.kubernetesSecurity.treeView.empty.description"
defaultMessage="Try searching over a longer period of time or modifying your search"
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiImage size="200" alt="" url={icon} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { useCallback, useEffect, useMemo, useState } from 'react';
import { KubernetesCollection, TreeNavSelection } from '../../types';
import { addTimerangeAndDefaultFilterToQuery } from '../../utils/add_timerange_and_default_filter_to_query';
import { addTreeNavSelectionToFilterQuery } from './helpers';
import { IndexPattern, GlobalFilter } from '../../types';

export type UseTreeViewProps = {
globalFilter: GlobalFilter;
indexPattern?: IndexPattern;
};

export const useTreeView = ({ globalFilter, indexPattern }: UseTreeViewProps) => {
const [noResults, setNoResults] = useState(false);
const [treeNavSelection, setTreeNavSelection] = useState<TreeNavSelection>({});

const filterQueryWithTimeRange = useMemo(() => {
return JSON.parse(
addTimerangeAndDefaultFilterToQuery(
globalFilter.filterQuery,
globalFilter.startDate,
globalFilter.endDate
)
);
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]);

const onTreeNavSelect = useCallback((selection: TreeNavSelection) => {
setTreeNavSelection(selection);
}, []);

const hasSelection = useMemo(
() => !!treeNavSelection[KubernetesCollection.cluster],
[treeNavSelection]
);

const sessionViewFilter = useMemo(
() => addTreeNavSelectionToFilterQuery(globalFilter.filterQuery, treeNavSelection),
[globalFilter.filterQuery, treeNavSelection]
);

// Resetting defaults whenever filter changes
useEffect(() => {
setNoResults(false);
setTreeNavSelection({});
}, [filterQueryWithTimeRange]);

return {
noResults,
setNoResults,
filterQueryWithTimeRange,
indexPattern: indexPattern?.title || '',
onTreeNavSelect,
hasSelection,
treeNavSelection,
sessionViewFilter,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 React from 'react';
import { TreeViewContainer } from '.';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import * as context from './contexts';

describe('TreeNav component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
const spy = jest.spyOn(context, 'useTreeViewContext');

const defaultProps = {
globalFilter: {
startDate: Date.now().toString(),
endDate: (Date.now() + 1).toString(),
},
renderSessionsView: <div>Session View</div>,
} as any;

beforeEach(() => {
mockedContext = createAppRootMockRenderer();
});
afterEach(() => {
spy.mockRestore();
});

it('shows empty message when there is no results', async () => {
spy.mockImplementation(() => ({
...jest.requireActual('./contexts').useTreeViewContext,
noResults: true,
}));

renderResult = mockedContext.render(<TreeViewContainer {...defaultProps} />);
expect(await renderResult.getByText(/no results/i)).toBeInTheDocument();
});
});
Loading

0 comments on commit e0280ea

Please sign in to comment.