Skip to content

Commit

Permalink
[Cases] [118684] feature update case selection modal with ability to …
Browse files Browse the repository at this point in the history
…filter and show cases by solution type (#120321)

* Add canUseCases functionality

* Allow case selection modal to display and filter by case type

* Update canUseCases to return an object, add tests for modal

* Add tests for canUseCases

* PR review changes

* add can use cases to mock function

* Update filterpopover to make optionsEmptyLabel optional

* PR suggestions: up date function description, and column safeguard

Co-authored-by: Kristof-Pierre Cummings <[email protected]>
  • Loading branch information
jamster10 and Kristof-Pierre Cummings authored Dec 23, 2021
1 parent 058caf8 commit 6c8974f
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 16 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface FilterOptions {
status: CaseStatusWithAllStatus;
tags: string[];
reporters: User[];
owner: string[];
onlyCollectionType?: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jest.mock('../../containers/use_get_action_license');
jest.mock('../../containers/configure/use_connectors');
jest.mock('../../common/lib/kibana');
jest.mock('../../common/navigation/hooks');
jest.mock('../app/use_available_owners', () => ({
useAvailableCasesOwners: () => ['securitySolution', 'observability'],
}));

const useDeleteCasesMock = useDeleteCases as jest.Mock;
const useGetCasesMock = useGetCases as jest.Mock;
Expand Down Expand Up @@ -780,6 +783,32 @@ describe('AllCasesListGeneric', () => {
expect(doRefresh).toHaveBeenCalled();
});

it('shows Solution column if there are no set owners', async () => {
const doRefresh = jest.fn();

const wrapper = mount(
<TestProviders owner={[]}>
<AllCasesList isSelectorView={false} doRefresh={doRefresh} />
</TestProviders>
);

const solutionHeader = wrapper.find({ children: 'Solution' });
expect(solutionHeader.exists()).toBeTruthy();
});

it('hides Solution column if there is a set owner', async () => {
const doRefresh = jest.fn();

const wrapper = mount(
<TestProviders>
<AllCasesList isSelectorView={false} doRefresh={doRefresh} />
</TestProviders>
);

const solutionHeader = wrapper.find({ children: 'Solution' });
expect(solutionHeader.exists()).toBeFalsy();
});

it('should deselect cases when refreshing', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations';
import { useGetCases } from '../../containers/use_get_cases';
import { usePostComment } from '../../containers/use_post_comment';

import { useAvailableCasesOwners } from '../app/use_available_owners';
import { useCasesColumns } from './columns';
import { getExpandedRowMap } from './expanded_row';
import { CasesTableFilters } from './table_filters';
Expand Down Expand Up @@ -66,10 +67,15 @@ export const AllCasesList = React.memo<AllCasesListProps>(
updateCase,
doRefresh,
}) => {
const { userCanCrud } = useCasesContext();
const { owner, userCanCrud } = useCasesContext();
const hasOwner = !!owner.length;
const availableSolutions = useAvailableCasesOwners();

const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses));
const initialFilterOptions =
!isEmpty(hiddenStatuses) && firstAvailableStatus ? { status: firstAvailableStatus } : {};
const initialFilterOptions = {
...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: firstAvailableStatus }),
owner: hasOwner ? owner : availableSolutions,
};

const {
data,
Expand Down Expand Up @@ -181,6 +187,7 @@ export const AllCasesList = React.memo<AllCasesListProps>(
alertData,
postComment,
updateCase,
showSolutionColumn: !hasOwner && availableSolutions.length > 1,
});

