Skip to content

Commit

Permalink
[8.7] [Security Solution][Exceptions] - Fix bug displaying empty view…
Browse files Browse the repository at this point in the history
… when no exception list search results found (#151530) (#151970)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Security Solution][Exceptions] - Fix bug displaying empty view when
no exception list search results found
(#151530)](#151530)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Yara
Tercero","email":"yctercero@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-02-23T08:01:32Z","message":"[Security
Solution][Exceptions] - Fix bug displaying empty view when no exception
list search results found (#151530)\n\n## Summary\r\n\r\nAddresses
#145717 the all
exceptions lists view to account for different possible\r\nstates (empty
search, loading, no lists) using an existing
component.","sha":"56777859bf8bbee17e11f669300465ec775cd28d","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Feature:Rule
Exceptions","auto-backport","Team:Security Solution
Platform","backport:prev-minor","v8.7.0","v8.8.0"],"number":151530,"url":"#151530
Solution][Exceptions] - Fix bug displaying empty view when no exception
list search results found (#151530)\n\n## Summary\r\n\r\nAddresses
#145717 the all
exceptions lists view to account for different possible\r\nstates (empty
search, loading, no lists) using an existing
component.","sha":"56777859bf8bbee17e11f669300465ec775cd28d"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"#151530
Solution][Exceptions] - Fix bug displaying empty view when no exception
list search results found (#151530)\n\n## Summary\r\n\r\nAddresses
#145717 the all
exceptions lists view to account for different possible\r\nstates (empty
search, loading, no lists) using an existing
component.","sha":"56777859bf8bbee17e11f669300465ec775cd28d"}}]}]
BACKPORT-->

Co-authored-by: Yara Tercero <yctercero@users.noreply.github.com>
  • Loading branch information
kibanamachine and yctercero committed Feb 23, 2023
1 parent 0f6ca53 commit 99f41e3
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.ERROR}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);

Expand All @@ -34,7 +34,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.ERROR}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
title="Error title"
body="Error body"
/>
Expand All @@ -49,7 +49,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.LOADING}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);

Expand All @@ -60,7 +60,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY_SEARCH}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);

Expand All @@ -77,7 +77,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY_SEARCH}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
title="Empty search title"
body="Empty search body"
/>
Expand All @@ -92,7 +92,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
body="There are no endpoint exceptions."
buttonText="Add endpoint exception"
/>
Expand All @@ -108,7 +108,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);

Expand All @@ -125,7 +125,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
listType={ListTypeText.ENDPOINT}
/>
);
Expand All @@ -143,7 +143,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={true}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
listType={ListTypeText.ENDPOINT}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface EmptyViewerStateProps {
listType?: ListTypeText;
isReadOnly: boolean;
viewerStatus: ViewerStatus;
onCreateExceptionListItem?: () => void | null;
onEmptyButtonStateClick?: () => void | null;
}

