Disable selection of filter status 'All' on AddToCaseAction (#99757)

* Fix: Disable selection of filter status 'All' on AddToCaseAction

* UI: Hide disabled statuses on AddToCaseAction

* Refactor: Rename disabledStatuses to hiddenStatuses

* Fix: Pick the first valid status for initialFilterOptions

Previously it was always picking 'open', but it wouldn't work when hiddenStatuses contains "open".

* Add missing test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2021-05-14 16:04:44 +02:00 committed by GitHub
parent b6635b00e7
commit fab96050a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 121 additions and 45 deletions

View file

@ -73,7 +73,7 @@ Arguments:
|---|---|
|alertData?|`Omit<CommentRequestAlertType, 'type'>;` alert data to post to case
|createCaseNavigation|`CasesNavigation` route configuration for create cases page
|disabledStatuses?|`CaseStatuses[];` array of disabled statuses
|hiddenStatuses?|`CaseStatuses[];` array of hidden statuses
|onRowClick|<code>(theCase?: Case &vert; SubCase) => void;</code> callback for row click, passing case in row
|updateCase?|<code>(theCase: Case &vert; SubCase) => void;</code> callback after case has been updated
|userCanCrud|`boolean;` user permissions to crud

View file

@ -0,0 +1,70 @@
/*
* 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 { mount } from 'enzyme';
import { AllCasesGeneric } from './all_cases_generic';
import { TestProviders } from '../../common/mock';
import { useGetTags } from '../../containers/use_get_tags';
import { useGetReporters } from '../../containers/use_get_reporters';
import { useGetActionLicense } from '../../containers/use_get_action_license';
import { StatusAll } from '../../containers/types';
import { CaseStatuses } from '../../../common';
import { act } from 'react-dom/test-utils';
jest.mock('../../containers/use_get_reporters');
jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/use_get_action_license');
jest.mock('../../containers/api');
const createCaseNavigation = { href: '', onClick: jest.fn() };
const alertDataMock = {
type: 'alert',
rule: {
id: 'rule-id',
name: 'rule',
},
index: 'index-id',
alertId: 'alert-id',
};
describe('AllCasesGeneric ', () => {
beforeEach(() => {
jest.resetAllMocks();
(useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() });
(useGetReporters as jest.Mock).mockReturnValue({
reporters: ['casetester'],
respReporters: [{ username: 'casetester' }],
isLoading: true,
isError: false,
fetchReporters: jest.fn(),
});
(useGetActionLicense as jest.Mock).mockReturnValue({
actionLicense: null,
isLoading: false,
});
});
it('renders the first available status when hiddenStatus is given', () =>
act(async () => {
const wrapper = mount(
<TestProviders>
<AllCasesGeneric
alertData={alertDataMock}
createCaseNavigation={createCaseNavigation}
hiddenStatuses={[StatusAll, CaseStatuses.open]}
isSelectorView={true}
userCanCrud={true}
/>
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="status-badge-in-progress"]`).exists()).toBeTruthy();
}));
});

View file

@ -8,7 +8,7 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { EuiProgress } from '@elastic/eui';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { isEmpty, memoize } from 'lodash/fp';
import { difference, head, isEmpty, memoize } from 'lodash/fp';
import styled, { css } from 'styled-components';
import classnames from 'classnames';
@ -17,10 +17,12 @@ import {
CaseStatuses,
CaseType,
CommentRequestAlertType,
CaseStatusWithAllStatus,
CommentType,
FilterOptions,
SortFieldCase,
SubCase,
caseStatuses,
} from '../../../common';
import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations';
import { useGetActionLicense } from '../../containers/use_get_action_license';
@ -59,7 +61,7 @@ interface AllCasesGenericProps {
caseDetailsNavigation?: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>; // if not passed, case name is not displayed as a link (Formerly dependant on isSelectorView)
configureCasesNavigation?: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelectorView)
createCaseNavigation: CasesNavigation;
disabledStatuses?: CaseStatuses[];
hiddenStatuses?: CaseStatusWithAllStatus[];
isSelectorView?: boolean;
onRowClick?: (theCase?: Case | SubCase) => void;
updateCase?: (newCase: Case) => void;
@ -72,13 +74,17 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
caseDetailsNavigation,
configureCasesNavigation,
createCaseNavigation,
disabledStatuses,
hiddenStatuses = [],
isSelectorView,
onRowClick,
updateCase,
userCanCrud,
}) => {
const { actionLicense } = useGetActionLicense();
const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses));
const initialFilterOptions =
!isEmpty(hiddenStatuses) && firstAvailableStatus ? { status: firstAvailableStatus } : {};
const {
data,
dispatchUpdateCaseProperty,
@ -90,7 +96,7 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
setFilters,
setQueryParams,
setSelectedCases,
} = useGetCases();
} = useGetCases({}, initialFilterOptions);
// Post Comment to Case
const { postComment, isLoading: isCommentUpdating } = usePostComment();
@ -288,7 +294,7 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
status: filterOptions.status,
}}
setFilterRefetch={setFilterRefetch}
disabledStatuses={disabledStatuses}
hiddenStatuses={hiddenStatuses}
/>
<CasesTable
columns={columns}

View file

@ -60,7 +60,7 @@ describe('AllCasesSelectorModal', () => {
index: 'index-id',
alertId: 'alert-id',
},
disabledStatuses: [],
hiddenStatuses: [],
updateCase,
};
mount(
@ -73,7 +73,7 @@ describe('AllCasesSelectorModal', () => {
expect.objectContaining({
alertData: fullProps.alertData,
createCaseNavigation,
disabledStatuses: fullProps.disabledStatuses,
hiddenStatuses: fullProps.hiddenStatuses,
isSelectorView: true,
userCanCrud: fullProps.userCanCrud,
updateCase,

View file

@ -8,7 +8,12 @@
import React, { useState, useCallback } from 'react';
import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import styled from 'styled-components';
import { Case, CaseStatuses, CommentRequestAlertType, SubCase } from '../../../../common';
import {
Case,
CaseStatusWithAllStatus,
CommentRequestAlertType,
SubCase,
} from '../../../../common';
import { CasesNavigation } from '../../links';
import * as i18n from '../../../common/translations';
import { AllCasesGeneric } from '../all_cases_generic';
@ -16,7 +21,7 @@ import { AllCasesGeneric } from '../all_cases_generic';
export interface AllCasesSelectorModalProps {
alertData?: Omit<CommentRequestAlertType, 'type'>;
createCaseNavigation: CasesNavigation;
disabledStatuses?: CaseStatuses[];
hiddenStatuses?: CaseStatusWithAllStatus[];
onRowClick: (theCase?: Case | SubCase) => void;
updateCase?: (newCase: Case) => void;
userCanCrud: boolean;
@ -32,7 +37,7 @@ const Modal = styled(EuiModal)`
export const AllCasesSelectorModal: React.FC<AllCasesSelectorModalProps> = ({
alertData,
createCaseNavigation,
disabledStatuses,
hiddenStatuses,
onRowClick,
updateCase,
userCanCrud,
@ -55,7 +60,7 @@ export const AllCasesSelectorModal: React.FC<AllCasesSelectorModalProps> = ({
<AllCasesGeneric
alertData={alertData}
createCaseNavigation={createCaseNavigation}
disabledStatuses={disabledStatuses}
hiddenStatuses={hiddenStatuses}
isSelectorView={true}
onRowClick={onClick}
userCanCrud={userCanCrud}

View file

@ -63,23 +63,20 @@ describe('StatusFilter', () => {
});
});
it('should disabled selected statuses', () => {
it('should not render hidden statuses', () => {
const wrapper = mount(
<StatusFilter {...defaultProps} disabledStatuses={[CaseStatuses.closed]} />
<StatusFilter {...defaultProps} hiddenStatuses={[StatusAll, CaseStatuses.closed]} />
);
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
expect(
wrapper.find('button[data-test-subj="case-status-filter-open"]').prop('disabled')
).toBeFalsy();
expect(wrapper.find(`[data-test-subj="case-status-filter-all"]`).exists()).toBeFalsy();
expect(wrapper.find('button[data-test-subj="case-status-filter-closed"]').exists()).toBeFalsy();
expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').exists()).toBeTruthy();
expect(
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').prop('disabled')
).toBeFalsy();
expect(
wrapper.find('button[data-test-subj="case-status-filter-closed"]').prop('disabled')
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').exists()
).toBeTruthy();
});
});

View file

@ -14,32 +14,30 @@ interface Props {
stats: Record<CaseStatusWithAllStatus, number | null>;
selectedStatus: CaseStatusWithAllStatus;
onStatusChanged: (status: CaseStatusWithAllStatus) => void;
disabledStatuses?: CaseStatusWithAllStatus[];
hiddenStatuses?: CaseStatusWithAllStatus[];
}
const StatusFilterComponent: React.FC<Props> = ({
stats,
selectedStatus,
onStatusChanged,
disabledStatuses = [],
hiddenStatuses = [],
}) => {
const caseStatuses = Object.keys(statuses) as CaseStatusWithAllStatus[];
const options: Array<EuiSuperSelectOption<CaseStatusWithAllStatus>> = [
StatusAll,
...caseStatuses,
].map((status) => ({
value: status,
inputDisplay: (
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
<EuiFlexItem grow={false}>
<Status type={status} />
</EuiFlexItem>
{status !== StatusAll && <EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>}
</EuiFlexGroup>
),
disabled: disabledStatuses.includes(status),
'data-test-subj': `case-status-filter-${status}`,
}));
const options: Array<EuiSuperSelectOption<CaseStatusWithAllStatus>> = [StatusAll, ...caseStatuses]
.filter((status) => !hiddenStatuses.includes(status))
.map((status) => ({
value: status,
inputDisplay: (
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
<EuiFlexItem grow={false}>
<Status type={status} />
</EuiFlexItem>
{status !== StatusAll && <EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>}
</EuiFlexGroup>
),
'data-test-subj': `case-status-filter-${status}`,
}));
return (
<EuiSuperSelect

View file

@ -25,7 +25,7 @@ interface CasesTableFiltersProps {
onFilterChanged: (filterOptions: Partial<FilterOptions>) => void;
initial: FilterOptions;
setFilterRefetch: (val: () => void) => void;
disabledStatuses?: CaseStatuses[];
hiddenStatuses?: CaseStatusWithAllStatus[];
}
// Fix the width of the status dropdown to prevent hiding long text items
@ -56,7 +56,7 @@ const CasesTableFiltersComponent = ({
onFilterChanged,
initial = defaultInitial,
setFilterRefetch,
disabledStatuses,
hiddenStatuses,
}: CasesTableFiltersProps) => {
const [selectedReporters, setSelectedReporters] = useState(
initial.reporters.map((r) => r.full_name ?? r.username ?? '')
@ -161,7 +161,7 @@ const CasesTableFiltersComponent = ({
selectedStatus={initial.status}
onStatusChanged={onStatusChanged}
stats={stats}
disabledStatuses={disabledStatuses}
hiddenStatuses={hiddenStatuses}
/>
</StatusFilterWrapper>
</EuiFlexGroup>

View file

@ -16,7 +16,7 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { Case, CaseStatuses } from '../../../../../cases/common';
import { Case, CaseStatuses, StatusAll } from '../../../../../cases/common';
import { APP_ID } from '../../../../common/constants';
import { Ecs } from '../../../../common/ecs';
import { SecurityPageName } from '../../../app/types';
@ -240,7 +240,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
href: formatUrl(getCreateCaseUrl()),
onClick: goToCreateCase,
},
disabledStatuses: [CaseStatuses.closed],
hiddenStatuses: [CaseStatuses.closed, StatusAll],
onRowClick: onCaseClicked,
updateCase: onCaseSuccess,
userCanCrud: userPermissions?.crud ?? false,