diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts
index d45c9fb3777e6..601599104641b 100644
--- a/x-pack/plugins/cases/common/constants.ts
+++ b/x-pack/plugins/cases/common/constants.ts
@@ -120,18 +120,21 @@ export const GENERAL_CASES_OWNER = APP_ID;
export const OWNER_INFO = {
[SECURITY_SOLUTION_OWNER]: {
+ id: SECURITY_SOLUTION_OWNER,
appId: 'securitySolutionUI',
label: 'Security',
iconType: 'logoSecurity',
appRoute: '/app/security',
},
[OBSERVABILITY_OWNER]: {
+ id: OBSERVABILITY_OWNER,
appId: 'observability-overview',
label: 'Observability',
iconType: 'logoObservability',
appRoute: '/app/observability',
},
[GENERAL_CASES_OWNER]: {
+ id: GENERAL_CASES_OWNER,
appId: 'management',
label: 'Stack',
iconType: 'casesApp',
diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
index 909bb1dd24ea0..e1e6e5ec42de8 100644
--- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
@@ -426,6 +426,20 @@ describe.skip('AllCasesListGeneric', () => {
});
});
+ it('should render only Name, CreatedOn and Severity columns when isSelectorView=true', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_title_0"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_createdAt_1"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_severity_2"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="tableHeaderCell_assignees_1"]').exists()).toBe(false);
+ });
+ });
+
it('should sort by severity', async () => {
const result = appMockRenderer.render();
@@ -698,12 +712,12 @@ describe.skip('AllCasesListGeneric', () => {
queryParams: DEFAULT_QUERY_PARAMS,
});
- userEvent.click(getByTestId('options-filter-popover-button-Solution'));
+ userEvent.click(getByTestId('solution-filter-popover-button'));
await waitForEuiPopoverOpen();
userEvent.click(
- getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
+ getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
undefined,
{
skipPointerEventsCheck: true,
@@ -725,7 +739,7 @@ describe.skip('AllCasesListGeneric', () => {
});
userEvent.click(
- getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
+ getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
undefined,
{
skipPointerEventsCheck: true,
@@ -754,7 +768,7 @@ describe.skip('AllCasesListGeneric', () => {
);
- expect(queryByTestId('options-filter-popover-button-Solution')).toBeFalsy();
+ expect(queryByTestId('solution-filter-popover-button')).toBeFalsy();
});
it('should call useGetCases with the correct owner on initial render', async () => {
diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
index ab50f16083f8b..7666d518fe9b4 100644
--- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
@@ -14,11 +14,13 @@ import styled, { css } from 'styled-components';
import type { Case, CaseStatusWithAllStatus, FilterOptions } from '../../../common/ui/types';
import { SortFieldCase, StatusAll } from '../../../common/ui/types';
import { CaseStatuses, caseStatuses } from '../../../common/api';
+import { OWNER_INFO } from '../../../common/constants';
+import type { CasesOwners } from '../../client/helpers/can_use_cases';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import { useCasesColumns } from './use_cases_columns';
import { CasesTableFilters } from './table_filters';
-import type { EuiBasicTableOnChange } from './types';
+import type { EuiBasicTableOnChange, Solution } from './types';
import { CasesTable } from './table';
import { useCasesContext } from '../cases_context/use_cases_context';
@@ -48,6 +50,17 @@ const getSortField = (field: string): SortFieldCase =>
// @ts-ignore
SortFieldCase[field] ?? SortFieldCase.title;
+const isValidSolution = (solution: string): solution is CasesOwners =>
+ Object.keys(OWNER_INFO).includes(solution);
+
+const mapToReadableSolutionName = (solution: string): Solution => {
+ if (isValidSolution(solution)) {
+ return OWNER_INFO[solution];
+ }
+
+ return { id: solution, label: solution, iconType: '' };
+};
+
export interface AllCasesListProps {
hiddenStatuses?: CaseStatusWithAllStatus[];
isSelectorView?: boolean;
@@ -228,6 +241,10 @@ export const AllCasesList = React.memo(
[]
);
+ const availableSolutionsLabels = availableSolutions.map((solution) =>
+ mapToReadableSolutionName(solution)
+ );
+
return (
<>
(
countOpenCases={data.countOpenCases}
countInProgressCases={data.countInProgressCases}
onFilterChanged={onFilterChangedCallback}
- availableSolutions={hasOwner ? [] : availableSolutions}
+ availableSolutions={hasOwner ? [] : availableSolutionsLabels}
initial={{
search: filterOptions.search,
searchFields: filterOptions.searchFields,
@@ -254,8 +271,8 @@ export const AllCasesList = React.memo(
severity: filterOptions.severity,
}}
hiddenStatuses={hiddenStatuses}
- displayCreateCaseButton={isSelectorView}
onCreateCasePressed={onRowClick}
+ isSelectorView={isSelectorView}
isLoading={isLoadingCurrentUserProfile}
currentUserProfile={currentUserProfile}
/>
diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
index 0ba5a65bf3207..c04ea59dfebc5 100644
--- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
@@ -7,7 +7,7 @@
import React, { useState, useCallback } from 'react';
import {
- EuiButton,
+ EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
@@ -29,7 +29,7 @@ export interface AllCasesSelectorModalProps {
const Modal = styled(EuiModal)`
${({ theme }) => `
- min-width: ${theme.eui.euiBreakpoints.l};
+ min-width: ${theme.eui.euiBreakpoints.m};
max-width: ${theme.eui.euiBreakpoints.xl};
`}
`;
@@ -68,13 +68,13 @@ export const AllCasesSelectorModal = React.memo(
/>
-
{i18n.CANCEL}
-
+
diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx
new file mode 100644
index 0000000000000..dcb469448b03f
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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 { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
+
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+import type { Solution } from './types';
+import {
+ OWNER_INFO,
+ SECURITY_SOLUTION_OWNER,
+ OBSERVABILITY_OWNER,
+} from '../../../common/constants';
+
+import { SolutionFilter } from './solution_filter';
+import userEvent from '@testing-library/user-event';
+
+describe('SolutionFilter ', () => {
+ let appMockRender: AppMockRenderer;
+ const onSelectedOptionsChanged = jest.fn();
+ const solutions: Solution[] = [
+ {
+ id: SECURITY_SOLUTION_OWNER,
+ label: OWNER_INFO[SECURITY_SOLUTION_OWNER].label,
+ iconType: OWNER_INFO[SECURITY_SOLUTION_OWNER].iconType,
+ },
+ {
+ id: OBSERVABILITY_OWNER,
+ label: OWNER_INFO[OBSERVABILITY_OWNER].label,
+ iconType: OWNER_INFO[OBSERVABILITY_OWNER].iconType,
+ },
+ ];
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('renders button correctly', () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ expect(getByTestId('solution-filter-popover-button')).toBeInTheDocument();
+ });
+
+ it('renders empty label correctly', async () => {
+ const { getByTestId, getByText } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByText('No options available')).toBeInTheDocument();
+ });
+
+ it('renders options correctly', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ expect(getByTestId('solution-filter-popover-button')).toBeInTheDocument();
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByTestId(`solution-filter-popover-item-${solutions[0].id}`)).toBeInTheDocument();
+ expect(getByTestId(`solution-filter-popover-item-${solutions[0].id}`)).toBeInTheDocument();
+ });
+
+ it('should call onSelectionChange with selected solution id', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`solution-filter-popover-item-${solutions[0].id}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([solutions[0].id]);
+ });
+
+ it('should call onSelectionChange with empty array when solution option is deselected', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`solution-filter-popover-item-${solutions[1].id}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([]);
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx
new file mode 100644
index 0000000000000..b776895e2fe9e
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx
@@ -0,0 +1,126 @@
+/*
+ * 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, { useCallback, useState } from 'react';
+import {
+ EuiFilterButton,
+ EuiFilterSelectItem,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiPopover,
+ EuiText,
+ EuiIcon,
+} from '@elastic/eui';
+import styled from 'styled-components';
+
+import * as i18n from './translations';
+import type { Solution } from './types';
+
+interface FilterPopoverProps {
+ onSelectedOptionsChanged: (value: string[]) => void;
+ options: Solution[];
+ optionsEmptyLabel?: string;
+ selectedOptions: string[];
+}
+
+const ScrollableDiv = styled.div`
+ max-height: 250px;
+ overflow: auto;
+`;
+
+const toggleSelectedGroup = (group: string, selectedGroups: string[]): string[] => {
+ const selectedGroupIndex = selectedGroups.indexOf(group);
+ if (selectedGroupIndex >= 0) {
+ return [
+ ...selectedGroups.slice(0, selectedGroupIndex),
+ ...selectedGroups.slice(selectedGroupIndex + 1),
+ ];
+ }
+ return [...selectedGroups, group];
+};
+
+/**
+ * Popover for selecting a field to filter on
+ *
+ * @param buttonLabel label on dropdwon button
+ * @param onSelectedOptionsChanged change listener to be notified when option selection changes
+ * @param options to display for filtering
+ * @param optionsEmptyLabel shows when options empty
+ * @param selectedOptions manage state of selectedOptions
+ */
+export const SolutionFilterComponent = ({
+ onSelectedOptionsChanged,
+ options,
+ optionsEmptyLabel,
+ selectedOptions,
+}: FilterPopoverProps) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const setIsPopoverOpenCb = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
+ const toggleSelectedGroupCb = useCallback(
+ (option) => onSelectedOptionsChanged(toggleSelectedGroup(option, selectedOptions)),
+ [selectedOptions, onSelectedOptionsChanged]
+ );
+
+ return (
+ 0}
+ numActiveFilters={selectedOptions.length}
+ aria-label={i18n.SOLUTION}
+ >
+ {i18n.SOLUTION}
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={setIsPopoverOpenCb}
+ panelPaddingSize="none"
+ repositionOnScroll
+ >
+
+ {options.map((option, index) => (
+
+
+
+
+
+ {option.label}
+
+
+ ))}
+
+ {options.length === 0 && optionsEmptyLabel != null && (
+
+
+
+ {optionsEmptyLabel}
+
+
+
+ )}
+
+ );
+};
+
+SolutionFilterComponent.displayName = 'SolutionFilterComponent';
+
+export const SolutionFilter = React.memo(SolutionFilterComponent);
+
+SolutionFilter.displayName = 'SolutionFilter';
diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx
index 6c0ce7569b1c6..ccfcf715a67ba 100644
--- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx
@@ -6,16 +6,20 @@
*/
import React from 'react';
-import { mount } from 'enzyme';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
+import { waitForComponentToUpdate } from '../../common/test_utils';
import { CaseStatuses } from '../../../common/api';
-import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
+import {
+ OWNER_INFO,
+ SECURITY_SOLUTION_OWNER,
+ OBSERVABILITY_OWNER,
+} from '../../../common/constants';
import type { AppMockRenderer } from '../../common/mock';
-import { createAppMockRenderer, TestProviders } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases';
import { CasesTableFilters } from './table_filters';
import { useGetTags } from '../../containers/use_get_tags';
@@ -52,37 +56,31 @@ describe('CasesTableFilters ', () => {
});
it('should render the case status filter dropdown', () => {
- const wrapper = mount(
-
-
-
- );
+ appMockRender.render();
- expect(wrapper.find(`[data-test-subj="case-status-filter"]`).first().exists()).toBeTruthy();
+ expect(screen.getByTestId('case-status-filter')).toBeInTheDocument();
});
it('should render the case severity filter dropdown', () => {
- const result = appMockRender.render();
- expect(result.getByTestId('case-severity-filter')).toBeTruthy();
+ appMockRender.render();
+ expect(screen.getByTestId('case-severity-filter')).toBeTruthy();
});
it('should call onFilterChange when the severity filter changes', async () => {
- const result = appMockRender.render();
- userEvent.click(result.getByTestId('case-severity-filter'));
+ appMockRender.render();
+ userEvent.click(screen.getByTestId('case-severity-filter'));
await waitForEuiPopoverOpen();
- userEvent.click(result.getByTestId('case-severity-filter-high'));
+ userEvent.click(screen.getByTestId('case-severity-filter-high'));
expect(onFilterChanged).toBeCalledWith({ severity: 'high' });
});
- it('should call onFilterChange when selected tags change', () => {
- const wrapper = mount(
-
-
-
- );
- wrapper.find(`[data-test-subj="options-filter-popover-button-Tags"]`).last().simulate('click');
- wrapper.find(`[data-test-subj="options-filter-popover-item-coke"]`).last().simulate('click');
+ it('should call onFilterChange when selected tags change', async () => {
+ appMockRender.render();
+
+ userEvent.click(screen.getByTestId('options-filter-popover-button-Tags'));
+ await waitForEuiPopoverOpen();
+ userEvent.click(screen.getByTestId('options-filter-popover-item-coke'));
expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] });
});
@@ -109,29 +107,21 @@ describe('CasesTableFilters ', () => {
`);
});
- it('should call onFilterChange when search changes', () => {
- const wrapper = mount(
-
-
-
- );
-
- wrapper
- .find(`[data-test-subj="search-cases"]`)
- .last()
- .simulate('keyup', { key: 'Enter', target: { value: 'My search' } });
+ it('should call onFilterChange when search changes', async () => {
+ appMockRender.render();
+
+ await userEvent.type(screen.getByTestId('search-cases'), 'My search{enter}');
+
expect(onFilterChanged).toBeCalledWith({ search: 'My search' });
});
- it('should call onFilterChange when changing status', () => {
- const wrapper = mount(
-
-
-
- );
+ it('should call onFilterChange when changing status', async () => {
+ appMockRender.render();
+
+ userEvent.click(screen.getByTestId('case-status-filter'));
+ await waitForEuiPopoverOpen();
+ userEvent.click(screen.getByTestId('case-status-filter-closed'));
- wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
- wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
expect(onFilterChanged).toBeCalledWith({ status: CaseStatuses.closed });
});
@@ -143,11 +133,8 @@ describe('CasesTableFilters ', () => {
tags: ['pepsi', 'rc'],
},
};
- mount(
-
-
-
- );
+
+ appMockRender.render();
expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] });
});
@@ -186,165 +173,104 @@ describe('CasesTableFilters ', () => {
});
it('StatusFilterWrapper should have a fixed width of 180px', () => {
- const wrapper = mount(
-
-
-
- );
-
- expect(wrapper.find('[data-test-subj="status-filter-wrapper"]').first()).toHaveStyleRule(
- 'flex-basis',
- '180px',
- {
- modifier: '&&',
- }
- );
+ appMockRender.render();
+
+ expect(screen.getByTestId('status-filter-wrapper')).toHaveStyleRule('flex-basis', '180px', {
+ modifier: '&&',
+ });
});
describe('Solution filter', () => {
+ const securitySolution = {
+ id: SECURITY_SOLUTION_OWNER,
+ label: OWNER_INFO[SECURITY_SOLUTION_OWNER].label,
+ iconType: OWNER_INFO[SECURITY_SOLUTION_OWNER].iconType,
+ };
+ const observabilitySolution = {
+ id: OBSERVABILITY_OWNER,
+ label: OWNER_INFO[OBSERVABILITY_OWNER].label,
+ iconType: OWNER_INFO[OBSERVABILITY_OWNER].iconType,
+ };
+
it('shows Solution filter when provided more than 1 availableSolutions', () => {
- const wrapper = mount(
-
-
-
+ appMockRender.render(
+
);
- expect(
- wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists()
- ).toBeTruthy();
+ expect(screen.getByTestId('solution-filter-popover-button')).toBeInTheDocument();
});
it('does not show Solution filter when provided less than 1 availableSolutions', () => {
- const wrapper = mount(
-
-
-
+ appMockRender.render(
+
);
- expect(
- wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists()
- ).toBeFalsy();
+ expect(screen.queryByTestId('solution-filter-popover-button')).not.toBeInTheDocument();
});
- it('should call onFilterChange when selected solution changes', () => {
- const wrapper = mount(
-
-
-
+ it('should call onFilterChange when selected solution changes', async () => {
+ appMockRender.render(
+
);
- wrapper
- .find(`[data-test-subj="options-filter-popover-button-Solution"]`)
- .last()
- .simulate('click');
+ userEvent.click(screen.getByTestId('solution-filter-popover-button'));
+
+ await waitForEuiPopoverOpen();
- wrapper
- .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`)
- .last()
- .simulate('click');
+ userEvent.click(
+ screen.getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`)
+ );
expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] });
});
- it('should deselect all solutions', () => {
- const wrapper = mount(
-
-
-
+ it('should deselect all solutions', async () => {
+ appMockRender.render(
+
);
- wrapper
- .find(`[data-test-subj="options-filter-popover-button-Solution"]`)
- .last()
- .simulate('click');
+ userEvent.click(screen.getByTestId('solution-filter-popover-button'));
- wrapper
- .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`)
- .last()
- .simulate('click');
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(
+ screen.getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`)
+ );
expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] });
- wrapper
- .find(`[data-test-subj="options-filter-popover-item-${SECURITY_SOLUTION_OWNER}"]`)
- .last()
- .simulate('click');
+ userEvent.click(
+ screen.getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`)
+ );
expect(onFilterChanged).toBeCalledWith({ owner: [] });
});
it('does not select a solution on initial render', () => {
- const wrapper = mount(
-
-
-
- );
-
- expect(
- wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).first().props()
- ).toEqual(expect.objectContaining({ hasActiveFilters: false }));
- });
- });
-
- describe('create case button', () => {
- it('should not render the create case button when displayCreateCaseButton and onCreateCasePressed are not passed', () => {
- const wrapper = mount(
-
-
-
+ appMockRender.render(
+
);
- expect(wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`).length).toBe(0);
- });
-
- it('should render the create case button when displayCreateCaseButton and onCreateCasePressed are passed', () => {
- const onCreateCasePressed = jest.fn();
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find(`[data-test-subj="cases-table-add-case-filter-bar"]`)).toBeTruthy();
- });
- it('should call the onCreateCasePressed when create case is clicked', () => {
- const onCreateCasePressed = jest.fn();
- const wrapper = mount(
-
-
-
+ expect(screen.getByTestId('solution-filter-popover-button')).not.toHaveAttribute(
+ 'hasActiveFilters'
);
- wrapper
- .find(`button[data-test-subj="cases-table-add-case-filter-bar"]`)
- .first()
- .simulate('click');
- wrapper.update();
- // NOTE: intentionally checking no arguments are passed
- expect(onCreateCasePressed).toHaveBeenCalledWith();
});
});
describe('assignees filter', () => {
it('should hide the assignees filters on basic license', async () => {
- const result = appMockRender.render();
+ appMockRender.render();
- expect(result.queryByTestId('options-filter-popover-button-assignees')).toBeNull();
+ expect(screen.queryByTestId('options-filter-popover-button-assignees')).toBeNull();
});
it('should show the assignees filters on platinum license', async () => {
@@ -353,9 +279,45 @@ describe('CasesTableFilters ', () => {
});
appMockRender = createAppMockRenderer({ license });
- const result = appMockRender.render();
+ appMockRender.render();
+
+ expect(screen.getByTestId('options-filter-popover-button-assignees')).toBeInTheDocument();
+ });
+ });
+
+ describe('create case button', () => {
+ it('should not render the create case button when isSelectorView is false and onCreateCasePressed are not passed', () => {
+ appMockRender.render();
+ expect(screen.queryByTestId('cases-table-add-case-filter-bar')).not.toBeInTheDocument();
+ });
+
+ it('should render the create case button when isSelectorView is true and onCreateCasePressed are passed', () => {
+ const onCreateCasePressed = jest.fn();
+ appMockRender.render(
+
+ );
+ expect(screen.getByTestId('cases-table-add-case-filter-bar')).toBeInTheDocument();
+ });
- expect(result.getByTestId('options-filter-popover-button-assignees')).toBeInTheDocument();
+ it('should call the onCreateCasePressed when create case is clicked', async () => {
+ const onCreateCasePressed = jest.fn();
+ appMockRender.render(
+
+ );
+
+ userEvent.click(screen.getByTestId('cases-table-add-case-filter-bar'));
+
+ await waitForComponentToUpdate();
+ // NOTE: intentionally checking no arguments are passed
+ expect(onCreateCasePressed).toHaveBeenCalledWith();
});
});
});
diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
index 5032922a06d12..41c46d5137e98 100644
--- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
@@ -15,6 +15,7 @@ import { StatusAll } from '../../../common/ui/types';
import { CaseStatuses } from '../../../common/api';
import type { FilterOptions } from '../../containers/types';
import { FilterPopover } from '../filter_popover';
+import { SolutionFilter } from './solution_filter';
import { StatusFilter } from './status_filter';
import * as i18n from './translations';
import { SeverityFilter } from './severity_filter';
@@ -24,6 +25,7 @@ import { AssigneesFilterPopover } from './assignees_filter';
import type { CurrentUserProfile } from '../types';
import { useCasesFeatures } from '../../common/use_cases_features';
import type { AssigneesFilteringSelection } from '../user_profiles/types';
+import type { Solution } from './types';
interface CasesTableFiltersProps {
countClosedCases: number | null;
@@ -32,8 +34,8 @@ interface CasesTableFiltersProps {
onFilterChanged: (filterOptions: Partial) => void;
initial: FilterOptions;
hiddenStatuses?: CaseStatusWithAllStatus[];
- availableSolutions: string[];
- displayCreateCaseButton?: boolean;
+ availableSolutions: Solution[];
+ isSelectorView?: boolean;
onCreateCasePressed?: () => void;
isLoading: boolean;
currentUserProfile: CurrentUserProfile;
@@ -60,7 +62,7 @@ const CasesTableFiltersComponent = ({
initial = DEFAULT_FILTER_OPTIONS,
hiddenStatuses,
availableSolutions,
- displayCreateCaseButton,
+ isSelectorView = false,
onCreateCasePressed,
isLoading,
currentUserProfile,
@@ -156,6 +158,18 @@ const CasesTableFiltersComponent = ({
+ {isSelectorView && onCreateCasePressed ? (
+
+
+ {i18n.CREATE_CASE_TITLE}
+
+
+ ) : null}
- {caseAssignmentAuthorized ? (
+ {caseAssignmentAuthorized && !isSelectorView ? (
{availableSolutions.length > 1 && (
-
- {displayCreateCaseButton && onCreateCasePressed ? (
-
-
- {i18n.CREATE_CASE_TITLE}
-
-
- ) : null}
);
};
diff --git a/x-pack/plugins/cases/public/components/all_cases/types.ts b/x-pack/plugins/cases/public/components/all_cases/types.ts
index 5014522177570..7cf9c410ec073 100644
--- a/x-pack/plugins/cases/public/components/all_cases/types.ts
+++ b/x-pack/plugins/cases/public/components/all_cases/types.ts
@@ -24,3 +24,8 @@ export interface EuiBasicTableOnChange {
};
sort?: EuiBasicTableSortTypes;
}
+export interface Solution {
+ id: string;
+ label: string;
+ iconType: string;
+}
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
index f11794bcf13e2..060ac64cafb08 100644
--- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
@@ -97,7 +97,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -148,38 +147,7 @@ describe('useCasesColumns ', () => {
"name": "Name",
"render": [Function],
"sortable": true,
- "width": undefined,
- },
- Object {
- "field": "assignees",
- "name": "Assignees",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "field": "tags",
- "name": "Tags",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "align": "right",
- "field": "totalAlerts",
- "name": "Alerts",
- "render": [Function],
- "width": "55px",
- },
- Object {
- "align": "right",
- "field": "owner",
- "name": "Solution",
- "render": [Function],
- },
- Object {
- "align": "right",
- "field": "totalComment",
- "name": "Comments",
- "render": [Function],
+ "width": "55%",
},
Object {
"field": "createdAt",
@@ -187,24 +155,6 @@ describe('useCasesColumns ', () => {
"render": [Function],
"sortable": true,
},
- Object {
- "field": "updatedAt",
- "name": "Updated on",
- "render": [Function],
- "sortable": true,
- "width": "80px",
- },
- Object {
- "name": "External Incident",
- "render": [Function],
- "width": "80px",
- },
- Object {
- "field": "status",
- "name": "Status",
- "render": [Function],
- "sortable": true,
- },
Object {
"field": "severity",
"name": "Severity",
@@ -280,7 +230,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -365,7 +314,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -445,7 +393,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -530,7 +477,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
@@ -575,32 +521,7 @@ describe('useCasesColumns ', () => {
"name": "Name",
"render": [Function],
"sortable": true,
- "width": undefined,
- },
- Object {
- "field": "tags",
- "name": "Tags",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "align": "right",
- "field": "totalAlerts",
- "name": "Alerts",
- "render": [Function],
- "width": "55px",
- },
- Object {
- "align": "right",
- "field": "owner",
- "name": "Solution",
- "render": [Function],
- },
- Object {
- "align": "right",
- "field": "totalComment",
- "name": "Comments",
- "render": [Function],
+ "width": "55%",
},
Object {
"field": "createdAt",
@@ -608,24 +529,6 @@ describe('useCasesColumns ', () => {
"render": [Function],
"sortable": true,
},
- Object {
- "field": "updatedAt",
- "name": "Updated on",
- "render": [Function],
- "sortable": true,
- "width": "80px",
- },
- Object {
- "name": "External Incident",
- "render": [Function],
- "width": "80px",
- },
- Object {
- "field": "status",
- "name": "Status",
- "render": [Function],
- "sortable": true,
- },
Object {
"field": "severity",
"name": "Severity",
@@ -657,32 +560,7 @@ describe('useCasesColumns ', () => {
"name": "Name",
"render": [Function],
"sortable": true,
- "width": undefined,
- },
- Object {
- "field": "tags",
- "name": "Tags",
- "render": [Function],
- "width": undefined,
- },
- Object {
- "align": "right",
- "field": "totalAlerts",
- "name": "Alerts",
- "render": [Function],
- "width": "55px",
- },
- Object {
- "align": "right",
- "field": "owner",
- "name": "Solution",
- "render": [Function],
- },
- Object {
- "align": "right",
- "field": "totalComment",
- "name": "Comments",
- "render": [Function],
+ "width": "55%",
},
Object {
"field": "createdAt",
@@ -690,24 +568,6 @@ describe('useCasesColumns ', () => {
"render": [Function],
"sortable": true,
},
- Object {
- "field": "updatedAt",
- "name": "Updated on",
- "render": [Function],
- "sortable": true,
- "width": "80px",
- },
- Object {
- "name": "External Incident",
- "render": [Function],
- "width": "80px",
- },
- Object {
- "field": "status",
- "name": "Status",
- "render": [Function],
- "sortable": true,
- },
Object {
"field": "severity",
"name": "Severity",
@@ -776,7 +636,6 @@ describe('useCasesColumns ', () => {
"name": "Updated on",
"render": [Function],
"sortable": true,
- "width": undefined,
},
Object {
"name": "External Incident",
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
index a2760c71a8ef5..1f267eca40a84 100644
--- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
@@ -118,7 +118,7 @@ export const useCasesColumns = ({
render: (title: string, theCase: Case) => {
if (theCase.id != null && theCase.title != null) {
const caseDetailsLinkComponent = isSelectorView ? (
-
+ theCase.title
) : (
@@ -137,70 +137,72 @@ export const useCasesColumns = ({
}
return getEmptyTagValue();
},
- width: !isSelectorView ? '20%' : undefined,
+ width: !isSelectorView ? '20%' : '55%',
},
];
- if (caseAssignmentAuthorized) {
+ if (caseAssignmentAuthorized && !isSelectorView) {
columns.push({
field: 'assignees',
name: i18n.ASSIGNEES,
render: (assignees: Case['assignees']) => (
),
- width: !isSelectorView ? '180px' : undefined,
+ width: '180px',
});
}
- columns.push({
- field: 'tags',
- name: i18n.TAGS,
- render: (tags: Case['tags']) => {
- if (tags != null && tags.length > 0) {
- const clampedBadges = (
-
- {tags.map((tag: string, i: number) => (
-
- {tag}
-
- ))}
-
- );
+ if (!isSelectorView) {
+ columns.push({
+ field: 'tags',
+ name: i18n.TAGS,
+ render: (tags: Case['tags']) => {
+ if (tags != null && tags.length > 0) {
+ const clampedBadges = (
+
+ {tags.map((tag: string, i: number) => (
+
+ {tag}
+
+ ))}
+
+ );
- const unclampedBadges = (
-
- {tags.map((tag: string, i: number) => (
-
- {tag}
-
- ))}
-
- );
+ const unclampedBadges = (
+
+ {tags.map((tag: string, i: number) => (
+
+ {tag}
+
+ ))}
+
+ );
- return (
-
- {clampedBadges}
-
- );
- }
- return getEmptyTagValue();
- },
- width: !isSelectorView ? '15%' : undefined,
- });
+ return (
+
+ {clampedBadges}
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ width: '15%',
+ });
+ }
- if (isAlertsEnabled) {
+ if (isAlertsEnabled && !isSelectorView) {
columns.push({
align: RIGHT_ALIGNMENT,
field: 'totalAlerts',
@@ -213,7 +215,7 @@ export const useCasesColumns = ({
});
}
- if (showSolutionColumn) {
+ if (showSolutionColumn && !isSelectorView) {
columns.push({
align: RIGHT_ALIGNMENT,
field: 'owner',
@@ -234,15 +236,17 @@ export const useCasesColumns = ({
});
}
- columns.push({
- align: RIGHT_ALIGNMENT,
- field: 'totalComment',
- name: i18n.COMMENTS,
- render: (totalComment: Case['totalComment']) =>
- totalComment != null
- ? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
- : getEmptyTagValue(),
- });
+ if (!isSelectorView) {
+ columns.push({
+ align: RIGHT_ALIGNMENT,
+ field: 'totalComment',
+ name: i18n.COMMENTS,
+ render: (totalComment: Case['totalComment']) =>
+ totalComment != null
+ ? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
+ : getEmptyTagValue(),
+ });
+ }
if (filterStatus === CaseStatuses.closed) {
columns.push({
@@ -278,67 +282,70 @@ export const useCasesColumns = ({
});
}
+ if (!isSelectorView) {
+ columns.push({
+ field: 'updatedAt',
+ name: i18n.UPDATED_ON,
+ sortable: true,
+ render: (updatedAt: Case['updatedAt']) => {
+ if (updatedAt != null) {
+ return (
+
+
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ });
+ }
+
+ if (!isSelectorView) {
+ columns.push(
+ {
+ name: i18n.EXTERNAL_INCIDENT,
+ render: (theCase: Case) => {
+ if (theCase.id != null) {
+ return ;
+ }
+ return getEmptyTagValue();
+ },
+ width: isSelectorView ? '80px' : undefined,
+ },
+ {
+ field: 'status',
+ name: i18n.STATUS,
+ sortable: true,
+ render: (status: Case['status']) => {
+ if (status != null) {
+ return ;
+ }
+
+ return getEmptyTagValue();
+ },
+ }
+ );
+ }
columns.push({
- field: 'updatedAt',
- name: i18n.UPDATED_ON,
+ field: 'severity',
+ name: i18n.SEVERITY,
sortable: true,
- render: (updatedAt: Case['updatedAt']) => {
- if (updatedAt != null) {
+ render: (severity: Case['severity']) => {
+ if (severity != null) {
+ const severityData = severities[severity ?? CaseSeverity.LOW];
return (
-
-
-
+
+ {severityData.label}
+
);
}
return getEmptyTagValue();
},
- width: isSelectorView ? '80px' : undefined,
});
- columns.push(
- {
- name: i18n.EXTERNAL_INCIDENT,
- render: (theCase: Case) => {
- if (theCase.id != null) {
- return ;
- }
- return getEmptyTagValue();
- },
- width: isSelectorView ? '80px' : undefined,
- },
- {
- field: 'status',
- name: i18n.STATUS,
- sortable: true,
- render: (status: Case['status']) => {
- if (status != null) {
- return ;
- }
-
- return getEmptyTagValue();
- },
- },
- {
- field: 'severity',
- name: i18n.SEVERITY,
- sortable: true,
- render: (severity: Case['severity']) => {
- if (severity != null) {
- const severityData = severities[severity ?? CaseSeverity.LOW];
- return (
-
- {severityData.label}
-
- );
- }
- return getEmptyTagValue();
- },
- }
- );
-
if (isSelectorView) {
columns.push({
align: RIGHT_ALIGNMENT,
@@ -351,7 +358,6 @@ export const useCasesColumns = ({
assignCaseAction(theCase);
}}
size="s"
- fill={true}
>
{i18n.SELECT}
diff --git a/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx b/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx
new file mode 100644
index 0000000000000..a6c6de8f19770
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/filter_popover/index.test.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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 { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
+
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+
+import { FilterPopover } from '.';
+import userEvent from '@testing-library/user-event';
+
+describe('FilterPopover ', () => {
+ let appMockRender: AppMockRenderer;
+ const onSelectedOptionsChanged = jest.fn();
+ const tags: string[] = ['coke', 'pepsi'];
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('renders button label correctly', () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ expect(getByTestId('options-filter-popover-button-Tags')).toBeInTheDocument();
+ });
+
+ it('renders empty label correctly', async () => {
+ const { getByTestId, getByText } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByText('No options available')).toBeInTheDocument();
+ });
+
+ it('renders string type options correctly', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ expect(getByTestId(`options-filter-popover-item-${tags[0]}`)).toBeInTheDocument();
+ expect(getByTestId(`options-filter-popover-item-${tags[1]}`)).toBeInTheDocument();
+ });
+
+ it('should call onSelectionChange with selected option', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`options-filter-popover-item-${tags[0]}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([tags[0]]);
+ });
+
+ it('should call onSelectionChange with empty array when option is deselected', async () => {
+ const { getByTestId } = appMockRender.render(
+
+ );
+
+ userEvent.click(getByTestId('options-filter-popover-button-Tags'));
+
+ await waitForEuiPopoverOpen();
+
+ userEvent.click(getByTestId(`options-filter-popover-item-${tags[0]}`));
+
+ expect(onSelectedOptionsChanged).toHaveBeenCalledWith([]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts
index f74be26963d45..2012d756d6354 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/cases/attach_timeline.cy.ts
@@ -85,9 +85,7 @@ describe('attach timeline to case', () => {
it('modal can be re-opened once closed', function () {
visitTimeline(this.timelineId);
attachTimelineToExistingCase();
- cy.get('[data-test-subj="all-cases-modal"] .euiButton')
- .contains('Cancel')
- .click({ force: true });
+ cy.get('[data-test-subj="all-cases-modal-cancel-button"]').click({ force: true });
cy.get('[data-test-subj="all-cases-modal"]').should('not.exist');
attachTimelineToExistingCase();
diff --git a/x-pack/test/functional/services/cases/create.ts b/x-pack/test/functional/services/cases/create.ts
index 71aec28374f7a..a1fd58d817e66 100644
--- a/x-pack/test/functional/services/cases/create.ts
+++ b/x-pack/test/functional/services/cases/create.ts
@@ -119,6 +119,7 @@ export function CasesCreateViewServiceProvider(
async createCaseFromModal(params: CreateCaseParams) {
await casesCommon.assertCaseModalVisible(true);
await testSubjects.click('cases-table-add-case-filter-bar');
+
await casesCommon.assertCaseModalVisible(false);
await this.creteCaseFromFlyout(params);
},
diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts
index 39713897afaba..1e420cd37368c 100644
--- a/x-pack/test/functional/services/cases/list.ts
+++ b/x-pack/test/functional/services/cases/list.ts
@@ -165,11 +165,11 @@ export function CasesTableServiceProvider(
async filterByOwner(owner: string) {
await common.clickAndValidate(
- 'options-filter-popover-button-Solution',
- `options-filter-popover-item-${owner}`
+ 'solution-filter-popover-button',
+ `solution-filter-popover-item-${owner}`
);
- await testSubjects.click(`options-filter-popover-item-${owner}`);
+ await testSubjects.click(`solution-filter-popover-item-${owner}`);
},
async refreshTable() {
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts
index 193b305d707ad..207434635829f 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/attachment_framework.ts
@@ -302,11 +302,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
it('renders different solutions', async () => {
await openModal();
- await testSubjects.existOrFail('options-filter-popover-button-Solution');
+ await testSubjects.existOrFail('solution-filter-popover-button');
- for (const [owner, caseId] of createdCases.entries()) {
- await testSubjects.existOrFail(`cases-table-row-${caseId}`);
- await testSubjects.existOrFail(`case-table-column-owner-icon-${owner}`);
+ for (const [, currentCaseId] of createdCases.entries()) {
+ await testSubjects.existOrFail(`cases-table-row-${currentCaseId}`);
}
await closeModal();