const panelCss = css`
Expand All @@ -45,7 +45,7 @@ const EmptyViewerStateComponent: FC<EmptyViewerStateProps> = ({
listType,
isReadOnly,
viewerStatus,
onCreateExceptionListItem,
onEmptyButtonStateClick,
}) => {
const { euiTheme } = useEuiTheme();

Expand Down Expand Up @@ -75,7 +75,7 @@ const EmptyViewerStateComponent: FC<EmptyViewerStateProps> = ({
actions: [
<EuiButton
data-test-subj="emptyStateButton"
onClick={onCreateExceptionListItem}
onClick={onEmptyButtonStateClick}
iconType="plusInCircle"
color="primary"
isDisabled={isReadOnly}
Expand Down Expand Up @@ -110,7 +110,7 @@ const EmptyViewerStateComponent: FC<EmptyViewerStateProps> = ({
euiTheme.colors.darkestShade,
title,
body,
onCreateExceptionListItem,
onEmptyButtonStateClick,
isReadOnly,
buttonText,
listType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const ExceptionItemsComponent: FC<ExceptionItemsProps> = ({
viewerStatus={viewerStatus}
buttonText={emptyViewerButtonText}
body={emptyViewerBody}
onCreateExceptionListItem={onCreateExceptionListItem}
onEmptyButtonStateClick={onCreateExceptionListItem}
/>
);
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const ListWithSearchComponent: FC<ListWithSearchComponentProps> = ({
<EmptyViewerState
isReadOnly={isReadOnly}
viewerStatus={viewerStatus as ViewerStatus}
onCreateExceptionListItem={onAddExceptionClick}
onEmptyButtonStateClick={onAddExceptionClick}
title={i18n.EXCEPTION_LIST_EMPTY_VIEWER_TITLE}
body={i18n.EXCEPTION_LIST_EMPTY_VIEWER_BODY(listName)}
buttonText={i18n.EXCEPTION_LIST_EMPTY_VIEWER_BUTTON}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useEffect, useCallback, useState } from 'react';
import React, { useMemo, useEffect, useCallback, useState } from 'react';
import type { EuiSearchBarProps } from '@elastic/eui';

import {
Expand All @@ -18,8 +18,6 @@ import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingContent,
EuiProgress,
EuiSpacer,
EuiPageHeader,
EuiHorizontalRule,
Expand All @@ -29,9 +27,9 @@ import {
import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
import { ViewerStatus, EmptyViewerState } from '@kbn/securitysolution-exception-list-components';

import { AutoDownload } from '../../../common/components/auto_download/auto_download';
import { Loader } from '../../../common/components/loader';
import { useKibana } from '../../../common/lib/kibana';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';

Expand Down Expand Up @@ -103,6 +101,8 @@ export const SharedLists = React.memo(() => {
);
const [filters, setFilters] = useState<ExceptionListFilter | undefined>();

const [viewerStatus, setViewStatus] = useState<ViewerStatus | null>(ViewerStatus.LOADING);

const [
loadingExceptions,
exceptions,
Expand Down Expand Up @@ -133,6 +133,12 @@ export const SharedLists = React.memo(() => {
const [displayImportListFlyout, setDisplayImportListFlyout] = useState(false);
const { addError, addSuccess } = useAppToasts();

// Loading states
const exceptionsLoaded = !loadingTableInfo && !initLoading;
const hasNoExceptions = !loadingExceptions && !exceptionListsWithRuleRefs.length;
const isSearchingExceptions = viewerStatus === ViewerStatus.SEARCHING;
const isLoadingExceptions = viewerStatus === ViewerStatus.LOADING;

const handleDeleteSuccess = useCallback(
(listId?: string) => () => {
notifications.toasts.addSuccess({
Expand Down Expand Up @@ -235,6 +241,7 @@ export const SharedLists = React.memo(() => {
query,
queryText,
}: Parameters<NonNullable<EuiSearchBarProps['onChange']>>[0]): Promise<void> => {
setViewStatus(ViewerStatus.SEARCHING);
const filterOptions = {
name: null,
list_id: null,
Expand Down Expand Up @@ -378,6 +385,23 @@ export const SharedLists = React.memo(() => {
setDisplayAddExceptionItemFlyout(false);
setIsCreatePopoverOpen(false);
};
const onCreateExceptionListOpenClick = () => setDisplayCreateSharedListFlyout(true);

const isReadOnly = useMemo(() => {
return (canUserREAD && !canUserCRUD) ?? true;
}, [canUserREAD, canUserCRUD]);

useEffect(() => {
if (isSearchingExceptions && hasNoExceptions) {
setViewStatus(ViewerStatus.EMPTY_SEARCH);
} else if (!exceptionsLoaded) {
setViewStatus(ViewerStatus.LOADING);
} else if (isLoadingExceptions && hasNoExceptions) {
setViewStatus(ViewerStatus.EMPTY);
} else if (isLoadingExceptions && exceptionsLoaded) {
setViewStatus(null);
}
}, [isSearchingExceptions, hasNoExceptions, exceptionsLoaded, isLoadingExceptions]);

return (
<>
Expand Down Expand Up @@ -428,7 +452,7 @@ export const SharedLists = React.memo(() => {
key={'createList'}
onClick={() => {
onCloseCreatePopover();
setDisplayCreateSharedListFlyout(true);
onCreateExceptionListOpenClick();
}}
>
{i18n.CREATE_SHARED_LIST_BUTTON}
Expand Down Expand Up @@ -468,7 +492,7 @@ export const SharedLists = React.memo(() => {
isEndpointItem={false}
isBulkAction={false}
showAlertCloseOptions
onCancel={(didRuleChange: boolean) => setDisplayAddExceptionItemFlyout(false)}
onCancel={() => setDisplayAddExceptionItemFlyout(false)}
onConfirm={(didRuleChange: boolean) => {
setDisplayAddExceptionItemFlyout(false);
if (didRuleChange) handleRefresh();
Expand All @@ -489,23 +513,17 @@ export const SharedLists = React.memo(() => {

<EuiHorizontalRule />
<div data-test-subj="allExceptionListsPanel">
{loadingTableInfo && (
<EuiProgress
data-test-subj="loadingRulesInfoProgress"
size="xs"
position="absolute"
color="accent"
/>
)}
{!initLoading && <ListsSearchBar onSearch={handleSearch} />}
<EuiSpacer size="m" />

{loadingTableInfo && !initLoading && !showReferenceErrorModal && (
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
)}

{initLoading || loadingTableInfo ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} />
{viewerStatus != null ? (
<EmptyViewerState
isReadOnly={isReadOnly}
title={i18n.NO_EXCEPTION_LISTS}
viewerStatus={viewerStatus}
buttonText={i18n.CREATE_SHARED_LIST_BUTTON}
body={i18n.NO_LISTS_BODY}
onEmptyButtonStateClick={onCreateExceptionListOpenClick}
/>
) : (
<>
<ExceptionsTableUtilityBar
Expand All @@ -515,13 +533,13 @@ export const SharedLists = React.memo(() => {
sort={sort}
sortFields={SORT_FIELDS}
/>
{exceptionListsWithRuleRefs.length > 0 && canUserCRUD !== null && canUserREAD !== null && (
{exceptionListsWithRuleRefs.length > 0 && (
<div data-test-subj="exceptionsTable">
{exceptionListsWithRuleRefs.map((excList) => (
<ExceptionsListCard
key={excList.list_id}
data-test-subj="exceptionsListCard"
readOnly={canUserREAD && !canUserCRUD}
readOnly={isReadOnly}
exceptionsList={excList}
handleDelete={handleDelete}
handleExport={handleExport}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,110 @@ describe('SharedLists', () => {
]);
});

it('renders empty view if no lists exist', async () => {
(useExceptionLists as jest.Mock).mockReturnValue([
false,
[],
{
page: 1,
perPage: 20,
total: 0,
},
jest.fn(),
]);

(useAllExceptionLists as jest.Mock).mockReturnValue([false, [], {}]);
const wrapper = render(
<TestProviders>
<SharedLists />
</TestProviders>
);

await waitFor(() => {
const emptyViewerState = wrapper.getByTestId('emptyViewerState');
expect(emptyViewerState).toBeInTheDocument();
});
});

it('renders loading state when fetching lists', async () => {
(useExceptionLists as jest.Mock).mockReturnValue([
true,
[],
{
page: 1,
perPage: 20,
total: 0,
},
jest.fn(),
]);

(useAllExceptionLists as jest.Mock).mockReturnValue([false, [], {}]);
const wrapper = render(
<TestProviders>
<SharedLists />
</TestProviders>
);

await waitFor(() => {
const loadingViewerState = wrapper.getByTestId('loadingViewerState');
expect(loadingViewerState).toBeInTheDocument();
});
});

it('renders loading state when fetching refs', async () => {
(useExceptionLists as jest.Mock).mockReturnValue([
false,
[exceptionList1, exceptionList2],
{
page: 1,
perPage: 20,
total: 2,
},
jest.fn(),
]);

(useAllExceptionLists as jest.Mock).mockReturnValue([true, [], {}]);
const wrapper = render(
<TestProviders>
<SharedLists />
</TestProviders>
);

await waitFor(() => {
const loadingViewerState = wrapper.getByTestId('loadingViewerState');
expect(loadingViewerState).toBeInTheDocument();
});
});

it('renders empty search state when no search results are found', async () => {
const wrapper = render(
<TestProviders>
<SharedLists />
</TestProviders>
);

(useExceptionLists as jest.Mock).mockReturnValue([
false,
[],
{
page: 1,
perPage: 20,
total: 0,
},
jest.fn(),
]);

(useAllExceptionLists as jest.Mock).mockReturnValue([false, [], {}]);

const searchBar = wrapper.getByTestId('exceptionsHeaderSearchInput');
fireEvent.change(searchBar, { target: { value: 'foo' } });

await waitFor(() => {
const emptySearchViewerState = wrapper.getByTestId('emptySearchViewerState');
expect(emptySearchViewerState).toBeInTheDocument();
});
});

it('renders delete option as disabled if list is "endpoint_list"', async () => {
const wrapper = render(
<TestProviders>
Expand Down

0 comments on commit 99f41e3

Please sign in to comment.