const itemIdToExpandedRowMap = useMemo(
Expand Down Expand Up @@ -235,11 +242,13 @@ export const AllCasesList = React.memo<AllCasesListProps>(
countOpenCases={data.countOpenCases}
countInProgressCases={data.countInProgressCases}
onFilterChanged={onFilterChangedCallback}
availableSolutions={hasOwner ? [] : availableSolutions}
initial={{
search: filterOptions.search,
reporters: filterOptions.reporters,
tags: filterOptions.tags,
status: filterOptions.status,
owner: filterOptions.owner,
}}
setFilterRefetch={setFilterRefetch}
hiddenStatuses={hiddenStatuses}
Expand Down
21 changes: 21 additions & 0 deletions x-pack/plugins/cases/public/components/all_cases/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
CommentRequestAlertType,
ActionConnector,
} from '../../../common/api';
import { OWNER_INFO } from '../../../common/constants';
import { getEmptyTagValue } from '../empty_value';
import { FormattedRelativePreferenceDate } from '../formatted_date';
import { CaseDetailsLink } from '../links';
Expand All @@ -45,6 +46,7 @@ import { StatusContextMenu } from '../case_action_bar/status_context_menu';
import { TruncatedText } from '../truncated_text';
import { getConnectorIcon } from '../utils';
import { PostComment } from '../../containers/use_post_comment';
import type { CasesOwners } from '../../methods/can_use_cases';

export type CasesColumns =
| EuiTableActionsColumnType<Case>
Expand Down Expand Up @@ -79,6 +81,7 @@ export interface GetCasesColumn {
alertData?: Omit<CommentRequestAlertType, 'type'>;
postComment?: (args: PostComment) => Promise<void>;
updateCase?: (newCase: Case) => void;
showSolutionColumn?: boolean;
}
export const useCasesColumns = ({
dispatchUpdateCaseProperty,
Expand All @@ -93,6 +96,7 @@ export const useCasesColumns = ({
alertData,
postComment,
updateCase,
showSolutionColumn,
}: GetCasesColumn): CasesColumns[] => {
// Delete case
const {
Expand Down Expand Up @@ -251,6 +255,23 @@ export const useCasesColumns = ({
? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`)
: getEmptyTagValue(),
},
...(showSolutionColumn
? [
{
align: RIGHT_ALIGNMENT,
field: 'owner',
name: i18n.SOLUTION,
render: (caseOwner: CasesOwners) => {
const ownerInfo = OWNER_INFO[caseOwner];
return ownerInfo ? (
<EuiIcon size="s" type={ownerInfo.iconType} title={ownerInfo.label} />
) : (
getEmptyTagValue()
);
},
},
]
: []),
{
align: RIGHT_ALIGNMENT,
field: 'totalComment',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';
import { mount } from 'enzyme';

import { CaseStatuses } from '../../../common/api';
import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { TestProviders } from '../../common/mock';
import { useGetTags } from '../../containers/use_get_tags';
import { useGetReporters } from '../../containers/use_get_reporters';
Expand All @@ -30,6 +31,7 @@ const props = {
onFilterChanged,
initial: DEFAULT_FILTER_OPTIONS,
setFilterRefetch,
availableSolutions: [],
};

describe('CasesTableFilters ', () => {
Expand Down Expand Up @@ -168,4 +170,50 @@ describe('CasesTableFilters ', () => {
}
);
});

describe('dynamic Solution filter', () => {
it('shows Solution filter when provided more than 1 availableSolutions', () => {
const wrapper = mount(
<TestProviders>
<CasesTableFilters
{...props}
availableSolutions={[SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER]}
/>
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists()
).toBeTruthy();
});

it('does not show Solution filter when provided less than 1 availableSolutions', () => {
const wrapper = mount(
<TestProviders>
<CasesTableFilters {...props} availableSolutions={[OBSERVABILITY_OWNER]} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="options-filter-popover-button-Solution"]`).exists()
).toBeFalsy();
});
});

it('should call onFilterChange when selected solution changes', () => {
const wrapper = mount(
<TestProviders>
<CasesTableFilters
{...props}
availableSolutions={[SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER]}
/>
</TestProviders>
);
wrapper
.find(`[data-test-subj="options-filter-popover-button-Solution"]`)
.last()
.simulate('click');

wrapper.find(`[data-test-subj="options-filter-popover-item-0"]`).last().simulate('click');

expect(onFilterChanged).toBeCalledWith({ owner: [SECURITY_SOLUTION_OWNER] });
});
});
22 changes: 22 additions & 0 deletions x-pack/plugins/cases/public/components/all_cases/table_filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface CasesTableFiltersProps {
initial: FilterOptions;
setFilterRefetch: (val: () => void) => void;
hiddenStatuses?: CaseStatusWithAllStatus[];
availableSolutions: string[];
}

// Fix the width of the status dropdown to prevent hiding long text items
Expand All @@ -48,6 +49,7 @@ const defaultInitial = {
reporters: [],
status: StatusAll,
tags: [],
owner: [],
};

