Skip to content

Commit

Permalink
[Cases] Refactor: Move cases action buttons out of timelines (#126265)
Browse files Browse the repository at this point in the history
* WIP

* WIP2

* Use new cases context hooks to open and close the flyout

* Update timelines to use new hooks

* CLose flyout on create success

* Add back sucess toast

* Move code to a dedicated component

* Add CasesContext to observability

* Remove dependency

* Small refactor

* Use observabilityAppId instead of observabilityFeatureId for buttons

* Add CasesContext to timetable

* Fix detection engine test cases

* Fix broken tests

* Fix broken tests

* Rename hook

* Add test cases for cases context ui

* Add test for new hook

* Remove state from the provider context

* Remove basevalue

* apply suggested renaming

* Add usecallback

* Add reducer types, fix test type, remove redundant check

* Accept attachments as a prop for the cases select modal

* Expose useCasesAddToExistingCase hook, reducer code and global component

* use the new hook to open the select cases modasl

* Fix tests and types

* Add tests for cases global components

* [Fleet] showing agent policy creation error message on UI (#125931)

* showing agent policy creation error message on UI

* mapping the error instead of showing from the backend

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [ResponseOps] Adds tooltip to time window selector in ES query rule flyout (#125764)

* [Lens] Allow detaching from global time range (#125563)

* allow detaching from global time range

* add test

* fix time field recognition

* fix tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Fleet] refactor auto upgrade package policies logic (#125909)

* refactor upgrade package policies

* fixed tests

* code cleanup

* review improvements

* added api test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* skip flaky suite (#126027)

* Remove deprecated api (#125524)

* [Fleet] Remove deprecated kibana APIs - License

* Remove basePath from FleetApp

* Replace AsyncPlugin with Plugin

* Get fieldFormats from fieldFormats plugin rather than data plugin

* Fix ts errors

* Attempt fixing wrong type

* Move licenseService to FleetStartDeps

* Fix types and mocks

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Upgrade `markdown-it` dependency (`10.0.0` → `12.3.2`). (#125526)

* skipping failing tests (#126039)

* remove unused deprecated code and use field format plugin directly for data view field editor (#126029)

* [data views] Improve preview pane (#126013)

* fix preview pane

* fix preview pane

* one less span tag

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Alerting] Provide services to set context for recovered alerts (#124972)

* Rename alert instance to alert and add create fn to alert factory

* Rename alert instance to alert and add create fn to alert factory

* Fixing types

* Fixing types

* Adding flag for rule types to opt into setting recovery context

* Only showing context in action variable menu if flag set to true

* Adding recovery functions to createAlertFactory

* Setting recovery in index threshold and fixing types

* Fixing lint issues and some refactoring

* Cleanup

* Functional tests for index threshold rule recovery context

* Return array of recovered alerts instead of record

* Fixing types

* Fixing types

* Cleanup

* Handling nulls and more tests

* Updating developer docs

* Making getRecoveryAlerts non-optional

* Setting unknown in index threshold recovery value

* PR feedback

* Adding a test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Discover] Re-introduce saved_searches test (#126059)

* [Archive Migration]  index pattern without timefield (#125870)

* kbn_archive date_nanos

* kbn_archive date_nanos in context and discover

* kbn_archiver more date_nanos tests

* split out kbnArchiver for index_pattern_without_timefield

* remove date_nanos files from a different PR

* update another test for usage of the same archives

* set default index pattern for test

* remove duplicate const kibanaServer

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* delete unused es_archive visualize_embedding (#126001)

* delete unused es_archive

* remove other unused es_archive

* more unused es_archives

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Bump packages (#126119)

* url-parse 1.5.3 -> 1.5.9
* follow-redirects 1.y.z -> 1.14.9

* Add tests for all cases selector and attachments

* Add tests for use add to existing case hook

* First version of the cases timeline actions

* export add alert to new case button from cases plugin

* Make Cases ECS compatible with timelines and security_solution

* Delete new case button

* Add helpers

* Use the cases hook directly for add to new case

* Remov unused dependencies

* Rename callbacks, remove timelines calls

* Fixing tests for the dropdown

* Fix broken test

* mocking cases for tests

* Fix detectiosn tests

* Observability now uses the new cases hooks

* Wrap events viewer into cases context

* Open the create case flyout if create case was selected in the modal

* Fix cases mocks for security_solution

* Update tests

* Add tests for use cases toast

* Improve cases mock

* delete security mock

* replace tests mocks for cases

* fix import mock

* Do not require onRowClick

* Show the toast inside the modal

* show the toast inside the flyout

* remove toast logic from the consumer plugin

* fix typescript types

* Rename type

* Fix broken test

* Fix file name and broken test

* Use internal navigation hook

* Update hook dependencies

* Move useCaseToast

* Fix mock paths

* fix eslint

* Add test cases for the toast content

* Add cases context to the overview page

Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: István Zoltán Szabó <szabosteve@gmail.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
Co-authored-by: Tiago Costa <tiago.costa@elastic.co>
Co-authored-by: Cristina Amico <criamico@users.noreply.github.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@elastic.co>
Co-authored-by: Gloria Hornero <gloria.hornero@elastic.co>
Co-authored-by: Matthew Kime <matt@mattki.me>
Co-authored-by: Ying Mao <ying.mao@elastic.co>
Co-authored-by: Maja Grubic <maja.grubic@elastic.co>
Co-authored-by: Lee Drengenberg <lee.drengenberg@elastic.co>
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
  • Loading branch information
14 people authored Mar 3, 2022
1 parent 555ec91 commit 83105bd
Show file tree
Hide file tree
Showing 34 changed files with 565 additions and 153 deletions.
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[];
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', () => {
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

0 comments on commit 83105bd

Please sign in to comment.