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:
parent
b6635b00e7
commit
fab96050a4
|
@ -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 | SubCase) => void;</code> callback for row click, passing case in row
|
||||
|updateCase?|<code>(theCase: Case | SubCase) => void;</code> callback after case has been updated
|
||||
|userCanCrud|`boolean;` user permissions to crud
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
});
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue