Skip to content

Commit

Permalink
Register Add to new case and add to existing case action (#154918)
Browse files Browse the repository at this point in the history
## Summary

Original issue: #154842

Collaboration with @cnasikas 

In dashboard, when clicking on `...` of each chart, we should be able
to:

- [x] - Add to New case
- [x] - Add to existing case
- [x] - Create case from existing case modal
- [x]  - Create case from existing case modal when no existing cases






https://github.com/elastic/kibana/assets/6295984/512b32f9-10df-4f8b-a456-08f28a987826






### Checklist

Delete any items that are not applicable to this PR.


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Christos Nasikas <[email protected]>
  • Loading branch information
3 people authored Jun 2, 2023
1 parent 1ba8be4 commit b8eed60
Show file tree
Hide file tree
Showing 26 changed files with 1,344 additions and 42 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pageLoadAssetSize:
banners: 17946
bfetch: 22837
canvas: 1066647
cases: 175000
cases: 180000
charts: 55000
cloud: 21076
cloudChat: 19894
Expand Down
28 changes: 21 additions & 7 deletions x-pack/plugins/cases/common/utils/owner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,30 @@
*/

import { OWNER_INFO } from '../constants';
import { isValidOwner } from './owner';
import { getCaseOwnerByAppId, isValidOwner } from './owner';

describe('isValidOwner', () => {
const owners = Object.keys(OWNER_INFO) as Array<keyof typeof OWNER_INFO>;
describe('owner utils', () => {
describe('isValidOwner', () => {
const owners = Object.keys(OWNER_INFO) as Array<keyof typeof OWNER_INFO>;

it.each(owners)('returns true for valid owner: %s', (owner) => {
expect(isValidOwner(owner)).toBe(true);
it.each(owners)('returns true for valid owner: %s', (owner) => {
expect(isValidOwner(owner)).toBe(true);
});

it('return false for invalid owner', () => {
expect(isValidOwner('not-valid')).toBe(false);
});
});

it('return false for invalid owner', () => {
expect(isValidOwner('not-valid')).toBe(false);
describe('getCaseOwnerByAppId', () => {
const tests = Object.values(OWNER_INFO).map((info) => [info.id, info.appId]);

it.each(tests)('for owner %s it returns %s', (owner, appId) => {
expect(getCaseOwnerByAppId(appId)).toBe(owner);
});

it('return undefined for invalid application ID', () => {
expect(getCaseOwnerByAppId('not-valid')).toBe(undefined);
});
});
});
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/common/utils/owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ import { OWNER_INFO } from '../constants';

export const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO =>
Object.keys(OWNER_INFO).includes(owner);

export const getCaseOwnerByAppId = (currentAppId?: string) =>
Object.values(OWNER_INFO).find((info) => info.appId === currentAppId)?.id;
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"ruleRegistry",
"files",
"savedObjectsFinder",
"savedObjectsManagement"
"savedObjectsManagement",
"uiActions",
],
"optionalPlugins": [
"home",
Expand Down
8 changes: 5 additions & 3 deletions x-pack/plugins/cases/public/common/lib/kibana/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@
import type { CoreStart } from '@kbn/core/public';
import type { CasesUiConfigType } from '../../../../common/ui/types';

type GlobalServices = Pick<CoreStart, 'http'>;
type GlobalServices = Pick<CoreStart, 'application' | 'http' | 'theme'>;

export class KibanaServices {
private static kibanaVersion?: string;
private static services?: GlobalServices;
private static config?: CasesUiConfigType;

public static init({
application,
config,
http,
kibanaVersion,
config,
theme,
}: GlobalServices & {
kibanaVersion: string;
config: CasesUiConfigType;
}) {
this.services = { http };
this.services = { application, http, theme };
this.kibanaVersion = kibanaVersion;
this.config = config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
const useUpdateCaseMock = useUpdateCase as jest.Mock;
const useLicenseMock = useLicense as jest.Mock;

const mockTriggersActionsUiService = triggersActionsUiMock.createStart();

const mockKibana = () => {
Expand Down Expand Up @@ -165,7 +164,6 @@ describe('AllCasesListGeneric', () => {

it('should render AllCasesList', async () => {
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });

appMockRenderer.render(<AllCasesList />);

await waitFor(() => {
Expand Down Expand Up @@ -260,6 +258,21 @@ describe('AllCasesListGeneric', () => {
});
});

it('should not call onCreateCasePressed if onRowClick is not provided when create case from case page', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
data: {
...defaultGetCases.data,
cases: [],
},
});
appMockRenderer.render(<AllCasesList isSelectorView={false} />);
userEvent.click(screen.getByTestId('cases-table-add-case'));
await waitFor(() => {
expect(onRowClick).not.toHaveBeenCalled();
});
});

it('should tableHeaderSortButton AllCasesList', async () => {
appMockRenderer.render(<AllCasesList />);

Expand Down Expand Up @@ -347,9 +360,10 @@ describe('AllCasesListGeneric', () => {
it('should call onRowClick with no cases and isSelectorView=true when create case is clicked', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={true} onRowClick={onRowClick} />);
userEvent.click(screen.getByTestId('cases-table-add-case-filter-bar'));

const isCreateCase = true;
await waitFor(() => {
expect(onRowClick).toHaveBeenCalled();
expect(onRowClick).toBeCalledWith(undefined, isCreateCase);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const mapToReadableSolutionName = (solution: string): Solution => {
export interface AllCasesListProps {
hiddenStatuses?: CaseStatusWithAllStatus[];
isSelectorView?: boolean;
onRowClick?: (theCase?: CaseUI) => void;
onRowClick?: (theCase?: CaseUI, isCreateCase?: boolean) => void;
}

export const AllCasesList = React.memo<AllCasesListProps>(
Expand Down Expand Up @@ -250,6 +250,10 @@ export const AllCasesList = React.memo<AllCasesListProps>(
mapToReadableSolutionName(solution)
);

const onCreateCasePressed = useCallback(() => {
onRowClick?.(undefined, true);
}, [onRowClick]);

return (
<>
<ProgressLoader
Expand All @@ -276,15 +280,15 @@ export const AllCasesList = React.memo<AllCasesListProps>(
severity: filterOptions.severity,
}}
hiddenStatuses={hiddenStatuses}
onCreateCasePressed={onRowClick}
onCreateCasePressed={onCreateCasePressed}
isSelectorView={isSelectorView}
isLoading={isLoadingCurrentUserProfile}
currentUserProfile={currentUserProfile}
/>
<CasesTable
columns={columns}
data={data}
goToCreateCase={onRowClick}
goToCreateCase={onRowClick ? onCreateCasePressed : undefined}
isCasesLoading={isLoadingCases}
isCommentUpdating={isLoadingCases}
isDataEmpty={isDataEmpty}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { AllCasesList } from '../all_cases_list';
export interface AllCasesSelectorModalProps {
hiddenStatuses?: CaseStatusWithAllStatus[];
onRowClick?: (theCase?: CaseUI) => void;
onClose?: () => void;
onClose?: (theCase?: CaseUI, isCreateCase?: boolean) => void;
onCreateCaseClicked?: () => void;
}

const Modal = styled(EuiModal)`
Expand All @@ -37,20 +38,18 @@ export const AllCasesSelectorModal = React.memo<AllCasesSelectorModalProps>(
({ hiddenStatuses, onRowClick, onClose }) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(true);
const closeModal = useCallback(() => {
if (onClose) {
onClose();
}
onClose?.();
setIsModalOpen(false);
}, [onClose]);

const onClick = useCallback(
(theCase?: CaseUI) => {
closeModal();
if (onRowClick) {
onRowClick(theCase);
}
(theCase?: CaseUI, isCreateCase?: boolean) => {
onClose?.(theCase, isCreateCase);
setIsModalOpen(false);

onRowClick?.(theCase);
},
[closeModal, onRowClick]
[onClose, onRowClick]
);

return isModalOpen ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingCaseModalProp
}
},
[
props,
appId,
casesToasts,
closeModal,
createAttachments,
createNewCaseFlyout,
props,
startTransaction,
appId,
createAttachments,
casesToasts,
]
);

Expand All @@ -130,11 +130,11 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingCaseModalProp
onRowClick: (theCase?: CaseUI) => {
handleOnRowClick(theCase, getAttachments);
},
onClose: () => {
onClose: (theCase?: CaseUI, isCreateCase?: boolean) => {
closeModal();

if (props.onClose) {
return props.onClose();
return props.onClose(theCase, isCreateCase);
}
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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';

export const ActionWrapper = jest
.fn()
.mockImplementation(({ children }) => <div data-test-subj="action-wrapper">{children}</div>);
Loading

0 comments on commit b8eed60

Please sign in to comment.