const CasesTableFiltersComponent = ({
Expand All @@ -58,12 +60,14 @@ const CasesTableFiltersComponent = ({
initial = defaultInitial,
setFilterRefetch,
hiddenStatuses,
availableSolutions,
}: CasesTableFiltersProps) => {
const [selectedReporters, setSelectedReporters] = useState(
initial.reporters.map((r) => r.full_name ?? r.username ?? '')
);
const [search, setSearch] = useState(initial.search);
const [selectedTags, setSelectedTags] = useState(initial.tags);
const [selectedOwner, setSelectedOwner] = useState(initial.owner);
const { tags, fetchTags } = useGetTags();
const { reporters, respReporters, fetchReporters } = useGetReporters();

Expand Down Expand Up @@ -108,6 +112,16 @@ const CasesTableFiltersComponent = ({
[onFilterChanged, selectedTags]
);

const handleSelectedSolution = useCallback(
(newOwner) => {
if (!isEqual(newOwner, selectedOwner) && newOwner.length) {
setSelectedOwner(newOwner);
onFilterChanged({ owner: newOwner });
}
},
[onFilterChanged, selectedOwner]
);

useEffect(() => {
if (selectedTags.length) {
const newTags = selectedTags.filter((t) => tags.includes(t));
Expand Down Expand Up @@ -183,6 +197,14 @@ const CasesTableFiltersComponent = ({
options={tags}
optionsEmptyLabel={i18n.NO_TAGS_AVAILABLE}
/>
{availableSolutions.length > 1 && (
<FilterPopover
buttonLabel={i18n.SOLUTION}
onSelectedOptionsChanged={handleSelectedSolution}
selectedOptions={selectedOwner}
options={availableSolutions}
/>
)}
</EuiFilterGroup>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
*/

import { renderHook } from '@testing-library/react-hooks';
import { SECURITY_SOLUTION_OWNER } from '../../../common';
import { OBSERVABILITY_OWNER } from '../../../common/constants';

import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { useKibana } from '../../common/lib/kibana';
import { useAvailableCasesOwners } from './use_available_owners';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface FilterPopoverProps {
buttonLabel: string;
onSelectedOptionsChanged: Dispatch<SetStateAction<string[]>>;
options: string[];
optionsEmptyLabel: string;
optionsEmptyLabel?: string;
selectedOptions: string[];
}

Expand Down Expand Up @@ -99,7 +99,7 @@ export const FilterPopoverComponent = ({
</EuiFilterSelectItem>
))}
</ScrollableDiv>
{options.length === 0 && (
{options.length === 0 && optionsEmptyLabel != null && (
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
<EuiFlexItem grow={true}>
<EuiPanel>
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/cases/public/containers/use_get_cases.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('useGetCases', () => {
});
await waitForNextUpdate();
expect(spyOnGetCases).toBeCalledWith({
filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] },
filterOptions: { ...DEFAULT_FILTER_OPTIONS },
queryParams: DEFAULT_QUERY_PARAMS,
signal: abortCtrl.signal,
});
Expand Down Expand Up @@ -174,6 +174,7 @@ describe('useGetCases', () => {
search: 'new',
tags: ['new'],
status: CaseStatuses.closed,
owner: [SECURITY_SOLUTION_OWNER],
};

const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases(), {
Expand Down Expand Up @@ -212,7 +213,7 @@ describe('useGetCases', () => {
await waitForNextUpdate();

expect(spyOnGetCases.mock.calls[1][0]).toEqual({
filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] },
filterOptions: { ...DEFAULT_FILTER_OPTIONS },
queryParams: {
...DEFAULT_QUERY_PARAMS,
...newQueryParams,
Expand Down
7 changes: 3 additions & 4 deletions x-pack/plugins/cases/public/containers/use_get_cases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
import { useToasts } from '../common/lib/kibana';
import * as i18n from './translations';
import { getCases, patchCase } from './api';
import { useCasesContext } from '../components/cases_context/use_cases_context';

export interface UseGetCasesState {
data: AllCases;
Expand Down Expand Up @@ -106,6 +105,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
status: StatusAll,
tags: [],
onlyCollectionType: false,
owner: [],
};

export const DEFAULT_QUERY_PARAMS: QueryParams = {
Expand Down Expand Up @@ -145,7 +145,6 @@ export const useGetCases = (
initialFilterOptions?: Partial<FilterOptions>;
} = {}
): UseGetCases => {
const { owner } = useCasesContext();
const { initialQueryParams = empty, initialFilterOptions = empty } = params;
const [state, dispatch] = useReducer(dataFetchReducer, {
data: initialData,
Expand Down Expand Up @@ -185,7 +184,7 @@ export const useGetCases = (
dispatch({ type: 'FETCH_INIT', payload: 'cases' });

const response = await getCases({
filterOptions: { ...filterOptions, owner },
filterOptions,
queryParams,
signal: abortCtrlFetchCases.current.signal,
});
Expand All @@ -208,7 +207,7 @@ export const useGetCases = (
}
}
},
[owner, toasts]
[toasts]
);

const dispatchUpdateCaseProperty = useCallback(
Expand Down
Loading

0 comments on commit 6c8974f

Please sign in to comment.