mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Cases] Integrate routes and navigation (#117582)
* getCases function and router * all pages router * navigation hooks created * external navigations removed * basePath in cases context * context optimization * no privileges screen * new files * CasesDeepLinkIds constant renamed * remove props spreading * AllCasesList tests * Fix types and tests: Part 1 * Fix types and tests: Part 2 * Move glasses badge logic inside cases * Fix export types * Improve helpers * observability changes integrated * Small fixes * Fix timelines unit tests * Add readonly badge test * test fixed * form context test fixed * fix breadcrumbs test * fix types in o11y routes * Fix more tests * Fix bug * urlType fixes * Fix cypress tests * configure header conflict solved * Fix i18n * fix breadcrumbs test * tests and suggestions * Add navigation tests * README updated * update plugin list docs * Add more tests * Fix i18n * More tests * Fix README * Fix types * fix resolve redirect paths * fix flyout z-index on timeline * add flyout z-index class comment * use kibana currentAppId and application observables instead of passing props * Get application info from the hook * Fix tests * Fix more tests * tests fixed * Fix container tests * Fix container tests * test updated Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
This commit is contained in:
parent
d6217470e6
commit
4eb797a8b2
210 changed files with 4031 additions and 6574 deletions
|
@ -358,8 +358,7 @@ The plugin exposes the static DefaultEditorController class to consume.
|
|||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cases/README.md[cases]
|
||||
|[![Issues][issues-shield]][issues-url]
|
||||
[![Pull Requests][pr-shield]][pr-url]
|
||||
|This plugin provides cases management in Kibana
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/cloud/README.md[cloud]
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# Case management in Kibana
|
||||
# Case
|
||||
|
||||
This plugin provides cases management in Kibana
|
||||
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![Pull Requests][pr-shield]][pr-url]
|
||||
|
||||
# Docs
|
||||
## Docs
|
||||
|
||||
![Cases Logo][cases-logo]
|
||||
![Cases Logo][cases-logo]
|
||||
|
||||
[Report Bug](https://github.com/elastic/kibana/issues/new?assignees=&labels=bug&template=Bug_report.md)
|
||||
·
|
||||
|
@ -18,16 +20,18 @@
|
|||
- [Cases UI](#cases-ui)
|
||||
- [Case Action Type](#case-action-type) _feature in development, disabled by default_
|
||||
|
||||
|
||||
## Cases API
|
||||
|
||||
[**Explore the API docs »**](https://www.elastic.co/guide/en/security/current/cases-api-overview.html)
|
||||
|
||||
## Cases Client API
|
||||
|
||||
[**Cases Client API docs**][cases-client-api-docs]
|
||||
|
||||
## Cases UI
|
||||
|
||||
#### Embed Cases UI components in any Kibana plugin
|
||||
|
||||
- Add `CasesUiStart` to Kibana plugin `StartServices` dependencies:
|
||||
|
||||
```ts
|
||||
|
@ -37,139 +41,107 @@ cases: CasesUiStart;
|
|||
#### Cases UI Methods
|
||||
|
||||
- From the UI component, get the component from the `useKibana` hook start services
|
||||
|
||||
```tsx
|
||||
const { cases } = useKibana().services;
|
||||
// call in the return as you would any component
|
||||
cases.getCreateCase({
|
||||
onCancel: handleSetIsCancel,
|
||||
onSuccess,
|
||||
lensIntegration?: {
|
||||
plugins: {
|
||||
parsingPlugin,
|
||||
processingPluginRenderer,
|
||||
uiPlugin,
|
||||
},
|
||||
hooks: {
|
||||
useInsertTimeline,
|
||||
},
|
||||
}
|
||||
timelineIntegration?: {
|
||||
plugins: {
|
||||
parsingPlugin,
|
||||
processingPluginRenderer,
|
||||
uiPlugin,
|
||||
},
|
||||
hooks: {
|
||||
useInsertTimeline,
|
||||
},
|
||||
const { cases } = useKibana().services;
|
||||
// call in the return as you would any component
|
||||
cases.getCases({
|
||||
basePath: '/investigate/cases',
|
||||
userCanCrud: true,
|
||||
owner: ['securitySolution'],
|
||||
timelineIntegration: {
|
||||
plugins: {
|
||||
parsingPlugin,
|
||||
processingPluginRenderer,
|
||||
uiPlugin,
|
||||
},
|
||||
})
|
||||
hooks: {
|
||||
useInsertTimeline,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
##### Methods:
|
||||
### `getAllCases`
|
||||
|
||||
### `getCases`
|
||||
|
||||
Arguments:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| caseDetailsNavigation | `CasesNavigation<CaseDetailsHrefSchema, 'configurable'>` route configuration to generate the case details url for the case details page |
|
||||
| configureCasesNavigation | `CasesNavigation` route configuration for configure cases page |
|
||||
| createCaseNavigation | `CasesNavigation` route configuration for create cases page |
|
||||
| userCanCrud | `boolean;` user permissions to crud |
|
||||
| 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, Ecs>];` fetch alerts |
|
||||
| disableAlerts? | `boolean` (default: false) flag to not show alerts information |
|
||||
| actionsNavigation? | <code>CasesNavigation<string, 'configurable'></code> |
|
||||
| ruleDetailsNavigation? | <code>CasesNavigation<string | null | undefined, 'configurable'></code> |
|
||||
| onComponentInitialized? | `() => void;` callback when component has initialized |
|
||||
| showAlertDetails? | `(alertId: string, index: string) => void;` callback to show alert details |
|
||||
| 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`
|
||||
|
||||
Arguments:
|
||||
|
||||
| Property | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| alertData? | `Omit<CommentRequestAlertType, 'type'>;` alert data to post to case |
|
||||
| createCaseNavigation | `CasesNavigation` route configuration for create cases page |
|
||||
| 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 |
|
||||
| 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 | SubCase) => void;</code> callback for row click, passing case in row |
|
||||
| updateCase? | <code>(theCase: Case | SubCase) => void;</code> callback after case has been updated |
|
||||
| onClose? | `() => void` called when the modal is closed without selecting a case |
|
||||
|
||||
UI component:
|
||||
![All Cases Selector Modal Component][all-cases-modal-img]
|
||||
|
||||
### `getCaseView`
|
||||
### `getCreateCaseFlyout`
|
||||
|
||||
Arguments:
|
||||
|
||||
| Property | Description |
|
||||
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| caseDetailsNavigation | `CasesNavigation<CaseDetailsHrefSchema, 'configurable'>` route configuration to generate the case details url for the case details page |
|
||||
| caseId | `string;` ID of the case |
|
||||
| configureCasesNavigation | `CasesNavigation` route configuration for configure cases page |
|
||||
| createCaseNavigation | `CasesNavigation` route configuration for create cases page |
|
||||
| getCaseDetailHrefWithCommentId | `(commentId: string) => string;` callback to generate the case details url with a comment id reference from the case id and comment id |
|
||||
| onComponentInitialized? | `() => void;` callback when component has initialized |
|
||||
| onCaseDataSuccess? | `(data: Case) => void;` optional callback to handle case data in consuming application |
|
||||
| ruleDetailsNavigation | <code>CasesNavigation<string | null | undefined, 'configurable'></code> |
|
||||
| showAlertDetails | `(alertId: string, index: string) => void;` callback to show alert details |
|
||||
| subCaseId? | `string;` subcase id |
|
||||
| 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` |
|
||||
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, Ecs>];` fetch alerts |
|
||||
| userCanCrud | `boolean;` user permissions to crud |
|
||||
| Property | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| userCanCrud | `boolean;` user permissions to crud |
|
||||
| owner | `string[];` owner ids of the cases |
|
||||
| onClose | `() => void;` callback when create case is canceled |
|
||||
| onSuccess | `(theCase: Case) => Promise<void>;` callback passing newly created case after pushCaseToExternalService is called |
|
||||
| afterCaseCreated? | `(theCase: Case) => Promise<void>;` callback passing newly created case before pushCaseToExternalService is called |
|
||||
| disableAlerts? | `boolean` (default: false) flag to not show alerts information |
|
||||
|
||||
UI component:
|
||||
![Case View Component][case-view-img]
|
||||
|
||||
### `getCreateCase`
|
||||
Arguments:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| afterCaseCreated? | `(theCase: Case) => Promise<void>;` callback passing newly created case before pushCaseToExternalService is called |
|
||||
| onCancel | `() => void;` callback when create case is canceled |
|
||||
| onSuccess | `(theCase: Case) => Promise<void>;` callback passing newly created case after pushCaseToExternalService is called |
|
||||
| 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` |
|
||||
|
||||
UI component:
|
||||
![Create Component][create-img]
|
||||
|
||||
### `getConfigureCases`
|
||||
Arguments:
|
||||
|
||||
| Property | Description |
|
||||
| ----------- | ------------------------------------ |
|
||||
| userCanCrud | `boolean;` user permissions to crud |
|
||||
|
||||
UI component:
|
||||
![Configure Component][configure-img]
|
||||
![Create Component][create-img]
|
||||
|
||||
### `getRecentCases`
|
||||
|
||||
Arguments:
|
||||
|
||||
| Property | Description |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| allCasesNavigation | `CasesNavigation` route configuration for configure cases page |
|
||||
| caseDetailsNavigation | `CasesNavigation<CaseDetailsHrefSchema, 'configurable'>` route configuration to generate the case details url for the case details page |
|
||||
| createCaseNavigation | `CasesNavigation` route configuration for create case page |
|
||||
| maxCasesToShow | `number;` number of cases to show in widget |
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------- |
|
||||
| userCanCrud | `boolean;` user permissions to crud |
|
||||
| owner | `string[];` owner ids of the cases |
|
||||
| maxCasesToShow | `number;` number of cases to show in widget |
|
||||
|
||||
UI component:
|
||||
![Recent Cases Component][recent-cases-img]
|
||||
![Recent Cases Component][recent-cases-img]
|
||||
|
||||
## Case Action Type
|
||||
|
||||
_***Feature in development, disabled by default**_
|
||||
_**\*Feature in development, disabled by default**_
|
||||
|
||||
See [Kibana Actions](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions) for more information.
|
||||
|
||||
|
||||
ID: `.case`
|
||||
|
||||
The params properties are modelled after the arguments to the [Cases API](https://www.elastic.co/guide/en/security/master/cases-api-overview.html).
|
||||
|
@ -281,13 +253,9 @@ Connectors of type (`.none`) should have the `fields` attribute set to `null`.
|
|||
| ---------- | ------------------------------ | ------- |
|
||||
| syncAlerts | Turn on or off alert synching. | boolean |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 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
|
||||
|
|
|
@ -32,3 +32,16 @@ export const useCurrentUser = jest.fn();
|
|||
export const withKibana = jest.fn(createWithKibanaMock());
|
||||
export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock());
|
||||
export const useGetUserSavedObjectPermissions = jest.fn();
|
||||
|
||||
export const useAppUrl = jest.fn().mockReturnValue({
|
||||
getAppUrl: jest.fn(),
|
||||
});
|
||||
|
||||
export const useNavigateTo = jest.fn().mockReturnValue({
|
||||
navigateTo: jest.fn(),
|
||||
});
|
||||
|
||||
export const useNavigation = jest.fn().mockReturnValue({
|
||||
getAppUrl: jest.fn(),
|
||||
navigateTo: jest.fn(),
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import { AuthenticatedUser } from '../../../../../security/common/model';
|
|||
import { convertToCamelCase } from '../../../containers/utils';
|
||||
import { StartServices } from '../../../types';
|
||||
import { useUiSetting, useKibana } from './kibana_react';
|
||||
import { NavigateToAppOptions } from '../../../../../../../src/core/public';
|
||||
|
||||
export const useDateFormat = (): string => useUiSetting<string>(DEFAULT_DATE_FORMAT);
|
||||
|
||||
|
@ -104,3 +105,53 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => {
|
|||
}, [fetchUser]);
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a full URL to the provided page path by using
|
||||
* kibana's `getUrlForApp()`
|
||||
*/
|
||||
export const useAppUrl = (appId: string) => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
|
||||
const getAppUrl = useCallback(
|
||||
(options?: { deepLinkId?: string; path?: string; absolute?: boolean }) =>
|
||||
getUrlForApp(appId, options),
|
||||
[appId, getUrlForApp]
|
||||
);
|
||||
return { getAppUrl };
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to any app using kibana's `navigateToApp()`
|
||||
* or by url using `navigateToUrl()`
|
||||
*/
|
||||
export const useNavigateTo = (appId: string) => {
|
||||
const { navigateToApp, navigateToUrl } = useKibana().services.application;
|
||||
|
||||
const navigateTo = useCallback(
|
||||
({
|
||||
url,
|
||||
...options
|
||||
}: {
|
||||
url?: string;
|
||||
} & NavigateToAppOptions) => {
|
||||
if (url) {
|
||||
navigateToUrl(url);
|
||||
} else {
|
||||
navigateToApp(appId, options);
|
||||
}
|
||||
},
|
||||
[appId, navigateToApp, navigateToUrl]
|
||||
);
|
||||
return { navigateTo };
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns navigateTo and getAppUrl navigation hooks
|
||||
*
|
||||
*/
|
||||
export const useNavigation = (appId: string) => {
|
||||
const { navigateTo } = useNavigateTo(appId);
|
||||
const { getAppUrl } = useAppUrl(appId);
|
||||
return { navigateTo, getAppUrl };
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { PublicAppInfo } from 'kibana/public';
|
||||
import { RecursivePartial } from '@elastic/eui/src/components/common';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -17,11 +18,12 @@ import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common';
|
|||
import { securityMock } from '../../../../../security/public/mocks';
|
||||
import { spacesPluginMock } from '../../../../../spaces/public/mocks';
|
||||
import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export const createStartServicesMock = (): StartServices =>
|
||||
({
|
||||
export const createStartServicesMock = (): StartServices => {
|
||||
const services = {
|
||||
...coreMock.createStart(),
|
||||
storage: { ...coreMock.createStorage(), remove: jest.fn() },
|
||||
storage: { ...coreMock.createStorage(), get: jest.fn(), set: jest.fn(), remove: jest.fn() },
|
||||
lens: {
|
||||
canUseEditor: jest.fn(),
|
||||
navigateToPrefilledEditor: jest.fn(),
|
||||
|
@ -29,7 +31,15 @@ export const createStartServicesMock = (): StartServices =>
|
|||
security: securityMock.createStart(),
|
||||
triggersActionsUi: triggersActionsUiMock.createStart(),
|
||||
spaces: spacesPluginMock.createStartContract(),
|
||||
} as unknown as StartServices);
|
||||
} as unknown as StartServices;
|
||||
|
||||
services.application.currentAppId$ = new BehaviorSubject<string>('testAppId');
|
||||
services.application.applications$ = new BehaviorSubject<Map<string, PublicAppInfo>>(
|
||||
new Map([['testAppId', { category: { label: 'Test' } } as unknown as PublicAppInfo]])
|
||||
);
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
export const createWithKibanaMock = () => {
|
||||
const services = createStartServicesMock();
|
||||
|
|
|
@ -8,31 +8,28 @@
|
|||
import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { OwnerProvider } from '../../components/owner_context';
|
||||
import {
|
||||
createKibanaContextProviderMock,
|
||||
createStartServicesMock,
|
||||
} from '../lib/kibana/kibana_react.mock';
|
||||
import { CasesProvider } from '../../components/cases_context';
|
||||
import { createKibanaContextProviderMock } from '../lib/kibana/kibana_react.mock';
|
||||
import { FieldHook } from '../shared_imports';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
userCanCrud?: boolean;
|
||||
}
|
||||
|
||||
export const kibanaObservable = new BehaviorSubject(createStartServicesMock());
|
||||
|
||||
window.scrollTo = jest.fn();
|
||||
const MockKibanaContextProvider = createKibanaContextProviderMock();
|
||||
|
||||
/** A utility for wrapping children in the providers required to run most tests */
|
||||
const TestProvidersComponent: React.FC<Props> = ({ children }) => (
|
||||
const TestProvidersComponent: React.FC<Props> = ({ children, userCanCrud = true }) => (
|
||||
<I18nProvider>
|
||||
<MockKibanaContextProvider>
|
||||
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
|
||||
<OwnerProvider owner={[SECURITY_SOLUTION_OWNER]}>{children}</OwnerProvider>
|
||||
<CasesProvider value={{ owner: [SECURITY_SOLUTION_OWNER], userCanCrud }}>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
</ThemeProvider>
|
||||
</MockKibanaContextProvider>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 const useCaseViewParams = jest.fn().mockReturnValue({ detailName: 'basic-case-id' });
|
||||
|
||||
export const useAllCasesNavigation = jest.fn().mockReturnValue({
|
||||
getAllCasesUrl: jest.fn().mockReturnValue('/app/security/cases'),
|
||||
navigateToAllCases: jest.fn(),
|
||||
});
|
||||
|
||||
export const useCreateCaseNavigation = jest.fn().mockReturnValue({
|
||||
getCreateCaseUrl: jest.fn().mockReturnValue('/app/security/cases/create'),
|
||||
navigateToCreateCase: jest.fn(),
|
||||
});
|
||||
|
||||
export const useCaseViewNavigation = jest.fn().mockReturnValue({
|
||||
getCaseViewUrl: jest.fn().mockReturnValue('/app/security/cases/test'),
|
||||
navigateToCaseView: jest.fn(),
|
||||
});
|
||||
|
||||
export const useConfigureCasesNavigation = jest.fn().mockReturnValue({
|
||||
getConfigureCasesUrl: jest.fn().mockReturnValue('/app/security/cases/configure'),
|
||||
navigateToConfigureCases: jest.fn(),
|
||||
});
|
162
x-pack/plugins/cases/public/common/navigation/deep_links.test.ts
Normal file
162
x-pack/plugins/cases/public/common/navigation/deep_links.test.ts
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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 { AppNavLinkStatus } from '../../../../../../src/core/public';
|
||||
import { getCasesDeepLinks } from './deep_links';
|
||||
|
||||
describe('getCasesDeepLinks', () => {
|
||||
it('it returns the deep links', () => {
|
||||
const deepLinks = getCasesDeepLinks({});
|
||||
expect(deepLinks).toEqual({
|
||||
id: 'cases',
|
||||
path: '/cases',
|
||||
title: 'Cases',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases_create',
|
||||
path: '/cases/create',
|
||||
title: 'Create New Case',
|
||||
},
|
||||
{
|
||||
id: 'cases_configure',
|
||||
path: '/cases/configure',
|
||||
title: 'Configure Cases',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('it returns the deep links with a different base bath', () => {
|
||||
const deepLinks = getCasesDeepLinks({ basePath: '/test' });
|
||||
expect(deepLinks).toEqual({
|
||||
id: 'cases',
|
||||
path: '/test',
|
||||
title: 'Cases',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases_create',
|
||||
path: '/test/create',
|
||||
title: 'Create New Case',
|
||||
},
|
||||
{
|
||||
id: 'cases_configure',
|
||||
path: '/test/configure',
|
||||
title: 'Configure Cases',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('it extends the deep links correctly', () => {
|
||||
const deepLinks = getCasesDeepLinks({
|
||||
extend: {
|
||||
cases: {
|
||||
searchable: false,
|
||||
},
|
||||
cases_create: {
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
cases_configure: {
|
||||
order: 8002,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(deepLinks).toEqual({
|
||||
id: 'cases',
|
||||
path: '/cases',
|
||||
title: 'Cases',
|
||||
searchable: false,
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases_create',
|
||||
path: '/cases/create',
|
||||
title: 'Create New Case',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
{
|
||||
id: 'cases_configure',
|
||||
path: '/cases/configure',
|
||||
title: 'Configure Cases',
|
||||
order: 8002,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('it does not overrides the id, the path, and the deepLinks', () => {
|
||||
const deepLinks = getCasesDeepLinks({
|
||||
extend: {
|
||||
cases: {
|
||||
id: 'cases_override',
|
||||
path: 'cases_path_override',
|
||||
deepLinks: [],
|
||||
},
|
||||
cases_create: {
|
||||
id: 'cases_create_override',
|
||||
path: 'cases_create_path_override',
|
||||
},
|
||||
cases_configure: {
|
||||
id: 'cases_configure_override',
|
||||
path: 'cases_configure_path_override',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(deepLinks).toEqual({
|
||||
id: 'cases',
|
||||
path: '/cases',
|
||||
title: 'Cases',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases_create',
|
||||
path: '/cases/create',
|
||||
title: 'Create New Case',
|
||||
},
|
||||
{
|
||||
id: 'cases_configure',
|
||||
path: '/cases/configure',
|
||||
title: 'Configure Cases',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('it overrides the title correctly', () => {
|
||||
const deepLinks = getCasesDeepLinks({
|
||||
extend: {
|
||||
cases: {
|
||||
title: 'My new cases title',
|
||||
},
|
||||
cases_create: {
|
||||
title: 'My new create cases title',
|
||||
},
|
||||
cases_configure: {
|
||||
title: 'My new configure cases title',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(deepLinks).toEqual({
|
||||
id: 'cases',
|
||||
path: '/cases',
|
||||
title: 'My new cases title',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases_create',
|
||||
path: '/cases/create',
|
||||
title: 'My new create cases title',
|
||||
},
|
||||
{
|
||||
id: 'cases_configure',
|
||||
path: '/cases/configure',
|
||||
title: 'My new configure cases title',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
51
x-pack/plugins/cases/public/common/navigation/deep_links.ts
Normal file
51
x-pack/plugins/cases/public/common/navigation/deep_links.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import type { AppDeepLink } from '../../../../../../src/core/public';
|
||||
import { DEFAULT_BASE_PATH, getCreateCasePath, getCasesConfigurePath } from './paths';
|
||||
|
||||
export const CasesDeepLinkId = {
|
||||
cases: 'cases',
|
||||
casesCreate: 'cases_create',
|
||||
casesConfigure: 'cases_configure',
|
||||
} as const;
|
||||
|
||||
export type ICasesDeepLinkId = typeof CasesDeepLinkId[keyof typeof CasesDeepLinkId];
|
||||
|
||||
export const getCasesDeepLinks = <T extends AppDeepLink = AppDeepLink>({
|
||||
basePath = DEFAULT_BASE_PATH,
|
||||
extend = {},
|
||||
}: {
|
||||
basePath?: string;
|
||||
extend?: Partial<Record<ICasesDeepLinkId, Partial<T>>>;
|
||||
}) => ({
|
||||
title: i18n.translate('xpack.cases.navigation.cases', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
...(extend[CasesDeepLinkId.cases] ?? {}),
|
||||
id: CasesDeepLinkId.cases,
|
||||
path: basePath,
|
||||
deepLinks: [
|
||||
{
|
||||
title: i18n.translate('xpack.cases.navigation.create', {
|
||||
defaultMessage: 'Create New Case',
|
||||
}),
|
||||
...(extend[CasesDeepLinkId.casesCreate] ?? {}),
|
||||
id: CasesDeepLinkId.casesCreate,
|
||||
path: getCreateCasePath(basePath),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.cases.navigation.configure', {
|
||||
defaultMessage: 'Configure Cases',
|
||||
}),
|
||||
...(extend[CasesDeepLinkId.casesConfigure] ?? {}),
|
||||
id: CasesDeepLinkId.casesConfigure,
|
||||
path: getCasesConfigurePath(basePath),
|
||||
},
|
||||
],
|
||||
});
|
170
x-pack/plugins/cases/public/common/navigation/hooks.test.tsx
Normal file
170
x-pack/plugins/cases/public/common/navigation/hooks.test.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useNavigation } from '../../common/lib/kibana';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
useCasesNavigation,
|
||||
useAllCasesNavigation,
|
||||
useCreateCaseNavigation,
|
||||
useConfigureCasesNavigation,
|
||||
useCaseViewNavigation,
|
||||
} from './hooks';
|
||||
import { CasesDeepLinkId } from './deep_links';
|
||||
|
||||
const useNavigationMock = useNavigation as jest.Mock;
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const navigateTo = jest.fn();
|
||||
const getAppUrl = jest.fn();
|
||||
|
||||
describe('hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useNavigationMock.mockReturnValue({ navigateTo, getAppUrl });
|
||||
});
|
||||
|
||||
describe('useCasesNavigation', () => {
|
||||
it('it calls getAppUrl with correct arguments', () => {
|
||||
const { result } = renderHook(() => useCasesNavigation(CasesDeepLinkId.cases), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
const [getCasesUrl] = result.current;
|
||||
|
||||
act(() => {
|
||||
getCasesUrl(false);
|
||||
});
|
||||
|
||||
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases' });
|
||||
});
|
||||
|
||||
it('it calls navigateToAllCases with correct arguments', () => {
|
||||
const { result } = renderHook(() => useCasesNavigation(CasesDeepLinkId.cases), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
const [, navigateToCases] = result.current;
|
||||
|
||||
act(() => {
|
||||
navigateToCases();
|
||||
});
|
||||
|
||||
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAllCasesNavigation', () => {
|
||||
it('it calls getAppUrl with correct arguments', () => {
|
||||
const { result } = renderHook(() => useAllCasesNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.getAllCasesUrl(false);
|
||||
});
|
||||
|
||||
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases' });
|
||||
});
|
||||
|
||||
it('it calls navigateToAllCases with correct arguments', () => {
|
||||
const { result } = renderHook(() => useAllCasesNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.navigateToAllCases();
|
||||
});
|
||||
|
||||
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateCaseNavigation', () => {
|
||||
it('it calls getAppUrl with correct arguments', () => {
|
||||
const { result } = renderHook(() => useCreateCaseNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.getCreateCaseUrl(false);
|
||||
});
|
||||
|
||||
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases_create' });
|
||||
});
|
||||
|
||||
it('it calls navigateToAllCases with correct arguments', () => {
|
||||
const { result } = renderHook(() => useCreateCaseNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.navigateToCreateCase();
|
||||
});
|
||||
|
||||
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases_create' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('useConfigureCasesNavigation', () => {
|
||||
it('it calls getAppUrl with correct arguments', () => {
|
||||
const { result } = renderHook(() => useConfigureCasesNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.getConfigureCasesUrl(false);
|
||||
});
|
||||
|
||||
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases_configure' });
|
||||
});
|
||||
|
||||
it('it calls navigateToAllCases with correct arguments', () => {
|
||||
const { result } = renderHook(() => useConfigureCasesNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.navigateToConfigureCases();
|
||||
});
|
||||
|
||||
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases_configure' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCaseViewNavigation', () => {
|
||||
it('it calls getAppUrl with correct arguments', () => {
|
||||
const { result } = renderHook(() => useCaseViewNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.getCaseViewUrl({ detailName: 'test' }, false);
|
||||
});
|
||||
|
||||
expect(getAppUrl).toHaveBeenCalledWith({
|
||||
absolute: false,
|
||||
deepLinkId: 'cases',
|
||||
path: '/test',
|
||||
});
|
||||
});
|
||||
|
||||
it('it calls navigateToAllCases with correct arguments', () => {
|
||||
const { result } = renderHook(() => useCaseViewNavigation(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.navigateToCaseView({ detailName: 'test' });
|
||||
});
|
||||
|
||||
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases', path: '/test' });
|
||||
});
|
||||
});
|
||||
});
|
73
x-pack/plugins/cases/public/common/navigation/hooks.ts
Normal file
73
x-pack/plugins/cases/public/common/navigation/hooks.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigation } from '../lib/kibana';
|
||||
import { useCasesContext } from '../../components/cases_context/use_cases_context';
|
||||
import { CasesDeepLinkId, ICasesDeepLinkId } from './deep_links';
|
||||
import { CaseViewPathParams, generateCaseViewPath } from './paths';
|
||||
|
||||
export const useCaseViewParams = () => useParams<CaseViewPathParams>();
|
||||
|
||||
type GetCasesUrl = (absolute?: boolean) => string;
|
||||
type NavigateToCases = () => void;
|
||||
type UseCasesNavigation = [GetCasesUrl, NavigateToCases];
|
||||
|
||||
export const useCasesNavigation = (deepLinkId: ICasesDeepLinkId): UseCasesNavigation => {
|
||||
const { appId } = useCasesContext();
|
||||
const { navigateTo, getAppUrl } = useNavigation(appId);
|
||||
const getCasesUrl = useCallback<GetCasesUrl>(
|
||||
(absolute) => getAppUrl({ deepLinkId, absolute }),
|
||||
[getAppUrl, deepLinkId]
|
||||
);
|
||||
const navigateToCases = useCallback<NavigateToCases>(
|
||||
() => navigateTo({ deepLinkId }),
|
||||
[navigateTo, deepLinkId]
|
||||
);
|
||||
return [getCasesUrl, navigateToCases];
|
||||
};
|
||||
|
||||
export const useAllCasesNavigation = () => {
|
||||
const [getAllCasesUrl, navigateToAllCases] = useCasesNavigation(CasesDeepLinkId.cases);
|
||||
return { getAllCasesUrl, navigateToAllCases };
|
||||
};
|
||||
|
||||
export const useCreateCaseNavigation = () => {
|
||||
const [getCreateCaseUrl, navigateToCreateCase] = useCasesNavigation(CasesDeepLinkId.casesCreate);
|
||||
return { getCreateCaseUrl, navigateToCreateCase };
|
||||
};
|
||||
|
||||
export const useConfigureCasesNavigation = () => {
|
||||
const [getConfigureCasesUrl, navigateToConfigureCases] = useCasesNavigation(
|
||||
CasesDeepLinkId.casesConfigure
|
||||
);
|
||||
return { getConfigureCasesUrl, navigateToConfigureCases };
|
||||
};
|
||||
|
||||
type GetCaseViewUrl = (pathParams: CaseViewPathParams, absolute?: boolean) => string;
|
||||
type NavigateToCaseView = (pathParams: CaseViewPathParams) => void;
|
||||
|
||||
export const useCaseViewNavigation = () => {
|
||||
const { appId } = useCasesContext();
|
||||
const { navigateTo, getAppUrl } = useNavigation(appId);
|
||||
const getCaseViewUrl = useCallback<GetCaseViewUrl>(
|
||||
(pathParams, absolute) =>
|
||||
getAppUrl({
|
||||
deepLinkId: CasesDeepLinkId.cases,
|
||||
absolute,
|
||||
path: generateCaseViewPath(pathParams),
|
||||
}),
|
||||
[getAppUrl]
|
||||
);
|
||||
const navigateToCaseView = useCallback<NavigateToCaseView>(
|
||||
(pathParams) =>
|
||||
navigateTo({ deepLinkId: CasesDeepLinkId.cases, path: generateCaseViewPath(pathParams) }),
|
||||
[navigateTo]
|
||||
);
|
||||
return { getCaseViewUrl, navigateToCaseView };
|
||||
};
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import md5 from 'md5';
|
||||
|
||||
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
|
||||
md5(ids.join(delimiter));
|
||||
export * from './deep_links';
|
||||
export * from './paths';
|
||||
export * from './hooks';
|
84
x-pack/plugins/cases/public/common/navigation/paths.test.ts
Normal file
84
x-pack/plugins/cases/public/common/navigation/paths.test.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 {
|
||||
getCreateCasePath,
|
||||
getSubCaseViewPath,
|
||||
getCaseViewPath,
|
||||
getCasesConfigurePath,
|
||||
getCaseViewWithCommentPath,
|
||||
getSubCaseViewWithCommentPath,
|
||||
generateCaseViewPath,
|
||||
} from './paths';
|
||||
|
||||
describe('Paths', () => {
|
||||
describe('getCreateCasePath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(getCreateCasePath('test')).toBe('test/create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCasesConfigurePath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(getCasesConfigurePath('test')).toBe('test/configure');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCaseViewPath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(getCaseViewPath('test')).toBe('test/:detailName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubCaseViewPath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(getSubCaseViewPath('test')).toBe('test/:detailName/sub-cases/:subCaseId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCaseViewWithCommentPath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(getCaseViewWithCommentPath('test')).toBe('test/:detailName/:commentId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubCaseViewWithCommentPath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(getSubCaseViewWithCommentPath('test')).toBe(
|
||||
'test/:detailName/sub-cases/:subCaseId/:commentId'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCaseViewPath', () => {
|
||||
it('returns the correct path', () => {
|
||||
expect(generateCaseViewPath({ detailName: 'my-case' })).toBe('/my-case');
|
||||
});
|
||||
|
||||
it('returns the correct path when subCaseId is provided', () => {
|
||||
expect(generateCaseViewPath({ detailName: 'my-case', subCaseId: 'my-sub-case' })).toBe(
|
||||
'/my-case/sub-cases/my-sub-case'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the correct path when commentId is provided', () => {
|
||||
expect(generateCaseViewPath({ detailName: 'my-case', commentId: 'my-comment' })).toBe(
|
||||
'/my-case/my-comment'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the correct path when subCaseId and commentId is provided', () => {
|
||||
expect(
|
||||
generateCaseViewPath({
|
||||
detailName: 'my-case',
|
||||
subCaseId: 'my-sub-case',
|
||||
commentId: 'my-comment',
|
||||
})
|
||||
).toBe('/my-case/sub-cases/my-sub-case/my-comment');
|
||||
});
|
||||
});
|
||||
});
|
49
x-pack/plugins/cases/public/common/navigation/paths.ts
Normal file
49
x-pack/plugins/cases/public/common/navigation/paths.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { generatePath } from 'react-router-dom';
|
||||
|
||||
export const DEFAULT_BASE_PATH = '/cases';
|
||||
export interface CaseViewPathParams {
|
||||
detailName: string;
|
||||
subCaseId?: string;
|
||||
commentId?: string;
|
||||
}
|
||||
|
||||
export const CASES_CREATE_PATH = '/create' as const;
|
||||
export const CASES_CONFIGURE_PATH = '/configure' as const;
|
||||
export const CASE_VIEW_PATH = '/:detailName' as const;
|
||||
export const SUB_CASE_VIEW_PATH = `${CASE_VIEW_PATH}/sub-cases/:subCaseId` as const;
|
||||
export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const;
|
||||
export const SUB_CASE_VIEW_COMMENT_PATH = `${SUB_CASE_VIEW_PATH}/:commentId` as const;
|
||||
|
||||
export const getCreateCasePath = (casesBasePath: string) => `${casesBasePath}${CASES_CREATE_PATH}`;
|
||||
export const getCasesConfigurePath = (casesBasePath: string) =>
|
||||
`${casesBasePath}${CASES_CONFIGURE_PATH}`;
|
||||
export const getCaseViewPath = (casesBasePath: string) => `${casesBasePath}${CASE_VIEW_PATH}`;
|
||||
export const getSubCaseViewPath = (casesBasePath: string) =>
|
||||
`${casesBasePath}${SUB_CASE_VIEW_PATH}`;
|
||||
export const getCaseViewWithCommentPath = (casesBasePath: string) =>
|
||||
`${casesBasePath}${CASE_VIEW_COMMENT_PATH}`;
|
||||
export const getSubCaseViewWithCommentPath = (casesBasePath: string) =>
|
||||
`${casesBasePath}${SUB_CASE_VIEW_COMMENT_PATH}`;
|
||||
|
||||
export const generateCaseViewPath = (params: CaseViewPathParams): string => {
|
||||
const { subCaseId, commentId } = params;
|
||||
// Cast for generatePath argument type constraint
|
||||
const pathParams = params as unknown as { [paramName: string]: string };
|
||||
if (subCaseId && commentId) {
|
||||
return generatePath(SUB_CASE_VIEW_COMMENT_PATH, pathParams);
|
||||
}
|
||||
if (subCaseId) {
|
||||
return generatePath(SUB_CASE_VIEW_PATH, pathParams);
|
||||
}
|
||||
if (commentId) {
|
||||
return generatePath(CASE_VIEW_COMMENT_PATH, pathParams);
|
||||
}
|
||||
return generatePath(CASE_VIEW_PATH, pathParams);
|
||||
};
|
|
@ -26,7 +26,7 @@ import { Form, useForm, UseField, useFormData } from '../../common/shared_import
|
|||
import * as i18n from './translations';
|
||||
import { schema, AddCommentFormSchema } from './schema';
|
||||
import { InsertTimeline } from '../insert_timeline';
|
||||
import { useOwnerContext } from '../owner_context/use_owner_context';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
const MySpinner = styled(EuiLoadingSpinner)`
|
||||
position: absolute;
|
||||
|
@ -72,7 +72,7 @@ export const AddComment = React.memo(
|
|||
) => {
|
||||
const editorRef = useRef<EuiMarkdownEditorRef>(null);
|
||||
const [focusOnContext, setFocusOnContext] = useState(false);
|
||||
const owner = useOwnerContext();
|
||||
const { owner } = useCasesContext();
|
||||
const { isLoading, postComment } = usePostComment();
|
||||
|
||||
const { form } = useForm<AddCommentFormSchema>({
|
||||
|
|
|
@ -1,101 +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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
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 { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { StatusAll } from '../../containers/types';
|
||||
import { CaseStatuses, SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { connectorsMock } from '../../containers/mock';
|
||||
import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks';
|
||||
import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors';
|
||||
|
||||
jest.mock('../../containers/use_get_reporters');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/use_get_action_license');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock('../../containers/api');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const createCaseNavigation = { href: '', onClick: jest.fn() };
|
||||
|
||||
const alertDataMock = {
|
||||
type: 'alert',
|
||||
rule: {
|
||||
id: 'rule-id',
|
||||
name: 'rule',
|
||||
},
|
||||
index: 'index-id',
|
||||
alertId: 'alert-id',
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
};
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const mockTriggersActionsUiService = triggersActionsUiMock.createStart();
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
triggersActionsUi: mockTriggersActionsUiService,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AllCasesGeneric ', () => {
|
||||
const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry;
|
||||
|
||||
beforeAll(() => {
|
||||
registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: 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();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,782 @@
|
|||
/*
|
||||
* 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 moment from 'moment-timezone';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import '../../common/mock/match_media';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
casesStatus,
|
||||
useGetCasesMockState,
|
||||
collectionCase,
|
||||
connectorsMock,
|
||||
} from '../../containers/mock';
|
||||
|
||||
import { CaseStatuses, CaseType, SECURITY_SOLUTION_OWNER, StatusAll } from '../../../common';
|
||||
import { getEmptyTagValue } from '../empty_value';
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
|
||||
import { useUpdateCases } from '../../containers/use_bulk_update_case';
|
||||
import { useGetActionLicense } from '../../containers/use_get_action_license';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { AllCasesList, AllCasesListProps } from './all_cases_list';
|
||||
import { CasesColumns, GetCasesColumn, useCasesColumns } from './columns';
|
||||
import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks';
|
||||
import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors';
|
||||
import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock';
|
||||
|
||||
jest.mock('../../containers/use_bulk_update_case');
|
||||
jest.mock('../../containers/use_delete_cases');
|
||||
jest.mock('../../containers/use_get_cases');
|
||||
jest.mock('../../containers/use_get_cases_status');
|
||||
jest.mock('../../containers/use_get_action_license');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
const useDeleteCasesMock = useDeleteCases as jest.Mock;
|
||||
const useGetCasesMock = useGetCases as jest.Mock;
|
||||
const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
|
||||
const useUpdateCasesMock = useUpdateCases as jest.Mock;
|
||||
const useGetActionLicenseMock = useGetActionLicense as jest.Mock;
|
||||
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
|
||||
const mockTriggersActionsUiService = triggersActionsUiMock.createStart();
|
||||
|
||||
const mockKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
...createStartServicesMock(),
|
||||
triggersActionsUi: mockTriggersActionsUiService,
|
||||
},
|
||||
} as unknown as ReturnType<typeof useKibana>);
|
||||
};
|
||||
|
||||
describe('AllCasesListGeneric', () => {
|
||||
const defaultAllCasesListProps: AllCasesListProps = {
|
||||
disableAlerts: false,
|
||||
};
|
||||
|
||||
const dispatchResetIsDeleted = jest.fn();
|
||||
const dispatchResetIsUpdated = jest.fn();
|
||||
const dispatchUpdateCaseProperty = jest.fn();
|
||||
const handleOnDeleteConfirm = jest.fn();
|
||||
const handleToggleModal = jest.fn();
|
||||
const refetchCases = jest.fn();
|
||||
const setFilters = jest.fn();
|
||||
const setQueryParams = jest.fn();
|
||||
const setSelectedCases = jest.fn();
|
||||
const updateBulkStatus = jest.fn();
|
||||
const fetchCasesStatus = jest.fn();
|
||||
const onRowClick = jest.fn();
|
||||
const emptyTag = getEmptyTagValue().props.children;
|
||||
|
||||
const defaultGetCases = {
|
||||
...useGetCasesMockState,
|
||||
dispatchUpdateCaseProperty,
|
||||
refetchCases,
|
||||
setFilters,
|
||||
setQueryParams,
|
||||
setSelectedCases,
|
||||
};
|
||||
|
||||
const defaultDeleteCases = {
|
||||
dispatchResetIsDeleted,
|
||||
handleOnDeleteConfirm,
|
||||
handleToggleModal,
|
||||
isDeleted: false,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const defaultCasesStatus = {
|
||||
...casesStatus,
|
||||
fetchCasesStatus,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const defaultUpdateCases = {
|
||||
isUpdated: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
dispatchResetIsUpdated,
|
||||
updateBulkStatus,
|
||||
};
|
||||
|
||||
const defaultActionLicense = {
|
||||
actionLicense: null,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
};
|
||||
|
||||
const defaultColumnArgs = {
|
||||
caseDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
dispatchUpdateCaseProperty: jest.fn,
|
||||
filterStatus: CaseStatuses.open,
|
||||
handleIsLoading: jest.fn(),
|
||||
isLoadingCases: [],
|
||||
isSelectorView: false,
|
||||
userCanCrud: true,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
mockKibana();
|
||||
const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry;
|
||||
registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useUpdateCasesMock.mockReturnValue(defaultUpdateCases);
|
||||
useGetCasesMock.mockReturnValue(defaultGetCases);
|
||||
useDeleteCasesMock.mockReturnValue(defaultDeleteCases);
|
||||
useGetCasesStatusMock.mockReturnValue(defaultCasesStatus);
|
||||
useGetActionLicenseMock.mockReturnValue(defaultActionLicense);
|
||||
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false }));
|
||||
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false }));
|
||||
mockKibana();
|
||||
moment.tz.setDefault('UTC');
|
||||
});
|
||||
|
||||
it('should render AllCasesList', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().prop('href')).toEqual(
|
||||
`/app/security/cases/test`
|
||||
);
|
||||
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().text()).toEqual(
|
||||
useGetCasesMockState.data.cases[0].title
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`span[data-test-subj="case-table-column-tags-0"]`).first().prop('title')
|
||||
).toEqual(useGetCasesMockState.data.cases[0].tags[0]);
|
||||
expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual(
|
||||
useGetCasesMockState.data.cases[0].createdBy.fullName
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="case-table-column-createdAt"]`)
|
||||
.first()
|
||||
.childAt(0)
|
||||
.prop('value')
|
||||
).toBe(useGetCasesMockState.data.cases[0].createdAt);
|
||||
expect(wrapper.find(`[data-test-subj="case-table-case-count"]`).first().text()).toEqual(
|
||||
'Showing 10 cases'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render empty fields', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
cases: [
|
||||
{
|
||||
...defaultGetCases.data.cases[0],
|
||||
id: null,
|
||||
createdAt: null,
|
||||
createdBy: null,
|
||||
status: null,
|
||||
subCases: null,
|
||||
tags: null,
|
||||
title: null,
|
||||
totalComment: null,
|
||||
totalAlerts: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const checkIt = (columnName: string, key: number) => {
|
||||
const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key);
|
||||
expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName);
|
||||
expect(column.find('span').text()).toEqual(emptyTag);
|
||||
};
|
||||
|
||||
const { result } = renderHook<GetCasesColumn, CasesColumns[]>(() =>
|
||||
useCasesColumns(defaultColumnArgs)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
result.current.map(
|
||||
(i, key) =>
|
||||
i.name != null &&
|
||||
!Object.prototype.hasOwnProperty.call(i, 'actions') &&
|
||||
checkIt(`${i.name}`, key)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render delete actions for case', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should enable correct actions for sub cases', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
cases: [
|
||||
{
|
||||
...defaultGetCases.data.cases[0],
|
||||
id: 'my-case-with-subcases',
|
||||
createdAt: null,
|
||||
createdBy: null,
|
||||
status: null,
|
||||
subCases: [
|
||||
{
|
||||
id: 'sub-case-id',
|
||||
},
|
||||
],
|
||||
tags: null,
|
||||
title: null,
|
||||
totalComment: null,
|
||||
totalAlerts: null,
|
||||
type: CaseType.collection,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should tableHeaderSortButton AllCasesList', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'createdAt',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Updates status when status context menu is updated', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).first().simulate('click');
|
||||
wrapper
|
||||
.find(`[data-test-subj="case-view-status-dropdown-closed"] button`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
const firstCase = useGetCasesMockState.data.cases[0];
|
||||
expect(dispatchUpdateCaseProperty.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
caseId: firstCase.id,
|
||||
updateKey: 'status',
|
||||
updateValue: CaseStatuses.closed,
|
||||
version: firstCase.version,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('Bulk delete', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
|
||||
selectedCases: [...useGetCasesMockState.data.cases, collectionCase],
|
||||
});
|
||||
|
||||
useDeleteCasesMock
|
||||
.mockReturnValueOnce({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
})
|
||||
.mockReturnValue({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click');
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
'[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]'
|
||||
)
|
||||
.last()
|
||||
.simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleToggleModal).toBeCalled();
|
||||
|
||||
expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([
|
||||
...useGetCasesMockState.data.cases.map(({ id, type, title }) => ({ id, type, title })),
|
||||
{
|
||||
id: collectionCase.id,
|
||||
title: collectionCase.title,
|
||||
type: collectionCase.type,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders only bulk delete on status all', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: StatusAll },
|
||||
selectedCases: [...useGetCasesMockState.data.cases],
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').exists()).toEqual(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-close-button"]').exists()).toEqual(false);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders correct bulk actions for case collection when filter status is set to all - enable only bulk delete if any collection is selected', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
selectedCases: [
|
||||
...useGetCasesMockState.data.cases,
|
||||
{
|
||||
...useGetCasesMockState.data.cases[0],
|
||||
type: CaseType.collection,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useDeleteCasesMock
|
||||
.mockReturnValueOnce({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
})
|
||||
.mockReturnValue({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().props().disabled
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().props().disabled
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk close status update', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
selectedCases: useGetCasesMockState.data.cases,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.closed);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk open status update', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
selectedCases: useGetCasesMockState.data.cases,
|
||||
filterOptions: {
|
||||
...defaultGetCases.filterOptions,
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.open);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk in-progress status update', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
selectedCases: useGetCasesMockState.data.cases,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(updateBulkStatus).toBeCalledWith(
|
||||
useGetCasesMockState.data.cases,
|
||||
CaseStatuses['in-progress']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('isDeleted is true, refetch', async () => {
|
||||
useDeleteCasesMock.mockReturnValue({
|
||||
...defaultDeleteCases,
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(refetchCases).toBeCalled();
|
||||
// expect(fetchCasesStatus).toBeCalled();
|
||||
expect(dispatchResetIsDeleted).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('isUpdated is true, refetch', async () => {
|
||||
useUpdateCasesMock.mockReturnValue({
|
||||
...defaultUpdateCases,
|
||||
isUpdated: true,
|
||||
});
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(refetchCases).toBeCalled();
|
||||
// expect(fetchCasesStatus).toBeCalled();
|
||||
expect(dispatchResetIsUpdated).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render table utility bar when isSelectorView=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-table-selected-case-count"]').exists()).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="case-table-bulk-actions"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('case table should not be selectable when isSelectorView=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-table"]').first().prop('isSelectable')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onRowClick with no cases and isSelectorView=true', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
total: 0,
|
||||
cases: [],
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={true} onRowClick={onRowClick} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onRowClick when clicking a case with modal=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={true} onRowClick={onRowClick} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).toHaveBeenCalledWith({
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
comments: [],
|
||||
connector: { fields: null, id: '123', name: 'My Connector', type: '.jira' },
|
||||
createdAt: '2020-02-19T23:06:33.798Z',
|
||||
createdBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
description: 'Security banana Issue',
|
||||
externalService: {
|
||||
connectorId: '123',
|
||||
connectorName: 'connector name',
|
||||
externalId: 'external_id',
|
||||
externalTitle: 'external title',
|
||||
externalUrl: 'basicPush.com',
|
||||
pushedAt: '2020-02-20T15:02:57.995Z',
|
||||
pushedBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
},
|
||||
id: '1',
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
status: 'open',
|
||||
subCaseIds: [],
|
||||
tags: ['coke', 'pepsi'],
|
||||
title: 'Another horrible breach!!',
|
||||
totalAlerts: 0,
|
||||
totalComment: 0,
|
||||
type: CaseType.individual,
|
||||
updatedAt: '2020-02-20T15:02:57.995Z',
|
||||
updatedBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
version: 'WzQ3LDFd',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT call onRowClick when clicking a case with modal=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status to closed', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
sortField: 'closedAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status to in-progress', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
sortField: 'createdAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status to open', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
sortField: 'createdAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the correct count on stats', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').text()).toBe(
|
||||
'Open (20)'
|
||||
);
|
||||
expect(wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').text()).toBe(
|
||||
'In progress (40)'
|
||||
);
|
||||
expect(wrapper.find('button[data-test-subj="case-status-filter-closed"]').text()).toBe(
|
||||
'Closed (130)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render status when isSelectorView=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const { result } = renderHook<GetCasesColumn, CasesColumns[]>(() =>
|
||||
useCasesColumns({
|
||||
...defaultColumnArgs,
|
||||
isSelectorView: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.find((i) => i.name === 'Status')).toBeFalsy();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-table"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="case-view-status-dropdown"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it.skip('renders the first available status when hiddenStatus is given', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList hiddenStatuses={[StatusAll, CaseStatuses.open]} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="status-badge-in-progress"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call doRefresh if provided', async () => {
|
||||
const doRefresh = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCasesList {...defaultAllCasesListProps} isSelectorView={false} doRefresh={doRefresh} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('[data-test-subj="all-cases-refresh"] button').first().simulate('click');
|
||||
});
|
||||
|
||||
expect(doRefresh).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -23,20 +23,17 @@ import {
|
|||
caseStatuses,
|
||||
} from '../../../common';
|
||||
import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations';
|
||||
import { useGetActionLicense } from '../../containers/use_get_action_license';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { CaseDetailsHrefSchema, CasesNavigation } from '../links';
|
||||
|
||||
import { getActionLicenseError } from '../use_push_to_service/helpers';
|
||||
import { useCasesColumns } from './columns';
|
||||
import { getExpandedRowMap } from './expanded_row';
|
||||
import { CasesTableHeader } from './header';
|
||||
import { CasesTableFilters } from './table_filters';
|
||||
import { EuiBasicTableOnChange } from './types';
|
||||
|
||||
import { CasesTable } from './table';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
const ProgressLoader = styled(EuiProgress)`
|
||||
${({ $isShow }: { $isShow: boolean }) =>
|
||||
|
@ -54,36 +51,27 @@ const ProgressLoader = styled(EuiProgress)`
|
|||
const getSortField = (field: string): SortFieldCase =>
|
||||
field === SortFieldCase.closedAt ? SortFieldCase.closedAt : SortFieldCase.createdAt;
|
||||
|
||||
interface AllCasesGenericProps {
|
||||
export interface AllCasesListProps {
|
||||
alertData?: Omit<CommentRequestAlertType, 'type'>;
|
||||
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;
|
||||
disableAlerts?: boolean;
|
||||
hiddenStatuses?: CaseStatusWithAllStatus[];
|
||||
isSelectorView?: boolean;
|
||||
onRowClick?: (theCase?: Case | SubCase) => void;
|
||||
showTitle?: boolean;
|
||||
updateCase?: (newCase: Case) => void;
|
||||
userCanCrud: boolean;
|
||||
doRefresh?: () => void;
|
||||
}
|
||||
|
||||
export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
|
||||
export const AllCasesList = React.memo<AllCasesListProps>(
|
||||
({
|
||||
alertData,
|
||||
caseDetailsNavigation,
|
||||
configureCasesNavigation,
|
||||
createCaseNavigation,
|
||||
disableAlerts,
|
||||
hiddenStatuses = [],
|
||||
isSelectorView,
|
||||
isSelectorView = false,
|
||||
onRowClick,
|
||||
showTitle,
|
||||
updateCase,
|
||||
userCanCrud,
|
||||
doRefresh,
|
||||
}) => {
|
||||
const { actionLicense } = useGetActionLicense();
|
||||
|
||||
const { userCanCrud } = useCasesContext();
|
||||
const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses));
|
||||
const initialFilterOptions =
|
||||
!isEmpty(hiddenStatuses) && firstAvailableStatus ? { status: firstAvailableStatus } : {};
|
||||
|
@ -120,34 +108,19 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
|
|||
},
|
||||
[filterRefetch]
|
||||
);
|
||||
const [refresh, doRefresh] = useState<number>(0);
|
||||
const [isLoading, handleIsLoading] = useState<boolean>(false);
|
||||
const refreshCases = useCallback(
|
||||
(dataRefresh = true) => {
|
||||
if (dataRefresh) refetchCases();
|
||||
doRefresh((prev) => prev + 1);
|
||||
if (doRefresh) doRefresh();
|
||||
setSelectedCases([]);
|
||||
if (filterRefetch.current != null) {
|
||||
filterRefetch.current();
|
||||
}
|
||||
},
|
||||
[filterRefetch, refetchCases, setSelectedCases]
|
||||
[doRefresh, filterRefetch, refetchCases, setSelectedCases]
|
||||
);
|
||||
|
||||
const { onClick: onCreateCaseNavClick } = createCaseNavigation;
|
||||
const goToCreateCase = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
if (isSelectorView && onRowClick != null) {
|
||||
onRowClick();
|
||||
} else if (onCreateCaseNavClick) {
|
||||
onCreateCaseNavClick(ev);
|
||||
}
|
||||
},
|
||||
[isSelectorView, onCreateCaseNavClick, onRowClick]
|
||||
);
|
||||
const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]);
|
||||
|
||||
const tableOnChangeCallback = useCallback(
|
||||
({ page, sort }: EuiBasicTableOnChange) => {
|
||||
let newQueryParams = queryParams;
|
||||
|
@ -195,15 +168,13 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
|
|||
const showActions = userCanCrud && !isSelectorView;
|
||||
|
||||
const columns = useCasesColumns({
|
||||
caseDetailsNavigation,
|
||||
disableAlerts,
|
||||
dispatchUpdateCaseProperty,
|
||||
filterStatus: filterOptions.status,
|
||||
handleIsLoading,
|
||||
isLoadingCases: loading,
|
||||
refreshCases,
|
||||
// isSelectorView is boolean | undefined. We need to convert it to a boolean.
|
||||
isSelectorView: !!isSelectorView,
|
||||
isSelectorView,
|
||||
userCanCrud,
|
||||
connectors,
|
||||
onRowClick,
|
||||
|
@ -253,16 +224,6 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
|
|||
|
||||
return (
|
||||
<>
|
||||
{configureCasesNavigation != null && (
|
||||
<CasesTableHeader
|
||||
actionsErrors={actionsErrors}
|
||||
createCaseNavigation={createCaseNavigation}
|
||||
configureCasesNavigation={configureCasesNavigation}
|
||||
refresh={refresh}
|
||||
showTitle={showTitle}
|
||||
userCanCrud={userCanCrud}
|
||||
/>
|
||||
)}
|
||||
<ProgressLoader
|
||||
size="xs"
|
||||
color="accent"
|
||||
|
@ -285,10 +246,9 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
|
|||
/>
|
||||
<CasesTable
|
||||
columns={columns}
|
||||
createCaseNavigation={createCaseNavigation}
|
||||
data={data}
|
||||
filterOptions={filterOptions}
|
||||
goToCreateCase={goToCreateCase}
|
||||
goToCreateCase={onRowClick}
|
||||
handleIsLoading={handleIsLoading}
|
||||
isCasesLoading={isCasesLoading}
|
||||
isCommentUpdating={isCommentUpdating}
|
||||
|
@ -311,4 +271,4 @@ export const AllCasesGeneric = React.memo<AllCasesGenericProps>(
|
|||
}
|
||||
);
|
||||
|
||||
AllCasesGeneric.displayName = 'AllCasesGeneric';
|
||||
AllCasesList.displayName = 'AllCasesList';
|
|
@ -34,7 +34,7 @@ import {
|
|||
} from '../../../common';
|
||||
import { getEmptyTagValue } from '../empty_value';
|
||||
import { FormattedRelativePreferenceDate } from '../formatted_date';
|
||||
import { CaseDetailsHrefSchema, CaseDetailsLink, CasesNavigation } from '../links';
|
||||
import { CaseDetailsLink } from '../links';
|
||||
import * as i18n from './translations';
|
||||
import { getSubCasesStatusCountsBadges, isSubCase } from './helpers';
|
||||
import { ALERTS } from '../../common/translations';
|
||||
|
@ -69,7 +69,6 @@ const renderStringField = (field: string, dataTestSubj: string) =>
|
|||
field != null ? <span data-test-subj={dataTestSubj}>{field}</span> : getEmptyTagValue();
|
||||
|
||||
export interface GetCasesColumn {
|
||||
caseDetailsNavigation?: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>;
|
||||
disableAlerts?: boolean;
|
||||
dispatchUpdateCaseProperty: (u: UpdateCase) => void;
|
||||
filterStatus: string;
|
||||
|
@ -85,7 +84,6 @@ export interface GetCasesColumn {
|
|||
updateCase?: (newCase: Case) => void;
|
||||
}
|
||||
export const useCasesColumns = ({
|
||||
caseDetailsNavigation,
|
||||
disableAlerts = false,
|
||||
dispatchUpdateCaseProperty,
|
||||
filterStatus,
|
||||
|
@ -179,19 +177,17 @@ export const useCasesColumns = ({
|
|||
name: i18n.NAME,
|
||||
render: (theCase: Case | SubCase) => {
|
||||
if (theCase.id != null && theCase.title != null) {
|
||||
const caseDetailsLinkComponent =
|
||||
caseDetailsNavigation != null ? (
|
||||
<CaseDetailsLink
|
||||
caseDetailsNavigation={caseDetailsNavigation}
|
||||
detailName={isSubCase(theCase) ? theCase.caseParentId : theCase.id}
|
||||
subCaseId={isSubCase(theCase) ? theCase.id : undefined}
|
||||
title={theCase.title}
|
||||
>
|
||||
<TruncatedText text={theCase.title} />
|
||||
</CaseDetailsLink>
|
||||
) : (
|
||||
const caseDetailsLinkComponent = isSelectorView ? (
|
||||
<TruncatedText text={theCase.title} />
|
||||
) : (
|
||||
<CaseDetailsLink
|
||||
detailName={isSubCase(theCase) ? theCase.caseParentId : theCase.id}
|
||||
subCaseId={isSubCase(theCase) ? theCase.id : undefined}
|
||||
title={theCase.title}
|
||||
>
|
||||
<TruncatedText text={theCase.title} />
|
||||
);
|
||||
</CaseDetailsLink>
|
||||
);
|
||||
return theCase.status !== CaseStatuses.closed ? (
|
||||
caseDetailsLinkComponent
|
||||
) : (
|
||||
|
|
|
@ -11,16 +11,12 @@ import styled, { css } from 'styled-components';
|
|||
import { HeaderPage } from '../header_page';
|
||||
import * as i18n from './translations';
|
||||
import { Count } from './count';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { ErrorMessage } from '../use_push_to_service/callout/types';
|
||||
import { NavButtons } from './nav_buttons';
|
||||
|
||||
interface OwnProps {
|
||||
actionsErrors: ErrorMessage[];
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
createCaseNavigation: CasesNavigation;
|
||||
refresh: number;
|
||||
showTitle?: boolean;
|
||||
userCanCrud: boolean;
|
||||
}
|
||||
|
||||
|
@ -43,13 +39,10 @@ const FlexItemDivider = styled(EuiFlexItem)`
|
|||
|
||||
export const CasesTableHeader: FunctionComponent<Props> = ({
|
||||
actionsErrors,
|
||||
configureCasesNavigation,
|
||||
createCaseNavigation,
|
||||
refresh,
|
||||
showTitle = true,
|
||||
userCanCrud,
|
||||
}) => (
|
||||
<HeaderPage title={showTitle ? i18n.PAGE_TITLE : ''} border>
|
||||
<HeaderPage title={i18n.PAGE_TITLE} border>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m" wrap={true} data-test-subj="all-cases-header">
|
||||
{userCanCrud ? (
|
||||
<>
|
||||
|
@ -58,11 +51,7 @@ export const CasesTableHeader: FunctionComponent<Props> = ({
|
|||
</FlexItemDivider>
|
||||
|
||||
<EuiFlexItem>
|
||||
<NavButtons
|
||||
actionsErrors={actionsErrors}
|
||||
configureCasesNavigation={configureCasesNavigation}
|
||||
createCaseNavigation={createCaseNavigation}
|
||||
/>
|
||||
<NavButtons actionsErrors={actionsErrors} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -7,94 +7,49 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import moment from 'moment-timezone';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import '../../common/mock/match_media';
|
||||
import { AllCases, AllCasesProps } from '.';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
casesStatus,
|
||||
useGetCasesMockState,
|
||||
collectionCase,
|
||||
connectorsMock,
|
||||
} from '../../containers/mock';
|
||||
|
||||
import { CaseStatuses, CaseType, SECURITY_SOLUTION_OWNER, StatusAll } from '../../../common';
|
||||
import { getEmptyTagValue } from '../empty_value';
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
|
||||
import { useUpdateCases } from '../../containers/use_bulk_update_case';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { useGetReporters } from '../../containers/use_get_reporters';
|
||||
import { useGetActionLicense } from '../../containers/use_get_action_license';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { AllCasesGeneric as AllCases } from './all_cases_generic';
|
||||
import { AllCasesProps } from '.';
|
||||
import { CasesColumns, GetCasesColumn, useCasesColumns } from './columns';
|
||||
import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks';
|
||||
import { CaseStatuses } from '../../../common';
|
||||
import { casesStatus, connectorsMock, useGetCasesMockState } from '../../containers/mock';
|
||||
import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
|
||||
|
||||
jest.mock('../../containers/use_bulk_update_case');
|
||||
jest.mock('../../containers/use_delete_cases');
|
||||
jest.mock('../../containers/use_get_cases');
|
||||
jest.mock('../../containers/use_get_cases_status');
|
||||
jest.mock('../../containers/use_get_reporters');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/use_get_action_license');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock('../../containers/api');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../containers/use_get_cases');
|
||||
jest.mock('../../containers/use_get_cases_status');
|
||||
|
||||
const defaultAllCasesProps: AllCasesProps = {
|
||||
disableAlerts: false,
|
||||
};
|
||||
|
||||
const useDeleteCasesMock = useDeleteCases as jest.Mock;
|
||||
const useGetCasesMock = useGetCases as jest.Mock;
|
||||
const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
|
||||
const useUpdateCasesMock = useUpdateCases as jest.Mock;
|
||||
const useGetActionLicenseMock = useGetActionLicense as jest.Mock;
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const useGetCasesMock = useGetCases as jest.Mock;
|
||||
const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
|
||||
const useGetActionLicenseMock = useGetActionLicense as jest.Mock;
|
||||
|
||||
const mockTriggersActionsUiService = triggersActionsUiMock.createStart();
|
||||
describe('AllCases', () => {
|
||||
const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry;
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
triggersActionsUi: mockTriggersActionsUiService,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AllCasesGeneric', () => {
|
||||
const defaultAllCasesProps: AllCasesProps = {
|
||||
configureCasesNavigation: {
|
||||
href: 'blah',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: jest.fn().mockReturnValue('testHref'), // string
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
createCaseNavigation: {
|
||||
href: 'bleh',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
userCanCrud: true,
|
||||
owner: [SECURITY_SOLUTION_OWNER],
|
||||
};
|
||||
|
||||
const dispatchResetIsDeleted = jest.fn();
|
||||
const dispatchResetIsUpdated = jest.fn();
|
||||
const dispatchUpdateCaseProperty = jest.fn();
|
||||
const handleOnDeleteConfirm = jest.fn();
|
||||
const handleToggleModal = jest.fn();
|
||||
const refetchCases = jest.fn();
|
||||
const setFilters = jest.fn();
|
||||
const setQueryParams = jest.fn();
|
||||
const setSelectedCases = jest.fn();
|
||||
const updateBulkStatus = jest.fn();
|
||||
const fetchCasesStatus = jest.fn();
|
||||
const onRowClick = jest.fn();
|
||||
const emptyTag = getEmptyTagValue().props.children;
|
||||
|
||||
const defaultGetCases = {
|
||||
...useGetCasesMockState,
|
||||
|
@ -105,15 +60,6 @@ describe('AllCasesGeneric', () => {
|
|||
setSelectedCases,
|
||||
};
|
||||
|
||||
const defaultDeleteCases = {
|
||||
dispatchResetIsDeleted,
|
||||
handleOnDeleteConfirm,
|
||||
handleToggleModal,
|
||||
isDeleted: false,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const defaultCasesStatus = {
|
||||
...casesStatus,
|
||||
fetchCasesStatus,
|
||||
|
@ -121,86 +67,34 @@ describe('AllCasesGeneric', () => {
|
|||
isLoading: false,
|
||||
};
|
||||
|
||||
const defaultUpdateCases = {
|
||||
isUpdated: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
dispatchResetIsUpdated,
|
||||
updateBulkStatus,
|
||||
};
|
||||
|
||||
const defaultActionLicense = {
|
||||
actionLicense: null,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
};
|
||||
|
||||
const defaultColumnArgs = {
|
||||
caseDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
dispatchUpdateCaseProperty: jest.fn,
|
||||
filterStatus: CaseStatuses.open,
|
||||
handleIsLoading: jest.fn(),
|
||||
isLoadingCases: [],
|
||||
isSelectorView: false,
|
||||
userCanCrud: true,
|
||||
};
|
||||
|
||||
const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry;
|
||||
|
||||
beforeAll(() => {
|
||||
registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock);
|
||||
(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,
|
||||
});
|
||||
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false }));
|
||||
useGetCasesStatusMock.mockReturnValue(defaultCasesStatus);
|
||||
useGetActionLicenseMock.mockReturnValue(defaultActionLicense);
|
||||
useGetCasesMock.mockReturnValue(defaultGetCases);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useUpdateCasesMock.mockReturnValue(defaultUpdateCases);
|
||||
useGetCasesMock.mockReturnValue(defaultGetCases);
|
||||
useDeleteCasesMock.mockReturnValue(defaultDeleteCases);
|
||||
useGetCasesStatusMock.mockReturnValue(defaultCasesStatus);
|
||||
useGetActionLicenseMock.mockReturnValue(defaultActionLicense);
|
||||
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false }));
|
||||
moment.tz.setDefault('UTC');
|
||||
});
|
||||
|
||||
it('should render AllCases', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().prop('href')).toEqual(
|
||||
`testHref`
|
||||
);
|
||||
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().text()).toEqual(
|
||||
useGetCasesMockState.data.cases[0].title
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`span[data-test-subj="case-table-column-tags-0"]`).first().prop('title')
|
||||
).toEqual(useGetCasesMockState.data.cases[0].tags[0]);
|
||||
expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual(
|
||||
useGetCasesMockState.data.cases[0].createdBy.fullName
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="case-table-column-createdAt"]`)
|
||||
.first()
|
||||
.childAt(0)
|
||||
.prop('value')
|
||||
).toBe(useGetCasesMockState.data.cases[0].createdAt);
|
||||
expect(wrapper.find(`[data-test-subj="case-table-case-count"]`).first().text()).toEqual(
|
||||
'Showing 10 cases'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the stats', async () => {
|
||||
|
@ -264,618 +158,6 @@ describe('AllCasesGeneric', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render empty fields', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
cases: [
|
||||
{
|
||||
...defaultGetCases.data.cases[0],
|
||||
id: null,
|
||||
createdAt: null,
|
||||
createdBy: null,
|
||||
status: null,
|
||||
subCases: null,
|
||||
tags: null,
|
||||
title: null,
|
||||
totalComment: null,
|
||||
totalAlerts: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const checkIt = (columnName: string, key: number) => {
|
||||
const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key);
|
||||
expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName);
|
||||
expect(column.find('span').text()).toEqual(emptyTag);
|
||||
};
|
||||
|
||||
const { result } = renderHook<GetCasesColumn, CasesColumns[]>(() =>
|
||||
useCasesColumns(defaultColumnArgs)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
result.current.map(
|
||||
(i, key) =>
|
||||
i.name != null &&
|
||||
!Object.prototype.hasOwnProperty.call(i, 'actions') &&
|
||||
checkIt(`${i.name}`, key)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render delete actions for case', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should enable correct actions for sub cases', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
cases: [
|
||||
{
|
||||
...defaultGetCases.data.cases[0],
|
||||
id: 'my-case-with-subcases',
|
||||
createdAt: null,
|
||||
createdBy: null,
|
||||
status: null,
|
||||
subCases: [
|
||||
{
|
||||
id: 'sub-case-id',
|
||||
},
|
||||
],
|
||||
tags: null,
|
||||
title: null,
|
||||
totalComment: null,
|
||||
totalAlerts: null,
|
||||
type: CaseType.collection,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render case link when caseDetailsNavigation is not passed or actions on showActions=false', async () => {
|
||||
const { caseDetailsNavigation, ...rest } = defaultAllCasesProps;
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...rest} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { result } = renderHook<GetCasesColumn, CasesColumns[]>(() =>
|
||||
useCasesColumns({
|
||||
dispatchUpdateCaseProperty: jest.fn,
|
||||
isLoadingCases: [],
|
||||
filterStatus: CaseStatuses.open,
|
||||
handleIsLoading: jest.fn(),
|
||||
isSelectorView: true,
|
||||
userCanCrud: true,
|
||||
})
|
||||
);
|
||||
await waitFor(() => {
|
||||
result.current.map(
|
||||
(i) => i.name != null && !Object.prototype.hasOwnProperty.call(i, 'actions')
|
||||
);
|
||||
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should tableHeaderSortButton AllCases', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'createdAt',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Updates status when status context menu is updated', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).first().simulate('click');
|
||||
wrapper
|
||||
.find(`[data-test-subj="case-view-status-dropdown-closed"] button`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
const firstCase = useGetCasesMockState.data.cases[0];
|
||||
expect(dispatchUpdateCaseProperty.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
caseId: firstCase.id,
|
||||
updateKey: 'status',
|
||||
updateValue: CaseStatuses.closed,
|
||||
version: firstCase.version,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk delete', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
|
||||
selectedCases: [...useGetCasesMockState.data.cases, collectionCase],
|
||||
});
|
||||
|
||||
useDeleteCasesMock
|
||||
.mockReturnValueOnce({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
})
|
||||
.mockReturnValue({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click');
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
'[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]'
|
||||
)
|
||||
.last()
|
||||
.simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleToggleModal).toBeCalled();
|
||||
|
||||
expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([
|
||||
...useGetCasesMockState.data.cases.map(({ id, type, title }) => ({ id, type, title })),
|
||||
{
|
||||
id: collectionCase.id,
|
||||
title: collectionCase.title,
|
||||
type: collectionCase.type,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders only bulk delete on status all', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: StatusAll },
|
||||
selectedCases: [...useGetCasesMockState.data.cases],
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').exists()).toEqual(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-close-button"]').exists()).toEqual(false);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders correct bulk actions for case collection when filter status is set to all - enable only bulk delete if any collection is selected', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
selectedCases: [
|
||||
...useGetCasesMockState.data.cases,
|
||||
{
|
||||
...useGetCasesMockState.data.cases[0],
|
||||
type: CaseType.collection,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useDeleteCasesMock
|
||||
.mockReturnValueOnce({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: false,
|
||||
})
|
||||
.mockReturnValue({
|
||||
...defaultDeleteCases,
|
||||
isDisplayConfirmDeleteModal: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().props().disabled
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().props().disabled
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk close status update', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
selectedCases: useGetCasesMockState.data.cases,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.closed);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk open status update', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
selectedCases: useGetCasesMockState.data.cases,
|
||||
filterOptions: {
|
||||
...defaultGetCases.filterOptions,
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.open);
|
||||
});
|
||||
});
|
||||
|
||||
it('Bulk in-progress status update', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
|
||||
selectedCases: useGetCasesMockState.data.cases,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(updateBulkStatus).toBeCalledWith(
|
||||
useGetCasesMockState.data.cases,
|
||||
CaseStatuses['in-progress']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('isDeleted is true, refetch', async () => {
|
||||
useDeleteCasesMock.mockReturnValue({
|
||||
...defaultDeleteCases,
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(refetchCases).toBeCalled();
|
||||
expect(fetchCasesStatus).toBeCalled();
|
||||
expect(dispatchResetIsDeleted).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('isUpdated is true, refetch', async () => {
|
||||
useUpdateCasesMock.mockReturnValue({
|
||||
...defaultUpdateCases,
|
||||
isUpdated: true,
|
||||
});
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(refetchCases).toBeCalled();
|
||||
expect(fetchCasesStatus).toBeCalled();
|
||||
expect(dispatchResetIsUpdated).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render header when configureCasesNavigation are not present', async () => {
|
||||
const { configureCasesNavigation, ...restProps } = defaultAllCasesProps;
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...restProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="all-cases-header"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render table utility bar when isSelectorView=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-table-selected-case-count"]').exists()).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="case-table-bulk-actions"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('case table should not be selectable when isSelectorView=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-table"]').first().prop('isSelectable')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onRowClick with no cases and isSelectorView=true', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
total: 0,
|
||||
cases: [],
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases
|
||||
{...defaultAllCasesProps}
|
||||
userCanCrud={true}
|
||||
isSelectorView={true}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call createCaseNavigation.onClick with no cases and isSelectorView=false', async () => {
|
||||
const createCaseNavigation = { href: '', onClick: jest.fn() };
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
total: 0,
|
||||
cases: [],
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases
|
||||
{...defaultAllCasesProps}
|
||||
createCaseNavigation={createCaseNavigation}
|
||||
isSelectorView={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(createCaseNavigation.onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onRowClick when clicking a case with modal=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases
|
||||
{...defaultAllCasesProps}
|
||||
userCanCrud={true}
|
||||
isSelectorView={true}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).toHaveBeenCalledWith({
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
comments: [],
|
||||
connector: { fields: null, id: '123', name: 'My Connector', type: '.jira' },
|
||||
createdAt: '2020-02-19T23:06:33.798Z',
|
||||
createdBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
description: 'Security banana Issue',
|
||||
externalService: {
|
||||
connectorId: '123',
|
||||
connectorName: 'connector name',
|
||||
externalId: 'external_id',
|
||||
externalTitle: 'external title',
|
||||
externalUrl: 'basicPush.com',
|
||||
pushedAt: '2020-02-20T15:02:57.995Z',
|
||||
pushedBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
},
|
||||
id: '1',
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
status: 'open',
|
||||
subCaseIds: [],
|
||||
tags: ['coke', 'pepsi'],
|
||||
title: 'Another horrible breach!!',
|
||||
totalAlerts: 0,
|
||||
totalComment: 0,
|
||||
type: CaseType.individual,
|
||||
updatedAt: '2020-02-20T15:02:57.995Z',
|
||||
updatedBy: {
|
||||
email: 'leslie.knope@elastic.co',
|
||||
fullName: 'Leslie Knope',
|
||||
username: 'lknope',
|
||||
},
|
||||
version: 'WzQ3LDFd',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT call onRowClick when clicking a case with modal=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status to closed', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
sortField: 'closedAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status to in-progress', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
sortField: 'createdAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the status to open', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(setQueryParams).toBeCalledWith({
|
||||
sortField: 'createdAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the correct count on stats', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').text()).toBe(
|
||||
'Open (20)'
|
||||
);
|
||||
expect(wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').text()).toBe(
|
||||
'In progress (40)'
|
||||
);
|
||||
expect(wrapper.find('button[data-test-subj="case-status-filter-closed"]').text()).toBe(
|
||||
'Closed (130)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow the user to enter configuration page with basic license', async () => {
|
||||
useGetActionLicenseMock.mockReturnValue({
|
||||
...defaultActionLicense,
|
||||
|
@ -927,27 +209,4 @@ describe('AllCasesGeneric', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render status when isSelectorView=true', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AllCases {...defaultAllCasesProps} isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const { result } = renderHook<GetCasesColumn, CasesColumns[]>(() =>
|
||||
useCasesColumns({
|
||||
...defaultColumnArgs,
|
||||
isSelectorView: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.find((i) => i.name === 'Status')).toBeFalsy();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="cases-table"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="case-view-status-dropdown"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,25 +5,38 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Owner } from '../../types';
|
||||
import { CaseDetailsHrefSchema, CasesNavigation } from '../links';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { AllCasesGeneric } from './all_cases_generic';
|
||||
export interface AllCasesProps extends Owner {
|
||||
caseDetailsNavigation: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>; // if not passed, case name is not displayed as a link (Formerly dependant on isSelector)
|
||||
configureCasesNavigation: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelector)
|
||||
createCaseNavigation: CasesNavigation;
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { CasesDeepLinkId } from '../../common/navigation';
|
||||
import { useGetActionLicense } from '../../containers/use_get_action_license';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useCasesBreadcrumbs } from '../use_breadcrumbs';
|
||||
import { getActionLicenseError } from '../use_push_to_service/helpers';
|
||||
import { AllCasesList } from './all_cases_list';
|
||||
import { CasesTableHeader } from './header';
|
||||
|
||||
export interface AllCasesProps {
|
||||
disableAlerts?: boolean;
|
||||
showTitle?: boolean;
|
||||
userCanCrud: boolean;
|
||||
}
|
||||
|
||||
export const AllCases: React.FC<AllCasesProps> = (props) => (
|
||||
<OwnerProvider owner={props.owner}>
|
||||
<AllCasesGeneric {...props} />
|
||||
</OwnerProvider>
|
||||
);
|
||||
export const AllCases: React.FC<AllCasesProps> = ({ disableAlerts }) => {
|
||||
const { userCanCrud } = useCasesContext();
|
||||
const [refresh, setRefresh] = useState<number>(0);
|
||||
useCasesBreadcrumbs(CasesDeepLinkId.cases);
|
||||
|
||||
const doRefresh = useCallback(() => {
|
||||
setRefresh((prev) => prev + 1);
|
||||
}, [setRefresh]);
|
||||
|
||||
const { actionLicense } = useGetActionLicense();
|
||||
const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CasesTableHeader actionsErrors={actionsErrors} refresh={refresh} userCanCrud={userCanCrud} />
|
||||
<AllCasesList disableAlerts={disableAlerts} doRefresh={doRefresh} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AllCases as default };
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ConfigureCaseButton } from '../configure_cases/button';
|
||||
import * as i18n from './translations';
|
||||
import { CasesNavigation, LinkButton } from '../links';
|
||||
import { ConfigureCaseButton, LinkButton } from '../links';
|
||||
import { ErrorMessage } from '../use_push_to_service/callout/types';
|
||||
import { useCreateCaseNavigation } from '../../common/navigation';
|
||||
|
||||
const ButtonFlexGroup = styled(EuiFlexGroup)`
|
||||
${({ theme }) => css`
|
||||
|
@ -26,38 +26,41 @@ const ButtonFlexGroup = styled(EuiFlexGroup)`
|
|||
|
||||
interface OwnProps {
|
||||
actionsErrors: ErrorMessage[];
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
createCaseNavigation: CasesNavigation;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
|
||||
export const NavButtons: FunctionComponent<Props> = ({
|
||||
actionsErrors,
|
||||
configureCasesNavigation,
|
||||
createCaseNavigation,
|
||||
}) => (
|
||||
<ButtonFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConfigureCaseButton
|
||||
configureCasesNavigation={configureCasesNavigation}
|
||||
label={i18n.CONFIGURE_CASES_BUTTON}
|
||||
isDisabled={!isEmpty(actionsErrors)}
|
||||
showToolTip={!isEmpty(actionsErrors)}
|
||||
msgTooltip={!isEmpty(actionsErrors) ? <>{actionsErrors[0].description}</> : <></>}
|
||||
titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<LinkButton
|
||||
fill
|
||||
onClick={createCaseNavigation.onClick}
|
||||
href={createCaseNavigation.href}
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createNewCaseBtn"
|
||||
>
|
||||
{i18n.CREATE_TITLE}
|
||||
</LinkButton>
|
||||
</EuiFlexItem>
|
||||
</ButtonFlexGroup>
|
||||
);
|
||||
export const NavButtons: FunctionComponent<Props> = ({ actionsErrors }) => {
|
||||
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
|
||||
const navigateToCreateCaseClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
navigateToCreateCase();
|
||||
},
|
||||
[navigateToCreateCase]
|
||||
);
|
||||
return (
|
||||
<ButtonFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConfigureCaseButton
|
||||
label={i18n.CONFIGURE_CASES_BUTTON}
|
||||
isDisabled={!isEmpty(actionsErrors)}
|
||||
showToolTip={!isEmpty(actionsErrors)}
|
||||
msgTooltip={!isEmpty(actionsErrors) ? <>{actionsErrors[0].description}</> : <></>}
|
||||
titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<LinkButton
|
||||
fill
|
||||
onClick={navigateToCreateCaseClick}
|
||||
href={getCreateCaseUrl()}
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createNewCaseBtn"
|
||||
>
|
||||
{i18n.CREATE_TITLE}
|
||||
</LinkButton>
|
||||
</EuiFlexItem>
|
||||
</ButtonFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,24 +10,21 @@ import { mount } from 'enzyme';
|
|||
|
||||
import { AllCasesSelectorModal } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { AllCasesGeneric } from '../all_cases_generic';
|
||||
import { AllCasesList } from '../all_cases_list';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../../common';
|
||||
|
||||
jest.mock('../../../methods');
|
||||
jest.mock('../all_cases_generic');
|
||||
jest.mock('../all_cases_list');
|
||||
|
||||
const onRowClick = jest.fn();
|
||||
const createCaseNavigation = { href: '', onClick: jest.fn() };
|
||||
const defaultProps = {
|
||||
createCaseNavigation,
|
||||
onRowClick,
|
||||
userCanCrud: true,
|
||||
owner: [SECURITY_SOLUTION_OWNER],
|
||||
};
|
||||
const updateCase = jest.fn();
|
||||
|
||||
describe('AllCasesSelectorModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
|
@ -66,19 +63,19 @@ describe('AllCasesSelectorModal', () => {
|
|||
hiddenStatuses: [],
|
||||
updateCase,
|
||||
};
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AllCasesSelectorModal {...fullProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
// @ts-ignore idk what this mock style is but it works ¯\_(ツ)_/¯
|
||||
expect(AllCasesGeneric.type.mock.calls[0][0]).toEqual(
|
||||
expect(AllCasesList.type.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
alertData: fullProps.alertData,
|
||||
createCaseNavigation,
|
||||
hiddenStatuses: fullProps.hiddenStatuses,
|
||||
isSelectorView: true,
|
||||
userCanCrud: fullProps.userCanCrud,
|
||||
updateCase,
|
||||
})
|
||||
);
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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, { useState, useCallback } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Case,
|
||||
CaseStatusWithAllStatus,
|
||||
CommentRequestAlertType,
|
||||
SubCase,
|
||||
} from '../../../../common';
|
||||
import * as i18n from '../../../common/translations';
|
||||
import { AllCasesList } from '../all_cases_list';
|
||||
export interface AllCasesSelectorModalProps {
|
||||
alertData?: Omit<CommentRequestAlertType, 'type'>;
|
||||
hiddenStatuses?: CaseStatusWithAllStatus[];
|
||||
onRowClick: (theCase?: Case | SubCase) => void;
|
||||
updateCase?: (newCase: Case) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const Modal = styled(EuiModal)`
|
||||
${({ theme }) => `
|
||||
width: ${theme.eui.euiBreakpoints.l};
|
||||
max-width: ${theme.eui.euiBreakpoints.l};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const AllCasesSelectorModal = React.memo<AllCasesSelectorModalProps>(
|
||||
({ alertData, hiddenStatuses, onRowClick, updateCase, onClose }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(true);
|
||||
const closeModal = useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
}, [onClose]);
|
||||
|
||||
const onClick = useCallback(
|
||||
(theCase?: Case | SubCase) => {
|
||||
closeModal();
|
||||
onRowClick(theCase);
|
||||
},
|
||||
[closeModal, onRowClick]
|
||||
);
|
||||
|
||||
return isModalOpen ? (
|
||||
<Modal onClose={closeModal} data-test-subj="all-cases-modal">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.SELECT_CASE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<AllCasesList
|
||||
alertData={alertData}
|
||||
hiddenStatuses={hiddenStatuses}
|
||||
isSelectorView={true}
|
||||
onRowClick={onClick}
|
||||
updateCase={updateCase}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton color="text" onClick={closeModal}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Modal>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
|
||||
AllCasesSelectorModal.displayName = 'AllCasesSelectorModal';
|
|
@ -4,103 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AllCasesSelectorModal, AllCasesSelectorModalProps } from './all_cases_selector_modal';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Case,
|
||||
CaseStatusWithAllStatus,
|
||||
CommentRequestAlertType,
|
||||
SubCase,
|
||||
} from '../../../../common';
|
||||
import { CasesNavigation } from '../../links';
|
||||
import * as i18n from '../../../common/translations';
|
||||
import { AllCasesGeneric } from '../all_cases_generic';
|
||||
import { Owner } from '../../../types';
|
||||
import { OwnerProvider } from '../../owner_context';
|
||||
|
||||
export interface AllCasesSelectorModalProps extends Owner {
|
||||
alertData?: Omit<CommentRequestAlertType, 'type'>;
|
||||
createCaseNavigation: CasesNavigation;
|
||||
hiddenStatuses?: CaseStatusWithAllStatus[];
|
||||
onRowClick: (theCase?: Case | SubCase) => void;
|
||||
updateCase?: (newCase: Case) => void;
|
||||
userCanCrud: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const Modal = styled(EuiModal)`
|
||||
${({ theme }) => `
|
||||
width: ${theme.eui.euiBreakpoints.l};
|
||||
max-width: ${theme.eui.euiBreakpoints.l};
|
||||
`}
|
||||
`;
|
||||
|
||||
const AllCasesSelectorModalComponent: React.FC<AllCasesSelectorModalProps> = ({
|
||||
alertData,
|
||||
createCaseNavigation,
|
||||
hiddenStatuses,
|
||||
onRowClick,
|
||||
updateCase,
|
||||
userCanCrud,
|
||||
onClose,
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(true);
|
||||
const closeModal = useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
}, [onClose]);
|
||||
const onClick = useCallback(
|
||||
(theCase?: Case | SubCase) => {
|
||||
closeModal();
|
||||
onRowClick(theCase);
|
||||
},
|
||||
[closeModal, onRowClick]
|
||||
);
|
||||
return isModalOpen ? (
|
||||
<Modal onClose={closeModal} data-test-subj="all-cases-modal">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.SELECT_CASE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<AllCasesGeneric
|
||||
alertData={alertData}
|
||||
createCaseNavigation={createCaseNavigation}
|
||||
hiddenStatuses={hiddenStatuses}
|
||||
isSelectorView={true}
|
||||
onRowClick={onClick}
|
||||
userCanCrud={userCanCrud}
|
||||
updateCase={updateCase}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton color="text" onClick={closeModal}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Modal>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const AllCasesSelectorModal: React.FC<AllCasesSelectorModalProps> = React.memo((props) => {
|
||||
return (
|
||||
<OwnerProvider owner={props.owner}>
|
||||
<AllCasesSelectorModalComponent {...props} />
|
||||
</OwnerProvider>
|
||||
);
|
||||
});
|
||||
|
||||
AllCasesSelectorModal.displayName = 'AllCasesSelectorModal';
|
||||
|
||||
export type { AllCasesSelectorModalProps };
|
||||
export { AllCasesSelectorModal };
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AllCasesSelectorModal as default };
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, MutableRefObject } from 'react';
|
||||
import React, { FunctionComponent, MutableRefObject, useCallback } from 'react';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingContent,
|
||||
|
@ -17,16 +17,16 @@ import classnames from 'classnames';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { CasesTableUtilityBar } from './utility_bar';
|
||||
import { CasesNavigation, LinkButton } from '../links';
|
||||
import { LinkButton } from '../links';
|
||||
import { AllCases, Case, FilterOptions } from '../../../common';
|
||||
import * as i18n from './translations';
|
||||
import { useCreateCaseNavigation } from '../../common/navigation';
|
||||
|
||||
interface CasesTableProps {
|
||||
columns: EuiBasicTableProps<Case>['columns']; // CasesColumns[];
|
||||
createCaseNavigation: CasesNavigation;
|
||||
data: AllCases;
|
||||
filterOptions: FilterOptions;
|
||||
goToCreateCase: (e: React.MouseEvent) => void;
|
||||
goToCreateCase?: () => void;
|
||||
handleIsLoading: (a: boolean) => void;
|
||||
isCasesLoading: boolean;
|
||||
isCommentUpdating: boolean;
|
||||
|
@ -76,7 +76,6 @@ const Div = styled.div`
|
|||
|
||||
export const CasesTable: FunctionComponent<CasesTableProps> = ({
|
||||
columns,
|
||||
createCaseNavigation,
|
||||
data,
|
||||
filterOptions,
|
||||
goToCreateCase,
|
||||
|
@ -96,8 +95,21 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
|
|||
tableRef,
|
||||
tableRowProps,
|
||||
userCanCrud,
|
||||
}) =>
|
||||
isCasesLoading && isDataEmpty ? (
|
||||
}) => {
|
||||
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
|
||||
const navigateToCreateCaseClick = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
if (goToCreateCase != null) {
|
||||
goToCreateCase();
|
||||
} else {
|
||||
navigateToCreateCase();
|
||||
}
|
||||
},
|
||||
[goToCreateCase, navigateToCreateCase]
|
||||
);
|
||||
|
||||
return isCasesLoading && isDataEmpty ? (
|
||||
<Div>
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelAllCases" lines={10} />
|
||||
</Div>
|
||||
|
@ -131,8 +143,8 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
|
|||
isDisabled={!userCanCrud}
|
||||
fill
|
||||
size="s"
|
||||
onClick={goToCreateCase}
|
||||
href={createCaseNavigation.href}
|
||||
onClick={navigateToCreateCaseClick}
|
||||
href={getCreateCaseUrl()}
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="cases-table-add-case"
|
||||
>
|
||||
|
@ -151,3 +163,4 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
|
|||
/>
|
||||
</Div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ const props = {
|
|||
|
||||
describe('CasesTableFilters ', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
(useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags });
|
||||
(useGetReporters as jest.Mock).mockReturnValue({
|
||||
reporters: ['casetester'],
|
||||
|
|
|
@ -141,7 +141,12 @@ export const CasesTableUtilityBar: FunctionComponent<Props> = ({
|
|||
</UtilityBarAction>
|
||||
</>
|
||||
)}
|
||||
<UtilityBarAction iconSide="left" iconType="refresh" onClick={refreshCases}>
|
||||
<UtilityBarAction
|
||||
iconSide="left"
|
||||
iconType="refresh"
|
||||
onClick={refreshCases}
|
||||
dataTestSubj="all-cases-refresh"
|
||||
>
|
||||
{i18n.REFRESH}
|
||||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import md5 from 'md5';
|
||||
import { CasesRoutes } from './routes';
|
||||
import { CasesRoutesProps } from './types';
|
||||
|
||||
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
|
||||
md5(ids.join(delimiter));
|
||||
export type CasesProps = CasesRoutesProps;
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CasesRoutes as default };
|
105
x-pack/plugins/cases/public/components/app/routes.test.tsx
Normal file
105
x-pack/plugins/cases/public/components/app/routes.test.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import { MemoryRouterProps } from 'react-router';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { CasesRoutes } from './routes';
|
||||
|
||||
jest.mock('../all_cases', () => ({
|
||||
AllCases: () => <div>{'All cases'}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../case_view', () => ({
|
||||
CaseView: () => <div>{'Case view'}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../create', () => ({
|
||||
CreateCase: () => <div>{'Create case'}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../configure_cases', () => ({
|
||||
ConfigureCases: () => <div>{'Configure cases'}</div>,
|
||||
}));
|
||||
|
||||
const getCaseViewPaths = () => [
|
||||
'/cases/test-id',
|
||||
'/cases/test-id/comment-id',
|
||||
'/cases/test-id/sub-cases/sub-case-id',
|
||||
'/cases/test-id/sub-cases/sub-case-id/comment-id',
|
||||
];
|
||||
|
||||
const renderWithRouter = (
|
||||
initialEntries: MemoryRouterProps['initialEntries'] = ['/cases'],
|
||||
userCanCrud = true
|
||||
) => {
|
||||
return render(
|
||||
<TestProviders userCanCrud={userCanCrud}>
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<CasesRoutes useFetchAlertData={(alertIds) => [false, {}]} />
|
||||
</MemoryRouter>
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Cases routes', () => {
|
||||
describe('All cases', () => {
|
||||
it('navigates to the all cases page', () => {
|
||||
renderWithRouter();
|
||||
expect(screen.getByText('All cases')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// User has read only privileges
|
||||
it('user can navigate to the all cases page with userCanCrud = false', () => {
|
||||
renderWithRouter(['/cases'], false);
|
||||
expect(screen.getByText('All cases')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Case view', () => {
|
||||
it.each(getCaseViewPaths())('navigates to the cases view page for path: %s', (path: string) => {
|
||||
renderWithRouter([path]);
|
||||
expect(screen.getByText('Case view')).toBeInTheDocument();
|
||||
// User has read only privileges
|
||||
});
|
||||
|
||||
it.each(getCaseViewPaths())(
|
||||
'user can navigate to the cases view page with userCanCrud = false and path: %s',
|
||||
(path: string) => {
|
||||
renderWithRouter([path], false);
|
||||
expect(screen.getByText('Case view')).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('Create case', () => {
|
||||
it('navigates to the create case page', () => {
|
||||
renderWithRouter(['/cases/create']);
|
||||
expect(screen.getByText('Create case')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the no privileges page if userCanCrud = false', () => {
|
||||
renderWithRouter(['/cases/create'], false);
|
||||
expect(screen.getByText('Privileges required')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configure cases', () => {
|
||||
it('navigates to the configure cases page', () => {
|
||||
renderWithRouter(['/cases/configure']);
|
||||
expect(screen.getByText('Configure cases')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the no privileges page if userCanCrud = false', () => {
|
||||
renderWithRouter(['/cases/configure'], false);
|
||||
expect(screen.getByText('Privileges required')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
106
x-pack/plugins/cases/public/components/app/routes.tsx
Normal file
106
x-pack/plugins/cases/public/components/app/routes.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { AllCases } from '../all_cases';
|
||||
import { CaseView } from '../case_view';
|
||||
import { CreateCase } from '../create';
|
||||
import { ConfigureCases } from '../configure_cases';
|
||||
import { CasesRoutesProps } from './types';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import {
|
||||
getCasesConfigurePath,
|
||||
getCreateCasePath,
|
||||
getCaseViewPath,
|
||||
getCaseViewWithCommentPath,
|
||||
getSubCaseViewPath,
|
||||
getSubCaseViewWithCommentPath,
|
||||
useAllCasesNavigation,
|
||||
useCaseViewNavigation,
|
||||
} from '../../common/navigation';
|
||||
import { NoPrivilegesPage } from '../no_privileges';
|
||||
import * as i18n from './translations';
|
||||
import { useReadonlyHeader } from './use_readonly_header';
|
||||
|
||||
const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
|
||||
disableAlerts,
|
||||
onComponentInitialized,
|
||||
actionsNavigation,
|
||||
ruleDetailsNavigation,
|
||||
showAlertDetails,
|
||||
useFetchAlertData,
|
||||
refreshRef,
|
||||
hideSyncAlerts,
|
||||
timelineIntegration,
|
||||
}) => {
|
||||
const { basePath, userCanCrud } = useCasesContext();
|
||||
const { navigateToAllCases } = useAllCasesNavigation();
|
||||
const { navigateToCaseView } = useCaseViewNavigation();
|
||||
useReadonlyHeader();
|
||||
|
||||
const onCreateCaseSuccess = useCallback(
|
||||
async ({ id }) => navigateToCaseView({ detailName: id }),
|
||||
[navigateToCaseView]
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route strict exact path={basePath}>
|
||||
<AllCases disableAlerts={disableAlerts} />
|
||||
</Route>
|
||||
|
||||
<Route path={getCreateCasePath(basePath)}>
|
||||
{userCanCrud ? (
|
||||
<CreateCase
|
||||
onSuccess={onCreateCaseSuccess}
|
||||
onCancel={navigateToAllCases}
|
||||
disableAlerts={disableAlerts}
|
||||
timelineIntegration={timelineIntegration}
|
||||
/>
|
||||
) : (
|
||||
<NoPrivilegesPage pageName={i18n.CREATE_CASE_PAGE_NAME} />
|
||||
)}
|
||||
</Route>
|
||||
|
||||
<Route path={getCasesConfigurePath(basePath)}>
|
||||
{userCanCrud ? (
|
||||
<ConfigureCases />
|
||||
) : (
|
||||
<NoPrivilegesPage pageName={i18n.CONFIGURE_CASES_PAGE_NAME} />
|
||||
)}
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={[
|
||||
getSubCaseViewWithCommentPath(basePath),
|
||||
getCaseViewWithCommentPath(basePath),
|
||||
getSubCaseViewPath(basePath),
|
||||
getCaseViewPath(basePath),
|
||||
]}
|
||||
>
|
||||
<CaseView
|
||||
onComponentInitialized={onComponentInitialized}
|
||||
actionsNavigation={actionsNavigation}
|
||||
ruleDetailsNavigation={ruleDetailsNavigation}
|
||||
showAlertDetails={showAlertDetails}
|
||||
useFetchAlertData={useFetchAlertData}
|
||||
refreshRef={refreshRef}
|
||||
hideSyncAlerts={hideSyncAlerts}
|
||||
timelineIntegration={timelineIntegration}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path={basePath}>
|
||||
<Redirect to={basePath} />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export const CasesRoutes = React.memo(CasesRoutesComponent);
|
39
x-pack/plugins/cases/public/components/app/translations.ts
Normal file
39
x-pack/plugins/cases/public/components/app/translations.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const NO_PRIVILEGES_MSG = (pageName: string) =>
|
||||
i18n.translate('xpack.cases.noPrivileges.message', {
|
||||
values: { pageName },
|
||||
defaultMessage:
|
||||
'To view {pageName} page, you must update privileges. For more information, contact your Kibana administrator.',
|
||||
});
|
||||
|
||||
export const NO_PRIVILEGES_TITLE = i18n.translate('xpack.cases.noPrivileges.title', {
|
||||
defaultMessage: 'Privileges required',
|
||||
});
|
||||
|
||||
export const NO_PRIVILEGES_BUTTON = i18n.translate('xpack.cases.noPrivileges.button', {
|
||||
defaultMessage: 'Back to Cases',
|
||||
});
|
||||
|
||||
export const CREATE_CASE_PAGE_NAME = i18n.translate('xpack.cases.createCase', {
|
||||
defaultMessage: 'Create Case',
|
||||
});
|
||||
|
||||
export const CONFIGURE_CASES_PAGE_NAME = i18n.translate('xpack.cases.configureCases', {
|
||||
defaultMessage: 'Configure Cases',
|
||||
});
|
||||
|
||||
export const READ_ONLY_BADGE_TEXT = i18n.translate('xpack.cases.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
});
|
||||
|
||||
export const READ_ONLY_BADGE_TOOLTIP = i18n.translate('xpack.cases.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to create or edit cases',
|
||||
});
|
27
x-pack/plugins/cases/public/components/app/types.ts
Normal file
27
x-pack/plugins/cases/public/components/app/types.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { MutableRefObject } from 'react';
|
||||
import { Ecs, CaseViewRefreshPropInterface } from '../../../common';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { CasesTimelineIntegration } from '../timeline_context';
|
||||
|
||||
export interface CasesRoutesProps {
|
||||
disableAlerts?: boolean;
|
||||
onComponentInitialized?: () => void;
|
||||
actionsNavigation?: CasesNavigation<string, 'configurable'>;
|
||||
ruleDetailsNavigation?: CasesNavigation<string | null | undefined, 'configurable'>;
|
||||
showAlertDetails?: (alertId: string, index: string) => void;
|
||||
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
|
||||
/**
|
||||
* A React `Ref` that Exposes data refresh callbacks.
|
||||
* **NOTE**: Do not hold on to the `.current` object, as it could become stale
|
||||
*/
|
||||
refreshRef?: MutableRefObject<CaseViewRefreshPropInterface>;
|
||||
hideSyncAlerts?: boolean;
|
||||
timelineIntegration?: CasesTimelineIntegration;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { useReadonlyHeader } from './use_readonly_header';
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const mockedSetBadge = jest.fn();
|
||||
|
||||
describe('CaseContainerComponent', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useKibanaMock().services.chrome.setBadge = mockedSetBadge;
|
||||
});
|
||||
|
||||
it('does not display the readonly glasses badge when the user has write permissions', () => {
|
||||
renderHook(() => useReadonlyHeader(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(mockedSetBadge).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('displays the readonly glasses badge read permissions but not write', () => {
|
||||
renderHook(() => useReadonlyHeader(), {
|
||||
wrapper: ({ children }) => <TestProviders userCanCrud={false}>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
expect(mockedSetBadge).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -7,27 +7,27 @@
|
|||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import * as i18n from '../components/app/cases/translations';
|
||||
import { useGetUserCasesPermissions } from '../hooks/use_get_user_cases_permissions';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
/**
|
||||
* This component places a read-only icon badge in the header if user only has read permissions
|
||||
*/
|
||||
export function useReadonlyHeader() {
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const { userCanCrud } = useCasesContext();
|
||||
const chrome = useKibana().services.chrome;
|
||||
|
||||
// if the user is read only then display the glasses badge in the global navigation header
|
||||
const setBadge = useCallback(() => {
|
||||
if (userPermissions != null && !userPermissions.crud && userPermissions.read) {
|
||||
if (!userCanCrud) {
|
||||
chrome.setBadge({
|
||||
text: i18n.READ_ONLY_BADGE_TEXT,
|
||||
tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
|
||||
iconType: 'glasses',
|
||||
});
|
||||
}
|
||||
}, [chrome, userPermissions]);
|
||||
}, [chrome, userCanCrud]);
|
||||
|
||||
useEffect(() => {
|
||||
setBadge();
|
|
@ -13,22 +13,18 @@ import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
|
|||
import { PropertyActions } from '../property_actions';
|
||||
import { Case } from '../../../common';
|
||||
import { CaseService } from '../../containers/use_get_case_user_actions';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { useAllCasesNavigation } from '../../common/navigation';
|
||||
|
||||
interface CaseViewActions {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
caseData: Case;
|
||||
currentExternalIncident: CaseService | null;
|
||||
}
|
||||
|
||||
const ActionsComponent: React.FC<CaseViewActions> = ({
|
||||
allCasesNavigation,
|
||||
caseData,
|
||||
currentExternalIncident,
|
||||
}) => {
|
||||
const ActionsComponent: React.FC<CaseViewActions> = ({ caseData, currentExternalIncident }) => {
|
||||
// Delete case
|
||||
const { handleToggleModal, handleOnDeleteConfirm, isDeleted, isDisplayConfirmDeleteModal } =
|
||||
useDeleteCases();
|
||||
const { navigateToAllCases } = useAllCasesNavigation();
|
||||
|
||||
const propertyActions = useMemo(
|
||||
() => [
|
||||
|
@ -51,7 +47,7 @@ const ActionsComponent: React.FC<CaseViewActions> = ({
|
|||
);
|
||||
|
||||
if (isDeleted) {
|
||||
allCasesNavigation.onClick(null);
|
||||
navigateToAllCases();
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -25,7 +25,6 @@ import { StatusContextMenu } from './status_context_menu';
|
|||
import { getStatusDate, getStatusTitle } from './helpers';
|
||||
import { SyncAlertsSwitch } from '../case_settings/sync_alerts_switch';
|
||||
import { OnUpdateFields } from '../case_view';
|
||||
import { CasesNavigation } from '../links';
|
||||
|
||||
const MyDescriptionList = styled(EuiDescriptionList)`
|
||||
${({ theme }) => css`
|
||||
|
@ -41,7 +40,6 @@ const MyDescriptionList = styled(EuiDescriptionList)`
|
|||
`;
|
||||
|
||||
interface CaseActionBarProps {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
caseData: Case;
|
||||
currentExternalIncident: CaseService | null;
|
||||
userCanCrud: boolean;
|
||||
|
@ -51,7 +49,6 @@ interface CaseActionBarProps {
|
|||
onUpdateField: (args: OnUpdateFields) => void;
|
||||
}
|
||||
const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
||||
allCasesNavigation,
|
||||
caseData,
|
||||
currentExternalIncident,
|
||||
disableAlerting,
|
||||
|
@ -157,11 +154,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
</EuiFlexItem>
|
||||
{userCanCrud && (
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-actions">
|
||||
<Actions
|
||||
allCasesNavigation={allCasesNavigation}
|
||||
caseData={caseData}
|
||||
currentExternalIncident={currentExternalIncident}
|
||||
/>
|
||||
<Actions caseData={caseData} currentExternalIncident={currentExternalIncident} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -9,24 +9,29 @@ import React from 'react';
|
|||
|
||||
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { useAllCasesNavigation } from '../../common/navigation';
|
||||
|
||||
interface Props {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
caseId: string;
|
||||
}
|
||||
|
||||
export const DoesNotExist = ({ allCasesNavigation, caseId }: Props) => (
|
||||
<EuiEmptyPrompt
|
||||
iconColor="default"
|
||||
iconType="addDataApp"
|
||||
title={<h2>{i18n.DOES_NOT_EXIST_TITLE}</h2>}
|
||||
titleSize="xs"
|
||||
body={<p>{i18n.DOES_NOT_EXIST_DESCRIPTION(caseId)}</p>}
|
||||
actions={
|
||||
<EuiButton onClick={allCasesNavigation.onClick} size="s" color="primary" fill>
|
||||
{i18n.DOES_NOT_EXIST_BUTTON}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
export const DoesNotExist = React.memo(({ caseId }: Props) => {
|
||||
const { navigateToAllCases } = useAllCasesNavigation();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconColor="default"
|
||||
iconType="addDataApp"
|
||||
title={<h2>{i18n.DOES_NOT_EXIST_TITLE}</h2>}
|
||||
titleSize="xs"
|
||||
body={<p>{i18n.DOES_NOT_EXIST_DESCRIPTION(caseId)}</p>}
|
||||
actions={
|
||||
<EuiButton onClick={navigateToAllCases} size="s" color="primary" fill>
|
||||
{i18n.DOES_NOT_EXIST_BUTTON}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DoesNotExist.displayName = 'DoesNotExist';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import '../../common/mock/match_media';
|
||||
import { CaseComponent, CaseComponentProps, CaseView, CaseViewProps } from '.';
|
||||
|
@ -22,7 +23,6 @@ import { SpacesApi } from '../../../../spaces/public';
|
|||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
import { useGetCase } from '../../containers/use_get_case';
|
||||
import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../containers/configure/mock';
|
||||
|
@ -30,10 +30,6 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service'
|
|||
import { CaseType, ConnectorTypes } from '../../../common';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
const mockId = basicCase.id;
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useParams: () => ({ detailName: mockId }),
|
||||
}));
|
||||
jest.mock('../../containers/use_update_case');
|
||||
jest.mock('../../containers/use_get_case_user_actions');
|
||||
jest.mock('../../containers/use_get_case');
|
||||
|
@ -41,13 +37,13 @@ jest.mock('../../containers/configure/use_connectors');
|
|||
jest.mock('../../containers/use_post_push_to_service');
|
||||
jest.mock('../user_action_tree/user_action_timestamp');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
const useUpdateCaseMock = useUpdateCase as jest.Mock;
|
||||
const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock;
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const usePostPushToServiceMock = usePostPushToService as jest.Mock;
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const onCaseDataSuccessMock = jest.fn();
|
||||
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
|
||||
const spacesUiApiMock = {
|
||||
redirectLegacyUrl: jest.fn().mockResolvedValue(undefined),
|
||||
|
@ -84,20 +80,7 @@ const alertsHit = [
|
|||
];
|
||||
|
||||
export const caseProps: CaseComponentProps = {
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseId: basicCase.id,
|
||||
configureCasesNavigation: {
|
||||
href: 'configure-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
onComponentInitialized: jest.fn(),
|
||||
actionsNavigation: {
|
||||
href: jest.fn(),
|
||||
|
@ -115,7 +98,6 @@ export const caseProps: CaseComponentProps = {
|
|||
'alert-id-2': alertsHit[1],
|
||||
},
|
||||
],
|
||||
userCanCrud: true,
|
||||
caseData: {
|
||||
...basicCase,
|
||||
comments: [...basicCase.comments, alertComment],
|
||||
|
@ -128,7 +110,6 @@ export const caseProps: CaseComponentProps = {
|
|||
},
|
||||
fetchCase: jest.fn(),
|
||||
updateCase: jest.fn(),
|
||||
onCaseDataSuccess: onCaseDataSuccessMock,
|
||||
};
|
||||
|
||||
export const caseClosedProps: CaseComponentProps = {
|
||||
|
@ -373,15 +354,6 @@ describe('CaseView ', () => {
|
|||
await waitFor(() => {
|
||||
expect(updateObject.updateKey).toEqual('title');
|
||||
expect(updateObject.updateValue).toEqual(newTitle);
|
||||
expect(updateObject.onSuccess).toBeDefined();
|
||||
});
|
||||
|
||||
updateObject.onSuccess(); // simulate the request has succeed
|
||||
await waitFor(() => {
|
||||
expect(onCaseDataSuccessMock).toHaveBeenCalledWith({
|
||||
...caseProps.caseData,
|
||||
title: newTitle,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -472,7 +444,7 @@ describe('CaseView ', () => {
|
|||
expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy();
|
||||
expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled();
|
||||
expect(spacesUiApiMock.redirectLegacyUrl).toHaveBeenCalledWith(
|
||||
`cases/${resolveAliasId}`,
|
||||
`/cases/${resolveAliasId}`,
|
||||
'case'
|
||||
);
|
||||
});
|
||||
|
@ -498,7 +470,7 @@ describe('CaseView ', () => {
|
|||
objectNoun: 'case',
|
||||
currentObjectId: defaultGetCase.data.id,
|
||||
otherObjectId: resolveAliasId,
|
||||
otherObjectPath: `cases/${resolveAliasId}`,
|
||||
otherObjectPath: `/cases/${resolveAliasId}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -749,54 +721,41 @@ describe('CaseView ', () => {
|
|||
describe('when a `refreshRef` prop is provided', () => {
|
||||
let refreshRef: CaseViewProps['refreshRef'];
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
(useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
|
||||
refreshRef = React.createRef();
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<CaseView
|
||||
{...{
|
||||
refreshRef,
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseId: '1234',
|
||||
configureCasesNavigation: {
|
||||
href: 'configure-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
onComponentInitialized: jest.fn(),
|
||||
ruleDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
showAlertDetails: jest.fn(),
|
||||
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
|
||||
userCanCrud: true,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
await act(async () => {
|
||||
mount(
|
||||
<TestProviders>
|
||||
<CaseView
|
||||
{...{
|
||||
refreshRef,
|
||||
caseId: '1234',
|
||||
onComponentInitialized: jest.fn(),
|
||||
showAlertDetails: jest.fn(),
|
||||
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
|
||||
userCanCrud: true,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set it with expected refresh interface', async () => {
|
||||
expect(refreshRef!.current).toEqual({
|
||||
refreshUserActionsAndComments: expect.any(Function),
|
||||
refreshCase: expect.any(Function),
|
||||
await waitFor(() => {
|
||||
expect(refreshRef!.current).toEqual({
|
||||
refreshUserActionsAndComments: expect.any(Function),
|
||||
refreshCase: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should refresh actions and comments', async () => {
|
||||
await waitFor(() => {
|
||||
refreshRef!.current!.refreshUserActionsAndComments();
|
||||
expect(fetchCaseUserActions).toBeCalledWith('1234', 'resilient-2', undefined);
|
||||
expect(fetchCaseUserActions).toBeCalledWith('basic-case-id', 'resilient-2', undefined);
|
||||
expect(fetchCase).toBeCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,24 +37,25 @@ import * as i18n from './translations';
|
|||
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
|
||||
import { useTimelineContext } from '../timeline_context/use_timeline_context';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { getConnectorById } from '../utils';
|
||||
import { DoesNotExist } from './does_not_exist';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import {
|
||||
generateCaseViewPath,
|
||||
useCaseViewNavigation,
|
||||
useCaseViewParams,
|
||||
} from '../../common/navigation';
|
||||
import { useCasesTitleBreadcrumbs } from '../use_breadcrumbs';
|
||||
|
||||
export interface CaseViewComponentProps {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
caseDetailsNavigation: CasesNavigation;
|
||||
caseId: string;
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
subCaseId?: string;
|
||||
onComponentInitialized?: () => void;
|
||||
actionsNavigation?: CasesNavigation<string, 'configurable'>;
|
||||
ruleDetailsNavigation?: CasesNavigation<string | null | undefined, 'configurable'>;
|
||||
showAlertDetails?: (alertId: string, index: string) => void;
|
||||
subCaseId?: string;
|
||||
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
|
||||
userCanCrud: boolean;
|
||||
/**
|
||||
* A React `Ref` that Exposes data refresh callbacks.
|
||||
* **NOTE**: Do not hold on to the `.current` object, as it could become stale
|
||||
|
@ -63,8 +64,7 @@ export interface CaseViewComponentProps {
|
|||
hideSyncAlerts?: boolean;
|
||||
}
|
||||
|
||||
export interface CaseViewProps extends CaseViewComponentProps {
|
||||
onCaseDataSuccess?: (data: Case) => void;
|
||||
export interface CaseViewProps extends Omit<CaseViewComponentProps, 'caseId' | 'subCaseId'> {
|
||||
timelineIntegration?: CasesTimelineIntegration;
|
||||
}
|
||||
|
||||
|
@ -83,19 +83,13 @@ export interface CaseComponentProps extends CaseViewComponentProps {
|
|||
fetchCase: UseGetCase['fetchCase'];
|
||||
caseData: Case;
|
||||
updateCase: (newCase: Case) => void;
|
||||
onCaseDataSuccess?: (newCase: Case) => void;
|
||||
}
|
||||
|
||||
export const CaseComponent = React.memo<CaseComponentProps>(
|
||||
({
|
||||
allCasesNavigation,
|
||||
caseData,
|
||||
caseDetailsNavigation,
|
||||
caseId,
|
||||
configureCasesNavigation,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
fetchCase,
|
||||
onCaseDataSuccess,
|
||||
onComponentInitialized,
|
||||
actionsNavigation,
|
||||
ruleDetailsNavigation,
|
||||
|
@ -103,10 +97,13 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
subCaseId,
|
||||
updateCase,
|
||||
useFetchAlertData,
|
||||
userCanCrud,
|
||||
refreshRef,
|
||||
hideSyncAlerts = false,
|
||||
}) => {
|
||||
const { userCanCrud } = useCasesContext();
|
||||
const { getCaseViewUrl } = useCaseViewNavigation();
|
||||
useCasesTitleBreadcrumbs(caseData.title);
|
||||
|
||||
const [initLoadingData, setInitLoadingData] = useState(true);
|
||||
const init = useRef(true);
|
||||
const timelineUi = useTimelineContext()?.ui;
|
||||
|
@ -321,13 +318,8 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
onUpdateField({
|
||||
key: 'title',
|
||||
value: newTitle,
|
||||
onSuccess: () => {
|
||||
if (onCaseDataSuccess) {
|
||||
onCaseDataSuccess({ ...caseData, title: newTitle });
|
||||
}
|
||||
},
|
||||
}),
|
||||
[caseData, onUpdateField, onCaseDataSuccess]
|
||||
[onUpdateField]
|
||||
);
|
||||
|
||||
const changeStatus = useCallback(
|
||||
|
@ -347,9 +339,9 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
const emailContent = useMemo(
|
||||
() => ({
|
||||
subject: i18n.EMAIL_SUBJECT(caseData.title),
|
||||
body: i18n.EMAIL_BODY(caseDetailsNavigation.href),
|
||||
body: i18n.EMAIL_BODY(getCaseViewUrl({ detailName: caseId, subCaseId })),
|
||||
}),
|
||||
[caseDetailsNavigation.href, caseData.title]
|
||||
[caseData.title, getCaseViewUrl, caseId, subCaseId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -358,16 +350,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
}
|
||||
}, [initLoadingData, isLoadingUserActions]);
|
||||
|
||||
const backOptions = useMemo(
|
||||
() => ({
|
||||
href: allCasesNavigation.href,
|
||||
text: i18n.BACK_TO_ALL,
|
||||
dataTestSubj: 'backToCases',
|
||||
onClick: allCasesNavigation.onClick,
|
||||
}),
|
||||
[allCasesNavigation]
|
||||
);
|
||||
|
||||
const onShowAlertDetails = useCallback(
|
||||
(alertId: string, index: string) => {
|
||||
if (showAlertDetails) {
|
||||
|
@ -390,7 +372,7 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
backOptions={backOptions}
|
||||
showBackButton={true}
|
||||
data-test-subj="case-view-title"
|
||||
titleNode={
|
||||
<EditableTitle
|
||||
|
@ -403,7 +385,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
title={caseData.title}
|
||||
>
|
||||
<CaseActionBar
|
||||
allCasesNavigation={allCasesNavigation}
|
||||
caseData={caseData}
|
||||
currentExternalIncident={currentExternalIncident}
|
||||
userCanCrud={userCanCrud}
|
||||
|
@ -424,7 +405,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
{!initLoadingData && (
|
||||
<>
|
||||
<UserActionTree
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
getRuleDetailsHref={ruleDetailsNavigation?.href}
|
||||
onRuleDetailsClick={ruleDetailsNavigation?.onClick}
|
||||
caseServices={caseServices}
|
||||
|
@ -485,7 +465,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
<EditConnector
|
||||
caseData={caseData}
|
||||
caseServices={caseServices}
|
||||
configureCasesNavigation={configureCasesNavigation}
|
||||
connectorName={connectorName}
|
||||
connectors={connectors}
|
||||
hasDataToPush={hasDataToPush && userCanCrud}
|
||||
|
@ -520,97 +499,71 @@ export const CaseViewLoading = () => (
|
|||
|
||||
export const CaseView = React.memo(
|
||||
({
|
||||
allCasesNavigation,
|
||||
caseDetailsNavigation,
|
||||
caseId,
|
||||
configureCasesNavigation,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
onCaseDataSuccess,
|
||||
onComponentInitialized,
|
||||
actionsNavigation,
|
||||
ruleDetailsNavigation,
|
||||
showAlertDetails,
|
||||
subCaseId,
|
||||
timelineIntegration,
|
||||
useFetchAlertData,
|
||||
userCanCrud,
|
||||
refreshRef,
|
||||
hideSyncAlerts,
|
||||
}: CaseViewProps) => {
|
||||
const { spaces: spacesApi } = useKibana().services;
|
||||
const { detailName: caseId, subCaseId } = useCaseViewParams();
|
||||
const { basePath } = useCasesContext();
|
||||
const { data, resolveOutcome, resolveAliasId, isLoading, isError, fetchCase, updateCase } =
|
||||
useGetCase(caseId, subCaseId);
|
||||
const { spaces: spacesApi, http } = useKibana().services;
|
||||
|
||||
useEffect(() => {
|
||||
if (onCaseDataSuccess && data) {
|
||||
onCaseDataSuccess(data);
|
||||
}
|
||||
}, [data, onCaseDataSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) {
|
||||
// CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and
|
||||
// Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded
|
||||
// under any another path, passing a path builder function by props from every parent plugin.
|
||||
const newPath = http.basePath.prepend(
|
||||
`cases/${resolveAliasId}${window.location.search}${window.location.hash}`
|
||||
);
|
||||
const newPath = `${basePath}${generateCaseViewPath({ detailName: resolveAliasId })}${
|
||||
window.location.search
|
||||
}${window.location.hash}`;
|
||||
spacesApi.ui.redirectLegacyUrl(newPath, i18n.CASE);
|
||||
}
|
||||
}, [resolveOutcome, resolveAliasId, spacesApi, http]);
|
||||
}, [resolveOutcome, resolveAliasId, basePath, spacesApi]);
|
||||
|
||||
const getLegacyUrlConflictCallout = useCallback(() => {
|
||||
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
|
||||
if (data && spacesApi && resolveOutcome === 'conflict' && resolveAliasId != null) {
|
||||
// We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a
|
||||
// callout with a warning for the user, and provide a way for them to navigate to the other object.
|
||||
const otherObjectId = resolveAliasId; // This is always defined if outcome === 'conflict'
|
||||
// CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and
|
||||
// Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded
|
||||
// under any another path, passing a path builder function by props from every parent plugin.
|
||||
const otherObjectPath = http.basePath.prepend(
|
||||
`cases/${otherObjectId}${window.location.search}${window.location.hash}`
|
||||
);
|
||||
const otherObjectPath = `${basePath}${generateCaseViewPath({
|
||||
detailName: resolveAliasId,
|
||||
})}${window.location.search}${window.location.hash}`;
|
||||
|
||||
return spacesApi.ui.components.getLegacyUrlConflict({
|
||||
objectNoun: i18n.CASE,
|
||||
currentObjectId: data.id,
|
||||
otherObjectId,
|
||||
otherObjectId: resolveAliasId,
|
||||
otherObjectPath,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [data, resolveAliasId, resolveOutcome, spacesApi, http.basePath]);
|
||||
}, [data, resolveAliasId, resolveOutcome, basePath, spacesApi]);
|
||||
|
||||
return isError ? (
|
||||
<DoesNotExist allCasesNavigation={allCasesNavigation} caseId={caseId} />
|
||||
<DoesNotExist caseId={caseId} />
|
||||
) : isLoading ? (
|
||||
<CaseViewLoading />
|
||||
) : (
|
||||
data && (
|
||||
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
|
||||
<OwnerProvider owner={[data.owner]}>
|
||||
{getLegacyUrlConflictCallout()}
|
||||
<CaseComponent
|
||||
allCasesNavigation={allCasesNavigation}
|
||||
caseData={data}
|
||||
caseDetailsNavigation={caseDetailsNavigation}
|
||||
caseId={caseId}
|
||||
configureCasesNavigation={configureCasesNavigation}
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
fetchCase={fetchCase}
|
||||
onCaseDataSuccess={onCaseDataSuccess}
|
||||
onComponentInitialized={onComponentInitialized}
|
||||
actionsNavigation={actionsNavigation}
|
||||
ruleDetailsNavigation={ruleDetailsNavigation}
|
||||
showAlertDetails={showAlertDetails}
|
||||
subCaseId={subCaseId}
|
||||
updateCase={updateCase}
|
||||
useFetchAlertData={useFetchAlertData}
|
||||
userCanCrud={userCanCrud}
|
||||
refreshRef={refreshRef}
|
||||
hideSyncAlerts={hideSyncAlerts}
|
||||
/>
|
||||
</OwnerProvider>
|
||||
{getLegacyUrlConflictCallout()}
|
||||
<CaseComponent
|
||||
caseData={data}
|
||||
caseId={caseId}
|
||||
fetchCase={fetchCase}
|
||||
onComponentInitialized={onComponentInitialized}
|
||||
actionsNavigation={actionsNavigation}
|
||||
ruleDetailsNavigation={ruleDetailsNavigation}
|
||||
showAlertDetails={showAlertDetails}
|
||||
subCaseId={subCaseId}
|
||||
updateCase={updateCase}
|
||||
useFetchAlertData={useFetchAlertData}
|
||||
refreshRef={refreshRef}
|
||||
hideSyncAlerts={hideSyncAlerts}
|
||||
/>
|
||||
</CasesTimelineIntegrationProvider>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import { DEFAULT_BASE_PATH } from '../../common/navigation';
|
||||
import { useApplication } from './use_application';
|
||||
|
||||
export interface CasesContextValue {
|
||||
owner: string[];
|
||||
appId: string;
|
||||
appTitle: string;
|
||||
userCanCrud: boolean;
|
||||
basePath: string;
|
||||
}
|
||||
|
||||
export const CasesContext = React.createContext<CasesContextValue | undefined>(undefined);
|
||||
|
||||
export interface CasesContextProps
|
||||
extends Omit<CasesContextValue, 'appId' | 'appTitle' | 'basePath'> {
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export interface CasesContextStateValue
|
||||
extends Omit<CasesContextValue, 'appId' | 'appTitle' | 'userCanCrud'> {
|
||||
appId?: string;
|
||||
appTitle?: string;
|
||||
userCanCrud?: boolean;
|
||||
}
|
||||
|
||||
export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
|
||||
children,
|
||||
value: { owner, userCanCrud, basePath = DEFAULT_BASE_PATH },
|
||||
}) => {
|
||||
const { appId, appTitle } = useApplication();
|
||||
const [value, setValue] = useState<CasesContextStateValue>({
|
||||
owner,
|
||||
userCanCrud,
|
||||
basePath,
|
||||
});
|
||||
|
||||
/**
|
||||
* `userCanCrud` prop may change by the parent plugin.
|
||||
* `appId` and `appTitle` are dynamically retrieved from kibana context.
|
||||
* We need to update the state if any of these values change, the rest of props are never updated.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (appId && appTitle) {
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
appId,
|
||||
appTitle,
|
||||
userCanCrud,
|
||||
}));
|
||||
}
|
||||
}, [appTitle, appId, userCanCrud]);
|
||||
|
||||
return isCasesContextValue(value) ? (
|
||||
<CasesContext.Provider value={value}>{children}</CasesContext.Provider>
|
||||
) : null;
|
||||
};
|
||||
|
||||
function isCasesContextValue(value: CasesContextStateValue): value is CasesContextValue {
|
||||
return value.appId != null && value.appTitle != null && value.userCanCrud != null;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 useObservable from 'react-use/lib/useObservable';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface UseApplicationReturn {
|
||||
appId: string | undefined;
|
||||
appTitle: string | undefined;
|
||||
}
|
||||
|
||||
export const useApplication = (): UseApplicationReturn => {
|
||||
const { currentAppId$, applications$ } = useKibana().services.application;
|
||||
// retrieve the most recent value from the BehaviorSubject
|
||||
const appId = useObservable(currentAppId$);
|
||||
const applications = useObservable(applications$);
|
||||
|
||||
const appTitle = appId ? applications?.get(appId)?.category?.label : undefined;
|
||||
|
||||
return { appId, appTitle };
|
||||
};
|
|
@ -6,16 +6,14 @@
|
|||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { OwnerContext } from '.';
|
||||
import { CasesContext } from '.';
|
||||
|
||||
export const useOwnerContext = () => {
|
||||
const ownerContext = useContext(OwnerContext);
|
||||
export const useCasesContext = () => {
|
||||
const casesContext = useContext(CasesContext);
|
||||
|
||||
if (ownerContext.length === 0) {
|
||||
throw new Error(
|
||||
'useOwnerContext must be used within an OwnerProvider and not be an empty array'
|
||||
);
|
||||
if (!casesContext) {
|
||||
throw new Error('useCasesContext must be used within a CasesProvider and have a defined value');
|
||||
}
|
||||
|
||||
return ownerContext;
|
||||
return casesContext;
|
||||
};
|
|
@ -1,60 +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.
|
||||
*/
|
||||
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { CasesNavigation, LinkButton } from '../links';
|
||||
|
||||
// TODO: Potentially move into links component?
|
||||
export interface ConfigureCaseButtonProps {
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
isDisabled: boolean;
|
||||
label: string;
|
||||
msgTooltip: JSX.Element;
|
||||
showToolTip: boolean;
|
||||
titleTooltip: string;
|
||||
}
|
||||
|
||||
const ConfigureCaseButtonComponent: React.FC<ConfigureCaseButtonProps> = ({
|
||||
configureCasesNavigation: { href, onClick },
|
||||
isDisabled,
|
||||
label,
|
||||
msgTooltip,
|
||||
showToolTip,
|
||||
titleTooltip,
|
||||
}: ConfigureCaseButtonProps) => {
|
||||
const configureCaseButton = useMemo(
|
||||
() => (
|
||||
<LinkButton
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={isDisabled}
|
||||
aria-label={label}
|
||||
data-test-subj="configure-case-button"
|
||||
>
|
||||
{label}
|
||||
</LinkButton>
|
||||
),
|
||||
[label, isDisabled, onClick, href]
|
||||
);
|
||||
|
||||
return showToolTip ? (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
title={titleTooltip}
|
||||
content={<p>{msgTooltip}</p>}
|
||||
data-test-subj="configure-case-tooltip"
|
||||
>
|
||||
{configureCaseButton}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<>{configureCaseButton}</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigureCaseButton = memo(ConfigureCaseButtonComponent);
|
|
@ -26,7 +26,7 @@ import {
|
|||
useConnectorsResponse,
|
||||
useActionTypesResponse,
|
||||
} from './__mock__';
|
||||
import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { ConnectorTypes } from '../../../common';
|
||||
import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
@ -67,7 +67,7 @@ describe('ConfigureCases', () => {
|
|||
useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] }));
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -120,7 +120,7 @@ describe('ConfigureCases', () => {
|
|||
}));
|
||||
useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] }));
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -167,7 +167,7 @@ describe('ConfigureCases', () => {
|
|||
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -189,12 +189,10 @@ describe('ConfigureCases', () => {
|
|||
});
|
||||
|
||||
test('it disables correctly when the user cannot crud', () => {
|
||||
const newWrapper = mount(
|
||||
<ConfigureCases userCanCrud={false} owner={[SECURITY_SOLUTION_OWNER]} />,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
}
|
||||
);
|
||||
const newWrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
wrappingComponentProps: { userCanCrud: false },
|
||||
});
|
||||
|
||||
expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe(
|
||||
true
|
||||
|
@ -254,7 +252,7 @@ describe('ConfigureCases', () => {
|
|||
}));
|
||||
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -289,7 +287,7 @@ describe('ConfigureCases', () => {
|
|||
|
||||
useActionTypesMock.mockImplementation(() => ({ ...useActionTypesResponse, loading: true }));
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
expect(wrapper.find(Connectors).prop('isLoading')).toBe(true);
|
||||
|
@ -313,7 +311,7 @@ describe('ConfigureCases', () => {
|
|||
|
||||
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -356,7 +354,7 @@ describe('ConfigureCases', () => {
|
|||
...useConnectorsResponse,
|
||||
}));
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -400,7 +398,7 @@ describe('ConfigureCases', () => {
|
|||
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -444,7 +442,7 @@ describe('ConfigureCases', () => {
|
|||
},
|
||||
}));
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
|
@ -491,7 +489,7 @@ describe('ConfigureCases', () => {
|
|||
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
|
||||
useGetUrlSearchMock.mockImplementation(() => searchURL);
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
});
|
||||
|
@ -540,7 +538,7 @@ describe('ConfigureCases', () => {
|
|||
});
|
||||
|
||||
test('it show the add flyout when pressing the add connector button', async () => {
|
||||
const wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
const wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
|
@ -591,7 +589,7 @@ describe('ConfigureCases', () => {
|
|||
.fn()
|
||||
.mockReturnValue(true);
|
||||
|
||||
const wrapper = mount(<ConfigureCases userCanCrud owner={[SECURITY_SOLUTION_OWNER]} />, {
|
||||
const wrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
wrapper
|
||||
|
|
|
@ -22,14 +22,16 @@ import { ClosureType } from '../../containers/configure/types';
|
|||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public/types';
|
||||
|
||||
import { SectionWrapper } from '../wrappers';
|
||||
import { SectionWrapper, ContentWrapper, WhitePageWrapper } from '../wrappers';
|
||||
import { Connectors } from './connectors';
|
||||
import { ClosureOptions } from './closure_options';
|
||||
import { getNoneConnector, normalizeActionConnector, normalizeCaseConnector } from './utils';
|
||||
import * as i18n from './translations';
|
||||
import { Owner } from '../../types';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { getConnectorById } from '../utils';
|
||||
import { HeaderPage } from '../header_page';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useCasesBreadcrumbs } from '../use_breadcrumbs';
|
||||
import { CasesDeepLinkId } from '../../common/navigation';
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
${({ theme }) => css`
|
||||
|
@ -49,12 +51,10 @@ const FormWrapper = styled.div`
|
|||
`}
|
||||
`;
|
||||
|
||||
export interface ConfigureCasesProps extends Owner {
|
||||
userCanCrud: boolean;
|
||||
}
|
||||
|
||||
const ConfigureCasesComponent: React.FC<Omit<ConfigureCasesProps, 'owner'>> = ({ userCanCrud }) => {
|
||||
export const ConfigureCases: React.FC = React.memo(() => {
|
||||
const { userCanCrud } = useCasesContext();
|
||||
const { triggersActionsUi } = useKibana().services;
|
||||
useCasesBreadcrumbs(CasesDeepLinkId.casesConfigure);
|
||||
|
||||
const [connectorIsValid, setConnectorIsValid] = useState(true);
|
||||
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
|
||||
|
@ -185,64 +185,64 @@ const ConfigureCasesComponent: React.FC<Omit<ConfigureCasesProps, 'owner'>> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<FormWrapper>
|
||||
{!connectorIsValid && (
|
||||
<SectionWrapper style={{ marginTop: 0 }}>
|
||||
<EuiCallOut
|
||||
title={i18n.WARNING_NO_CONNECTOR_TITLE}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
data-test-subj="configure-cases-warning-callout"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The selected connector has been deleted or you do not have the {appropriateLicense} to use it. Either select a different connector or create a new one."
|
||||
id="xpack.cases.configure.connectorDeletedOrLicenseWarning"
|
||||
values={{
|
||||
appropriateLicense: (
|
||||
<EuiLink href="https://www.elastic.co/subscriptions" target="_blank">
|
||||
{i18n.LINK_APPROPRIATE_LICENSE}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
</SectionWrapper>
|
||||
)}
|
||||
<SectionWrapper>
|
||||
<ClosureOptions
|
||||
closureTypeSelected={closureType}
|
||||
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
|
||||
onChangeClosureType={onChangeClosureType}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
<SectionWrapper>
|
||||
<Connectors
|
||||
actionTypes={actionTypes}
|
||||
connectors={connectors ?? []}
|
||||
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
|
||||
handleShowEditFlyout={onClickUpdateConnector}
|
||||
isLoading={isLoadingAny}
|
||||
mappings={mappings}
|
||||
onChangeConnector={onChangeConnector}
|
||||
selectedConnector={connector}
|
||||
updateConnectorDisabled={updateConnectorDisabled || !userCanCrud}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
{ConnectorAddFlyout}
|
||||
{ConnectorEditFlyout}
|
||||
</FormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigureCases: React.FC<ConfigureCasesProps> = React.memo((props) => {
|
||||
return (
|
||||
<OwnerProvider owner={props.owner}>
|
||||
<ConfigureCasesComponent {...props} />
|
||||
</OwnerProvider>
|
||||
<>
|
||||
<HeaderPage
|
||||
showBackButton={true}
|
||||
data-test-subj="case-configure-title"
|
||||
title={i18n.CONFIGURE_CASES_PAGE_TITLE}
|
||||
/>
|
||||
<WhitePageWrapper>
|
||||
<ContentWrapper>
|
||||
<FormWrapper>
|
||||
{!connectorIsValid && (
|
||||
<SectionWrapper style={{ marginTop: 0 }}>
|
||||
<EuiCallOut
|
||||
title={i18n.WARNING_NO_CONNECTOR_TITLE}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
data-test-subj="configure-cases-warning-callout"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The selected connector has been deleted or you do not have the {appropriateLicense} to use it. Either select a different connector or create a new one."
|
||||
id="xpack.cases.configure.connectorDeletedOrLicenseWarning"
|
||||
values={{
|
||||
appropriateLicense: (
|
||||
<EuiLink href="https://www.elastic.co/subscriptions" target="_blank">
|
||||
{i18n.LINK_APPROPRIATE_LICENSE}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
</SectionWrapper>
|
||||
)}
|
||||
<SectionWrapper>
|
||||
<ClosureOptions
|
||||
closureTypeSelected={closureType}
|
||||
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
|
||||
onChangeClosureType={onChangeClosureType}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
<SectionWrapper>
|
||||
<Connectors
|
||||
actionTypes={actionTypes}
|
||||
connectors={connectors ?? []}
|
||||
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
|
||||
handleShowEditFlyout={onClickUpdateConnector}
|
||||
isLoading={isLoadingAny}
|
||||
mappings={mappings}
|
||||
onChangeConnector={onChangeConnector}
|
||||
selectedConnector={connector}
|
||||
updateConnectorDisabled={updateConnectorDisabled || !userCanCrud}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
{ConnectorAddFlyout}
|
||||
{ConnectorEditFlyout}
|
||||
</FormWrapper>
|
||||
</ContentWrapper>
|
||||
</WhitePageWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ConfigureCases.displayName = 'ConfigureCases';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ConfigureCases;
|
||||
|
|
|
@ -176,3 +176,7 @@ export const DEPRECATED_TOOLTIP_CONTENT = i18n.translate(
|
|||
defaultMessage: 'This connector is deprecated. Update it, or create a new one.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate('xpack.cases.configureCases.headerTitle', {
|
||||
defaultMessage: 'Configure cases',
|
||||
});
|
||||
|
|
|
@ -12,13 +12,12 @@ import styled from 'styled-components';
|
|||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { ActionParamsProps } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { CommentType, SECURITY_SOLUTION_OWNER } from '../../../../common';
|
||||
import { CommentType } from '../../../../common';
|
||||
|
||||
import { CaseActionParams } from './types';
|
||||
import { ExistingCase } from './existing_case';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { OwnerProvider } from '../../owner_context';
|
||||
|
||||
const Container = styled.div`
|
||||
${({ theme }) => `
|
||||
|
@ -96,9 +95,7 @@ const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionPara
|
|||
*/
|
||||
return (
|
||||
<Container>
|
||||
<OwnerProvider owner={[SECURITY_SOLUTION_OWNER]}>
|
||||
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
||||
</OwnerProvider>
|
||||
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_TITLE} iconType="iInCircle">
|
||||
<p>{i18n.CASE_CONNECTOR_CALL_OUT_MSG}</p>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { CaseType } from '../../../../common';
|
||||
import {
|
||||
useGetCases,
|
||||
DEFAULT_QUERY_PARAMS,
|
||||
|
@ -43,7 +42,6 @@ const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, sel
|
|||
|
||||
const { modal, openModal } = useCreateCaseModal({
|
||||
onCaseCreated,
|
||||
caseType: CaseType.collection,
|
||||
// FUTURE DEVELOPER
|
||||
// We are making the assumption that this component is only used in rules creation
|
||||
// that's why we want to hide ServiceNow SIR
|
||||
|
|
|
@ -21,22 +21,10 @@ import { schema, FormProps } from './schema';
|
|||
import { TestProviders } from '../../common/mock';
|
||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
|
||||
import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors';
|
||||
|
||||
const mockTriggersActionsUiService = triggersActionsUiMock.createStart();
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
notifications: {},
|
||||
http: {},
|
||||
triggersActionsUi: mockTriggersActionsUiService,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../connectors/resilient/use_get_incident_types');
|
||||
jest.mock('../connectors/resilient/use_get_severity');
|
||||
jest.mock('../connectors/servicenow/use_get_choices');
|
||||
|
@ -93,7 +81,7 @@ describe('Connector', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
|
||||
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
|
||||
useGetChoicesMock.mockReturnValue(useGetChoicesResponse);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CreateCaseFlyout } from './create_case_flyout';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const onClose = jest.fn();
|
||||
const onSuccess = jest.fn();
|
||||
const defaultProps = {
|
||||
onClose,
|
||||
onSuccess,
|
||||
owner: 'securitySolution',
|
||||
};
|
||||
|
||||
describe('CreateCaseFlyout', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CreateCaseFlyout {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await act(async () => {
|
||||
expect(getByTestId('create-case-flyout')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('Closing flyout calls onCloseCaseModal', async () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CreateCaseFlyout {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('euiFlyoutCloseButton'));
|
||||
});
|
||||
expect(onClose).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -5,28 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { Case } from '../../../../../../../cases/common';
|
||||
import type { TimelinesStartServices } from '../../../../../types';
|
||||
import { Case } from '../../../../common';
|
||||
import { CreateCaseForm } from '../form';
|
||||
|
||||
export interface CreateCaseModalProps {
|
||||
export interface CreateCaseFlyoutProps {
|
||||
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
||||
onCloseFlyout: () => void;
|
||||
onClose: () => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
useInsertTimeline?: Function;
|
||||
owner: string;
|
||||
disableAlerts?: boolean;
|
||||
}
|
||||
|
||||
const StyledFlyout = styled(EuiFlyout)`
|
||||
${({ theme }) => `
|
||||
z-index: ${theme.eui.euiZLevel5};
|
||||
`}
|
||||
z-index: ${theme.eui.euiZModal};
|
||||
`}
|
||||
`;
|
||||
|
||||
const maskOverlayClassName = 'create-case-flyout-mask-overlay';
|
||||
|
@ -49,47 +46,31 @@ const GlobalStyle = createGlobalStyle<{ theme: { eui: { euiZLevel5: number } } }
|
|||
// bottom bar gonna hide the submit button.
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
${({ theme }) => `
|
||||
&& .euiFlyoutBody__overflow {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&& .euiFlyoutBody__overflowContent {
|
||||
display: block;
|
||||
padding: ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 70px;
|
||||
height: auto;
|
||||
}
|
||||
`}
|
||||
&& .euiFlyoutBody__overflow {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&& .euiFlyoutBody__overflowContent {
|
||||
display: block;
|
||||
padding: ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 70px;
|
||||
height: auto;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
|
||||
afterCaseCreated,
|
||||
onCloseFlyout,
|
||||
onSuccess,
|
||||
owner,
|
||||
disableAlerts,
|
||||
}) => {
|
||||
const { cases } = useKibana<TimelinesStartServices>().services;
|
||||
const createCaseProps = useMemo(() => {
|
||||
return {
|
||||
afterCaseCreated,
|
||||
onCancel: onCloseFlyout,
|
||||
onSuccess,
|
||||
withSteps: false,
|
||||
owner: [owner],
|
||||
disableAlerts,
|
||||
};
|
||||
}, [afterCaseCreated, onCloseFlyout, onSuccess, owner, disableAlerts]);
|
||||
return (
|
||||
export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
|
||||
({ afterCaseCreated, onClose, onSuccess, disableAlerts }) => (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<StyledFlyout
|
||||
onClose={onCloseFlyout}
|
||||
onClose={onClose}
|
||||
data-test-subj="create-case-flyout"
|
||||
// maskProps is needed in order to apply the z-index to the parent overlay element, not to the flyout only
|
||||
maskProps={{ className: maskOverlayClassName }}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
|
@ -98,13 +79,19 @@ const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<StyledEuiFlyoutBody>
|
||||
<FormWrapper>{cases.getCreateCase(createCaseProps)}</FormWrapper>
|
||||
<FormWrapper>
|
||||
<CreateCaseForm
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
onCancel={onClose}
|
||||
onSuccess={onSuccess}
|
||||
withSteps={false}
|
||||
disableAlerts={disableAlerts}
|
||||
/>
|
||||
</FormWrapper>
|
||||
</StyledEuiFlyoutBody>
|
||||
</StyledFlyout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent);
|
||||
)
|
||||
);
|
||||
|
||||
CreateCaseFlyout.displayName = 'CreateCaseFlyout';
|
|
@ -4,10 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { CreateCaseFlyout, CreateCaseFlyoutProps } from './create_case_flyout';
|
||||
|
||||
export interface ErrorMessage {
|
||||
id: string;
|
||||
title: string;
|
||||
description: JSX.Element;
|
||||
errorType?: 'primary' | 'success' | 'warning' | 'danger';
|
||||
}
|
||||
export type { CreateCaseFlyoutProps };
|
||||
export { CreateCaseFlyout };
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CreateCaseFlyout as default };
|
|
@ -14,11 +14,10 @@ import { useGetTags } from '../../containers/use_get_tags';
|
|||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../containers/mock';
|
||||
import { schema, FormProps } from './schema';
|
||||
import { CreateCaseForm } from './form';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { CreateCaseForm, CreateCaseFormFields, CreateCaseFormProps } from './form';
|
||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
|
@ -38,6 +37,11 @@ const initialCaseValue: FormProps = {
|
|||
syncAlerts: true,
|
||||
};
|
||||
|
||||
const casesFormProps: CreateCaseFormProps = {
|
||||
onCancel: jest.fn(),
|
||||
onSuccess: jest.fn(),
|
||||
};
|
||||
|
||||
describe('CreateCaseForm', () => {
|
||||
let globalForm: FormHook;
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
|
@ -50,9 +54,9 @@ describe('CreateCaseForm', () => {
|
|||
globalForm = form;
|
||||
|
||||
return (
|
||||
<OwnerProvider owner={[SECURITY_SOLUTION_OWNER]}>
|
||||
<TestProviders>
|
||||
<Form form={form}>{children}</Form>
|
||||
</OwnerProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -66,7 +70,7 @@ describe('CreateCaseForm', () => {
|
|||
it('it renders with steps', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm />
|
||||
<CreateCaseForm {...casesFormProps} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
|
@ -76,7 +80,7 @@ describe('CreateCaseForm', () => {
|
|||
it('it renders without steps', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm withSteps={false} />
|
||||
<CreateCaseForm {...casesFormProps} withSteps={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
|
@ -86,7 +90,7 @@ describe('CreateCaseForm', () => {
|
|||
it('it renders all form fields', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm />
|
||||
<CreateCaseForm {...casesFormProps} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
|
@ -97,21 +101,40 @@ describe('CreateCaseForm', () => {
|
|||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render spinner when loading', async () => {
|
||||
const wrapper = mount(
|
||||
it('hides the sync alerts toggle', () => {
|
||||
const { queryByText } = render(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm />
|
||||
<CreateCaseForm {...casesFormProps} disableAlerts />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
globalForm.setFieldValue('title', 'title');
|
||||
globalForm.setFieldValue('description', 'description');
|
||||
globalForm.submit();
|
||||
// For some weird reason this is needed to pass the test.
|
||||
// It does not do anything useful
|
||||
await wrapper.find(`[data-test-subj="caseTitle"]`);
|
||||
await wrapper.update();
|
||||
expect(queryByText('Sync alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('CreateCaseFormFields', () => {
|
||||
it('should render spinner when loading', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseFormFields
|
||||
connectors={[]}
|
||||
isLoadingConnectors={false}
|
||||
disableAlerts={false}
|
||||
hideConnectorServiceNowSir={false}
|
||||
withSteps={true}
|
||||
/>
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
globalForm.setFieldValue('title', 'title');
|
||||
globalForm.setFieldValue('description', 'description');
|
||||
globalForm.submit();
|
||||
// For some weird reason this is needed to pass the test.
|
||||
// It does not do anything useful
|
||||
await wrapper.find(`[data-test-subj="caseTitle"]`);
|
||||
await wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()
|
||||
|
@ -119,14 +142,4 @@ describe('CreateCaseForm', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the sync alerts toggle', () => {
|
||||
const { queryByText } = render(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm disableAlerts />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(queryByText('Sync alert')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,18 +6,30 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiLoadingSpinner, EuiSteps } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSteps,
|
||||
} from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { useFormContext } from '../../common/shared_imports';
|
||||
|
||||
import { Title } from './title';
|
||||
import { Description } from './description';
|
||||
import { Description, fieldName as descriptionFieldName } from './description';
|
||||
import { Tags } from './tags';
|
||||
import { Connector } from './connector';
|
||||
import * as i18n from './translations';
|
||||
import { SyncAlertsToggle } from './sync_alerts_toggle';
|
||||
import { ActionConnector } from '../../../common';
|
||||
import { ActionConnector, CaseType } from '../../../common';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
|
||||
import { InsertTimeline } from '../insert_timeline';
|
||||
import { UsePostComment } from '../../containers/use_post_comment';
|
||||
import { SubmitCaseButton } from './submit_button';
|
||||
import { FormContext } from './form_context';
|
||||
|
||||
interface ContainerProps {
|
||||
big?: boolean;
|
||||
|
@ -36,22 +48,28 @@ const MySpinner = styled(EuiLoadingSpinner)`
|
|||
z-index: 99;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
connectors?: ActionConnector[];
|
||||
disableAlerts?: boolean;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
isLoadingConnectors?: boolean;
|
||||
withSteps?: boolean;
|
||||
export interface CreateCaseFormFieldsProps {
|
||||
connectors: ActionConnector[];
|
||||
isLoadingConnectors: boolean;
|
||||
disableAlerts: boolean;
|
||||
hideConnectorServiceNowSir: boolean;
|
||||
withSteps: boolean;
|
||||
}
|
||||
export interface CreateCaseFormProps
|
||||
extends Pick<
|
||||
Partial<CreateCaseFormFieldsProps>,
|
||||
'disableAlerts' | 'hideConnectorServiceNowSir' | 'withSteps'
|
||||
> {
|
||||
onCancel: () => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise<void>;
|
||||
caseType?: CaseType;
|
||||
timelineIntegration?: CasesTimelineIntegration;
|
||||
}
|
||||
|
||||
const empty: ActionConnector[] = [];
|
||||
export const CreateCaseForm: React.FC<Props> = React.memo(
|
||||
({
|
||||
connectors = empty,
|
||||
disableAlerts = false,
|
||||
isLoadingConnectors = false,
|
||||
hideConnectorServiceNowSir = false,
|
||||
withSteps = true,
|
||||
}) => {
|
||||
export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.memo(
|
||||
({ connectors, disableAlerts, isLoadingConnectors, hideConnectorServiceNowSir, withSteps }) => {
|
||||
const { isSubmitting } = useFormContext();
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
|
@ -126,4 +144,61 @@ export const CreateCaseForm: React.FC<Props> = React.memo(
|
|||
}
|
||||
);
|
||||
|
||||
CreateCaseFormFields.displayName = 'CreateCaseFormFields';
|
||||
|
||||
export const CreateCaseForm: React.FC<CreateCaseFormProps> = React.memo(
|
||||
({
|
||||
disableAlerts = false,
|
||||
hideConnectorServiceNowSir = false,
|
||||
withSteps = true,
|
||||
afterCaseCreated,
|
||||
caseType,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
timelineIntegration,
|
||||
}) => (
|
||||
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
|
||||
<FormContext
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
caseType={caseType}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
onSuccess={onSuccess}
|
||||
// if we are disabling alerts, then we should not sync alerts
|
||||
syncAlertsDefaultValue={!disableAlerts}
|
||||
>
|
||||
<CreateCaseFormFields
|
||||
connectors={empty}
|
||||
disableAlerts={disableAlerts}
|
||||
isLoadingConnectors={false}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
withSteps={withSteps}
|
||||
/>
|
||||
<Container>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="create-case-cancel"
|
||||
iconType="cross"
|
||||
onClick={onCancel}
|
||||
size="s"
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SubmitCaseButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Container>
|
||||
<InsertTimeline fieldName={descriptionFieldName} />
|
||||
</FormContext>
|
||||
</CasesTimelineIntegrationProvider>
|
||||
)
|
||||
);
|
||||
|
||||
CreateCaseForm.displayName = 'CreateCaseForm';
|
||||
|
|
|
@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme';
|
|||
import { act, waitFor } from '@testing-library/react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { ConnectorTypes } from '../../../common';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { usePostCase } from '../../containers/use_post_case';
|
||||
|
@ -36,7 +36,7 @@ import {
|
|||
useGetChoicesResponse,
|
||||
} from './mock';
|
||||
import { FormContext } from './form_context';
|
||||
import { CreateCaseForm } from './form';
|
||||
import { CreateCaseFormFields, CreateCaseFormFieldsProps } from './form';
|
||||
import { SubmitCaseButton } from './submit_button';
|
||||
import { usePostPushToService } from '../../containers/use_post_push_to_service';
|
||||
|
||||
|
@ -77,10 +77,12 @@ const defaultPostCase = {
|
|||
postCase,
|
||||
};
|
||||
|
||||
const defaultCreateCaseForm = {
|
||||
const defaultCreateCaseForm: CreateCaseFormFieldsProps = {
|
||||
isLoadingConnectors: false,
|
||||
connectors: [],
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
disableAlerts: false,
|
||||
withSteps: true,
|
||||
hideConnectorServiceNowSir: false,
|
||||
};
|
||||
|
||||
const defaultPostPushToService = {
|
||||
|
@ -150,12 +152,14 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.update();
|
||||
});
|
||||
expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy();
|
||||
|
@ -174,7 +178,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -192,7 +196,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -224,7 +228,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -248,7 +252,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} syncAlertsDefaultValue={false}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} disableAlerts={true} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} disableAlerts={true} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -282,7 +286,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -332,7 +336,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -357,7 +361,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -424,7 +428,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -494,7 +498,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -584,7 +588,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -682,7 +686,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
@ -719,7 +723,7 @@ describe('Create case', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}>
|
||||
<CreateCaseForm {...defaultCreateCaseForm} />
|
||||
<CreateCaseFormFields {...defaultCreateCaseForm} />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
|
|
|
@ -16,7 +16,7 @@ import { useConnectors } from '../../containers/configure/use_connectors';
|
|||
import { Case } from '../../containers/types';
|
||||
import { CaseType } from '../../../common';
|
||||
import { UsePostComment, usePostComment } from '../../containers/use_post_comment';
|
||||
import { useOwnerContext } from '../owner_context/use_owner_context';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { getConnectorById } from '../utils';
|
||||
|
||||
const initialCaseValue: FormProps = {
|
||||
|
@ -46,7 +46,7 @@ export const FormContext: React.FC<Props> = ({
|
|||
syncAlertsDefaultValue = true,
|
||||
}) => {
|
||||
const { connectors, loading: isLoadingConnectors } = useConnectors();
|
||||
const owner = useOwnerContext();
|
||||
const { owner } = useCasesContext();
|
||||
const { postCase } = usePostCase();
|
||||
const { postComment } = usePostComment();
|
||||
const { pushCaseToExternalService } = usePostPushToService();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { TestProviders } from '../../common/mock';
|
||||
|
@ -29,7 +29,6 @@ import {
|
|||
useGetFieldsByIssueTypeResponse,
|
||||
} from './mock';
|
||||
import { CreateCase } from '.';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
|
||||
jest.mock('../../containers/api');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
|
@ -78,7 +77,7 @@ const defaultProps = {
|
|||
|
||||
describe('CreateCase case', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
useConnectorsMock.mockReturnValue(sampleConnectorData);
|
||||
useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
|
||||
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
|
||||
|
@ -94,36 +93,38 @@ describe('CreateCase case', () => {
|
|||
it('it renders', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CreateCase {...defaultProps} owner={[SECURITY_SOLUTION_OWNER]} />
|
||||
<CreateCase {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).exists()).toBeTruthy();
|
||||
await act(async () => {
|
||||
expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call cancel on cancel click', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CreateCase {...defaultProps} owner={[SECURITY_SOLUTION_OWNER]} />
|
||||
<CreateCase {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
|
||||
});
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to new case when posting the case', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CreateCase {...defaultProps} owner={[SECURITY_SOLUTION_OWNER]} />
|
||||
<CreateCase {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSuccess).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
fillForm(wrapper);
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
});
|
||||
expect(defaultProps.onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,100 +6,49 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Field, getUseField } from '../../common/shared_imports';
|
||||
import * as i18n from './translations';
|
||||
import { CreateCaseForm } from './form';
|
||||
import { FormContext } from './form_context';
|
||||
import { SubmitCaseButton } from './submit_button';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CaseType } from '../../../common';
|
||||
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
|
||||
import { fieldName as descriptionFieldName } from './description';
|
||||
import { InsertTimeline } from '../insert_timeline';
|
||||
import { UsePostComment } from '../../containers/use_post_comment';
|
||||
import { Owner } from '../../types';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { CreateCaseForm, CreateCaseFormProps } from './form';
|
||||
import { HeaderPage } from '../header_page';
|
||||
import { useCasesBreadcrumbs } from '../use_breadcrumbs';
|
||||
import { CasesDeepLinkId } from '../../common/navigation';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const Container = styled.div`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.eui.euiSize};
|
||||
`}
|
||||
`;
|
||||
export const CreateCase = React.memo<CreateCaseFormProps>(
|
||||
({
|
||||
afterCaseCreated,
|
||||
caseType,
|
||||
hideConnectorServiceNowSir,
|
||||
disableAlerts,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
timelineIntegration,
|
||||
withSteps,
|
||||
}) => {
|
||||
useCasesBreadcrumbs(CasesDeepLinkId.casesCreate);
|
||||
|
||||
export interface CreateCaseProps extends Owner {
|
||||
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise<void>;
|
||||
caseType?: CaseType;
|
||||
disableAlerts?: boolean;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
timelineIntegration?: CasesTimelineIntegration;
|
||||
withSteps?: boolean;
|
||||
}
|
||||
|
||||
const CreateCaseComponent = ({
|
||||
afterCaseCreated,
|
||||
caseType,
|
||||
hideConnectorServiceNowSir,
|
||||
disableAlerts,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
timelineIntegration,
|
||||
withSteps,
|
||||
}: Omit<CreateCaseProps, 'owner'>) => (
|
||||
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
|
||||
<FormContext
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
caseType={caseType}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
onSuccess={onSuccess}
|
||||
// if we are disabling alerts, then we should not sync alerts
|
||||
syncAlertsDefaultValue={!disableAlerts}
|
||||
>
|
||||
<CreateCaseForm
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
disableAlerts={disableAlerts}
|
||||
withSteps={withSteps}
|
||||
/>
|
||||
<Container>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="create-case-cancel"
|
||||
iconType="cross"
|
||||
onClick={onCancel}
|
||||
size="s"
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SubmitCaseButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Container>
|
||||
<InsertTimeline fieldName={descriptionFieldName} />
|
||||
</FormContext>
|
||||
</CasesTimelineIntegrationProvider>
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
showBackButton={true}
|
||||
data-test-subj="case-create-title"
|
||||
title={i18n.CREATE_PAGE_TITLE}
|
||||
/>
|
||||
<CreateCaseForm
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
caseType={caseType}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
disableAlerts={disableAlerts}
|
||||
onCancel={onCancel}
|
||||
onSuccess={onSuccess}
|
||||
timelineIntegration={timelineIntegration}
|
||||
withSteps={withSteps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const CreateCase: React.FC<CreateCaseProps> = React.memo((props) => (
|
||||
<OwnerProvider owner={props.owner}>
|
||||
<CreateCaseComponent {...props} />
|
||||
</OwnerProvider>
|
||||
));
|
||||
|
||||
CreateCase.displayName = 'CreateCase';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CreateCase as default };
|
||||
|
|
|
@ -14,10 +14,11 @@ import { useForm, Form, FormHook } from '../../common/shared_imports';
|
|||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { Tags } from './tags';
|
||||
import { schema, FormProps } from './schema';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
|
||||
const useGetTagsMock = useGetTags as jest.Mock;
|
||||
|
||||
describe('Tags', () => {
|
||||
|
@ -34,14 +35,14 @@ describe('Tags', () => {
|
|||
globalForm = form;
|
||||
|
||||
return (
|
||||
<OwnerProvider owner={[SECURITY_SOLUTION_OWNER]}>
|
||||
<TestProviders>
|
||||
<Form form={form}>{children}</Form>
|
||||
</OwnerProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
useGetTagsMock.mockReturnValue({ tags: ['test'] });
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,10 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
export * from '../../common/translations';
|
||||
|
||||
export const CREATE_PAGE_TITLE = i18n.translate('xpack.cases.create.title', {
|
||||
defaultMessage: 'Create new case',
|
||||
});
|
||||
|
||||
export const STEP_ONE_TITLE = i18n.translate('xpack.cases.create.stepOneTitle', {
|
||||
defaultMessage: 'Case fields',
|
||||
});
|
||||
|
|
|
@ -32,10 +32,6 @@ const caseServices = {
|
|||
const defaultProps: EditConnectorProps = {
|
||||
caseData: basicCase,
|
||||
caseServices,
|
||||
configureCasesNavigation: {
|
||||
href: 'blah',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
connectorName: connectorsMock[0].name,
|
||||
connectors: connectorsMock,
|
||||
hasDataToPush: true,
|
||||
|
|
|
@ -30,13 +30,11 @@ import { getConnectorFieldsFromUserActions } from './helpers';
|
|||
import * as i18n from './translations';
|
||||
import { getConnectorById, getConnectorsFormValidators } from '../utils';
|
||||
import { usePushToService } from '../use_push_to_service';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { CaseServices } from '../../containers/use_get_case_user_actions';
|
||||
|
||||
export interface EditConnectorProps {
|
||||
caseData: Case;
|
||||
caseServices: CaseServices;
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
connectorName: string;
|
||||
connectors: ActionConnector[];
|
||||
hasDataToPush: boolean;
|
||||
|
@ -116,7 +114,6 @@ export const EditConnector = React.memo(
|
|||
({
|
||||
caseData,
|
||||
caseServices,
|
||||
configureCasesNavigation,
|
||||
connectorName,
|
||||
connectors,
|
||||
hasDataToPush,
|
||||
|
@ -250,7 +247,6 @@ export const EditConnector = React.memo(
|
|||
});
|
||||
|
||||
const { pushButton, pushCallouts } = usePushToService({
|
||||
configureCasesNavigation,
|
||||
connector: {
|
||||
...caseData.connector,
|
||||
name: isEmpty(connectorName) ? caseData.connector.name : connectorName,
|
||||
|
|
|
@ -14,16 +14,7 @@ import { TestProviders } from '../../common/mock';
|
|||
import { HeaderPage } from './index';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useHistory: () => ({
|
||||
useHistory: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
describe('HeaderPage', () => {
|
||||
const mount = useMountAppended();
|
||||
|
@ -47,10 +38,7 @@ describe('HeaderPage', () => {
|
|||
test('it renders the back link when provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderPage
|
||||
backOptions={{ href: '#', text: 'Test link', onClick: jest.fn() }}
|
||||
title="Test title"
|
||||
/>
|
||||
<HeaderPage showBackButton title="Test title" />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,14 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { useAllCasesNavigation } from '../../common/navigation';
|
||||
|
||||
import { LinkIcon, LinkIconProps } from '../link_icon';
|
||||
import { LinkIcon } from '../link_icon';
|
||||
import { Subtitle, SubtitleProps } from '../subtitle';
|
||||
import { Title } from './title';
|
||||
import { BadgeOptions, TitleProp } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface HeaderProps {
|
||||
border?: boolean;
|
||||
isLoading?: boolean;
|
||||
|
@ -57,17 +60,8 @@ const Badge = styled(EuiBadge)`
|
|||
` as unknown as typeof EuiBadge;
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
interface BackOptions {
|
||||
href: LinkIconProps['href'];
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
text: LinkIconProps['children'];
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
||||
export interface HeaderPageProps extends HeaderProps {
|
||||
backOptions?: BackOptions;
|
||||
/** A component to be displayed as the back button. Used only if `backOption` is not defined */
|
||||
backComponent?: React.ReactNode;
|
||||
showBackButton?: boolean;
|
||||
badgeOptions?: BadgeOptions;
|
||||
children?: React.ReactNode;
|
||||
subtitle?: SubtitleProps['items'];
|
||||
|
@ -77,8 +71,7 @@ export interface HeaderPageProps extends HeaderProps {
|
|||
}
|
||||
|
||||
const HeaderPageComponent: React.FC<HeaderPageProps> = ({
|
||||
backOptions,
|
||||
backComponent,
|
||||
showBackButton = false,
|
||||
badgeOptions,
|
||||
border,
|
||||
children,
|
||||
|
@ -88,39 +81,51 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
|
|||
title,
|
||||
titleNode,
|
||||
...rest
|
||||
}) => (
|
||||
<Header border={border} {...rest}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<FlexItem>
|
||||
{backOptions && (
|
||||
<LinkBack>
|
||||
<LinkIcon
|
||||
dataTestSubj={backOptions.dataTestSubj}
|
||||
onClick={backOptions.onClick}
|
||||
href={backOptions.href}
|
||||
iconType="arrowLeft"
|
||||
>
|
||||
{backOptions.text}
|
||||
</LinkIcon>
|
||||
</LinkBack>
|
||||
)}
|
||||
}) => {
|
||||
const { getAllCasesUrl, navigateToAllCases } = useAllCasesNavigation();
|
||||
|
||||
{!backOptions && backComponent && <>{backComponent}</>}
|
||||
const navigateToAllCasesClick = useCallback(
|
||||
(e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
navigateToAllCases();
|
||||
},
|
||||
[navigateToAllCases]
|
||||
);
|
||||
|
||||
{titleNode || <Title title={title} badgeOptions={badgeOptions} />}
|
||||
return (
|
||||
<Header border={border} {...rest}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<FlexItem>
|
||||
{showBackButton && (
|
||||
<LinkBack>
|
||||
<LinkIcon
|
||||
dataTestSubj="backToCases"
|
||||
onClick={navigateToAllCasesClick}
|
||||
href={getAllCasesUrl()}
|
||||
iconType="arrowLeft"
|
||||
>
|
||||
{i18n.BACK_TO_ALL}
|
||||
</LinkIcon>
|
||||
</LinkBack>
|
||||
)}
|
||||
|
||||
{subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />}
|
||||
{subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />}
|
||||
{border && isLoading && <EuiProgress size="xs" color="accent" />}
|
||||
</FlexItem>
|
||||
{titleNode || <Title title={title} badgeOptions={badgeOptions} />}
|
||||
|
||||
{children && (
|
||||
<FlexItem data-test-subj="header-page-supplements" grow={false}>
|
||||
{children}
|
||||
{subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />}
|
||||
{subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />}
|
||||
{border && isLoading && <EuiProgress size="xs" color="accent" />}
|
||||
</FlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Header>
|
||||
);
|
||||
|
||||
{children && (
|
||||
<FlexItem data-test-subj="header-page-supplements" grow={false}>
|
||||
{children}
|
||||
</FlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeaderPage = React.memo(HeaderPageComponent);
|
||||
|
|
|
@ -7,30 +7,25 @@
|
|||
|
||||
import React from 'react';
|
||||
import { ReactWrapper, mount } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
|
||||
import '../../common/mock/match_media';
|
||||
import { ConfigureCaseButton, ConfigureCaseButtonProps } from './button';
|
||||
import {
|
||||
ConfigureCaseButton,
|
||||
ConfigureCaseButtonProps,
|
||||
CaseDetailsLink,
|
||||
CaseDetailsLinkProps,
|
||||
} from '.';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { useCaseViewNavigation } from '../../common/navigation/hooks';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useHistory: () => ({
|
||||
useHistory: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
describe('Configuration button', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const props: ConfigureCaseButtonProps = {
|
||||
configureCasesNavigation: {
|
||||
href: 'testHref',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
isDisabled: false,
|
||||
label: 'My label',
|
||||
msgTooltip: <></>,
|
||||
|
@ -50,7 +45,7 @@ describe('Configuration button', () => {
|
|||
|
||||
test('it pass the correct props to the button', () => {
|
||||
expect(wrapper.find('[data-test-subj="configure-case-button"]').first().props()).toMatchObject({
|
||||
href: `testHref`,
|
||||
href: `/app/security/cases/configure`,
|
||||
iconType: 'controlsHorizontal',
|
||||
isDisabled: false,
|
||||
'aria-label': 'My label',
|
||||
|
@ -111,3 +106,60 @@ describe('Configuration button', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CaseDetailsLink', () => {
|
||||
const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock;
|
||||
const getCaseViewUrl = jest.fn().mockReturnValue('/cases/test');
|
||||
const navigateToCaseView = jest.fn();
|
||||
|
||||
const props: CaseDetailsLinkProps = {
|
||||
detailName: 'test detail name',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useCaseViewNavigationMock.mockReturnValue({ getCaseViewUrl, navigateToCaseView });
|
||||
});
|
||||
|
||||
test('it renders', () => {
|
||||
render(<CaseDetailsLink {...props} />);
|
||||
expect(screen.getByText('test detail name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it renders the children instead of the detail name if provided', () => {
|
||||
render(<CaseDetailsLink {...props}>{'children'}</CaseDetailsLink>);
|
||||
expect(screen.queryByText('test detail name')).toBeFalsy();
|
||||
expect(screen.getByText('children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it uses the detailName in the aria-label if the title is not provided', () => {
|
||||
render(<CaseDetailsLink {...props} />);
|
||||
expect(
|
||||
screen.getByLabelText(`click to visit case with title ${props.detailName}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it uses the title in the aria-label if provided', () => {
|
||||
render(<CaseDetailsLink {...props} title={'my title'} />);
|
||||
expect(screen.getByText('test detail name')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(`click to visit case with title my title`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it calls navigateToCaseViewClick on click', () => {
|
||||
render(<CaseDetailsLink {...props} subCaseId="sub-case-id" />);
|
||||
userEvent.click(screen.getByText('test detail name'));
|
||||
expect(navigateToCaseView).toHaveBeenCalledWith({
|
||||
detailName: props.detailName,
|
||||
subCaseId: 'sub-case-id',
|
||||
});
|
||||
});
|
||||
|
||||
test('it set the href correctly', () => {
|
||||
render(<CaseDetailsLink {...props} subCaseId="sub-case-id" />);
|
||||
expect(getCaseViewUrl).toHaveBeenCalledWith({
|
||||
detailName: props.detailName,
|
||||
subCaseId: 'sub-case-id',
|
||||
});
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', '/cases/test');
|
||||
});
|
||||
});
|
|
@ -10,10 +10,12 @@ import {
|
|||
EuiButtonProps,
|
||||
EuiLink,
|
||||
EuiLinkProps,
|
||||
EuiToolTip,
|
||||
PropsForAnchor,
|
||||
PropsForButton,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCaseViewNavigation, useConfigureCasesNavigation } from '../../common/navigation';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface CasesNavigation<T = React.MouseEvent | MouseEvent | null, K = null> {
|
||||
|
@ -30,36 +32,32 @@ export const LinkAnchor: React.FC<EuiLinkProps> = ({ children, ...props }) => (
|
|||
<EuiLink {...props}>{children}</EuiLink>
|
||||
);
|
||||
|
||||
export interface CaseDetailsHrefSchema {
|
||||
detailName: string;
|
||||
search?: string;
|
||||
subCaseId?: string;
|
||||
}
|
||||
|
||||
const CaseDetailsLinkComponent: React.FC<{
|
||||
export interface CaseDetailsLinkProps {
|
||||
children?: React.ReactNode;
|
||||
detailName: string;
|
||||
caseDetailsNavigation: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>;
|
||||
subCaseId?: string;
|
||||
title?: string;
|
||||
}> = ({ caseDetailsNavigation, children, detailName, subCaseId, title }) => {
|
||||
const { href: getHref, onClick } = caseDetailsNavigation;
|
||||
const goToCaseDetails = useCallback(
|
||||
(ev) => {
|
||||
if (onClick) {
|
||||
ev.preventDefault();
|
||||
onClick({ detailName, subCaseId }, ev);
|
||||
}
|
||||
},
|
||||
[detailName, onClick, subCaseId]
|
||||
);
|
||||
}
|
||||
|
||||
const href = getHref({ detailName, subCaseId });
|
||||
const CaseDetailsLinkComponent: React.FC<CaseDetailsLinkProps> = ({
|
||||
children,
|
||||
detailName,
|
||||
subCaseId,
|
||||
title,
|
||||
}) => {
|
||||
const { getCaseViewUrl, navigateToCaseView } = useCaseViewNavigation();
|
||||
const navigateToCaseViewClick = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
navigateToCaseView({ detailName, subCaseId });
|
||||
},
|
||||
[navigateToCaseView, detailName, subCaseId]
|
||||
);
|
||||
|
||||
return (
|
||||
<LinkAnchor
|
||||
onClick={goToCaseDetails}
|
||||
href={href}
|
||||
onClick={navigateToCaseViewClick}
|
||||
href={getCaseViewUrl({ detailName, subCaseId })}
|
||||
data-test-subj="case-details-link"
|
||||
aria-label={i18n.CASE_DETAILS_LINK_ARIA(title ?? detailName)}
|
||||
>
|
||||
|
@ -69,3 +67,61 @@ const CaseDetailsLinkComponent: React.FC<{
|
|||
};
|
||||
export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent);
|
||||
CaseDetailsLink.displayName = 'CaseDetailsLink';
|
||||
|
||||
export interface ConfigureCaseButtonProps {
|
||||
isDisabled: boolean;
|
||||
label: string;
|
||||
msgTooltip: JSX.Element;
|
||||
showToolTip: boolean;
|
||||
titleTooltip: string;
|
||||
}
|
||||
|
||||
const ConfigureCaseButtonComponent: React.FC<ConfigureCaseButtonProps> = ({
|
||||
isDisabled,
|
||||
label,
|
||||
msgTooltip,
|
||||
showToolTip,
|
||||
titleTooltip,
|
||||
}: ConfigureCaseButtonProps) => {
|
||||
const { getConfigureCasesUrl, navigateToConfigureCases } = useConfigureCasesNavigation();
|
||||
|
||||
const navigateToConfigureCasesClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
navigateToConfigureCases();
|
||||
},
|
||||
[navigateToConfigureCases]
|
||||
);
|
||||
|
||||
const configureCaseButton = useMemo(
|
||||
() => (
|
||||
<LinkButton
|
||||
onClick={navigateToConfigureCasesClick}
|
||||
href={getConfigureCasesUrl()}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={isDisabled}
|
||||
aria-label={label}
|
||||
data-test-subj="configure-case-button"
|
||||
>
|
||||
{label}
|
||||
</LinkButton>
|
||||
),
|
||||
[label, isDisabled, navigateToConfigureCasesClick, getConfigureCasesUrl]
|
||||
);
|
||||
|
||||
return showToolTip ? (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
title={titleTooltip}
|
||||
content={<p>{msgTooltip}</p>}
|
||||
data-test-subj="configure-case-tooltip"
|
||||
>
|
||||
{configureCaseButton}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<>{configureCaseButton}</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigureCaseButton = React.memo(ConfigureCaseButtonComponent);
|
||||
ConfigureCaseButton.displayName = 'ConfigureCaseButton';
|
||||
|
|
|
@ -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 React from 'react';
|
||||
|
||||
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import { useAllCasesNavigation } from '../../common/navigation';
|
||||
|
||||
interface NoPrivilegesPageProps {
|
||||
pageName: string;
|
||||
}
|
||||
|
||||
export const NoPrivilegesPage = React.memo(({ pageName }: NoPrivilegesPageProps) => {
|
||||
const { navigateToAllCases } = useAllCasesNavigation();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconColor="default"
|
||||
iconType="addDataApp"
|
||||
title={<h2>{i18n.NO_PRIVILEGES_TITLE}</h2>}
|
||||
titleSize="xs"
|
||||
body={<p>{i18n.NO_PRIVILEGES_MSG(pageName)}</p>}
|
||||
actions={
|
||||
<EuiButton onClick={navigateToAllCases} size="s" color="primary" fill>
|
||||
{i18n.NO_PRIVILEGES_BUTTON}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
NoPrivilegesPage.displayName = 'NoPrivilegesPage';
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const NO_PRIVILEGES_MSG = (pageName: string) =>
|
||||
i18n.translate('xpack.cases.noPrivileges.message', {
|
||||
values: { pageName },
|
||||
defaultMessage:
|
||||
'To view {pageName} page, you must update privileges. For more information, contact your Kibana administrator.',
|
||||
});
|
||||
|
||||
export const NO_PRIVILEGES_TITLE = i18n.translate('xpack.cases.noPrivileges.title', {
|
||||
defaultMessage: 'Privileges required',
|
||||
});
|
||||
|
||||
export const NO_PRIVILEGES_BUTTON = i18n.translate('xpack.cases.noPrivileges.button', {
|
||||
defaultMessage: 'Back to Cases',
|
||||
});
|
|
@ -1,18 +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.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const OwnerContext = React.createContext<string[]>([]);
|
||||
|
||||
export const OwnerProvider: React.FC<{
|
||||
owner: string[];
|
||||
}> = ({ children, owner }) => {
|
||||
const [currentOwner] = useState(owner);
|
||||
|
||||
return <OwnerContext.Provider value={currentOwner}>{children}</OwnerContext.Provider>;
|
||||
};
|
|
@ -8,33 +8,19 @@
|
|||
import React from 'react';
|
||||
import { configure, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RecentCases from '.';
|
||||
import RecentCases, { RecentCasesProps } from '.';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { useGetCasesMockState } from '../../containers/mock';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { useCurrentUser } from '../../common/lib/kibana/hooks';
|
||||
|
||||
jest.mock('../../containers/use_get_cases');
|
||||
jest.mock('../../common/lib/kibana/hooks');
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
configure({ testIdAttribute: 'data-test-subj' });
|
||||
const defaultProps = {
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: () => 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
createCaseNavigation: {
|
||||
href: 'create-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
hasWritePermissions: true,
|
||||
const defaultProps: RecentCasesProps = {
|
||||
maxCasesToShow: 10,
|
||||
owner: [SECURITY_SOLUTION_OWNER],
|
||||
};
|
||||
|
||||
const setFilters = jest.fn();
|
||||
|
|
|
@ -6,36 +6,35 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { CaseDetailsHrefSchema, CasesNavigation, LinkAnchor } from '../links';
|
||||
import { LinkAnchor } from '../links';
|
||||
import { RecentCasesFilters } from './filters';
|
||||
import { RecentCasesComp } from './recent_cases';
|
||||
import { FilterMode as RecentCasesFilterMode } from './types';
|
||||
import { useCurrentUser } from '../../common/lib/kibana';
|
||||
import { Owner } from '../../types';
|
||||
import { OwnerProvider } from '../owner_context';
|
||||
import { useAllCasesNavigation } from '../../common/navigation';
|
||||
|
||||
export interface RecentCasesProps extends Owner {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
caseDetailsNavigation: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>;
|
||||
createCaseNavigation: CasesNavigation;
|
||||
hasWritePermissions: boolean;
|
||||
export interface RecentCasesProps {
|
||||
maxCasesToShow: number;
|
||||
}
|
||||
|
||||
const RecentCasesComponent = ({
|
||||
allCasesNavigation,
|
||||
caseDetailsNavigation,
|
||||
createCaseNavigation,
|
||||
maxCasesToShow,
|
||||
hasWritePermissions,
|
||||
}: Omit<RecentCasesProps, 'owner'>) => {
|
||||
const RecentCases = React.memo(({ maxCasesToShow }: RecentCasesProps) => {
|
||||
const currentUser = useCurrentUser();
|
||||
const { getAllCasesUrl, navigateToAllCases } = useAllCasesNavigation();
|
||||
|
||||
const [recentCasesFilterBy, setRecentCasesFilterBy] =
|
||||
useState<RecentCasesFilterMode>('recentlyCreated');
|
||||
|
||||
const navigateToAllCasesClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
navigateToAllCases();
|
||||
},
|
||||
[navigateToAllCases]
|
||||
);
|
||||
|
||||
const recentCasesFilterOptions = useMemo(
|
||||
() =>
|
||||
recentCasesFilterBy === 'myRecentlyReported' && currentUser != null
|
||||
|
@ -73,16 +72,10 @@ const RecentCasesComponent = ({
|
|||
<EuiHorizontalRule margin="s" />
|
||||
</>
|
||||
<EuiText color="subdued" size="s">
|
||||
<RecentCasesComp
|
||||
caseDetailsNavigation={caseDetailsNavigation}
|
||||
createCaseNavigation={createCaseNavigation}
|
||||
filterOptions={recentCasesFilterOptions}
|
||||
maxCasesToShow={maxCasesToShow}
|
||||
hasWritePermissions={hasWritePermissions}
|
||||
/>
|
||||
<RecentCasesComp filterOptions={recentCasesFilterOptions} maxCasesToShow={maxCasesToShow} />
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiText size="xs">
|
||||
<LinkAnchor onClick={allCasesNavigation.onClick} href={allCasesNavigation.href}>
|
||||
<LinkAnchor onClick={navigateToAllCasesClick} href={getAllCasesUrl()}>
|
||||
{' '}
|
||||
{i18n.VIEW_ALL_CASES}
|
||||
</LinkAnchor>
|
||||
|
@ -90,14 +83,6 @@ const RecentCasesComponent = ({
|
|||
</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RecentCases: React.FC<RecentCasesProps> = React.memo((props) => {
|
||||
return (
|
||||
<OwnerProvider owner={props.owner}>
|
||||
<RecentCasesComponent {...props} />
|
||||
</OwnerProvider>
|
||||
);
|
||||
});
|
||||
|
||||
RecentCases.displayName = 'RecentCases';
|
||||
|
|
|
@ -11,24 +11,24 @@ import { mount } from 'enzyme';
|
|||
import { TestProviders } from '../../../common/mock';
|
||||
import { NoCases } from '.';
|
||||
|
||||
describe('RecentCases', () => {
|
||||
jest.mock('../../../common/navigation/hooks');
|
||||
|
||||
describe('NoCases', () => {
|
||||
it('if no cases, a link to create cases will exist', () => {
|
||||
const createCaseHref = '/create';
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<NoCases createCaseHref={createCaseHref} hasWritePermissions />
|
||||
<NoCases />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().prop('href')).toEqual(
|
||||
createCaseHref
|
||||
'/app/security/cases/create'
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a message without a link to create a case when the user does not have write permissions', () => {
|
||||
const createCaseHref = '/create';
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<NoCases createCaseHref={createCaseHref} hasWritePermissions={false} />
|
||||
<TestProviders userCanCrud={false}>
|
||||
<NoCases />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="no-cases-create-case"]`).exists()).toBeFalsy();
|
||||
|
|
|
@ -8,31 +8,29 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { LinkAnchor } from '../../links';
|
||||
import { useCasesContext } from '../../cases_context/use_cases_context';
|
||||
import { useCreateCaseNavigation } from '../../../common/navigation';
|
||||
|
||||
const NoCasesComponent = ({
|
||||
createCaseHref,
|
||||
hasWritePermissions,
|
||||
}: {
|
||||
createCaseHref: string;
|
||||
hasWritePermissions: boolean;
|
||||
}) => {
|
||||
const { navigateToUrl } = useKibana().services.application;
|
||||
const goToCaseCreation = useCallback(
|
||||
const NoCasesComponent = () => {
|
||||
const { userCanCrud } = useCasesContext();
|
||||
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
|
||||
|
||||
const navigateToCreateCaseClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
navigateToUrl(createCaseHref);
|
||||
navigateToCreateCase();
|
||||
},
|
||||
[createCaseHref, navigateToUrl]
|
||||
[navigateToCreateCase]
|
||||
);
|
||||
return hasWritePermissions ? (
|
||||
|
||||
return userCanCrud ? (
|
||||
<>
|
||||
<span>{i18n.NO_CASES}</span>
|
||||
<LinkAnchor
|
||||
data-test-subj="no-cases-create-case"
|
||||
onClick={goToCaseCreation}
|
||||
href={createCaseHref}
|
||||
onClick={navigateToCreateCaseClick}
|
||||
href={getCreateCaseUrl()}
|
||||
>{` ${i18n.START_A_NEW_CASE}`}</LinkAnchor>
|
||||
{'!'}
|
||||
</>
|
||||
|
|
|
@ -13,7 +13,7 @@ import styled from 'styled-components';
|
|||
import { IconWithCount } from './icon_with_count';
|
||||
import * as i18n from './translations';
|
||||
import { useGetCases } from '../../containers/use_get_cases';
|
||||
import { CaseDetailsHrefSchema, CaseDetailsLink, CasesNavigation } from '../links';
|
||||
import { CaseDetailsLink } from '../links';
|
||||
import { LoadingPlaceholders } from './loading_placeholders';
|
||||
import { NoCases } from './no_cases';
|
||||
import { isSubCase } from '../all_cases/helpers';
|
||||
|
@ -29,10 +29,7 @@ const MarkdownContainer = styled.div`
|
|||
|
||||
export interface RecentCasesProps {
|
||||
filterOptions: Partial<FilterOptions>;
|
||||
caseDetailsNavigation: CasesNavigation<CaseDetailsHrefSchema, 'configurable'>;
|
||||
createCaseNavigation: CasesNavigation;
|
||||
maxCasesToShow: number;
|
||||
hasWritePermissions: boolean;
|
||||
}
|
||||
|
||||
const usePrevious = (value: Partial<FilterOptions>) => {
|
||||
|
@ -43,13 +40,7 @@ const usePrevious = (value: Partial<FilterOptions>) => {
|
|||
return ref.current;
|
||||
};
|
||||
|
||||
export const RecentCasesComp = ({
|
||||
caseDetailsNavigation,
|
||||
createCaseNavigation,
|
||||
filterOptions,
|
||||
maxCasesToShow,
|
||||
hasWritePermissions,
|
||||
}: RecentCasesProps) => {
|
||||
export const RecentCasesComp = ({ filterOptions, maxCasesToShow }: RecentCasesProps) => {
|
||||
const previousFilterOptions = usePrevious(filterOptions);
|
||||
const { data, loading, setFilters } = useGetCases({
|
||||
initialQueryParams: { perPage: maxCasesToShow },
|
||||
|
@ -69,7 +60,7 @@ export const RecentCasesComp = ({
|
|||
return isLoadingCases ? (
|
||||
<LoadingPlaceholders lines={2} placeholders={3} />
|
||||
) : !isLoadingCases && data.cases.length === 0 ? (
|
||||
<NoCases createCaseHref={createCaseNavigation.href} hasWritePermissions={hasWritePermissions} />
|
||||
<NoCases />
|
||||
) : (
|
||||
<>
|
||||
{data.cases.map((c, i) => (
|
||||
|
@ -77,7 +68,6 @@ export const RecentCasesComp = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<CaseDetailsLink
|
||||
caseDetailsNavigation={caseDetailsNavigation}
|
||||
detailName={isSubCase(c) ? c.caseParentId : c.id}
|
||||
title={c.title}
|
||||
subCaseId={isSubCase(c) ? c.id : undefined}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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, { ReactNode } from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { useCasesBreadcrumbs, useCasesTitleBreadcrumbs } from '.';
|
||||
import { CasesDeepLinkId } from '../../common/navigation';
|
||||
|
||||
const mockSetBreadcrumbs = jest.fn();
|
||||
const mockSetTitle = jest.fn();
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useNavigation: jest.fn().mockReturnValue({
|
||||
getAppUrl: jest.fn((params?: { deepLinkId: string }) => params?.deepLinkId ?? '/test'),
|
||||
}),
|
||||
useKibana: () => {
|
||||
const { services } = originalModule.useKibana();
|
||||
return {
|
||||
services: {
|
||||
...services,
|
||||
chrome: { setBreadcrumbs: mockSetBreadcrumbs, docTitle: { change: mockSetTitle } },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children?: ReactNode }) => (
|
||||
<TestProviders>{children}</TestProviders>
|
||||
);
|
||||
|
||||
describe('useCasesBreadcrumbs', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('set all_cases breadcrumbs', () => {
|
||||
it('call setBreadcrumbs with all items', async () => {
|
||||
renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.cases), { wrapper });
|
||||
expect(mockSetBreadcrumbs).toHaveBeenCalledWith([
|
||||
{ href: '/test', onClick: expect.any(Function), text: 'Test' },
|
||||
{ text: 'Cases' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sets the cases title', () => {
|
||||
renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.cases), { wrapper });
|
||||
expect(mockSetTitle).toHaveBeenCalledWith(['Cases', 'Test']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set create_case breadcrumbs', () => {
|
||||
it('call setBreadcrumbs with all items', () => {
|
||||
renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesCreate), { wrapper });
|
||||
expect(mockSetBreadcrumbs).toHaveBeenCalledWith([
|
||||
{ href: '/test', onClick: expect.any(Function), text: 'Test' },
|
||||
{ href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' },
|
||||
{ text: 'Create' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sets the cases title', () => {
|
||||
renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesCreate), { wrapper });
|
||||
expect(mockSetTitle).toHaveBeenCalledWith(['Create', 'Cases', 'Test']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set case_view breadcrumbs', () => {
|
||||
const title = 'Fake Title';
|
||||
it('call setBreadcrumbs with title', () => {
|
||||
renderHook(() => useCasesTitleBreadcrumbs(title), { wrapper });
|
||||
expect(mockSetBreadcrumbs).toHaveBeenCalledWith([
|
||||
{ href: '/test', onClick: expect.any(Function), text: 'Test' },
|
||||
{ href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' },
|
||||
{ text: title },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sets the cases title', () => {
|
||||
renderHook(() => useCasesTitleBreadcrumbs(title), { wrapper });
|
||||
expect(mockSetTitle).toHaveBeenCalledWith([title, 'Cases', 'Test']);
|
||||
});
|
||||
});
|
||||
});
|
107
x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts
Normal file
107
x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ChromeBreadcrumb } from 'kibana/public';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useKibana, useNavigation } from '../../common/lib/kibana';
|
||||
import { CasesDeepLinkId, ICasesDeepLinkId } from '../../common/navigation';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
const casesBreadcrumbTitle: Record<ICasesDeepLinkId, string> = {
|
||||
[CasesDeepLinkId.cases]: i18n.translate('xpack.cases.breadcrumbs.all_cases', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
[CasesDeepLinkId.casesCreate]: i18n.translate('xpack.cases.breadcrumbs.create_case', {
|
||||
defaultMessage: 'Create',
|
||||
}),
|
||||
[CasesDeepLinkId.casesConfigure]: i18n.translate('xpack.cases.breadcrumbs.configure_cases', {
|
||||
defaultMessage: 'Configure',
|
||||
}),
|
||||
};
|
||||
|
||||
function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] {
|
||||
return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse();
|
||||
}
|
||||
|
||||
const useApplyBreadcrumbs = () => {
|
||||
const {
|
||||
chrome: { docTitle, setBreadcrumbs },
|
||||
application: { navigateToUrl },
|
||||
} = useKibana().services;
|
||||
return useCallback(
|
||||
(breadcrumbs: ChromeBreadcrumb[]) => {
|
||||
docTitle.change(getTitleFromBreadcrumbs(breadcrumbs));
|
||||
setBreadcrumbs(
|
||||
breadcrumbs.map((breadcrumb) => {
|
||||
const { href, onClick } = breadcrumb;
|
||||
return {
|
||||
...breadcrumb,
|
||||
...(href && !onClick
|
||||
? {
|
||||
onClick: (event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
navigateToUrl(href);
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[docTitle, setBreadcrumbs, navigateToUrl]
|
||||
);
|
||||
};
|
||||
|
||||
export const useCasesBreadcrumbs = (pageDeepLink: ICasesDeepLinkId) => {
|
||||
const { appId, appTitle } = useCasesContext();
|
||||
const { getAppUrl } = useNavigation(appId);
|
||||
const applyBreadcrumbs = useApplyBreadcrumbs();
|
||||
|
||||
useEffect(() => {
|
||||
applyBreadcrumbs([
|
||||
{ text: appTitle, href: getAppUrl() },
|
||||
{
|
||||
text: casesBreadcrumbTitle[CasesDeepLinkId.cases],
|
||||
...(pageDeepLink !== CasesDeepLinkId.cases
|
||||
? {
|
||||
href: getAppUrl({ deepLinkId: CasesDeepLinkId.cases }),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
...(pageDeepLink !== CasesDeepLinkId.cases
|
||||
? [
|
||||
{
|
||||
text: casesBreadcrumbTitle[pageDeepLink],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}, [pageDeepLink, appTitle, getAppUrl, applyBreadcrumbs]);
|
||||
};
|
||||
|
||||
export const useCasesTitleBreadcrumbs = (caseTitle: string) => {
|
||||
const { appId, appTitle } = useCasesContext();
|
||||
const { getAppUrl } = useNavigation(appId);
|
||||
const applyBreadcrumbs = useApplyBreadcrumbs();
|
||||
|
||||
useEffect(() => {
|
||||
const casesBreadcrumbs: ChromeBreadcrumb[] = [
|
||||
{ text: appTitle, href: getAppUrl() },
|
||||
{
|
||||
text: casesBreadcrumbTitle[CasesDeepLinkId.cases],
|
||||
href: getAppUrl({ deepLinkId: CasesDeepLinkId.cases }),
|
||||
},
|
||||
{
|
||||
text: caseTitle,
|
||||
},
|
||||
];
|
||||
applyBreadcrumbs(casesBreadcrumbs);
|
||||
}, [caseTitle, appTitle, getAppUrl, applyBreadcrumbs]);
|
||||
};
|
|
@ -10,11 +10,15 @@ import { mount } from 'enzyme';
|
|||
|
||||
import { CreateCaseModal } from './create_case_modal';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { getCreateCaseLazy as getCreateCase } from '../../methods';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { CreateCase } from '../create';
|
||||
|
||||
jest.mock('../create', () => ({
|
||||
CreateCase: jest.fn(),
|
||||
}));
|
||||
|
||||
const CreateCaseMock = CreateCase as unknown as jest.Mock;
|
||||
|
||||
jest.mock('../../methods');
|
||||
const getCreateCaseMock = getCreateCase as jest.Mock;
|
||||
const onCloseCaseModal = jest.fn();
|
||||
const onSuccess = jest.fn();
|
||||
const defaultProps = {
|
||||
|
@ -26,8 +30,8 @@ const defaultProps = {
|
|||
|
||||
describe('CreateCaseModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
getCreateCaseMock.mockReturnValue(<></>);
|
||||
jest.clearAllMocks();
|
||||
CreateCaseMock.mockReturnValue(<></>);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
|
@ -68,7 +72,7 @@ describe('CreateCaseModal', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getCreateCaseMock.mock.calls[0][0]).toEqual(
|
||||
expect(CreateCaseMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
onSuccess,
|
||||
onCancel: onCloseCaseModal,
|
||||
|
|
|
@ -10,25 +10,20 @@ import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@el
|
|||
|
||||
import { Case } from '../../containers/types';
|
||||
import * as i18n from '../../common/translations';
|
||||
import { CaseType } from '../../../common';
|
||||
import { getCreateCaseLazy as getCreateCase } from '../../methods';
|
||||
import { CreateCase } from '../create';
|
||||
|
||||
export interface CreateCaseModalProps {
|
||||
caseType?: CaseType;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
isModalOpen: boolean;
|
||||
onCloseCaseModal: () => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
||||
caseType = CaseType.individual,
|
||||
hideConnectorServiceNowSir,
|
||||
isModalOpen,
|
||||
onCloseCaseModal,
|
||||
onSuccess,
|
||||
owner,
|
||||
}) =>
|
||||
isModalOpen ? (
|
||||
<EuiModal onClose={onCloseCaseModal} data-test-subj="create-case-modal">
|
||||
|
@ -36,14 +31,12 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
|||
<EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
{getCreateCase({
|
||||
caseType,
|
||||
hideConnectorServiceNowSir,
|
||||
onCancel: onCloseCaseModal,
|
||||
onSuccess,
|
||||
withSteps: false,
|
||||
owner: [owner],
|
||||
})}
|
||||
<CreateCase
|
||||
onCancel={onCloseCaseModal}
|
||||
onSuccess={onSuccess}
|
||||
withSteps={false}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
) : null;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, act as reactAct } from '@testing-library/react';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useCreateCaseModal, UseCreateCaseModalProps, UseCreateCaseModalReturnedValues } from '.';
|
||||
|
@ -95,8 +95,10 @@ describe('useCreateCaseModal', () => {
|
|||
result.current.openModal();
|
||||
});
|
||||
|
||||
const modal = result.current.modal;
|
||||
render(<TestProviders>{modal}</TestProviders>);
|
||||
await reactAct(async () => {
|
||||
const modal = result.current.modal;
|
||||
render(<TestProviders>{modal}</TestProviders>);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.modal.props.onSuccess({ id: 'case-id' });
|
||||
|
|
|
@ -6,13 +6,11 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Case, CaseType } from '../../../common';
|
||||
import { useOwnerContext } from '../owner_context/use_owner_context';
|
||||
import { Case } from '../../../common';
|
||||
import { CreateCaseModal } from './create_case_modal';
|
||||
|
||||
export interface UseCreateCaseModalProps {
|
||||
onCaseCreated: (theCase: Case) => void;
|
||||
caseType?: CaseType;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
export interface UseCreateCaseModalReturnedValues {
|
||||
|
@ -23,11 +21,9 @@ export interface UseCreateCaseModalReturnedValues {
|
|||
}
|
||||
|
||||
export const useCreateCaseModal = ({
|
||||
caseType = CaseType.individual,
|
||||
onCaseCreated,
|
||||
hideConnectorServiceNowSir = false,
|
||||
}: UseCreateCaseModalProps) => {
|
||||
const owner = useOwnerContext();
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||
const openModal = useCallback(() => setIsModalOpen(true), []);
|
||||
|
@ -43,18 +39,16 @@ export const useCreateCaseModal = ({
|
|||
() => ({
|
||||
modal: (
|
||||
<CreateCaseModal
|
||||
caseType={caseType}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
isModalOpen={isModalOpen}
|
||||
onCloseCaseModal={closeModal}
|
||||
onSuccess={onSuccess}
|
||||
owner={owner[0]}
|
||||
/>
|
||||
),
|
||||
isModalOpen,
|
||||
closeModal,
|
||||
openModal,
|
||||
}),
|
||||
[caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal, owner]
|
||||
[closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,10 +18,6 @@ describe('CaseCallOut ', () => {
|
|||
});
|
||||
|
||||
const defaultProps: CaseCallOutProps = {
|
||||
configureCasesNavigation: {
|
||||
href: 'testHref',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
hasConnectors: true,
|
||||
messages: [
|
||||
{ id: 'message-one', title: 'title', description: <p>{'we have two messages'}</p> },
|
||||
|
@ -83,7 +79,6 @@ describe('CaseCallOut ', () => {
|
|||
const id = createCalloutId(['message-one', 'message-two']);
|
||||
wrapper.find(`[data-test-subj="callout-onclick-${id}"]`).last().simulate('click');
|
||||
expect(defaultProps.onEditClick).toHaveBeenCalled();
|
||||
expect(defaultProps.configureCasesNavigation.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Redirects to configure page when hasConnectors=false', () => {
|
||||
|
@ -100,6 +95,5 @@ describe('CaseCallOut ', () => {
|
|||
const id = createCalloutId(['message-one', 'message-two']);
|
||||
wrapper.find(`[data-test-subj="callout-onclick-${id}"]`).last().simulate('click');
|
||||
expect(defaultProps.onEditClick).not.toHaveBeenCalled();
|
||||
expect(defaultProps.configureCasesNavigation.onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,12 +11,11 @@ import React, { memo, useCallback, useMemo } from 'react';
|
|||
import { CallOut } from './callout';
|
||||
import { ErrorMessage } from './types';
|
||||
import { createCalloutId } from './helpers';
|
||||
import { CasesNavigation } from '../../links';
|
||||
import { useConfigureCasesNavigation } from '../../../common/navigation';
|
||||
|
||||
export * from './helpers';
|
||||
|
||||
export interface CaseCallOutProps {
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
hasConnectors: boolean;
|
||||
hasLicenseError: boolean;
|
||||
messages?: ErrorMessage[];
|
||||
|
@ -30,23 +29,24 @@ type GroupByTypeMessages = {
|
|||
};
|
||||
};
|
||||
const CaseCallOutComponent = ({
|
||||
configureCasesNavigation,
|
||||
hasConnectors,
|
||||
hasLicenseError,
|
||||
onEditClick,
|
||||
messages = [],
|
||||
}: CaseCallOutProps) => {
|
||||
const { navigateToConfigureCases } = useConfigureCasesNavigation();
|
||||
const handleCallOut = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
// if theres connectors open dropdown editor
|
||||
// if no connectors, redirect to create case page
|
||||
if (hasConnectors) {
|
||||
onEditClick();
|
||||
} else {
|
||||
configureCasesNavigation.onClick(e);
|
||||
navigateToConfigureCases();
|
||||
}
|
||||
},
|
||||
[hasConnectors, onEditClick, configureCasesNavigation]
|
||||
[hasConnectors, onEditClick, navigateToConfigureCases]
|
||||
);
|
||||
|
||||
const groupedByTypeErrorMessages = useMemo(
|
||||
|
|
|
@ -20,20 +20,10 @@ import { connectorsMock } from '../../containers/configure/mock';
|
|||
import { CLOSED_CASE_PUSH_ERROR_ID } from './callout/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useHistory: () => ({
|
||||
useHistory: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../containers/use_get_action_license');
|
||||
jest.mock('../../containers/use_post_push_to_service');
|
||||
jest.mock('../../containers/configure/api');
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
describe('usePushToService', () => {
|
||||
const caseId = '12345';
|
||||
|
@ -81,7 +71,7 @@ describe('usePushToService', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
(usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush);
|
||||
(useGetActionLicense as jest.Mock).mockImplementation(() => ({
|
||||
isLoading: false,
|
||||
|
|
|
@ -21,14 +21,12 @@ import {
|
|||
import * as i18n from './translations';
|
||||
import { Case, CaseConnector, ActionConnector, CaseStatuses } from '../../../common';
|
||||
import { CaseServices } from '../../containers/use_get_case_user_actions';
|
||||
import { CasesNavigation } from '../links';
|
||||
import { ErrorMessage } from './callout/types';
|
||||
|
||||
export interface UsePushToService {
|
||||
caseId: string;
|
||||
caseServices: CaseServices;
|
||||
caseStatus: string;
|
||||
configureCasesNavigation: CasesNavigation;
|
||||
connector: CaseConnector;
|
||||
connectors: ActionConnector[];
|
||||
hasDataToPush: boolean;
|
||||
|
@ -47,7 +45,6 @@ export const usePushToService = ({
|
|||
caseId,
|
||||
caseServices,
|
||||
caseStatus,
|
||||
configureCasesNavigation,
|
||||
connector,
|
||||
connectors,
|
||||
hasDataToPush,
|
||||
|
@ -175,7 +172,6 @@ export const usePushToService = ({
|
|||
pushCallouts:
|
||||
errorsMsg.length > 0 ? (
|
||||
<CaseCallOut
|
||||
configureCasesNavigation={configureCasesNavigation}
|
||||
hasConnectors={connectors.length > 0}
|
||||
hasLicenseError={hasLicenseError}
|
||||
messages={errorsMsg}
|
||||
|
@ -184,7 +180,6 @@ export const usePushToService = ({
|
|||
) : null,
|
||||
}),
|
||||
[
|
||||
configureCasesNavigation,
|
||||
connector.name,
|
||||
connectors.length,
|
||||
errorsMsg,
|
||||
|
|
|
@ -190,12 +190,10 @@ const getUpdateActionIcon = (actionField: string): string => {
|
|||
|
||||
export const getUpdateAction = ({
|
||||
action,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
label,
|
||||
handleOutlineComment,
|
||||
}: {
|
||||
action: CaseUserActions;
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
label: string | JSX.Element;
|
||||
handleOutlineComment: (id: string) => void;
|
||||
}): EuiCommentProps => ({
|
||||
|
@ -213,10 +211,7 @@ export const getUpdateAction = ({
|
|||
actions: (
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserActionCopyLink
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
id={action.actionId}
|
||||
/>
|
||||
<UserActionCopyLink id={action.actionId} />
|
||||
</EuiFlexItem>
|
||||
{action.action === 'update' && action.commentId != null && (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -230,7 +225,6 @@ export const getUpdateAction = ({
|
|||
export const getAlertAttachment = ({
|
||||
action,
|
||||
alertId,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
getRuleDetailsHref,
|
||||
index,
|
||||
loadingAlertData,
|
||||
|
@ -241,7 +235,6 @@ export const getAlertAttachment = ({
|
|||
}: {
|
||||
action: CaseUserActions;
|
||||
alertId: string;
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
getRuleDetailsHref: RuleDetailsNavigation['href'];
|
||||
index: string;
|
||||
loadingAlertData: boolean;
|
||||
|
@ -275,10 +268,7 @@ export const getAlertAttachment = ({
|
|||
actions: (
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserActionCopyLink
|
||||
id={action.actionId}
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
/>
|
||||
<UserActionCopyLink id={action.actionId} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserActionShowAlert
|
||||
|
@ -330,7 +320,6 @@ export const toStringArray = (value: unknown): string[] => {
|
|||
export const getGeneratedAlertsAttachment = ({
|
||||
action,
|
||||
alertIds,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
getRuleDetailsHref,
|
||||
onRuleDetailsClick,
|
||||
renderInvestigateInTimelineActionComponent,
|
||||
|
@ -339,7 +328,6 @@ export const getGeneratedAlertsAttachment = ({
|
|||
}: {
|
||||
action: CaseUserActions;
|
||||
alertIds: string[];
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
getRuleDetailsHref: RuleDetailsNavigation['href'];
|
||||
onRuleDetailsClick?: RuleDetailsNavigation['onClick'];
|
||||
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
|
||||
|
@ -366,10 +354,7 @@ export const getGeneratedAlertsAttachment = ({
|
|||
actions: (
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserActionCopyLink
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
id={action.actionId}
|
||||
/>
|
||||
<UserActionCopyLink id={action.actionId} />
|
||||
</EuiFlexItem>
|
||||
{renderInvestigateInTimelineActionComponent ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -402,14 +387,12 @@ export const getActionAttachment = ({
|
|||
comment,
|
||||
userCanCrud,
|
||||
isLoadingIds,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
actionsNavigation,
|
||||
action,
|
||||
}: {
|
||||
comment: Comment & CommentRequestActionsType;
|
||||
userCanCrud: boolean;
|
||||
isLoadingIds: string[];
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
actionsNavigation?: ActionsNavigation;
|
||||
action: CaseUserActions;
|
||||
}): EuiCommentProps => ({
|
||||
|
@ -431,12 +414,7 @@ export const getActionAttachment = ({
|
|||
'data-test-subj': 'endpoint-action',
|
||||
timestamp: <UserActionTimestamp createdAt={action.actionAt} />,
|
||||
timelineIcon: <ActionIcon actionType={comment.actions.type} />,
|
||||
actions: (
|
||||
<UserActionCopyLink
|
||||
id={comment.id}
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
/>
|
||||
),
|
||||
actions: <UserActionCopyLink id={comment.id} />,
|
||||
children: comment.comment.trim().length > 0 && (
|
||||
<ContentWrapper data-test-subj="user-action-markdown">
|
||||
<MarkdownRenderer>{comment.comment}</MarkdownRenderer>
|
||||
|
|
|
@ -33,7 +33,6 @@ const defaultProps = {
|
|||
caseServices: {},
|
||||
caseUserActions: [],
|
||||
connectors: [],
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
actionsNavigation: { href: jest.fn(), onClick: jest.fn() },
|
||||
getRuleDetailsHref: jest.fn(),
|
||||
onRuleDetailsClick: jest.fn(),
|
||||
|
|
|
@ -17,7 +17,6 @@ import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils/technical
|
|||
import classNames from 'classnames';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { isRight } from 'fp-ts/Either';
|
||||
|
||||
|
@ -58,6 +57,7 @@ import { UserActionUsername } from './user_action_username';
|
|||
import { UserActionContentToolbar } from './user_action_content_toolbar';
|
||||
import { getManualAlertIdsWithNoRuleId } from '../case_view/helpers';
|
||||
import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment';
|
||||
import { useCaseViewParams } from '../../common/navigation';
|
||||
|
||||
export interface UserActionTreeProps {
|
||||
caseServices: CaseServices;
|
||||
|
@ -65,7 +65,6 @@ export interface UserActionTreeProps {
|
|||
connectors: ActionConnector[];
|
||||
data: Case;
|
||||
fetchUserActions: () => void;
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
getRuleDetailsHref?: RuleDetailsNavigation['href'];
|
||||
actionsNavigation?: ActionsNavigation;
|
||||
isLoadingDescription: boolean;
|
||||
|
@ -149,7 +148,6 @@ export const UserActionTree = React.memo(
|
|||
connectors,
|
||||
data: caseData,
|
||||
fetchUserActions,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
getRuleDetailsHref,
|
||||
actionsNavigation,
|
||||
isLoadingDescription,
|
||||
|
@ -163,15 +161,7 @@ export const UserActionTree = React.memo(
|
|||
useFetchAlertData,
|
||||
userCanCrud,
|
||||
}: UserActionTreeProps) => {
|
||||
const {
|
||||
detailName: caseId,
|
||||
commentId,
|
||||
subCaseId,
|
||||
} = useParams<{
|
||||
detailName: string;
|
||||
commentId?: string;
|
||||
subCaseId?: string;
|
||||
}>();
|
||||
const { detailName: caseId, subCaseId, commentId } = useCaseViewParams();
|
||||
const handlerTimeoutId = useRef(0);
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState('');
|
||||
|
@ -325,7 +315,6 @@ export const UserActionTree = React.memo(
|
|||
actions: (
|
||||
<UserActionContentToolbar
|
||||
commentMarkdown={caseData.description}
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
id={DESCRIPTION_ID}
|
||||
editLabel={i18n.EDIT_DESCRIPTION}
|
||||
quoteLabel={i18n.QUOTE}
|
||||
|
@ -339,7 +328,6 @@ export const UserActionTree = React.memo(
|
|||
[
|
||||
MarkdownDescription,
|
||||
caseData,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
handleManageMarkdownEditId,
|
||||
handleManageQuote,
|
||||
isLoadingDescription,
|
||||
|
@ -403,7 +391,6 @@ export const UserActionTree = React.memo(
|
|||
),
|
||||
actions: (
|
||||
<UserActionContentToolbar
|
||||
getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId}
|
||||
id={comment.id}
|
||||
commentMarkdown={comment.comment}
|
||||
editLabel={i18n.EDIT_COMMENT}
|
||||
|
@ -456,7 +443,6 @@ export const UserActionTree = React.memo(
|
|||
getAlertAttachment({
|
||||
action,
|
||||
alertId,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
getRuleDetailsHref,
|
||||
index: alertIndex,
|
||||
loadingAlertData,
|
||||
|
@ -485,7 +471,6 @@ export const UserActionTree = React.memo(
|
|||
getGeneratedAlertsAttachment({
|
||||
action,
|
||||
alertIds,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
getRuleDetailsHref,
|
||||
onRuleDetailsClick,
|
||||
renderInvestigateInTimelineActionComponent,
|
||||
|
@ -508,7 +493,6 @@ export const UserActionTree = React.memo(
|
|||
comment,
|
||||
userCanCrud,
|
||||
isLoadingIds,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
actionsNavigation,
|
||||
action,
|
||||
}),
|
||||
|
@ -526,7 +510,6 @@ export const UserActionTree = React.memo(
|
|||
getUpdateAction({
|
||||
action,
|
||||
label,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
handleOutlineComment,
|
||||
}),
|
||||
];
|
||||
|
@ -589,7 +572,6 @@ export const UserActionTree = React.memo(
|
|||
getUpdateAction({
|
||||
action,
|
||||
label,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
handleOutlineComment,
|
||||
}),
|
||||
...footers,
|
||||
|
@ -612,7 +594,6 @@ export const UserActionTree = React.memo(
|
|||
getUpdateAction({
|
||||
action,
|
||||
label,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
handleOutlineComment,
|
||||
}),
|
||||
];
|
||||
|
@ -630,7 +611,6 @@ export const UserActionTree = React.memo(
|
|||
manageMarkdownEditIds,
|
||||
handleManageMarkdownEditId,
|
||||
handleSaveComment,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
actionsNavigation,
|
||||
userCanCrud,
|
||||
isLoadingIds,
|
||||
|
|
|
@ -15,7 +15,6 @@ import { CommentType } from '../../../common';
|
|||
|
||||
const props = {
|
||||
alertId: 'alert-id-1',
|
||||
getCaseDetailHrefWithCommentId: jest.fn().mockReturnValue('someCaseDetail-withcomment'),
|
||||
getRuleDetailsHref: jest.fn().mockReturnValue('some-detection-rule-link'),
|
||||
onRuleDetailsClick: jest.fn(),
|
||||
ruleId: 'rule-id-1',
|
||||
|
|
|
@ -12,20 +12,11 @@ import {
|
|||
UserActionContentToolbarProps,
|
||||
} from './user_action_content_toolbar';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const originalModule = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useParams: jest.fn().mockReturnValue({ detailName: 'case-1' }),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const props: UserActionContentToolbarProps = {
|
||||
commentMarkdown: '',
|
||||
getCaseDetailHrefWithCommentId: jest.fn().mockReturnValue('case-detail-url-with-comment-id-1'),
|
||||
id: '1',
|
||||
editLabel: 'edit',
|
||||
quoteLabel: 'quote',
|
||||
|
|
|
@ -14,7 +14,6 @@ import { UserActionPropertyActions } from './user_action_property_actions';
|
|||
export interface UserActionContentToolbarProps {
|
||||
commentMarkdown: string;
|
||||
id: string;
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
editLabel: string;
|
||||
quoteLabel: string;
|
||||
isLoading: boolean;
|
||||
|
@ -26,7 +25,6 @@ export interface UserActionContentToolbarProps {
|
|||
const UserActionContentToolbarComponent = ({
|
||||
commentMarkdown,
|
||||
id,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
editLabel,
|
||||
quoteLabel,
|
||||
isLoading,
|
||||
|
@ -36,7 +34,7 @@ const UserActionContentToolbarComponent = ({
|
|||
}: UserActionContentToolbarProps) => (
|
||||
<EuiFlexGroup responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserActionCopyLink id={id} getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId} />
|
||||
<UserActionCopyLink id={id} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserActionPropertyActions
|
||||
|
|
|
@ -9,57 +9,45 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { UserActionCopyLink } from './user_action_copy_link';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const originalModule = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useParams: jest.fn(),
|
||||
};
|
||||
});
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
jest.mock('copy-to-clipboard', () => jest.fn());
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const mockGetUrlForApp = jest.fn(
|
||||
(appId: string, options?: { path?: string; absolute?: boolean }) =>
|
||||
`${appId}${options?.path ?? ''}`
|
||||
);
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
application: {
|
||||
getUrlForApp: mockGetUrlForApp,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const props = {
|
||||
id: 'comment-id',
|
||||
getCaseDetailHrefWithCommentId: jest.fn().mockReturnValue('random-url'),
|
||||
};
|
||||
|
||||
describe('UserActionCopyLink ', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
beforeAll(() => {
|
||||
(useParams as jest.Mock).mockReturnValue({ detailName: 'case-1' });
|
||||
wrapper = mount(<UserActionCopyLink {...props} />, { wrappingComponent: TestProviders });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useKibanaMock().services.application.getUrlForApp = mockGetUrlForApp;
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
expect(wrapper.find(`[data-test-subj="copy-link-${props.id}"]`).first().exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls copy clipboard correctly', async () => {
|
||||
wrapper.find(`[data-test-subj="copy-link-${props.id}"]`).first().simulate('click');
|
||||
expect(copy).toHaveBeenCalledWith('random-url');
|
||||
expect(copy).toHaveBeenCalledWith('/app/security/cases/test');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,19 +10,19 @@ import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
|
|||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useCaseViewNavigation, useCaseViewParams } from '../../common/navigation';
|
||||
|
||||
interface UserActionCopyLinkProps {
|
||||
id: string;
|
||||
getCaseDetailHrefWithCommentId: (commentId: string) => string;
|
||||
}
|
||||
|
||||
const UserActionCopyLinkComponent = ({
|
||||
id: commentId,
|
||||
getCaseDetailHrefWithCommentId,
|
||||
}: UserActionCopyLinkProps) => {
|
||||
const UserActionCopyLinkComponent = ({ id: commentId }: UserActionCopyLinkProps) => {
|
||||
const { getCaseViewUrl } = useCaseViewNavigation();
|
||||
const { detailName, subCaseId } = useCaseViewParams();
|
||||
|
||||
const handleAnchorLink = useCallback(() => {
|
||||
copy(getCaseDetailHrefWithCommentId(commentId));
|
||||
}, [getCaseDetailHrefWithCommentId, commentId]);
|
||||
copy(getCaseViewUrl({ detailName, subCaseId, commentId }, true));
|
||||
}, [detailName, subCaseId, commentId, getCaseViewUrl]);
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={<p>{i18n.COPY_REFERENCE_LINK}</p>}>
|
||||
|
|
|
@ -50,14 +50,11 @@ describe('useConfigure', () => {
|
|||
});
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(
|
||||
() => useCaseConfigure(),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const { result } = renderHook<string, ReturnUseCaseConfigure>(() => useCaseConfigure(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
expect(result.current).toEqual({
|
||||
...initialState,
|
||||
refetchCaseConfigure: result.current.refetchCaseConfigure,
|
||||
|
@ -66,8 +63,8 @@ describe('useConfigure', () => {
|
|||
setConnector: result.current.setConnector,
|
||||
setClosureType: result.current.setClosureType,
|
||||
setMappings: result.current.setMappings,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('fetch case configuration', async () => {
|
||||
|
@ -79,7 +76,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
...initialState,
|
||||
closureType: caseConfigurationCamelCaseResponseMock.closureType,
|
||||
|
@ -114,7 +110,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.refetchCaseConfigure();
|
||||
expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
@ -129,7 +124,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current.mappings).toEqual([]);
|
||||
result.current.setMappings(mappings);
|
||||
expect(result.current.mappings).toEqual(mappings);
|
||||
|
@ -145,7 +139,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.refetchCaseConfigure();
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
@ -161,7 +154,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.persistCaseConfigure(configuration);
|
||||
expect(result.current.persistLoading).toBeTruthy();
|
||||
});
|
||||
|
@ -193,7 +185,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(mockErrorToast).not.toHaveBeenCalled();
|
||||
|
||||
result.current.persistCaseConfigure(configuration);
|
||||
|
@ -222,7 +213,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(mockErrorToast).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -254,7 +244,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(mockErrorToast).not.toHaveBeenCalled();
|
||||
|
||||
result.current.persistCaseConfigure(configuration);
|
||||
|
@ -281,7 +270,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.persistCaseConfigure(configuration);
|
||||
|
||||
|
@ -305,7 +293,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
|
@ -344,7 +331,6 @@ describe('useConfigure', () => {
|
|||
}
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.persistCaseConfigure(configuration);
|
||||
|
|
|
@ -12,7 +12,7 @@ import * as i18n from './translations';
|
|||
import { ClosureType, CaseConfigure, CaseConnector, CaseConnectorMapping } from './types';
|
||||
import { ConnectorTypes } from '../../../common';
|
||||
import { useToasts } from '../../common/lib/kibana';
|
||||
import { useOwnerContext } from '../../components/owner_context/use_owner_context';
|
||||
import { useCasesContext } from '../../components/cases_context/use_cases_context';
|
||||
|
||||
export type ConnectorConfiguration = { connector: CaseConnector } & {
|
||||
closureType: CaseConfigure['closureType'];
|
||||
|
@ -156,7 +156,7 @@ export const initialState: State = {
|
|||
};
|
||||
|
||||
export const useCaseConfigure = (): ReturnUseCaseConfigure => {
|
||||
const owner = useOwnerContext();
|
||||
const { owner } = useCasesContext();
|
||||
const [state, dispatch] = useReducer(configureCasesReducer, initialState);
|
||||
const toasts = useToasts();
|
||||
const setCurrentConfiguration = useCallback((configuration: ConnectorConfiguration) => {
|
||||
|
|
|
@ -25,24 +25,23 @@ jest.mock('../common/lib/kibana');
|
|||
|
||||
describe('useGetCases', () => {
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
const { result } = renderHook<string, UseGetCases>(() => useGetCases(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
expect(result.current).toEqual({
|
||||
data: initialData,
|
||||
dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty,
|
||||
filterOptions: DEFAULT_FILTER_OPTIONS,
|
||||
isError: false,
|
||||
loading: [],
|
||||
loading: ['cases'],
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
refetchCases: result.current.refetchCases,
|
||||
selectedCases: [],
|
||||
|
@ -60,7 +59,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnGetCases).toBeCalledWith({
|
||||
filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
|
@ -75,7 +73,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
data: allCases,
|
||||
dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty,
|
||||
|
@ -106,7 +103,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.dispatchUpdateCaseProperty(updateCase);
|
||||
expect(result.current.loading).toEqual(['caseUpdate']);
|
||||
expect(spyOnPatchCase).toBeCalledWith(
|
||||
|
@ -125,7 +121,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.refetchCases();
|
||||
expect(spyOnGetCases).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
@ -137,7 +132,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.refetchCases();
|
||||
|
||||
expect(result.current.loading).toEqual(['cases']);
|
||||
|
@ -155,7 +149,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
data: initialData,
|
||||
|
@ -186,7 +179,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.setFilters(newFilters);
|
||||
await waitForNextUpdate();
|
||||
|
@ -214,7 +206,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.setQueryParams(newQueryParams);
|
||||
await waitForNextUpdate();
|
||||
|
@ -237,7 +228,6 @@ describe('useGetCases', () => {
|
|||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.setSelectedCases(selectedCases);
|
||||
expect(result.current.selectedCases).toEqual(selectedCases);
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { useToasts } from '../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
import { getCases, patchCase } from './api';
|
||||
import { useOwnerContext } from '../components/owner_context/use_owner_context';
|
||||
import { useCasesContext } from '../components/cases_context/use_cases_context';
|
||||
|
||||
export interface UseGetCasesState {
|
||||
data: AllCases;
|
||||
|
@ -145,7 +145,7 @@ export const useGetCases = (
|
|||
initialFilterOptions?: Partial<FilterOptions>;
|
||||
} = {}
|
||||
): UseGetCases => {
|
||||
const owner = useOwnerContext();
|
||||
const { owner } = useCasesContext();
|
||||
const { initialQueryParams = empty, initialFilterOptions = empty } = params;
|
||||
const [state, dispatch] = useReducer(dataFetchReducer, {
|
||||
data: initialData,
|
||||
|
|
|
@ -24,14 +24,11 @@ describe('useGetCasesStatus', () => {
|
|||
});
|
||||
|
||||
it('init', async () => {
|
||||
const { result } = renderHook<string, UseGetCasesStatus>(() => useGetCasesStatus(), {
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetCasesStatus>(
|
||||
() => useGetCasesStatus(),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
countClosedCases: null,
|
||||
countOpenCases: null,
|
||||
|
@ -53,7 +50,6 @@ describe('useGetCasesStatus', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnGetCasesStatus).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
});
|
||||
});
|
||||
|
@ -67,7 +63,6 @@ describe('useGetCasesStatus', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
countClosedCases: casesStatus.countClosedCases,
|
||||
countOpenCases: casesStatus.countOpenCases,
|
||||
|
@ -93,7 +88,6 @@ describe('useGetCasesStatus', () => {
|
|||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
countClosedCases: 0,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import { useOwnerContext } from '../components/owner_context/use_owner_context';
|
||||
import { useCasesContext } from '../components/cases_context/use_cases_context';
|
||||
import { getCasesStatus } from './api';
|
||||
import * as i18n from './translations';
|
||||
import { CasesStatus } from './types';
|
||||
|
@ -31,7 +31,7 @@ export interface UseGetCasesStatus extends CasesStatusState {
|
|||
}
|
||||
|
||||
export const useGetCasesStatus = (): UseGetCasesStatus => {
|
||||
const owner = useOwnerContext();
|
||||
const { owner } = useCasesContext();
|
||||
const [casesStatusState, setCasesStatusState] = useState<CasesStatusState>(initialData);
|
||||
const toasts = useToasts();
|
||||
const isCancelledRef = useRef(false);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue