[Cases] Export getRelatedCases API from cases client (#127065)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2022-03-10 11:33:03 +02:00 committed by GitHub
parent 74659aff4f
commit 60d5a993b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 310 additions and 200 deletions

View file

@ -18,7 +18,6 @@ This plugin provides cases management in Kibana
- [Cases API](#cases-api)
- [Cases Client API](#cases-client-api)
- [Cases UI](#cases-ui)
- [Case Action Type](#case-action-type) _feature in development, disabled by default_
## Cases API
@ -30,7 +29,7 @@ This plugin provides cases management in Kibana
## Cases UI
#### Embed Cases UI components in any Kibana plugin
### Embed Cases UI components in any Kibana plugin
- Add `CasesUiStart` to Kibana plugin `StartServices` dependencies:
@ -38,7 +37,7 @@ This plugin provides cases management in Kibana
cases: CasesUiStart;
```
#### CasesContext setup
### CasesContext setup
To use any of the Cases UI hooks you must first initialize `CasesContext` in your plugin.
@ -64,14 +63,25 @@ To initialize the `CasesContext` you can use this code:
props:
| prop | type | description |
|-----------------------|-----------------|----------------------------------------------------------------|
| --------------------- | --------------- | -------------------------------------------------------------- |
| PLUGIN_CASES_OWNER_ID | `string` | The owner string for your plugin. e.g: securitySolution |
| CASES_USER_CAN_CRUD | `boolean` | Defines if the user has access to cases to CRUD |
| CASES_FEATURES | `CasesFeatures` | `CasesFeatures` object defining the features to enable/disable |
#### Cases UI Methods
- From the UI component, get the component from the `useKibana` hook start services
### Cases UI client
The cases UI client exports the following contract:
| Property | Description | Type |
| -------- | -------------------------------- | ------ |
| api | Methods related to the Cases API | object |
| ui | Cases UI components | object |
| hooks | Cases React hooks | object |
| helpers | Cases helpers | object |
You can get the cases UI client from the `useKibana` hook start services. Example:
```tsx
const { cases } = useKibana().services;
@ -94,55 +104,76 @@ cases.getCases({
});
```
##### Methods:
### api
### `getCases`
#### `getRelatedCases`
Returns all cases where the alert is attached to.
Arguments
| Property | Description | Type |
| -------- | ------------ | ------ |
| alertId | The alert ID | string |
Response
An array of:
| Property | Description | Type |
| -------- | --------------------- | ------ |
| id | The ID of the case | string |
| title | The title of the case | string |
### ui
#### `getCases`
Arguments:
| Property | Description |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| owner | `string[];` owner ids of the cases |
| basePath | `string;` path to mount the Cases router on top of |
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, unknown>];` fetch alerts |
| disableAlerts? | `boolean` (default: false) flag to not show alerts information |
| actionsNavigation? | <code>CasesNavigation<string, 'configurable'></code> |
| ruleDetailsNavigation? | <code>CasesNavigation<string &vert; null &vert; undefined, 'configurable'></code> |
| onComponentInitialized? | `() => void;` callback when component has initialized |
| showAlertDetails? | `(alertId: string, index: string) => void;` callback to show alert details |
| features? | `CasesFeatures` object defining the features to enable/disable |
| features?.alerts.sync | `boolean` (default: `true`) defines wether the alert sync action should be enabled/disabled |
| Property | Description |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| owner | `string[];` owner ids of the cases |
| basePath | `string;` path to mount the Cases router on top of |
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, unknown>];` fetch alerts |
| disableAlerts? | `boolean` (default: false) flag to not show alerts information |
| actionsNavigation? | <code>CasesNavigation<string, 'configurable'></code> |
| ruleDetailsNavigation? | <code>CasesNavigation<string &vert; null &vert; undefined, 'configurable'></code> |
| onComponentInitialized? | `() => void;` callback when component has initialized |
| showAlertDetails? | `(alertId: string, index: string) => void;` callback to show alert details |
| features? | `CasesFeatures` object defining the features to enable/disable |
| features?.alerts.sync | `boolean` (default: `true`) defines wether the alert sync action should be enabled/disabled |
| features?.metrics | `string[]` (default: `[]`) defines the metrics to show in the Case Detail View. Allowed metrics: "alerts.count", "alerts.users", "alerts.hosts", "connectors", "lifespan". |
| timelineIntegration?.editor_plugins | Plugins needed for integrating timeline into markdown editor. |
| timelineIntegration?.editor_plugins.parsingPlugin | `Plugin;` |
| timelineIntegration?.editor_plugins.processingPluginRenderer | `React.FC<TimelineProcessingPluginRendererProps & { position: EuiMarkdownAstNodePosition }>` |
| timelineIntegration?.editor_plugins.uiPlugin? | `EuiMarkdownEditorUiPlugin` |
| timelineIntegration?.hooks.useInsertTimeline | `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn` |
| timelineIntegration?.ui?.renderInvestigateInTimelineActionComponent? | `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent` |
| timelineIntegration?.ui?renderTimelineDetailsPanel? | `() => JSX.Element;` space to render `TimelineDetailsPanel` |
| timelineIntegration?.editor_plugins | Plugins needed for integrating timeline into markdown editor. |
| timelineIntegration?.editor_plugins.parsingPlugin | `Plugin;` |
| timelineIntegration?.editor_plugins.processingPluginRenderer | `React.FC<TimelineProcessingPluginRendererProps & { position: EuiMarkdownAstNodePosition }>` |
| timelineIntegration?.editor_plugins.uiPlugin? | `EuiMarkdownEditorUiPlugin` |
| timelineIntegration?.hooks.useInsertTimeline | `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn` |
| timelineIntegration?.ui?.renderInvestigateInTimelineActionComponent? | `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent` |
| timelineIntegration?.ui?renderTimelineDetailsPanel? | `() => JSX.Element;` space to render `TimelineDetailsPanel` |
UI component:
![All Cases Component][all-cases-img]
### `getAllCasesSelectorModal`
#### `getAllCasesSelectorModal`
Arguments:
| Property | Description |
| --------------- | ------------------------------------------------------------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| owner | `string[];` owner ids of the cases |
| alertData? | `Omit<CommentRequestAlertType, 'type'>;` alert data to post to case |
| hiddenStatuses? | `CaseStatuses[];` array of hidden statuses |
| Property | Description |
| --------------- | ---------------------------------------------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| owner | `string[];` owner ids of the cases |
| alertData? | `Omit<CommentRequestAlertType, 'type'>;` alert data to post to case |
| hiddenStatuses? | `CaseStatuses[];` array of hidden statuses |
| onRowClick | <code>(theCase?: Case) => void;</code> callback for row click, passing case in row |
| updateCase? | <code>(theCase: Case) => void;</code> callback after case has been updated |
| onClose? | `() => void` called when the modal is closed without selecting a case |
| onClose? | `() => void` called when the modal is closed without selecting a case |
UI component:
![All Cases Selector Modal Component][all-cases-modal-img]
### `getCreateCaseFlyout`
#### `getCreateCaseFlyout`
Arguments:
@ -158,7 +189,7 @@ Arguments:
UI component:
![Create Component][create-img]
### `getRecentCases`
#### `getRecentCases`
Arguments:
@ -203,31 +234,40 @@ You can use this hook to prompt the user to select a case and get the selected c
Arguments:
| Property | Description |
| --------------- | ------------------------------------------------------------------------------------------------- |
| onRowClick | <code>(theCase?: Case) => void;</code> callback for row click, passing case in row |
| updateCase? | <code>(theCase: Case) => void;</code> callback after case has been updated |
| onClose? | `() => void` called when the modal is closed without selecting a case |
| attachments? | `CaseAttachments`; array of `SupportedCaseAttachment` (see types) that will be attached to the newly created case |
| Property | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------------- |
| onRowClick | <code>(theCase?: Case) => void;</code> callback for row click, passing case in row |
| updateCase? | <code>(theCase: Case) => void;</code> callback after case has been updated |
| onClose? | `() => void` called when the modal is closed without selecting a case |
| attachments? | `CaseAttachments`; array of `SupportedCaseAttachment` (see types) that will be attached to the newly created case |
### helpers
#### canUseCases
Returns the Cases capabilities for the current user. Specifically:
| Property | Description | Type |
| -------- | -------------------------------------------- | ------- |
| crud | Denotes if the user has all access to Cases | boolean |
| read? | Denotes if the user has read access to Cases | boolean |
#### getRuleIdFromEvent
Returns an object with a rule `id` and `name` of the event passed. This helper method is necessary to bridge the gap between previous events schema and new ones.
Arguments:
| property | description |
|----------|----------------------------------------------------------------------------------------------|
| event | Event containing an `ecs` attribute with ecs data and a `data` attribute with `nonEcs` data. |
| property | Description | Type |
| -------- | -------------------------------------------------------------------------------------------- | ------ |
| event | Event containing an `ecs` attribute with ecs data and a `data` attribute with `nonEcs` data. | object |
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[pr-shield]: https://img.shields.io/github/issues-pr/elastic/kibana/Team:Threat%20Hunting:Cases?label=pull%20requests&style=for-the-badge
[pr-url]: https://github.com/elastic/kibana/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3A%22Team%3AThreat+Hunting%3ACases%22
[issues-shield]: https://img.shields.io/github/issues-search?label=issue&query=repo%3Aelastic%2Fkibana%20is%3Aissue%20is%3Aopen%20label%3A%22Team%3AThreat%20Hunting%3ACases%22&style=for-the-badge
[pr-shield]: https://img.shields.io/github/issues-pr/elastic/kibana/Feature:Cases?label=pull%20requests&style=for-the-badge
[pr-url]: https://github.com/elastic/kibana/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3A%22Feature%3ACases%22
[issues-shield]: https://img.shields.io/github/issues-search?label=issue&query=repo%3Aelastic%2Fkibana%20is%3Aissue%20is%3Aopen%20label%3A%22Feature%3ACases%22&style=for-the-badge
[issues-url]: https://github.com/elastic/kibana/issues?q=is%3Aopen+is%3Aissue+label%3AFeature%3ACases
[cases-logo]: images/logo.png
[configure-img]: images/configure.png

View file

@ -0,0 +1,37 @@
/*
* 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 { httpServiceMock } from '../../../../../../src/core/public/mocks';
import { createClientAPI } from '.';
describe('createClientAPI', () => {
const http = httpServiceMock.createStartContract({ basePath: '' });
const api = createClientAPI({ http });
beforeEach(() => {
jest.clearAllMocks();
});
describe('getRelatedCases', () => {
const res = [
{
id: 'test-id',
title: 'test',
},
];
http.get.mockResolvedValue(res);
it('should return the correct response', async () => {
expect(await api.getRelatedCases('alert-id')).toEqual(res);
});
it('should have been called with the correct path', async () => {
await api.getRelatedCases('alert-id');
expect(http.get).toHaveBeenCalledWith('/api/cases/alerts/alert-id');
});
});
});

View file

@ -0,0 +1,16 @@
/*
* 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 { HttpStart } from 'kibana/public';
import { CasesByAlertId, getCasesFromAlertsUrl } from '../../../common/api';
export const createClientAPI = ({ http }: { http: HttpStart }) => {
return {
getRelatedCases: async (alertId: string): Promise<CasesByAlertId> =>
http.get<CasesByAlertId>(getCasesFromAlertsUrl(alertId)),
};
};

View file

@ -6,7 +6,7 @@
*/
import type { ApplicationStart } from 'kibana/public';
import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../common/constants';
import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
export type CasesOwners = typeof SECURITY_SOLUTION_OWNER | typeof OBSERVABILITY_OWNER;

View file

@ -7,7 +7,7 @@
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { get } from 'lodash/fp';
import { Ecs } from '../../common';
import { Ecs } from '../../../common';
type Maybe<T> = T | null;
interface Event {

View file

@ -7,13 +7,13 @@
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { AllCasesSelectorModalProps } from '../components/all_cases/selector_modal';
import { CasesProvider, CasesContextProps } from '../components/cases_context';
import { AllCasesSelectorModalProps } from '../../components/all_cases/selector_modal';
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
export type GetAllCasesSelectorModalProps = AllCasesSelectorModalProps & CasesContextProps;
const AllCasesSelectorModalLazy: React.FC<AllCasesSelectorModalProps> = lazy(
() => import('../components/all_cases/selector_modal')
() => import('../../components/all_cases/selector_modal')
);
export const getAllCasesSelectorModalLazy = ({
owner,

View file

@ -7,12 +7,12 @@
import { EuiLoadingSpinner } from '@elastic/eui';
import React, { lazy, Suspense } from 'react';
import type { CasesProps } from '../components/app';
import { CasesProvider, CasesContextProps } from '../components/cases_context';
import type { CasesProps } from '../../components/app';
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
export type GetCasesProps = CasesProps & CasesContextProps;
const CasesRoutesLazy: React.FC<CasesProps> = lazy(() => import('../components/app/routes'));
const CasesRoutesLazy: React.FC<CasesProps> = lazy(() => import('../../components/app/routes'));
export const getCasesLazy = ({
owner,

View file

@ -7,12 +7,12 @@
import { EuiLoadingSpinner } from '@elastic/eui';
import React, { lazy, ReactNode, Suspense } from 'react';
import { CasesContextProps } from '../components/cases_context';
import { CasesContextProps } from '../../components/cases_context';
export type GetCasesContextProps = CasesContextProps;
const CasesProviderLazy: React.FC<{ value: GetCasesContextProps }> = lazy(
() => import('../components/cases_context')
() => import('../../components/cases_context')
);
const CasesProviderLazyWrapper = ({

View file

@ -7,13 +7,13 @@
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { CreateCaseFlyoutProps } from '../components/create/flyout';
import { CasesProvider, CasesContextProps } from '../components/cases_context';
import type { CreateCaseFlyoutProps } from '../../components/create/flyout';
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
export type GetCreateCaseFlyoutProps = CreateCaseFlyoutProps & CasesContextProps;
export const CreateCaseFlyoutLazy: React.FC<CreateCaseFlyoutProps> = lazy(
() => import('../components/create/flyout')
() => import('../../components/create/flyout')
);
export const getCreateCaseFlyoutLazy = ({
owner,

View file

@ -7,13 +7,13 @@
import { EuiLoadingSpinner } from '@elastic/eui';
import React, { lazy, Suspense } from 'react';
import { CasesProvider, CasesContextProps } from '../components/cases_context';
import { RecentCasesProps } from '../components/recent_cases';
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
import { RecentCasesProps } from '../../components/recent_cases';
export type GetRecentCasesProps = RecentCasesProps & CasesContextProps;
const RecentCasesLazy: React.FC<RecentCasesProps> = lazy(
() => import('../components/recent_cases')
() => import('../../components/recent_cases')
);
export const getRecentCasesLazy = ({ owner, userCanCrud, maxCasesToShow }: GetRecentCasesProps) => (
<CasesProvider value={{ owner, userCanCrud }}>

View file

@ -39,8 +39,8 @@ import { StatusContextMenu } from '../case_action_bar/status_context_menu';
import { TruncatedText } from '../truncated_text';
import { getConnectorIcon } from '../utils';
import { PostComment } from '../../containers/use_post_comment';
import type { CasesOwners } from '../../methods/can_use_cases';
import { CaseAttachments } from '../../types';
import type { CasesOwners } from '../../client/helpers/can_use_cases';
export type CasesColumns =
| EuiTableActionsColumnType<Case>

View file

@ -13,7 +13,6 @@ import { TestProviders } from '../../../common/mock';
import { AllCasesList } from '../all_cases_list';
import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants';
jest.mock('../../../methods');
jest.mock('../all_cases_list');
const onRowClick = jest.fn();

View file

@ -7,9 +7,9 @@
import React from 'react';
import { APP_OWNER } from '../../../common/constants';
import { getCasesLazy } from '../../client/ui/get_cases';
import { useApplicationCapabilities } from '../../common/lib/kibana';
import { getCasesLazy } from '../../methods';
import { Wrapper } from '../wrappers';
import { CasesRoutesProps } from './types';

View file

@ -6,15 +6,14 @@
*/
import React from 'react';
import { getAllCasesSelectorModalNoProviderLazy } from '../../client/ui/get_all_cases_selector_modal';
import { getCreateCaseFlyoutLazyNoProvider } from '../../client/ui/get_create_case_flyout';
import { AppMockRenderer, createAppMockRenderer } from '../../common/mock';
import {
getAllCasesSelectorModalNoProviderLazy,
getCreateCaseFlyoutLazyNoProvider,
} from '../../methods';
import { getInitialCasesContextState } from './cases_context_reducer';
import { CasesGlobalComponents } from './cases_global_components';
jest.mock('../../methods');
jest.mock('../../client/ui/get_create_case_flyout');
jest.mock('../../client/ui/get_all_cases_selector_modal');
const getCreateCaseFlyoutLazyNoProviderMock = getCreateCaseFlyoutLazyNoProvider as jest.Mock;
const getAllCasesSelectorModalNoProviderLazyMock =

View file

@ -6,10 +6,8 @@
*/
import React from 'react';
import {
getAllCasesSelectorModalNoProviderLazy,
getCreateCaseFlyoutLazyNoProvider,
} from '../../methods';
import { getAllCasesSelectorModalNoProviderLazy } from '../../client/ui/get_all_cases_selector_modal';
import { getCreateCaseFlyoutLazyNoProvider } from '../../client/ui/get_create_case_flyout';
import { CasesContextState } from './cases_context_reducer';
export const CasesGlobalComponents = React.memo(({ state }: { state: CasesContextState }) => {

View file

@ -16,10 +16,10 @@ export { DRAFT_COMMENT_STORAGE_ID } from './components/markdown_editor/plugins/l
export type { CasesUiPlugin };
export type { CasesUiStart } from './types';
export type { GetCasesProps } from './methods/get_cases';
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 { GetCasesProps } from './client/ui/get_cases';
export type { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout';
export type { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal';
export type { GetRecentCasesProps } from './client/ui/get_recent_cases';
export type { CaseAttachments, SupportedCaseAttachment } from './types';

View file

@ -1,12 +0,0 @@
/*
* 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.
*/
export * from './can_use_cases';
export * from './get_cases';
export * from './get_recent_cases';
export * from './get_all_cases_selector_modal';
export * from './get_create_case_flyout';

View file

@ -8,20 +8,40 @@
import { mockCasesContext } from './mocks/mock_cases_context';
import { CasesUiStart } from './types';
export const mockCasesContract = (): jest.Mocked<CasesUiStart> => ({
canUseCases: jest.fn(),
const apiMock: jest.Mocked<CasesUiStart['api']> = {
getRelatedCases: jest.fn(),
};
const uiMock: jest.Mocked<CasesUiStart['ui']> = {
getCases: jest.fn(),
getCasesContext: jest.fn().mockImplementation(() => mockCasesContext),
getAllCasesSelectorModal: jest.fn(),
getCreateCaseFlyout: jest.fn(),
getRecentCases: jest.fn(),
hooks: {
getUseCasesAddToNewCaseFlyout: jest.fn(),
getUseCasesAddToExistingCaseModal: jest.fn(),
},
helpers: {
getRuleIdFromEvent: jest.fn(),
},
};
const hooksMock: jest.Mocked<CasesUiStart['hooks']> = {
getUseCasesAddToNewCaseFlyout: jest.fn(),
getUseCasesAddToExistingCaseModal: jest.fn(),
};
const helpersMock: jest.Mocked<CasesUiStart['helpers']> = {
canUseCases: jest.fn(),
getRuleIdFromEvent: jest.fn(),
};
export interface CaseUiClientMock {
api: jest.Mocked<CasesUiStart['api']>;
ui: jest.Mocked<CasesUiStart['ui']>;
hooks: jest.Mocked<CasesUiStart['hooks']>;
helpers: jest.Mocked<CasesUiStart['helpers']>;
}
export const mockCasesContract = (): CaseUiClientMock => ({
api: apiMock,
ui: uiMock,
hooks: hooksMock,
helpers: helpersMock,
});
export const casesPluginMock = {

View file

@ -8,23 +8,22 @@
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { CasesUiStart, CasesPluginSetup, CasesPluginStart } from './types';
import { KibanaServices } from './common/lib/kibana';
import {
getCasesLazy,
getRecentCasesLazy,
getAllCasesSelectorModalLazy,
getCreateCaseFlyoutLazy,
canUseCases,
} from './methods';
import { CasesUiConfigType } from '../common/ui/types';
import { APP_ID, APP_PATH } from '../common/constants';
import { APP_TITLE, APP_DESC } from './common/translations';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { ManagementAppMountParams } from '../../../../src/plugins/management/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { getCasesContextLazy } from './methods/get_cases_context';
import { useCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal';
import { useCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
import { getRuleIdFromEvent } from './methods/get_rule_id_from_event';
import { createClientAPI } from './client/api';
import { canUseCases } from './client/helpers/can_use_cases';
import { getRuleIdFromEvent } from './client/helpers/get_rule_id_from_event';
import { getAllCasesSelectorModalLazy } from './client/ui/get_all_cases_selector_modal';
import { getCasesLazy } from './client/ui/get_cases';
import { getCasesContextLazy } from './client/ui/get_cases_context';
import { getCreateCaseFlyoutLazy } from './client/ui/get_create_case_flyout';
import { getRecentCasesLazy } from './client/ui/get_recent_cases';
/**
* @public
@ -87,19 +86,22 @@ export class CasesUiPlugin
const config = this.initializerContext.config.get<CasesUiConfigType>();
KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config });
return {
canUseCases: canUseCases(core.application.capabilities),
getCases: getCasesLazy,
getCasesContext: getCasesContextLazy,
getRecentCases: getRecentCasesLazy,
// @deprecated Please use the hook getUseCasesAddToNewCaseFlyout
getCreateCaseFlyout: getCreateCaseFlyoutLazy,
// @deprecated Please use the hook getUseCasesAddToExistingCaseModal
getAllCasesSelectorModal: getAllCasesSelectorModalLazy,
api: createClientAPI({ http: core.http }),
ui: {
getCases: getCasesLazy,
getCasesContext: getCasesContextLazy,
getRecentCases: getRecentCasesLazy,
// @deprecated Please use the hook getUseCasesAddToNewCaseFlyout
getCreateCaseFlyout: getCreateCaseFlyoutLazy,
// @deprecated Please use the hook getUseCasesAddToExistingCaseModal
getAllCasesSelectorModal: getAllCasesSelectorModalLazy,
},
hooks: {
getUseCasesAddToNewCaseFlyout: useCasesAddToNewCaseFlyout,
getUseCasesAddToExistingCaseModal: useCasesAddToExistingCaseModal,
},
helpers: {
canUseCases: canUseCases(core.application.capabilities),
getRuleIdFromEvent,
},
};

View file

@ -20,19 +20,16 @@ import type { LensPublicStart } from '../../lens/public';
import type { SecurityPluginSetup } from '../../security/public';
import type { SpacesPluginStart } from '../../spaces/public';
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '../../triggers_actions_ui/public';
import { CommentRequestAlertType, CommentRequestUserType } from '../common/api';
import { CasesByAlertId, CommentRequestAlertType, CommentRequestUserType } from '../common/api';
import { UseCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal';
import { UseCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
import type {
CasesOwners,
GetAllCasesSelectorModalProps,
GetCasesProps,
GetCreateCaseFlyoutProps,
GetRecentCasesProps,
} from './methods';
import { GetCasesContextProps } from './methods/get_cases_context';
import { getRuleIdFromEvent } from './methods/get_rule_id_from_event';
import type { CasesOwners } from './client/helpers/can_use_cases';
import { getRuleIdFromEvent } from './client/helpers/get_rule_id_from_event';
import type { GetCasesContextProps } from './client/ui/get_cases_context';
import type { GetCasesProps } from './client/ui/get_cases';
import { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal';
import { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout';
import { GetRecentCasesProps } from './client/ui/get_recent_cases';
export interface CasesPluginSetup {
security: SecurityPluginSetup;
@ -70,49 +67,56 @@ export interface RenderAppProps {
}
export interface CasesUiStart {
/**
* Returns an object denoting the current user's ability to read and crud cases.
* If any owner(securitySolution, Observability) is found with crud or read capability respectively,
* then crud or read is set to true.
* Permissions for specific owners can be found by passing an owner array
* @param owners an array of CaseOwners that should be queried for permission
* @returns An object denoting the case permissions of the current user
*/
canUseCases: (owners?: CasesOwners[]) => { crud: boolean; read: boolean };
/**
* Get cases
* @param props GetCasesProps
* @return {ReactElement<GetCasesProps>}
*/
getCases: (props: GetCasesProps) => ReactElement<GetCasesProps>;
getCasesContext: () => (
props: GetCasesContextProps & { children: ReactNode }
) => ReactElement<GetCasesContextProps>;
/**
* Modal to select a case in a list of all owner cases
* @param props GetAllCasesSelectorModalProps
* @returns A react component that is a modal for selecting a case
*/
getAllCasesSelectorModal: (
props: GetAllCasesSelectorModalProps
) => ReactElement<GetAllCasesSelectorModalProps>;
/**
* Flyout with the form to create a case for the owner
* @param props GetCreateCaseFlyoutProps
* @returns A react component that is a flyout for creating a case
*/
getCreateCaseFlyout: (props: GetCreateCaseFlyoutProps) => ReactElement<GetCreateCaseFlyoutProps>;
/**
* Get the recent cases component
* @param props GetRecentCasesProps
* @returns A react component for showing recent cases
*/
getRecentCases: (props: GetRecentCasesProps) => ReactElement<GetRecentCasesProps>;
api: {
getRelatedCases: (alertId: string) => Promise<CasesByAlertId>;
};
ui: {
/**
* Get cases
* @param props GetCasesProps
* @return {ReactElement<GetCasesProps>}
*/
getCases: (props: GetCasesProps) => ReactElement<GetCasesProps>;
getCasesContext: () => (
props: GetCasesContextProps & { children: ReactNode }
) => ReactElement<GetCasesContextProps>;
/**
* Modal to select a case in a list of all owner cases
* @param props GetAllCasesSelectorModalProps
* @returns A react component that is a modal for selecting a case
*/
getAllCasesSelectorModal: (
props: GetAllCasesSelectorModalProps
) => ReactElement<GetAllCasesSelectorModalProps>;
/**
* Flyout with the form to create a case for the owner
* @param props GetCreateCaseFlyoutProps
* @returns A react component that is a flyout for creating a case
*/
getCreateCaseFlyout: (
props: GetCreateCaseFlyoutProps
) => ReactElement<GetCreateCaseFlyoutProps>;
/**
* Get the recent cases component
* @param props GetRecentCasesProps
* @returns A react component for showing recent cases
*/
getRecentCases: (props: GetRecentCasesProps) => ReactElement<GetRecentCasesProps>;
};
hooks: {
getUseCasesAddToNewCaseFlyout: UseCasesAddToNewCaseFlyout;
getUseCasesAddToExistingCaseModal: UseCasesAddToExistingCaseModal;
};
helpers: {
/**
* Returns an object denoting the current user's ability to read and crud cases.
* If any owner(securitySolution, Observability) is found with crud or read capability respectively,
* then crud or read is set to true.
* Permissions for specific owners can be found by passing an owner array
* @param owners an array of CaseOwners that should be queried for permission
* @returns An object denoting the case permissions of the current user
*/
canUseCases: (owners?: CasesOwners[]) => { crud: boolean; read: boolean };
getRuleIdFromEvent: typeof getRuleIdFromEvent;
};
}

View file

@ -102,8 +102,8 @@ describe('AddToCaseAction', function () {
);
fireEvent.click(await findByText('Add to case'));
expect(core?.cases?.getAllCasesSelectorModal).toHaveBeenCalledTimes(1);
expect(core?.cases?.getAllCasesSelectorModal).toHaveBeenCalledWith(
expect(core?.cases?.ui.getAllCasesSelectorModal).toHaveBeenCalledTimes(1);
expect(core?.cases?.ui.getAllCasesSelectorModal).toHaveBeenCalledWith(
expect.objectContaining({
owner: ['observability'],
userCanCrud: true,

View file

@ -114,7 +114,7 @@ export function AddToCaseAction({
)}
{isCasesOpen &&
lensAttributes &&
cases.getAllCasesSelectorModal(getAllCasesSelectorModalProps)}
cases.ui.getAllCasesSelectorModal(getAllCasesSelectorModalProps)}
</>
);
}

View file

@ -215,7 +215,7 @@ function AlertsPage() {
const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false);
const kibana = useKibana<ObservabilityAppServices>();
const CasesContext = kibana.services.cases.getCasesContext();
const CasesContext = kibana.services.cases.ui.getCasesContext();
const userPermissions = useGetUserCasesPermissions();
if (!hasAnyData && !isAllRequestsComplete) {

View file

@ -19,7 +19,7 @@ interface CasesProps {
}
export const Cases = React.memo<CasesProps>(({ userCanCrud }) => {
const {
cases: casesUi,
cases,
application: { getUrlForApp, navigateToApp },
} = useKibana().services;
const { observabilityRuleTypeRegistry } = usePluginContext();
@ -42,7 +42,7 @@ export const Cases = React.memo<CasesProps>(({ userCanCrud }) => {
/>
</Suspense>
)}
{casesUi.getCases({
{cases.ui.getCases({
basePath: CASES_PATH,
userCanCrud,
owner: [CASES_OWNER],

View file

@ -90,7 +90,7 @@ export function OverviewPage({ routeParams }: Props) {
}, []);
const kibana = useKibana<ObservabilityAppServices>();
const CasesContext = kibana.services.cases.getCasesContext();
const CasesContext = kibana.services.cases.ui.getCasesContext();
const userPermissions = useGetUserCasesPermissions();
if (hasAnyData === undefined) {

View file

@ -43,7 +43,7 @@ const TimelineDetailsPanel = () => {
};
const CaseContainerComponent: React.FC = () => {
const { cases: casesUi } = useKibana().services;
const { cases } = useKibana().services;
const { getAppUrl, navigateTo } = useNavigation();
const userPermissions = useGetUserCasesPermissions();
const dispatch = useDispatch();
@ -98,7 +98,7 @@ const CaseContainerComponent: React.FC = () => {
return (
<SecuritySolutionPageWrapper noPadding>
<CaseDetailsRefreshContext.Provider value={refreshRef}>
{casesUi.getCases({
{cases.ui.getCases({
basePath: CASES_PATH,
owner: [APP_ID],
features: {

View file

@ -109,7 +109,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
unit,
}) => {
const dispatch = useDispatch();
const { timelines: timelinesUi, cases: casesUi } = useKibana().services;
const { timelines: timelinesUi, cases } = useKibana().services;
const {
browserFields,
dataViewId,
@ -184,7 +184,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
});
const casesPermissions = useGetUserCasesPermissions();
const CasesContext = casesUi.getCasesContext();
const CasesContext = cases.ui.getCasesContext();
return (
<>

View file

@ -357,7 +357,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []);
const casesPermissions = useGetUserCasesPermissions();
const CasesContext = kibana.services.cases.getCasesContext();
const CasesContext = kibana.services.cases.ui.getCasesContext();
if (loading || isEmpty(selectedPatterns)) {
return null;

View file

@ -74,7 +74,7 @@ jest.mock('../../../common/lib/kibana', () => {
},
},
cases: {
getCasesContext: mockCasesContext,
ui: { getCasesContext: mockCasesContext },
},
uiSettings: {
get: jest.fn(),

View file

@ -12,11 +12,11 @@ import { APP_ID } from '../../../../common/constants';
const MAX_CASES_TO_SHOW = 3;
const RecentCasesComponent = () => {
const { cases: casesUi } = useKibana().services;
const { cases } = useKibana().services;
const userCanCrud = useGetUserCasesPermissions()?.crud ?? false;
return casesUi.getRecentCases({
return cases.ui.getRecentCases({
userCanCrud,
maxCasesToShow: MAX_CASES_TO_SHOW,
owner: [APP_ID],

View file

@ -11,19 +11,18 @@ import { waitFor } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { Sidebar } from './sidebar';
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { casesPluginMock } from '../../../../../cases/public/mocks';
import { CasesUiStart } from '../../../../../cases/public';
import { casesPluginMock, CaseUiClientMock } from '../../../../../cases/public/mocks';
jest.mock('../../../common/lib/kibana');
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
describe('Sidebar', () => {
let casesMock: jest.Mocked<CasesUiStart>;
let casesMock: CaseUiClientMock;
beforeEach(() => {
casesMock = casesPluginMock.createStartContract();
casesMock.getRecentCases.mockImplementation(() => <>{'test'}</>);
casesMock.ui.getRecentCases.mockImplementation(() => <>{'test'}</>);
useKibanaMock.mockReturnValue({
services: {
cases: casesMock,
@ -50,7 +49,7 @@ describe('Sidebar', () => {
)
);
expect(casesMock.getRecentCases).not.toHaveBeenCalled();
expect(casesMock.ui.getRecentCases).not.toHaveBeenCalled();
});
it('does render the recently created cases section when the user has read permissions', async () => {
@ -67,6 +66,6 @@ describe('Sidebar', () => {
)
);
expect(casesMock.getRecentCases).toHaveBeenCalled();
expect(casesMock.ui.getRecentCases).toHaveBeenCalled();
});
});

View file

@ -47,7 +47,7 @@ describe('AddToCaseButton', () => {
it('navigates to the correct path without id', async () => {
const here = jest.fn();
useKibanaMock().services.cases.getAllCasesSelectorModal = here.mockImplementation(
useKibanaMock().services.cases.ui.getAllCasesSelectorModal = here.mockImplementation(
({ onRowClick }) => {
onRowClick();
return <></>;
@ -69,7 +69,7 @@ describe('AddToCaseButton', () => {
});
it('navigates to the correct path with id', async () => {
useKibanaMock().services.cases.getAllCasesSelectorModal = jest
useKibanaMock().services.cases.ui.getAllCasesSelectorModal = jest
.fn()
.mockImplementation(({ onRowClick }) => {
onRowClick({ id: 'case-id' });

View file

@ -161,7 +161,7 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
<EuiContextMenuPanel items={items} />
</EuiPopover>
{isCaseModalOpen &&
cases.getAllCasesSelectorModal({
cases.ui.getAllCasesSelectorModal({
onRowClick,
userCanCrud: userPermissions?.crud ?? false,
owner: [APP_ID],

View file

@ -137,7 +137,9 @@ describe('event details footer component', () => {
get: jest.fn().mockReturnValue([]),
},
cases: {
getCasesContext: () => mockCasesContext,
ui: {
getCasesContext: () => mockCasesContext,
},
},
},
});

View file

@ -100,7 +100,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
services: { cases },
} = useKibana();
const CasesContext = cases.getCasesContext();
const CasesContext = cases.ui.getCasesContext();
const casesPermissions = useGetUserCasesPermissions();
const [isIsolateActionSuccessBannerVisible, setIsIsolateActionSuccessBannerVisible] =

View file

@ -125,7 +125,7 @@ describe('Details Panel Component', () => {
navigateToApp: jest.fn(),
},
cases: {
getCasesContext: () => mockCasesContext,
ui: { getCasesContext: () => mockCasesContext },
},
},
});

View file

@ -229,7 +229,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
);
const kibana = useKibana();
const casesPermissions = useGetUserCasesPermissions();
const CasesContext = kibana.services.cases.getCasesContext();
const CasesContext = kibana.services.cases.ui.getCasesContext();
return (
<>

View file

@ -58,7 +58,9 @@ jest.mock('../../../../common/lib/kibana', () => {
getUrlForApp: jest.fn(),
},
cases: {
getCasesContext: () => mockCasesContext,
ui: {
getCasesContext: () => mockCasesContext,
},
},
docLinks: { links: { query: { eql: 'url-eql_doc' } } },
uiSettings: {

View file

@ -53,7 +53,9 @@ jest.mock('../../../../common/lib/kibana', () => {
getUrlForApp: jest.fn(),
},
cases: {
getCasesContext: () => mockCasesContext,
ui: {
getCasesContext: () => mockCasesContext,
},
},
uiSettings: {
get: jest.fn(),

View file

@ -61,7 +61,9 @@ jest.mock('../../../../common/lib/kibana', () => {
getUrlForApp: jest.fn(),
},
cases: {
getCasesContext: () => mockCasesContext,
ui: {
getCasesContext: () => mockCasesContext,
},
},
uiSettings: {
get: jest.fn(),