[8.7] [Security Solution][Exceptions] - Fix bug displaying empty view 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)](https://github.com/elastic/kibana/pull/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
https://github.com/elastic/kibana/issues/145717\r\n\r\nUpdated 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":"https://github.com/elastic/kibana/pull/151530","mergeCommit":{"message":"[Security
Solution][Exceptions] - Fix bug displaying empty view when no exception
list search results found (#151530)\n\n## Summary\r\n\r\nAddresses
https://github.com/elastic/kibana/issues/145717\r\n\r\nUpdated 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":"https://github.com/elastic/kibana/pull/151530","number":151530,"mergeCommit":{"message":"[Security
Solution][Exceptions] - Fix bug displaying empty view when no exception
list search results found (#151530)\n\n## Summary\r\n\r\nAddresses
https://github.com/elastic/kibana/issues/145717\r\n\r\nUpdated 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>
This commit is contained in:
Kibana Machine 2023-02-23 04:40:16 -05:00 committed by GitHub
parent 0f6ca53f3e
commit 99f41e39e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 38 deletions

View file

@ -19,7 +19,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.ERROR}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);
@ -34,7 +34,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.ERROR}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
title="Error title"
body="Error body"
/>
@ -49,7 +49,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.LOADING}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);
@ -60,7 +60,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY_SEARCH}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);
@ -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"
/>
@ -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"
/>
@ -108,7 +108,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
/>
);
@ -125,7 +125,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={false}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
listType={ListTypeText.ENDPOINT}
/>
);
@ -143,7 +143,7 @@ describe('EmptyViewerState', () => {
<EmptyViewerState
isReadOnly={true}
viewerStatus={ViewerStatus.EMPTY}
onCreateExceptionListItem={jest.fn()}
onEmptyButtonStateClick={jest.fn()}
listType={ListTypeText.ENDPOINT}
/>
);

View file

@ -31,7 +31,7 @@ interface EmptyViewerStateProps {
listType?: ListTypeText;
isReadOnly: boolean;
viewerStatus: ViewerStatus;
onCreateExceptionListItem?: () => void | null;
onEmptyButtonStateClick?: () => void | null;
}
const panelCss = css`
@ -45,7 +45,7 @@ const EmptyViewerStateComponent: FC<EmptyViewerStateProps> = ({
listType,
isReadOnly,
viewerStatus,
onCreateExceptionListItem,
onEmptyButtonStateClick,
}) => {
const { euiTheme } = useEuiTheme();
@ -75,7 +75,7 @@ const EmptyViewerStateComponent: FC<EmptyViewerStateProps> = ({
actions: [
<EuiButton
data-test-subj="emptyStateButton"
onClick={onCreateExceptionListItem}
onClick={onEmptyButtonStateClick}
iconType="plusInCircle"
color="primary"
isDisabled={isReadOnly}
@ -110,7 +110,7 @@ const EmptyViewerStateComponent: FC<EmptyViewerStateProps> = ({
euiTheme.colors.darkestShade,
title,
body,
onCreateExceptionListItem,
onEmptyButtonStateClick,
isReadOnly,
buttonText,
listType,

View file

@ -92,7 +92,7 @@ const ExceptionItemsComponent: FC<ExceptionItemsProps> = ({
viewerStatus={viewerStatus}
buttonText={emptyViewerButtonText}
body={emptyViewerBody}
onCreateExceptionListItem={onCreateExceptionListItem}
onEmptyButtonStateClick={onCreateExceptionListItem}
/>
);
return (

View file

@ -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}

View file

@ -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 {
@ -18,8 +18,6 @@ import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingContent,
EuiProgress,
EuiSpacer,
EuiPageHeader,
EuiHorizontalRule,
@ -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';
@ -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,
@ -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({
@ -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,
@ -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 (
<>
@ -428,7 +452,7 @@ export const SharedLists = React.memo(() => {
key={'createList'}
onClick={() => {
onCloseCreatePopover();
setDisplayCreateSharedListFlyout(true);
onCreateExceptionListOpenClick();
}}
>
{i18n.CREATE_SHARED_LIST_BUTTON}
@ -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();
@ -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
@ -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}

View file

@ -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>