mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
0f6ca53f3e
commit
99f41e39e4
6 changed files with 160 additions and 38 deletions
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -92,7 +92,7 @@ const ExceptionItemsComponent: FC<ExceptionItemsProps> = ({
|
|||
viewerStatus={viewerStatus}
|
||||
buttonText={emptyViewerButtonText}
|
||||
body={emptyViewerBody}
|
||||
onCreateExceptionListItem={onCreateExceptionListItem}
|
||||
onEmptyButtonStateClick={onCreateExceptionListItem}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue