Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Refactor: Move cases action buttons out of timelines #126265

Merged
Merged
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
bcfad7e
WIP
Feb 10, 2022
905abb5
WIP2
Feb 11, 2022
dc424af
Use new cases context hooks to open and close the flyout
Feb 14, 2022
143d99d
Update timelines to use new hooks
Feb 14, 2022
6959d73
CLose flyout on create success
Feb 14, 2022
0b40092
Add back sucess toast
Feb 14, 2022
e3f4133
Move code to a dedicated component
Feb 14, 2022
f14fdaf
Add CasesContext to observability
Feb 14, 2022
7e1e92f
Remove dependency
Feb 14, 2022
352ca84
Small refactor
Feb 14, 2022
0ff9cbb
Use observabilityAppId instead of observabilityFeatureId for buttons
Feb 14, 2022
49b2985
Add CasesContext to timetable
Feb 15, 2022
4db364e
Fix detection engine test cases
Feb 15, 2022
0c1d07c
Fix broken tests
Feb 15, 2022
b48bdb1
Fix broken tests
Feb 16, 2022
05ad4c4
Rename hook
Feb 16, 2022
ac2c745
Add test cases for cases context ui
Feb 16, 2022
8df4ff9
Add test for new hook
Feb 16, 2022
c48e33f
Remove state from the provider context
Feb 16, 2022
74a5fa3
Remove basevalue
Feb 16, 2022
49387de
apply suggested renaming
Feb 16, 2022
1ca5435
Add usecallback
Feb 16, 2022
0573285
Add reducer types, fix test type, remove redundant check
Feb 17, 2022
2114d79
Accept attachments as a prop for the cases select modal
Feb 21, 2022
2e6307a
Expose useCasesAddToExistingCase hook, reducer code and global component
Feb 21, 2022
cd78b3c
use the new hook to open the select cases modasl
Feb 21, 2022
a2c0428
Fix tests and types
Feb 21, 2022
5b48b85
Add tests for cases global components
Feb 21, 2022
9ecb270
[Fleet] showing agent policy creation error message on UI (#125931)
juliaElastic Feb 21, 2022
09154c2
[ResponseOps] Adds tooltip to time window selector in ES query rule f…
szabosteve Feb 21, 2022
531eb8f
[Lens] Allow detaching from global time range (#125563)
flash1293 Feb 21, 2022
95c437c
[Fleet] refactor auto upgrade package policies logic (#125909)
juliaElastic Feb 21, 2022
6140292
skip flaky suite (#126027)
mistic Feb 21, 2022
b9af573
Remove deprecated api (#125524)
criamico Feb 21, 2022
62cfeac
Upgrade `markdown-it` dependency (`10.0.0` → `12.3.2`). (#125526)
azasypkin Feb 21, 2022
fc43fb7
skipping failing tests (#126039)
MadameSheema Feb 21, 2022
89b17a2
remove unused deprecated code and use field format plugin directly fo…
mattkime Feb 21, 2022
5be8008
[data views] Improve preview pane (#126013)
mattkime Feb 21, 2022
c06f45b
[Alerting] Provide services to set context for recovered alerts (#124…
ymao1 Feb 21, 2022
fef5006
[Discover] Re-introduce saved_searches test (#126059)
Feb 21, 2022
d0f0f0a
[Archive Migration] index pattern without timefield (#125870)
Feb 21, 2022
43f9642
delete unused es_archive visualize_embedding (#126001)
Feb 21, 2022
b954622
Bump packages (#126119)
jportner Feb 22, 2022
4cb5e60
Add tests for all cases selector and attachments
Feb 22, 2022
e3c0706
Add tests for use add to existing case hook
Feb 22, 2022
6a1d4e6
Merge remote-tracking branch 'upstream/main' into refactor/cases-add-…
Feb 22, 2022
3b1a27e
Merge remote-tracking branch 'upstream/main' into refactor/cases-add-…
Feb 22, 2022
051d73c
Merge remote-tracking branch 'upstream/main' into refactor/cases-add-…
Feb 23, 2022
b3aa5b4
First version of the cases timeline actions
Feb 23, 2022
e8bb69f
export add alert to new case button from cases plugin
Feb 23, 2022
dc0799d
Make Cases ECS compatible with timelines and security_solution
Feb 23, 2022
8276423
Delete new case button
Feb 23, 2022
00a4045
Add helpers
Feb 23, 2022
564067a
Use the cases hook directly for add to new case
Feb 23, 2022
8da19dc
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Feb 23, 2022
f59c2af
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Feb 24, 2022
5defb0b
Remov unused dependencies
Feb 24, 2022
00953b0
Rename callbacks, remove timelines calls
Feb 24, 2022
2226ed7
Fixing tests for the dropdown
Feb 24, 2022
dcecbbd
Fix broken test
Feb 24, 2022
edbf3ca
mocking cases for tests
Feb 24, 2022
c55e626
Fix detectiosn tests
Feb 24, 2022
a46c296
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Feb 24, 2022
7bfb741
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Feb 28, 2022
5f06c64
Observability now uses the new cases hooks
Feb 28, 2022
9f1be0b
Wrap events viewer into cases context
Feb 28, 2022
e357096
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Feb 28, 2022
b8218df
Merge branch 'main' into refactor/cases-action-buttons-out-of-timelines
academo Feb 28, 2022
0b35248
Open the create case flyout if create case was selected in the modal
Feb 28, 2022
8e23e5c
Fix cases mocks for security_solution
Feb 28, 2022
a8485ba
Update tests
Feb 28, 2022
9ba06da
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Mar 1, 2022
480263e
Add tests for use cases toast
Mar 1, 2022
c5b55da
Improve cases mock
Mar 1, 2022
c5a5518
delete security mock
Mar 1, 2022
1181fe7
replace tests mocks for cases
Mar 1, 2022
c092167
fix import mock
Mar 1, 2022
302316b
Do not require onRowClick
Mar 1, 2022
d525312
Show the toast inside the modal
Mar 1, 2022
ba0da91
show the toast inside the flyout
Mar 1, 2022
2eab5da
remove toast logic from the consumer plugin
Mar 1, 2022
a594b93
fix typescript types
Mar 1, 2022
c322fa1
Rename type
Mar 1, 2022
b5b3a59
Fix broken test
Mar 1, 2022
cd0be88
Fix file name and broken test
Mar 1, 2022
de1c0f1
Use internal navigation hook
Mar 1, 2022
b92c00c
Update hook dependencies
Mar 1, 2022
cf47c49
Move useCaseToast
Mar 1, 2022
f80a1b7
Fix mock paths
Mar 1, 2022
9294657
fix eslint
Mar 1, 2022
5147ade
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Mar 2, 2022
115998c
Add test cases for the toast content
Mar 2, 2022
fbee51e
Merge remote-tracking branch 'upstream/main' into refactor/cases-acti…
Mar 2, 2022
8878fa1
Add cases context to the overview page
Mar 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export interface RuleEcs {
id?: string[];
rule_id?: string[];
name?: string[];
false_positives: string[];
false_positives?: string[];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is necessary to allow integration with security and observability that define this as possible undefined on their own ECS types.

saved_id?: string[];
timeline_id?: string[];
timeline_title?: string[];
Expand Down
14 changes: 14 additions & 0 deletions x-pack/plugins/cases/public/common/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,17 @@ export const MAX_LENGTH_ERROR = (field: string, length: number) =>
export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appropriateLicense', {
defaultMessage: 'appropriate license',
});

export const CASE_SUCCESS_TOAST = (title: string) =>
i18n.translate('xpack.cases.actions.caseSuccessToast', {
values: { title },
defaultMessage: 'An alert has been added to "{title}"',
});

export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSuccessSyncText', {
defaultMessage: 'Alerts in this case have their status synched with the case status',
});

export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', {
defaultMessage: 'View Case',
});
75 changes: 75 additions & 0 deletions x-pack/plugins/cases/public/common/use_cases_toast.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useToasts } from '../common/lib/kibana';
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../common/mock';
import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast';
import { mockCase } from '../containers/mock';
import React from 'react';
import userEvent from '@testing-library/user-event';

jest.mock('../common/lib/kibana');

const useToastsMock = useToasts as jest.Mock;

describe('Use cases toast hook', () => {
academo marked this conversation as resolved.
Show resolved Hide resolved
describe('Toast hook', () => {
const successMock = jest.fn();
useToastsMock.mockImplementation(() => {
return {
addSuccess: successMock,
};
});
it('should create a success tost when invoked with a case', () => {
const { result } = renderHook(
() => {
return useCasesToast();
},
{ wrapper: TestProviders }
);
result.current.showSuccessAttach(mockCase);
expect(successMock).toHaveBeenCalled();
});
});
describe('Toast content', () => {
let appMockRender: AppMockRenderer;
const onViewCaseClick = jest.fn();
beforeEach(() => {
appMockRender = createAppMockRenderer();
onViewCaseClick.mockReset();
});

it('renders a correct successfull message with synced alerts', () => {
const result = appMockRender.render(
<CaseToastSuccessContent syncAlerts={true} onViewCaseClick={onViewCaseClick} />
);
expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent(
'Alerts in this case have their status synched with the case status'
);
expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case');
expect(onViewCaseClick).not.toHaveBeenCalled();
});

it('renders a correct successfull message with not synced alerts', () => {
const result = appMockRender.render(
<CaseToastSuccessContent syncAlerts={false} onViewCaseClick={onViewCaseClick} />
);
expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy();
expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case');
expect(onViewCaseClick).not.toHaveBeenCalled();
});

it('Calls the onViewCaseClick when clicked', () => {
const result = appMockRender.render(
<CaseToastSuccessContent syncAlerts={false} onViewCaseClick={onViewCaseClick} />
);
userEvent.click(result.getByTestId('toaster-content-case-view-link'));
expect(onViewCaseClick).toHaveBeenCalled();
});
});
});
82 changes: 82 additions & 0 deletions x-pack/plugins/cases/public/common/use_cases_toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 { EuiButtonEmpty, EuiText } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { Case } from '../../common';
import { useToasts } from '../common/lib/kibana';
import { useCaseViewNavigation } from '../common/navigation';
import { CASE_SUCCESS_SYNC_TEXT, CASE_SUCCESS_TOAST, VIEW_CASE } from './translations';

const LINE_CLAMP = 3;
const Title = styled.span`
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: ${LINE_CLAMP};
-webkit-box-orient: vertical;
overflow: hidden;
`;
const EuiTextStyled = styled(EuiText)`
${({ theme }) => `
margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px;
`}
`;

export const useCasesToast = () => {
const { navigateToCaseView } = useCaseViewNavigation();

const toasts = useToasts();

return {
showSuccessAttach: (theCase: Case) => {
const onViewCaseClick = () => {
navigateToCaseView({
detailName: theCase.id,
});
};
return toasts.addSuccess({
color: 'success',
iconType: 'check',
title: toMountPoint(<Title>{CASE_SUCCESS_TOAST(theCase.title)}</Title>),
text: toMountPoint(
<CaseToastSuccessContent
syncAlerts={theCase.settings.syncAlerts}
onViewCaseClick={onViewCaseClick}
/>
),
});
},
};
};
export const CaseToastSuccessContent = ({
syncAlerts,
onViewCaseClick,
}: {
syncAlerts: boolean;
onViewCaseClick: () => void;
}) => {
return (
<>
{syncAlerts && (
<EuiTextStyled size="s" data-test-subj="toaster-content-sync-text">
{CASE_SUCCESS_SYNC_TEXT}
</EuiTextStyled>
)}
<EuiButtonEmpty
size="xs"
flush="left"
onClick={onViewCaseClick}
data-test-subj="toaster-content-case-view-link"
>
{VIEW_CASE}
</EuiButtonEmpty>
</>
);
};
CaseToastSuccessContent.displayName = 'CaseToastSuccessContent';
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface AllCasesSelectorModalProps {
*/
alertData?: Omit<CommentRequestAlertType, 'type'>;
hiddenStatuses?: CaseStatusWithAllStatus[];
onRowClick: (theCase?: Case) => void;
onRowClick?: (theCase?: Case) => void;
updateCase?: (newCase: Case) => void;
onClose?: () => void;
attachments?: CaseAttachments;
Expand All @@ -52,7 +52,9 @@ export const AllCasesSelectorModal = React.memo<AllCasesSelectorModalProps>(
const onClick = useCallback(
(theCase?: Case) => {
closeModal();
onRowClick(theCase);
if (onRowClick) {
onRowClick(theCase);
}
},
[closeModal, onRowClick]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React from 'react';
import { CasesContext } from '../../cases_context';
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal';
jest.mock('../../../common/use_cases_toast');

describe('use cases add to existing case modal hook', () => {
const dispatch = jest.fn();
Expand Down Expand Up @@ -65,7 +66,7 @@ describe('use cases add to existing case modal hook', () => {
);
});

it('should dispatch the close action when invoked', () => {
it('should dispatch the close action for modal and flyout when invoked', () => {
const { result } = renderHook(
() => {
return useCasesAddToExistingCaseModal(defaultParams());
Expand All @@ -78,5 +79,10 @@ describe('use cases add to existing case modal hook', () => {
type: CasesContextStoreActionsList.CLOSE_ADD_TO_CASE_MODAL,
})
);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: CasesContextStoreActionsList.CLOSE_CREATE_CASE_FLYOUT,
})
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,56 @@

import { useCallback } from 'react';
import { AllCasesSelectorModalProps } from '.';
import { useCasesToast } from '../../../common/use_cases_toast';
import { Case } from '../../../containers/types';
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
import { useCasesContext } from '../../cases_context/use_cases_context';
import { useCasesAddToNewCaseFlyout } from '../../create/flyout/use_cases_add_to_new_case_flyout';

export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps) => {
const createNewCaseFlyout = useCasesAddToNewCaseFlyout({
attachments: props.attachments,
onClose: props.onClose,
// TODO there's no need for onSuccess to be async. This will be fixed
// in a follow up clean up
onSuccess: async (theCase?: Case) => {
if (props.onRowClick) {
return props.onRowClick(theCase);
}
},
});
const { dispatch } = useCasesContext();
const casesToasts = useCasesToast();

const closeModal = useCallback(() => {
dispatch({
type: CasesContextStoreActionsList.CLOSE_ADD_TO_CASE_MODAL,
});
// in case the flyout was also open when selecting
// create a new case
dispatch({
type: CasesContextStoreActionsList.CLOSE_CREATE_CASE_FLYOUT,
});
}, [dispatch]);

const openModal = useCallback(() => {
dispatch({
type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL,
payload: {
...props,
onRowClick: (theCase?: Case) => {
// when the case is undefined in the modal
// the user clicked "create new case"
if (theCase === undefined) {
closeModal();
createNewCaseFlyout.open();
} else {
casesToasts.showSuccessAttach(theCase);
if (props.onRowClick) {
props.onRowClick(theCase);
}
}
},
onClose: () => {
closeModal();
if (props.onClose) {
Expand All @@ -37,7 +71,7 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps
},
},
});
}, [closeModal, dispatch, props]);
}, [casesToasts, closeModal, createNewCaseFlyout, dispatch, props]);
return {
open: openModal,
close: closeModal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React from 'react';
import { CasesContext } from '../../cases_context';
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
import { useCasesAddToNewCaseFlyout } from './use_cases_add_to_new_case_flyout';
jest.mock('../../../common/use_cases_toast');

describe('use cases add to new case flyout hook', () => {
const dispatch = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
*/

import { useCallback } from 'react';
import { useCasesToast } from '../../../common/use_cases_toast';
import { Case } from '../../../containers/types';
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
import { useCasesContext } from '../../cases_context/use_cases_context';
import { CreateCaseFlyoutProps } from './create_case_flyout';

export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => {
const { dispatch } = useCasesContext();
const casesToasts = useCasesToast();

const closeFlyout = useCallback(() => {
dispatch({
Expand All @@ -30,6 +33,14 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => {
return props.onClose();
}
},
onSuccess: async (theCase: Case) => {
if (theCase) {
casesToasts.showSuccessAttach(theCase);
}
if (props.onSuccess) {
return props.onSuccess(theCase);
}
},
afterCaseCreated: async (...args) => {
closeFlyout();
if (props.afterCaseCreated) {
Expand All @@ -38,7 +49,7 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => {
},
},
});
}, [closeFlyout, dispatch, props]);
}, [casesToasts, closeFlyout, dispatch, props]);
return {
open: openFlyout,
close: closeFlyout,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type { GetCreateCaseFlyoutProps } from './methods/get_create_case_flyout'
export type { GetAllCasesSelectorModalProps } from './methods/get_all_cases_selector_modal';
export type { GetRecentCasesProps } from './methods/get_recent_cases';

export type { CaseAttachments } from './types';
export type { CaseAttachments, SupportedCaseAttachment } from './types';

export type { ICasesDeepLinkId } from './common/navigation';
export {
Expand Down
Loading