[Cases][ResponseOps] Remove userCanCrud from props (#135353)

* Starting conversion to permissions from userCanCrud

* Migrating userCanCrud to context

* Fixing tests

* Fix type error

* Removing missed userCanCrud

* Fixing tests and addressing permissions.all feedback

* Fixing test

* Addressing observability test feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jonathan Buttner 2022-07-06 08:58:43 -04:00 committed by GitHub
parent eae262edad
commit de9a822c2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 709 additions and 531 deletions

View file

@ -48,7 +48,7 @@ To initialize the `CasesContext` you can use this code:
// somewhere high on your plugin render tree
<CasesContext
owner={[PLUGIN_CASES_OWNER_ID]}
userCanCrud={CASES_USER_CAN_CRUD}
permissions={CASES_PERMISSIONS}
features={CASES_FEATURES}
>
<RouteRender /> {/* or something similar */}
@ -57,11 +57,11 @@ To initialize the `CasesContext` you can use this code:
props:
| prop | type | description |
| --------------------- | --------------- | -------------------------------------------------------------- |
| PLUGIN_CASES_OWNER_ID | `string` | The owner string for your plugin. e.g: securitySolution |
| CASES_USER_CAN_CRUD | `boolean` | Defines if the user has access to cases to CRUD |
| CASES_FEATURES | `CasesFeatures` | `CasesFeatures` object defining the features to enable/disable |
| prop | type | description |
| --------------------- | ------------------- | -------------------------------------------------------------- |
| PLUGIN_CASES_OWNER_ID | `string` | The owner string for your plugin. e.g: securitySolution |
| CASES_PERMISSIONS | `CasesPermissions` | `CasesPermissions` object defining the user's permissions |
| CASES_FEATURES | `CasesFeatures` | `CasesFeatures` object defining the features to enable/disable |
### Cases UI client
@ -83,7 +83,10 @@ const { cases } = useKibana().services;
// call in the return as you would any component
cases.getCases({
basePath: '/investigate/cases',
userCanCrud: true,
permissions: {
all: true,
read: true,
},
owner: ['securitySolution'],
features: { alerts: { sync: false }, metrics: ['alerts.count', 'lifespan'] }
timelineIntegration: {
@ -206,7 +209,7 @@ Arguments:
| Property | Description |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| permissions | `CasesPermissions` object defining the user's permissions |
| owner | `string[];` owner ids of the cases |
| basePath | `string;` path to mount the Cases router on top of |
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, unknown>];` fetch alerts |
@ -236,7 +239,7 @@ Arguments:
| Property | Description |
| --------------- | ---------------------------------------------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| permissions | `CasesPermissions` object defining the user's permissions |
| owner | `string[];` owner ids of the cases |
| alertData? | `Omit<CommentRequestAlertType, 'type'>;` alert data to post to case |
| hiddenStatuses? | `CaseStatuses[];` array of hidden statuses |
@ -253,7 +256,7 @@ Arguments:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
| userCanCrud | `boolean;` user permissions to crud |
| permissions | `CasesPermissions` object defining the user's permissions |
| 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 |
@ -267,11 +270,11 @@ UI component:
Arguments:
| Property | Description |
| -------------- | ------------------------------------------- |
| userCanCrud | `boolean;` user permissions to crud |
| owner | `string[];` owner ids of the cases |
| maxCasesToShow | `number;` number of cases to show in widget |
| Property | Description |
| -------------- | ---------------------------------------------------------- |
| permissions | `CasesPermissions` object defining the user's permissions |
| owner | `string[];` owner ids of the cases |
| maxCasesToShow | `number;` number of cases to show in widget |
UI component:
![Recent Cases Component][recent-cases-img]
@ -289,7 +292,7 @@ Arguments:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
| userCanCrud | `boolean;` user permissions to crud |
| permissions | `CasesPermissions` object defining the user's permissions |
| 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 |

View file

@ -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.
*/
export interface CasesPermissions {
all: boolean;
read: boolean;
}
export const getUICapabilities = (
featureCapabilities: Partial<Record<string, boolean | Record<string, boolean>>>
): CasesPermissions => {
const read = !!featureCapabilities?.read_cases;
const all = !!featureCapabilities?.crud_cases;
return {
all,
read,
};
};

View file

@ -22,12 +22,12 @@ const AllCasesSelectorModalLazy: React.FC<AllCasesSelectorModalProps> = lazy(
export const getAllCasesSelectorModalLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
hiddenStatuses,
onRowClick,
onClose,
}: GetAllCasesSelectorModalPropsInternal) => (
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, userCanCrud }}>
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, permissions }}>
<Suspense fallback={<EuiLoadingSpinner />}>
<AllCasesSelectorModalLazy
hiddenStatuses={hiddenStatuses}

View file

@ -18,7 +18,7 @@ const CasesRoutesLazy: React.FC<CasesProps> = lazy(() => import('../../component
export const getCasesLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
basePath,
onComponentInitialized,
actionsNavigation,
@ -34,7 +34,7 @@ export const getCasesLazy = ({
value={{
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
basePath,
features,
releasePhase,

View file

@ -22,7 +22,7 @@ const CasesProviderLazy: React.FC<{ value: GetCasesContextPropsInternal }> = laz
const CasesProviderLazyWrapper = ({
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
features,
children,
releasePhase,
@ -33,7 +33,7 @@ const CasesProviderLazyWrapper = ({
value={{
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
features,
releasePhase,
}}

View file

@ -22,14 +22,14 @@ export const CreateCaseFlyoutLazy: React.FC<CreateCaseFlyoutProps> = lazy(
export const getCreateCaseFlyoutLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
features,
afterCaseCreated,
onClose,
onSuccess,
attachments,
}: GetCreateCaseFlyoutPropsInternal) => (
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, userCanCrud, features }}>
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, permissions, features }}>
<Suspense fallback={<EuiLoadingSpinner />}>
<CreateCaseFlyoutLazy
afterCaseCreated={afterCaseCreated}

View file

@ -22,10 +22,10 @@ const RecentCasesLazy: React.FC<RecentCasesProps> = lazy(
export const getRecentCasesLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
maxCasesToShow,
}: GetRecentCasesPropsInternal) => (
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, userCanCrud }}>
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, permissions }}>
<Suspense fallback={<EuiLoadingSpinner />}>
<RecentCasesLazy maxCasesToShow={maxCasesToShow} />
</Suspense>

View file

@ -23,7 +23,7 @@ describe('hooks', () => {
expect(result.current).toEqual({
actions: { crud: true, read: true },
generalCases: { crud: true, read: true },
generalCases: { all: true, read: true },
visualize: { crud: true, read: true },
dashboard: { crud: true, read: true },
});

View file

@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
import { AuthenticatedUser } from '@kbn/security-plugin/common/model';
import { NavigateToAppOptions } from '@kbn/core/public';
import { CasesPermissions, getUICapabilities } from '../../../client/helpers/capabilities';
import { convertToCamelCase } from '../../../api/utils';
import {
FEATURE_ID,
@ -166,7 +167,7 @@ interface Capabilities {
}
interface UseApplicationCapabilities {
actions: Capabilities;
generalCases: Capabilities;
generalCases: CasesPermissions;
visualize: Capabilities;
dashboard: Capabilities;
}
@ -179,13 +180,14 @@ interface UseApplicationCapabilities {
export const useApplicationCapabilities = (): UseApplicationCapabilities => {
const capabilities = useKibana().services?.application?.capabilities;
const casesCapabilities = capabilities[FEATURE_ID];
const permissions = getUICapabilities(casesCapabilities);
return useMemo(
() => ({
actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show },
generalCases: {
crud: !!casesCapabilities?.crud_cases,
read: !!casesCapabilities?.read_cases,
all: permissions.all,
read: permissions.read,
},
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
dashboard: {
@ -200,8 +202,8 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
capabilities.dashboard?.show,
capabilities.visualize?.save,
capabilities.visualize?.show,
casesCapabilities?.crud_cases,
casesCapabilities?.read_cases,
permissions.all,
permissions.read,
]
);
};

View file

@ -23,13 +23,14 @@ import {
import { FieldHook } from '../shared_imports';
import { StartServices } from '../../types';
import { ReleasePhase } from '../../components/types';
import { CasesPermissions } from '../../client/helpers/capabilities';
import { AttachmentTypeRegistry } from '../../client/attachment_framework/registry';
import { ExternalReferenceAttachmentType } from '../../client/attachment_framework/types';
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
interface TestProviderProps {
children: React.ReactNode;
userCanCrud?: boolean;
permissions?: CasesPermissions;
features?: CasesFeatures;
owner?: string[];
releasePhase?: ReleasePhase;
@ -45,7 +46,7 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
children,
features,
owner = [SECURITY_SOLUTION_OWNER],
userCanCrud = true,
permissions = allCasesPermissions(),
releasePhase = 'ga',
externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(),
}) => {
@ -63,7 +64,7 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<QueryClientProvider client={queryClient}>
<CasesProvider
value={{ externalReferenceAttachmentTypeRegistry, features, owner, userCanCrud }}
value={{ externalReferenceAttachmentTypeRegistry, features, owner, permissions }}
>
{children}
</CasesProvider>
@ -92,10 +93,24 @@ export const testQueryClient = new QueryClient({
},
});
export const buildCasesPermissions = (overrides: Partial<CasesPermissions> = {}) => {
const read = overrides.read ?? true;
const all = overrides.all ?? true;
return {
all,
read,
};
};
export const allCasesPermissions = () => buildCasesPermissions();
export const noCasesPermissions = () => buildCasesPermissions({ read: false, all: false });
export const readCasesPermissions = () => buildCasesPermissions({ all: false });
export const createAppMockRenderer = ({
features,
owner = [SECURITY_SOLUTION_OWNER],
userCanCrud = true,
permissions = allCasesPermissions(),
releasePhase = 'ga',
externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(),
}: Omit<TestProviderProps, 'children'> = {}): AppMockRenderer => {
@ -118,7 +133,7 @@ export const createAppMockRenderer = ({
externalReferenceAttachmentTypeRegistry,
features,
owner,
userCanCrud,
permissions,
releasePhase,
}}
>

View file

@ -29,7 +29,6 @@ const createAttachments = jest.fn();
const addCommentProps: AddCommentProps = {
id: 'newComment',
caseId: '1234',
userCanCrud: true,
onCommentSaving,
onCommentPosted,
showLoading: false,
@ -120,8 +119,8 @@ describe('AddComment ', () => {
isLoading: true,
}));
const wrapper = mount(
<TestProviders>
<AddComment {...{ ...addCommentProps, userCanCrud: false }} />
<TestProviders permissions={{ all: false, read: false }}>
<AddComment {...{ ...addCommentProps }} />
</TestProviders>
);

View file

@ -47,7 +47,6 @@ export interface AddCommentRefObject {
export interface AddCommentProps {
id: string;
caseId: string;
userCanCrud?: boolean;
onCommentSaving?: () => void;
onCommentPosted: (newCase: Case) => void;
showLoading?: boolean;
@ -57,20 +56,12 @@ export interface AddCommentProps {
export const AddComment = React.memo(
forwardRef<AddCommentRefObject, AddCommentProps>(
(
{
id,
caseId,
userCanCrud,
onCommentPosted,
onCommentSaving,
showLoading = true,
statusActionButton,
},
{ id, caseId, onCommentPosted, onCommentSaving, showLoading = true, statusActionButton },
ref
) => {
const editorRef = useRef<EuiMarkdownEditorRef>(null);
const [focusOnContext, setFocusOnContext] = useState(false);
const { owner } = useCasesContext();
const { permissions, owner } = useCasesContext();
const { isLoading, createAttachments } = useCreateAttachments();
const { form } = useForm<AddCommentFormSchema>({
@ -156,7 +147,7 @@ export const AddComment = React.memo(
return (
<span id="add-comment-permLink">
{isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />}
{userCanCrud && (
{permissions.all && (
<Form form={form}>
<UseField
path={fieldName}

View file

@ -139,7 +139,6 @@ describe('AllCasesListGeneric', () => {
handleIsLoading: jest.fn(),
isLoadingCases: [],
isSelectorView: false,
userCanCrud: true,
};
let appMockRenderer: AppMockRenderer;

View file

@ -61,7 +61,7 @@ export interface AllCasesListProps {
export const AllCasesList = React.memo<AllCasesListProps>(
({ hiddenStatuses = [], isSelectorView = false, onRowClick, doRefresh }) => {
const { owner, userCanCrud } = useCasesContext();
const { owner, permissions } = useCasesContext();
const availableSolutions = useAvailableCasesOwners();
const [refresh, setRefresh] = useState(0);
@ -185,14 +185,13 @@ export const AllCasesList = React.memo<AllCasesListProps>(
[deselectCases, setFilterOptions, refreshCases, setQueryParams]
);
const showActions = userCanCrud && !isSelectorView;
const showActions = permissions.all && !isSelectorView;
const columns = useCasesColumns({
filterStatus: filterOptions.status ?? StatusAll,
handleIsLoading,
refreshCases,
isSelectorView,
userCanCrud,
connectors,
onRowClick,
showSolutionColumn: !hasOwner && availableSolutions.length > 1,
@ -271,7 +270,6 @@ export const AllCasesList = React.memo<AllCasesListProps>(
sorting={sorting}
tableRef={tableRef}
tableRowProps={tableRowProps}
userCanCrud={userCanCrud}
/>
</>
);

View file

@ -43,6 +43,7 @@ import type { CasesOwners } from '../../client/helpers/can_use_cases';
import { useCasesFeatures } from '../cases_context/use_cases_features';
import { severities } from '../severity/config';
import { useUpdateCase } from '../../containers/use_update_case';
import { useCasesContext } from '../cases_context/use_cases_context';
export type CasesColumns =
| EuiTableActionsColumnType<Case>
@ -61,7 +62,6 @@ export interface GetCasesColumn {
handleIsLoading: (a: boolean) => void;
refreshCases?: (a?: boolean) => void;
isSelectorView: boolean;
userCanCrud: boolean;
connectors?: ActionConnector[];
onRowClick?: (theCase: Case) => void;
@ -72,7 +72,6 @@ export const useCasesColumns = ({
handleIsLoading,
refreshCases,
isSelectorView,
userCanCrud,
connectors = [],
onRowClick,
showSolutionColumn,
@ -88,6 +87,7 @@ export const useCasesColumns = ({
} = useDeleteCases();
const { isAlertsEnabled } = useCasesFeatures();
const { permissions } = useCasesContext();
const [deleteThisCase, setDeleteThisCase] = useState<DeleteCase>({
id: '',
@ -319,7 +319,7 @@ export const useCasesColumns = ({
return (
<StatusContextMenu
currentStatus={theCase.status}
disabled={!userCanCrud || isLoadingUpdateCase}
disabled={!permissions.all || isLoadingUpdateCase}
onStatusChanged={(status) =>
handleDispatchUpdate({
updateKey: 'status',
@ -372,7 +372,7 @@ export const useCasesColumns = ({
},
]
: []),
...(userCanCrud && !isSelectorView
...(permissions.all && !isSelectorView
? [
{
name: (

View file

@ -11,23 +11,32 @@ import { HeaderPage } from '../header_page';
import * as i18n from './translations';
import { ErrorMessage } from '../use_push_to_service/callout/types';
import { NavButtons } from './nav_buttons';
import { useCasesContext } from '../cases_context/use_cases_context';
interface OwnProps {
actionsErrors: ErrorMessage[];
userCanCrud: boolean;
}
type Props = OwnProps;
export const CasesTableHeader: FunctionComponent<Props> = ({ actionsErrors, userCanCrud }) => (
<HeaderPage title={i18n.PAGE_TITLE} border data-test-subj="cases-all-title">
<EuiFlexGroup alignItems="center" gutterSize="m" wrap={true} data-test-subj="all-cases-header">
{userCanCrud ? (
<EuiFlexItem>
<NavButtons actionsErrors={actionsErrors} />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</HeaderPage>
);
export const CasesTableHeader: FunctionComponent<Props> = ({ actionsErrors }) => {
const { permissions } = useCasesContext();
return (
<HeaderPage title={i18n.PAGE_TITLE} border data-test-subj="cases-all-title">
<EuiFlexGroup
alignItems="center"
gutterSize="m"
wrap={true}
data-test-subj="all-cases-header"
>
{permissions.all ? (
<EuiFlexItem>
<NavButtons actionsErrors={actionsErrors} />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</HeaderPage>
);
};
CasesTableHeader.displayName = 'CasesTableHeader';

View file

@ -8,14 +8,12 @@
import React, { 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 const AllCases: React.FC = () => {
const { userCanCrud } = useCasesContext();
useCasesBreadcrumbs(CasesDeepLinkId.cases);
const { data: actionLicense = null } = useGetActionLicense();
@ -23,7 +21,7 @@ export const AllCases: React.FC = () => {
return (
<>
<CasesTableHeader actionsErrors={actionsErrors} userCanCrud={userCanCrud} />
<CasesTableHeader actionsErrors={actionsErrors} />
<AllCasesList />
</>
);

View file

@ -61,7 +61,10 @@ describe('use cases add to existing case modal hook', () => {
value={{
externalReferenceAttachmentTypeRegistry,
owner: ['test'],
userCanCrud: true,
permissions: {
all: true,
read: true,
},
appId: 'test',
appTitle: 'jest',
basePath: '/jest',

View file

@ -22,6 +22,7 @@ import { LinkButton } from '../links';
import { Cases, Case, FilterOptions } from '../../../common/ui/types';
import * as i18n from './translations';
import { useCreateCaseNavigation } from '../../common/navigation';
import { useCasesContext } from '../cases_context/use_cases_context';
interface CasesTableProps {
columns: EuiBasicTableProps<Case>['columns'];
@ -42,7 +43,6 @@ interface CasesTableProps {
sorting: EuiBasicTableProps<Case>['sorting'];
tableRef: MutableRefObject<EuiBasicTable | null>;
tableRowProps: EuiBasicTableProps<Case>['rowProps'];
userCanCrud: boolean;
}
const Div = styled.div`
@ -68,8 +68,8 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
sorting,
tableRef,
tableRowProps,
userCanCrud,
}) => {
const { permissions } = useCasesContext();
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
const navigateToCreateCaseClick = useCallback(
(ev) => {
@ -109,11 +109,11 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
<EuiEmptyPrompt
title={<h3>{i18n.NO_CASES}</h3>}
titleSize="xs"
body={userCanCrud ? i18n.NO_CASES_BODY : i18n.NO_CASES_BODY_READ_ONLY}
body={permissions.all ? i18n.NO_CASES_BODY : i18n.NO_CASES_BODY_READ_ONLY}
actions={
userCanCrud && (
permissions.all && (
<LinkButton
isDisabled={!userCanCrud}
isDisabled={!permissions.all}
fill
size="s"
onClick={navigateToCreateCaseClick}

View file

@ -31,7 +31,7 @@ const CasesAppComponent: React.FC<CasesAppProps> = ({
externalReferenceAttachmentTypeRegistry,
owner: [APP_OWNER],
useFetchAlertData: () => [false, {}],
userCanCrud: userCapabilities.generalCases.crud,
permissions: userCapabilities.generalCases,
basePath: '/',
features: { alerts: { enabled: false } },
releasePhase: 'experimental',

View file

@ -10,8 +10,9 @@ import React from 'react';
import { MemoryRouterProps } from 'react-router';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { TestProviders } from '../../common/mock';
import { readCasesPermissions, TestProviders } from '../../common/mock';
import { CasesRoutes } from './routes';
import { CasesPermissions } from '../../client/helpers/capabilities';
jest.mock('../all_cases', () => ({
AllCases: () => <div>{'All cases'}</div>,
@ -29,10 +30,10 @@ const getCaseViewPaths = () => ['/cases/test-id', '/cases/test-id/comment-id'];
const renderWithRouter = (
initialEntries: MemoryRouterProps['initialEntries'] = ['/cases'],
userCanCrud = true
permissions?: CasesPermissions
) => {
return render(
<TestProviders userCanCrud={userCanCrud}>
<TestProviders permissions={permissions}>
<MemoryRouter initialEntries={initialEntries}>
<CasesRoutes useFetchAlertData={(alertIds) => [false, {}]} />
</MemoryRouter>
@ -48,8 +49,8 @@ describe('Cases routes', () => {
});
// User has read only privileges
it('user can navigate to the all cases page with userCanCrud = false', () => {
renderWithRouter(['/cases'], false);
it('user can navigate to the all cases page with only read permissions', () => {
renderWithRouter(['/cases'], readCasesPermissions());
expect(screen.getByText('All cases')).toBeInTheDocument();
});
});
@ -68,9 +69,9 @@ describe('Cases routes', () => {
);
it.each(getCaseViewPaths())(
'user can navigate to the cases view page with userCanCrud = false and path: %s',
'user can navigate to the cases view page with read permissions and path: %s',
async (path: string) => {
renderWithRouter([path], false);
renderWithRouter([path], readCasesPermissions());
await waitFor(() => {
expect(screen.getByTestId('case-view-loading')).toBeInTheDocument();
});
@ -84,8 +85,8 @@ describe('Cases routes', () => {
expect(screen.getByText('Create case')).toBeInTheDocument();
});
it('shows the no privileges page if userCanCrud = false', () => {
renderWithRouter(['/cases/create'], false);
it('shows the no privileges page if user is read only', () => {
renderWithRouter(['/cases/create'], readCasesPermissions());
expect(screen.getByText('Privileges required')).toBeInTheDocument();
});
});
@ -96,8 +97,8 @@ describe('Cases routes', () => {
expect(screen.getByText('Configure cases')).toBeInTheDocument();
});
it('shows the no privileges page if userCanCrud = false', () => {
renderWithRouter(['/cases/configure'], false);
it('shows the no privileges page if user is read only', () => {
renderWithRouter(['/cases/configure'], readCasesPermissions());
expect(screen.getByText('Privileges required')).toBeInTheDocument();
});
});

View file

@ -40,7 +40,7 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
refreshRef,
timelineIntegration,
}) => {
const { basePath, userCanCrud } = useCasesContext();
const { basePath, permissions } = useCasesContext();
const { navigateToAllCases } = useAllCasesNavigation();
const { navigateToCaseView } = useCaseViewNavigation();
useReadonlyHeader();
@ -58,7 +58,7 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
</Route>
<Route path={getCreateCasePath(basePath)}>
{userCanCrud ? (
{permissions.all ? (
<CreateCase
onSuccess={onCreateCaseSuccess}
onCancel={navigateToAllCases}
@ -70,7 +70,7 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
</Route>
<Route path={getCasesConfigurePath(basePath)}>
{userCanCrud ? (
{permissions.all ? (
<ConfigureCases />
) : (
<NoPrivilegesPage pageName={i18n.CONFIGURE_CASES_PAGE_NAME} />

View file

@ -9,7 +9,7 @@ import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { useKibana } from '../../common/lib/kibana';
import { TestProviders } from '../../common/mock';
import { readCasesPermissions, TestProviders } from '../../common/mock';
import { useReadonlyHeader } from './use_readonly_header';
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
@ -33,7 +33,9 @@ describe('CaseContainerComponent', () => {
it('displays the readonly glasses badge read permissions but not write', () => {
renderHook(() => useReadonlyHeader(), {
wrapper: ({ children }) => <TestProviders userCanCrud={false}>{children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}>{children}</TestProviders>
),
});
expect(mockedSetBadge).toBeCalledTimes(1);

View file

@ -15,19 +15,19 @@ 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 { userCanCrud } = useCasesContext();
const { permissions } = 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 (!userCanCrud) {
if (!permissions.all && permissions.read) {
chrome.setBadge({
text: i18n.READ_ONLY_BADGE_TEXT,
tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
iconType: 'glasses',
});
}
}, [chrome, userCanCrud]);
}, [chrome, permissions]);
useEffect(() => {
setBadge();

View file

@ -42,7 +42,6 @@ describe('CaseActionBar', () => {
isLoading: false,
onUpdateField,
currentExternalIncident: null,
userCanCrud: true,
metricsFeatures: [],
};

View file

@ -29,6 +29,7 @@ import { useCasesFeatures } from '../cases_context/use_cases_features';
import { FormattedRelativePreferenceDate } from '../formatted_date';
import { getStatusDate, getStatusTitle } from './helpers';
import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page';
import { useCasesContext } from '../cases_context/use_cases_context';
const MyDescriptionList = styled(EuiDescriptionList)`
${({ theme }) => css`
@ -45,16 +46,15 @@ const MyDescriptionList = styled(EuiDescriptionList)`
export interface CaseActionBarProps {
caseData: Case;
userCanCrud: boolean;
isLoading: boolean;
onUpdateField: (args: OnUpdateFields) => void;
}
const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
caseData,
userCanCrud,
isLoading,
onUpdateField,
}) => {
const { permissions } = useCasesContext();
const { isSyncAlertsEnabled, metricsFeatures } = useCasesFeatures();
const date = useMemo(() => getStatusDate(caseData), [caseData]);
const title = useMemo(() => getStatusTitle(caseData.status), [caseData.status]);
@ -107,7 +107,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
<EuiDescriptionListDescription>
<StatusContextMenu
currentStatus={caseData.status}
disabled={!userCanCrud || isLoading}
disabled={!permissions.all || isLoading}
onStatusChanged={onStatusChanged}
/>
</EuiDescriptionListDescription>
@ -134,7 +134,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
responsive={false}
justifyContent="spaceBetween"
>
{userCanCrud && isSyncAlertsEnabled && (
{permissions.all && isSyncAlertsEnabled && (
<EuiFlexItem grow={false}>
<EuiDescriptionListTitle>
<EuiFlexGroup
@ -172,7 +172,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
</EuiButtonEmpty>
</span>
</EuiFlexItem>
{userCanCrud && (
{permissions.all && (
<EuiFlexItem grow={false} data-test-subj="case-view-actions">
<Actions
caseData={caseData}

View file

@ -40,7 +40,7 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
showAlertDetails,
useFetchAlertData,
}) => {
const { userCanCrud, features } = useCasesContext();
const { features } = useCasesContext();
const { navigateToCaseView } = useCaseViewNavigation();
const { urlParams } = useUrlParams();
const refreshCaseViewPage = useRefreshCaseViewPage();
@ -171,7 +171,6 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
data-test-subj="case-view-title"
titleNode={
<EditableTitle
userCanCrud={userCanCrud}
isLoading={isLoading && loadingKey === 'title'}
title={caseData.title}
onSubmit={onSubmitTitle}
@ -181,7 +180,6 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
>
<CaseActionBar
caseData={caseData}
userCanCrud={userCanCrud}
isLoading={isLoading && (loadingKey === 'status' || loadingKey === 'settings')}
onUpdateField={onUpdateField}
/>

View file

@ -39,7 +39,7 @@ export const CaseViewActivity = ({
showAlertDetails?: (alertId: string, index: string) => void;
useFetchAlertData: UseFetchAlertData;
}) => {
const { userCanCrud } = useCasesContext();
const { permissions } = useCasesContext();
const { getCaseViewUrl } = useCaseViewNavigation();
const { data: userActionsData, isLoading: isLoadingUserActions } = useGetCaseUserActions(
@ -133,7 +133,7 @@ export const CaseViewActivity = ({
onShowAlertDetails={onShowAlertDetails}
onUpdateField={onUpdateField}
statusActionButton={
userCanCrud ? (
permissions.all ? (
<StatusActionButton
status={caseData.status}
onStatusChanged={changeStatus}
@ -142,7 +142,6 @@ export const CaseViewActivity = ({
) : null
}
useFetchAlertData={useFetchAlertData}
userCanCrud={userCanCrud}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -150,7 +149,7 @@ export const CaseViewActivity = ({
</EuiFlexItem>
<EuiFlexItem grow={2}>
<SeveritySidebarSelector
isDisabled={!userCanCrud}
isDisabled={!permissions.all}
isLoading={isLoading}
selectedSeverity={caseData.severity}
onSeverityChange={onUpdateSeverity}
@ -171,7 +170,6 @@ export const CaseViewActivity = ({
/>
) : null}
<TagList
userCanCrud={userCanCrud}
tags={caseData.tags}
onSubmit={onSubmitTags}
isLoading={isLoading && loadingKey === 'tags'}
@ -182,12 +180,11 @@ export const CaseViewActivity = ({
caseServices={userActionsData.caseServices}
connectorName={connectorName}
connectors={connectors}
hasDataToPush={userActionsData.hasDataToPush && userCanCrud}
hasDataToPush={userActionsData.hasDataToPush}
isLoading={isLoadingConnectors || (isLoading && loadingKey === 'connector')}
isValidConnector={isLoadingConnectors ? true : isValidConnector}
onSubmit={onSubmitConnector}
userActions={userActionsData.caseUserActions}
userCanCrud={userCanCrud}
/>
) : null}
</EuiFlexItem>

View file

@ -189,7 +189,6 @@ describe('CaseView', () => {
onComponentInitialized: jest.fn(),
showAlertDetails: jest.fn(),
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
userCanCrud: true,
}}
/>
);

View file

@ -7,6 +7,7 @@
import React, { useState, useEffect, useReducer, Dispatch } from 'react';
import { merge } from 'lodash';
import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect';
import { DEFAULT_FEATURES } from '../../../common/constants';
import { DEFAULT_BASE_PATH } from '../../common/navigation';
import { useApplication } from './use_application';
@ -27,7 +28,10 @@ export interface CasesContextValue {
owner: string[];
appId: string;
appTitle: string;
userCanCrud: boolean;
permissions: {
all: boolean;
read: boolean;
};
basePath: string;
features: CasesFeaturesAllRequired;
releasePhase: ReleasePhase;
@ -37,7 +41,7 @@ export interface CasesContextValue {
export interface CasesContextProps
extends Pick<
CasesContextValue,
'owner' | 'userCanCrud' | 'externalReferenceAttachmentTypeRegistry'
'owner' | 'permissions' | 'externalReferenceAttachmentTypeRegistry'
> {
basePath?: string;
features?: CasesFeatures;
@ -56,7 +60,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
value: {
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
basePath = DEFAULT_BASE_PATH,
features = {},
releasePhase = 'ga',
@ -67,7 +71,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
const [value, setValue] = useState<CasesContextStateValue>(() => ({
externalReferenceAttachmentTypeRegistry,
owner,
userCanCrud,
permissions,
basePath,
/**
* The empty object at the beginning avoids the mutation
@ -83,7 +87,14 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
}));
/**
* `userCanCrud` prop may change by the parent plugin.
* Only update the context if the nested permissions fields changed, this avoids a rerender when the object's reference
* changes.
*/
useDeepCompareEffect(() => {
setValue((prev) => ({ ...prev, permissions }));
}, [permissions]);
/**
* `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.
*/
@ -93,10 +104,9 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
...prev,
appId,
appTitle,
userCanCrud,
}));
}
}, [appTitle, appId, userCanCrud]);
}, [appTitle, appId]);
return isCasesContextValue(value) ? (
<CasesContext.Provider value={value}>
@ -108,7 +118,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
CasesProvider.displayName = 'CasesProvider';
function isCasesContextValue(value: CasesContextStateValue): value is CasesContextValue {
return value.appId != null && value.appTitle != null && value.userCanCrud != null;
return value.appId != null && value.appTitle != null && value.permissions != null;
}
// eslint-disable-next-line import/no-default-export

View file

@ -10,7 +10,7 @@ import { ReactWrapper, mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { ConfigureCases } from '.';
import { TestProviders } from '../../common/mock';
import { noCasesPermissions, TestProviders } from '../../common/mock';
import { Connectors } from './connectors';
import { ClosureOptions } from './closure_options';
@ -191,7 +191,7 @@ describe('ConfigureCases', () => {
test('it disables correctly when the user cannot crud', () => {
const newWrapper = mount(<ConfigureCases />, {
wrappingComponent: TestProviders,
wrappingComponentProps: { userCanCrud: false },
wrappingComponentProps: { permissions: noCasesPermissions() },
});
expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe(

View file

@ -51,7 +51,7 @@ const FormWrapper = styled.div`
`;
export const ConfigureCases: React.FC = React.memo(() => {
const { userCanCrud } = useCasesContext();
const { permissions } = useCasesContext();
const { triggersActionsUi } = useKibana().services;
useCasesBreadcrumbs(CasesDeepLinkId.casesConfigure);
@ -225,7 +225,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
<SectionWrapper>
<ClosureOptions
closureTypeSelected={closureType}
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
disabled={persistLoading || isLoadingConnectors || !permissions.all}
onChangeClosureType={onChangeClosureType}
/>
</SectionWrapper>
@ -233,13 +233,13 @@ export const ConfigureCases: React.FC = React.memo(() => {
<Connectors
actionTypes={actionTypes}
connectors={connectors ?? []}
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
disabled={persistLoading || isLoadingConnectors || !permissions.all}
handleShowEditFlyout={onClickUpdateConnector}
isLoading={isLoadingAny}
mappings={mappings}
onChangeConnector={onChangeConnector}
selectedConnector={connector}
updateConnectorDisabled={updateConnectorDisabled || !userCanCrud}
updateConnectorDisabled={updateConnectorDisabled || !permissions.all}
/>
</SectionWrapper>
{ConnectorAddFlyout}

View file

@ -13,6 +13,7 @@ import React from 'react';
import { CasesContext } from '../../cases_context';
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
import { useCasesAddToNewCaseFlyout } from './use_cases_add_to_new_case_flyout';
import { allCasesPermissions } from '../../../common/mock';
import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry';
jest.mock('../../../common/use_cases_toast');
@ -30,7 +31,7 @@ describe('use cases add to new case flyout hook', () => {
value={{
externalReferenceAttachmentTypeRegistry,
owner: ['test'],
userCanCrud: true,
permissions: allCasesPermissions(),
appId: 'test',
appTitle: 'jest',
basePath: '/jest',

View file

@ -11,7 +11,12 @@ import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EditConnector, EditConnectorProps } from '.';
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
import {
AppMockRenderer,
createAppMockRenderer,
readCasesPermissions,
TestProviders,
} from '../../common/mock';
import { basicCase, basicPush, caseUserActions, connectorsMock } from '../../containers/mock';
import { CaseConnector } from '../../containers/configure/types';
@ -36,7 +41,6 @@ const getDefaultProps = (): EditConnectorProps => {
isValidConnector: true,
onSubmit,
userActions: caseUserActions,
userCanCrud: true,
};
};
@ -201,11 +205,9 @@ describe('EditConnector ', () => {
});
it('does not allow the connector to be edited when the user does not have write permissions', async () => {
const defaultProps = getDefaultProps();
const props = { ...defaultProps, userCanCrud: false };
const wrapper = mount(
<TestProviders>
<EditConnector {...props} />
<TestProviders permissions={readCasesPermissions()}>
<EditConnector {...getDefaultProps()} />
</TestProviders>
);
await waitFor(() =>

View file

@ -32,6 +32,7 @@ import { getConnectorById, getConnectorsFormValidators } from '../utils';
import { usePushToService } from '../use_push_to_service';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { useApplicationCapabilities } from '../../common/lib/kibana';
import { useCasesContext } from '../cases_context/use_cases_context';
export interface EditConnectorProps {
caseData: Case;
@ -48,7 +49,6 @@ export interface EditConnectorProps {
onSuccess: () => void
) => void;
userActions: CaseUserActions[];
userCanCrud?: boolean;
}
const MyFlexGroup = styled(EuiFlexGroup)`
@ -119,8 +119,8 @@ export const EditConnector = React.memo(
isValidConnector,
onSubmit,
userActions,
userCanCrud = true,
}: EditConnectorProps) => {
const { permissions } = useCasesContext();
const caseFields = caseData.connector.fields;
const selectedConnector = caseData.connector.id;
@ -273,7 +273,6 @@ export const EditConnector = React.memo(
connectors,
hasDataToPush,
onEditClick,
userCanCrud,
isValidConnector,
});
@ -289,7 +288,7 @@ export const EditConnector = React.memo(
<h4>{i18n.CONNECTORS}</h4>
</EuiFlexItem>
{isLoading && <EuiLoadingSpinner data-test-subj="connector-loading" />}
{!isLoading && !editConnector && userCanCrud && actionsReadCapabilities && (
{!isLoading && !editConnector && permissions.all && actionsReadCapabilities && (
<EuiFlexItem data-test-subj="connector-edit" grow={false}>
<EuiButtonIcon
data-test-subj="connector-edit-button"
@ -317,7 +316,7 @@ export const EditConnector = React.memo(
connectors,
dataTestSubj: 'caseConnectors',
defaultValue: selectedConnector,
disabled: !userCanCrud,
disabled: !permissions.all,
idAria: 'caseConnectors',
isEdit: editConnector,
isLoading,
@ -373,7 +372,7 @@ export const EditConnector = React.memo(
{pushCallouts == null &&
!isLoading &&
!editConnector &&
userCanCrud &&
permissions.all &&
actionsReadCapabilities && (
<EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}>
<span>{pushButton}</span>

View file

@ -41,7 +41,10 @@ exports[`EditableTitle renders 1`] = `
"owner": Array [
"securitySolution",
],
"userCanCrud": true,
"permissions": Object {
"all": true,
"read": true,
},
}
}
>
@ -49,7 +52,6 @@ exports[`EditableTitle renders 1`] = `
isLoading={false}
onSubmit={[MockFunction]}
title="Test title"
userCanCrud={true}
/>
</CasesProvider>
</QueryClientProvider>

View file

@ -41,7 +41,10 @@ exports[`HeaderPage it renders 1`] = `
"owner": Array [
"securitySolution",
],
"userCanCrud": true,
"permissions": Object {
"all": true,
"read": true,
},
}
}
>

View file

@ -9,7 +9,12 @@ import { shallow } from 'enzyme';
import React from 'react';
import '../../common/mock/match_media';
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
import {
AppMockRenderer,
createAppMockRenderer,
readCasesPermissions,
TestProviders,
} from '../../common/mock';
import { EditableTitle, EditableTitleProps } from './editable_title';
import { useMountAppended } from '../../utils/use_mount_appended';
@ -20,7 +25,6 @@ describe('EditableTitle', () => {
title: 'Test title',
onSubmit: submitTitle,
isLoading: false,
userCanCrud: true,
};
beforeEach(() => {
@ -39,8 +43,8 @@ describe('EditableTitle', () => {
it('does not show the edit icon when the user does not have edit permissions', () => {
const wrapper = mount(
<TestProviders>
<EditableTitle {...{ ...defaultProps, userCanCrud: false }} />
<TestProviders permissions={readCasesPermissions()}>
<EditableTitle {...defaultProps} />
</TestProviders>
);

View file

@ -37,19 +37,13 @@ const MySpinner = styled(EuiLoadingSpinner)`
`;
export interface EditableTitleProps {
userCanCrud: boolean;
isLoading: boolean;
title: string;
onSubmit: (title: string) => void;
}
const EditableTitleComponent: React.FC<EditableTitleProps> = ({
userCanCrud = false,
onSubmit,
isLoading,
title,
}) => {
const { releasePhase } = useCasesContext();
const EditableTitleComponent: React.FC<EditableTitleProps> = ({ onSubmit, isLoading, title }) => {
const { releasePhase, permissions } = useCasesContext();
const [editMode, setEditMode] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
const [newTitle, setNewTitle] = useState<string>(title);
@ -124,7 +118,7 @@ const EditableTitleComponent: React.FC<EditableTitleProps> = ({
) : (
<Title title={title} releasePhase={releasePhase}>
{isLoading && <MySpinner data-test-subj="editable-title-loading" />}
{!isLoading && userCanCrud && (
{!isLoading && permissions.all && (
<MyEuiButtonIcon
aria-label={i18n.EDIT_TITLE_ARIA(title as string)}
iconType="pencil"

View file

@ -8,7 +8,7 @@
import React from 'react';
import { mount } from 'enzyme';
import { TestProviders } from '../../../common/mock';
import { readCasesPermissions, TestProviders } from '../../../common/mock';
import { NoCases } from '.';
jest.mock('../../../common/navigation/hooks');
@ -27,7 +27,7 @@ describe('NoCases', () => {
it('displays a message without a link to create a case when the user does not have write permissions', () => {
const wrapper = mount(
<TestProviders userCanCrud={false}>
<TestProviders permissions={readCasesPermissions()}>
<NoCases />
</TestProviders>
);

View file

@ -13,7 +13,7 @@ import { useCasesContext } from '../../cases_context/use_cases_context';
import { useCreateCaseNavigation } from '../../../common/navigation';
const NoCasesComponent = () => {
const { userCanCrud } = useCasesContext();
const { permissions } = useCasesContext();
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
const navigateToCreateCaseClick = useCallback(
@ -24,7 +24,7 @@ const NoCasesComponent = () => {
[navigateToCreateCase]
);
return userCanCrud ? (
return permissions.all ? (
<>
<span>{i18n.NO_CASES}</span>
<LinkAnchor

View file

@ -10,7 +10,7 @@ import { mount } from 'enzyme';
import { TagList, TagListProps } from '.';
import { getFormMock } from '../__mock__/form';
import { TestProviders } from '../../common/mock';
import { readCasesPermissions, TestProviders } from '../../common/mock';
import { waitFor } from '@testing-library/react';
import { useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib/hooks/use_form';
import { useGetTags } from '../../containers/use_get_tags';
@ -33,7 +33,6 @@ jest.mock('@elastic/eui', () => {
});
const onSubmit = jest.fn();
const defaultProps: TagListProps = {
userCanCrud: true,
isLoading: false,
onSubmit,
tags: [],
@ -109,10 +108,9 @@ describe('TagList ', () => {
});
it('does not render when the user does not have write permissions', () => {
const props = { ...defaultProps, userCanCrud: false };
const wrapper = mount(
<TestProviders>
<TagList {...props} />
<TestProviders permissions={readCasesPermissions()}>
<TagList {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="tag-list-edit"]`).exists()).toBeFalsy();

View file

@ -24,11 +24,11 @@ import { schema } from './schema';
import { useGetTags } from '../../containers/use_get_tags';
import { Tags } from './tags';
import { useCasesContext } from '../cases_context/use_cases_context';
const CommonUseField = getUseField({ component: Field });
export interface TagListProps {
userCanCrud?: boolean;
isLoading: boolean;
onSubmit: (a: string[]) => void;
tags: string[];
@ -55,144 +55,143 @@ const ColumnFlexGroup = styled(EuiFlexGroup)`
`}
`;
export const TagList = React.memo(
({ userCanCrud = true, isLoading, onSubmit, tags }: TagListProps) => {
const initialState = { tags };
const { form } = useForm({
defaultValue: initialState,
options: { stripEmptyFields: false },
schema,
});
const { submit } = form;
const [isEditTags, setIsEditTags] = useState(false);
export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) => {
const { permissions } = useCasesContext();
const initialState = { tags };
const { form } = useForm({
defaultValue: initialState,
options: { stripEmptyFields: false },
schema,
});
const { submit } = form;
const [isEditTags, setIsEditTags] = useState(false);
const onSubmitTags = useCallback(async () => {
const { isValid, data: newData } = await submit();
if (isValid && newData.tags) {
onSubmit(newData.tags);
form.reset({ defaultValue: newData });
setIsEditTags(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onSubmit, submit]);
const onSubmitTags = useCallback(async () => {
const { isValid, data: newData } = await submit();
if (isValid && newData.tags) {
onSubmit(newData.tags);
form.reset({ defaultValue: newData });
setIsEditTags(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onSubmit, submit]);
const { data: tagOptions = [] } = useGetTags();
const [options, setOptions] = useState(
tagOptions.map((label) => ({
label,
}))
);
const { data: tagOptions = [] } = useGetTags();
const [options, setOptions] = useState(
tagOptions.map((label) => ({
label,
}))
);
useEffect(
() =>
setOptions(
tagOptions.map((label) => ({
label,
}))
),
[tagOptions]
);
return (
<EuiText data-test-subj="case-view-tag-list">
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceBetween"
responsive={false}
>
<EuiFlexItem grow={false}>
<h4>{i18n.TAGS}</h4>
useEffect(
() =>
setOptions(
tagOptions.map((label) => ({
label,
}))
),
[tagOptions]
);
return (
<EuiText data-test-subj="case-view-tag-list">
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceBetween"
responsive={false}
>
<EuiFlexItem grow={false}>
<h4>{i18n.TAGS}</h4>
</EuiFlexItem>
{isLoading && <EuiLoadingSpinner data-test-subj="tag-list-loading" />}
{!isLoading && permissions.all && (
<EuiFlexItem data-test-subj="tag-list-edit" grow={false}>
<EuiButtonIcon
data-test-subj="tag-list-edit-button"
aria-label={i18n.EDIT_TAGS_ARIA}
iconType={'pencil'}
onClick={setIsEditTags.bind(null, true)}
/>
</EuiFlexItem>
{isLoading && <EuiLoadingSpinner data-test-subj="tag-list-loading" />}
{!isLoading && userCanCrud && (
<EuiFlexItem data-test-subj="tag-list-edit" grow={false}>
<EuiButtonIcon
data-test-subj="tag-list-edit-button"
aria-label={i18n.EDIT_TAGS_ARIA}
iconType={'pencil'}
onClick={setIsEditTags.bind(null, true)}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
<MyFlexGroup gutterSize="none" data-test-subj="case-tags">
{tags.length === 0 && !isEditTags && <p data-test-subj="no-tags">{i18n.NO_TAGS}</p>}
{!isEditTags && (
)}
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
<MyFlexGroup gutterSize="none" data-test-subj="case-tags">
{tags.length === 0 && !isEditTags && <p data-test-subj="no-tags">{i18n.NO_TAGS}</p>}
{!isEditTags && (
<EuiFlexItem>
<Tags tags={tags} color="hollow" />
</EuiFlexItem>
)}
{isEditTags && (
<ColumnFlexGroup data-test-subj="edit-tags" direction="column">
<EuiFlexItem>
<Tags tags={tags} color="hollow" />
</EuiFlexItem>
)}
{isEditTags && (
<ColumnFlexGroup data-test-subj="edit-tags" direction="column">
<EuiFlexItem>
<Form form={form}>
<CommonUseField
path="tags"
componentProps={{
idAria: 'caseTags',
'data-test-subj': 'caseTags',
euiFieldProps: {
fullWidth: true,
placeholder: '',
options,
noSuggestions: false,
},
}}
/>
<FormDataProvider pathsToWatch="tags">
{({ tags: anotherTags }) => {
const current: string[] = options.map((opt) => opt.label);
const newOptions = anotherTags.reduce((acc: string[], item: string) => {
if (!acc.includes(item)) {
return [...acc, item];
}
return acc;
}, current);
if (!isEqual(current, newOptions)) {
setOptions(
newOptions.map((label: string) => ({
label,
}))
);
<Form form={form}>
<CommonUseField
path="tags"
componentProps={{
idAria: 'caseTags',
'data-test-subj': 'caseTags',
euiFieldProps: {
fullWidth: true,
placeholder: '',
options,
noSuggestions: false,
},
}}
/>
<FormDataProvider pathsToWatch="tags">
{({ tags: anotherTags }) => {
const current: string[] = options.map((opt) => opt.label);
const newOptions = anotherTags.reduce((acc: string[], item: string) => {
if (!acc.includes(item)) {
return [...acc, item];
}
return null;
}}
</FormDataProvider>
</Form>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
color="success"
data-test-subj="edit-tags-submit"
fill
iconType="save"
onClick={onSubmitTags}
size="s"
>
{i18n.SAVE}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="edit-tags-cancel"
iconType="cross"
onClick={setIsEditTags.bind(null, false)}
size="s"
>
{i18n.CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</ColumnFlexGroup>
)}
</MyFlexGroup>
</EuiText>
);
}
);
return acc;
}, current);
if (!isEqual(current, newOptions)) {
setOptions(
newOptions.map((label: string) => ({
label,
}))
);
}
return null;
}}
</FormDataProvider>
</Form>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
color="success"
data-test-subj="edit-tags-submit"
fill
iconType="save"
onClick={onSubmitTags}
size="s"
>
{i18n.SAVE}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="edit-tags-cancel"
iconType="cross"
onClick={setIsEditTags.bind(null, false)}
size="s"
>
{i18n.CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</ColumnFlexGroup>
)}
</MyFlexGroup>
</EuiText>
);
});
TagList.displayName = 'TagList';

View file

@ -11,7 +11,7 @@ import { render, screen } from '@testing-library/react';
import '../../common/mock/match_media';
import { usePushToService, ReturnUsePushToService, UsePushToService } from '.';
import { TestProviders } from '../../common/mock';
import { readCasesPermissions, TestProviders } from '../../common/mock';
import { CaseStatuses, ConnectorTypes } from '../../../common/api';
import { usePostPushToService } from '../../containers/use_post_push_to_service';
import { basicPush, actionLicenses, connectorsMock } from '../../containers/mock';
@ -70,7 +70,6 @@ describe('usePushToService', () => {
hasDataToPush: true,
onEditClick,
isValidConnector: true,
userCanCrud: true,
};
beforeEach(() => {
@ -281,8 +280,6 @@ describe('usePushToService', () => {
});
describe('user does not have write permissions', () => {
const noWriteProps = { ...defaultArgs, userCanCrud: false };
it('does not display a message when user does not have a premium license', async () => {
useFetchActionLicenseMock.mockImplementation(() => ({
isLoading: false,
@ -293,9 +290,11 @@ describe('usePushToService', () => {
}));
await act(async () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() => usePushToService(noWriteProps),
() => usePushToService(defaultArgs),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();
@ -313,9 +312,11 @@ describe('usePushToService', () => {
}));
await act(async () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() => usePushToService(noWriteProps),
() => usePushToService(defaultArgs),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();
@ -328,7 +329,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() =>
usePushToService({
...noWriteProps,
...defaultArgs,
connectors: [],
connector: {
id: 'none',
@ -338,7 +339,9 @@ describe('usePushToService', () => {
},
}),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();
@ -351,7 +354,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() =>
usePushToService({
...noWriteProps,
...defaultArgs,
connector: {
id: 'none',
name: 'none',
@ -360,7 +363,9 @@ describe('usePushToService', () => {
},
}),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();
@ -373,7 +378,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() =>
usePushToService({
...noWriteProps,
...defaultArgs,
connector: {
id: 'not-exist',
name: 'not-exist',
@ -383,7 +388,9 @@ describe('usePushToService', () => {
isValidConnector: false,
}),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();
@ -396,7 +403,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() =>
usePushToService({
...noWriteProps,
...defaultArgs,
connectors: [],
connector: {
id: 'not-exist',
@ -407,7 +414,9 @@ describe('usePushToService', () => {
isValidConnector: false,
}),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();
@ -420,11 +429,13 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
() =>
usePushToService({
...noWriteProps,
...defaultArgs,
caseStatus: CaseStatuses.closed,
}),
{
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
wrapper: ({ children }) => (
<TestProviders permissions={readCasesPermissions()}> {children}</TestProviders>
),
}
);
await waitForNextUpdate();

View file

@ -23,6 +23,7 @@ import { CaseServices } from '../../containers/use_get_case_user_actions';
import { ErrorMessage } from './callout/types';
import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page';
import { useGetActionLicense } from '../../containers/use_get_action_license';
import { useCasesContext } from '../cases_context/use_cases_context';
export interface UsePushToService {
caseId: string;
@ -33,7 +34,6 @@ export interface UsePushToService {
hasDataToPush: boolean;
isValidConnector: boolean;
onEditClick: () => void;
userCanCrud: boolean;
}
export interface ReturnUsePushToService {
@ -50,8 +50,8 @@ export const usePushToService = ({
hasDataToPush,
isValidConnector,
onEditClick,
userCanCrud,
}: UsePushToService): ReturnUsePushToService => {
const { permissions } = useCasesContext();
const { isLoading, pushCaseToExternalService } = usePostPushToService();
const { isLoading: loadingLicense, data: actionLicense = null } = useGetActionLicense();
@ -76,7 +76,7 @@ export const usePushToService = ({
// these message require that the user do some sort of write action as a result of the message, readonly users won't
// be able to perform such an action so let's not display the error to the user in that situation
if (!userCanCrud) {
if (!permissions.all) {
return errors;
}
@ -114,7 +114,7 @@ export const usePushToService = ({
return errors;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, userCanCrud]);
}, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, permissions.all]);
const pushToServiceButton = useMemo(
() => (
@ -126,7 +126,7 @@ export const usePushToService = ({
isLoading ||
loadingLicense ||
errorsMsg.length > 0 ||
!userCanCrud ||
!permissions.all ||
!isValidConnector ||
!hasDataToPush
}
@ -146,29 +146,26 @@ export const usePushToService = ({
hasDataToPush,
isLoading,
loadingLicense,
userCanCrud,
permissions.all,
isValidConnector,
]
);
const objToReturn = useMemo(
() => ({
pushButton:
errorsMsg.length > 0 || !hasDataToPush ? (
<EuiToolTip
position="top"
title={
errorsMsg.length > 0 ? errorsMsg[0].title : i18n.PUSH_LOCKED_TITLE(connector.name)
}
content={
<p>{errorsMsg.length > 0 ? errorsMsg[0].description : i18n.PUSH_LOCKED_DESC}</p>
}
>
{pushToServiceButton}
</EuiToolTip>
) : (
<>{pushToServiceButton}</>
),
const objToReturn = useMemo(() => {
const hidePushButton = errorsMsg.length > 0 || !hasDataToPush || !permissions.all;
return {
pushButton: hidePushButton ? (
<EuiToolTip
position="top"
title={errorsMsg.length > 0 ? errorsMsg[0].title : i18n.PUSH_LOCKED_TITLE(connector.name)}
content={<p>{errorsMsg.length > 0 ? errorsMsg[0].description : i18n.PUSH_LOCKED_DESC}</p>}
>
{pushToServiceButton}
</EuiToolTip>
) : (
<>{pushToServiceButton}</>
),
pushCallouts:
errorsMsg.length > 0 ? (
<CaseCallOut
@ -178,17 +175,17 @@ export const usePushToService = ({
onEditClick={onEditClick}
/>
) : null,
}),
[
connector.name,
connectors.length,
errorsMsg,
hasDataToPush,
hasLicenseError,
onEditClick,
pushToServiceButton,
]
);
};
}, [
connector.name,
connectors.length,
errorsMsg,
hasDataToPush,
hasLicenseError,
onEditClick,
pushToServiceButton,
permissions.all,
]);
return objToReturn;
};

View file

@ -42,7 +42,6 @@ const getCreateCommentUserAction = ({
caseData,
externalReferenceAttachmentTypeRegistry,
comment,
userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
@ -68,7 +67,6 @@ const getCreateCommentUserAction = ({
case CommentType.user:
const userBuilder = createUserAttachmentUserActionBuilder({
comment,
userCanCrud,
outlined: comment.id === selectedOutlineCommentId,
isEdit: manageMarkdownEditIds.includes(comment.id),
commentRefs,
@ -116,7 +114,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({
caseData,
externalReferenceAttachmentTypeRegistry,
userAction,
userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
@ -152,7 +149,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({
userAction: commentUserAction,
externalReferenceAttachmentTypeRegistry,
comment,
userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,

View file

@ -20,7 +20,6 @@ import { UserActionBuilderArgs, UserActionBuilder } from '../types';
type BuilderArgs = Pick<
UserActionBuilderArgs,
| 'userCanCrud'
| 'handleManageMarkdownEditId'
| 'handleSaveComment'
| 'handleManageQuote'
@ -35,7 +34,6 @@ type BuilderArgs = Pick<
export const createUserAttachmentUserActionBuilder = ({
comment,
userCanCrud,
outlined,
isEdit,
isLoading,
@ -95,7 +93,6 @@ export const createUserAttachmentUserActionBuilder = ({
onEdit={handleManageMarkdownEditId.bind(null, comment.id)}
onQuote={handleManageQuote.bind(null, comment.comment)}
onDelete={handleDeleteComment.bind(null, comment.id)}
userCanCrud={userCanCrud}
/>
),
},

View file

@ -8,6 +8,7 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { UserActionContentToolbar, UserActionContentToolbarProps } from './content_toolbar';
import { TestProviders } from '../../common/mock';
jest.mock('../../common/navigation/hooks');
jest.mock('../../common/lib/kibana');
@ -17,7 +18,6 @@ const props: UserActionContentToolbarProps = {
id: '1',
editLabel: 'edit',
quoteLabel: 'quote',
userCanCrud: true,
isLoading: false,
onEdit: jest.fn(),
onQuote: jest.fn(),
@ -27,7 +27,11 @@ describe('UserActionContentToolbar ', () => {
let wrapper: ReactWrapper;
beforeAll(() => {
wrapper = mount(<UserActionContentToolbar {...props} />);
wrapper = mount(
<TestProviders>
<UserActionContentToolbar {...props} />
</TestProviders>
);
});
it('it renders', async () => {

View file

@ -22,7 +22,6 @@ export interface UserActionContentToolbarProps {
onEdit: (id: string) => void;
onQuote: (id: string) => void;
onDelete?: (id: string) => void;
userCanCrud: boolean;
}
const UserActionContentToolbarComponent = ({
@ -36,7 +35,6 @@ const UserActionContentToolbarComponent = ({
onEdit,
onQuote,
onDelete,
userCanCrud,
}: UserActionContentToolbarProps) => (
<EuiFlexGroup responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
@ -53,7 +51,6 @@ const UserActionContentToolbarComponent = ({
onEdit={onEdit}
onQuote={onQuote}
onDelete={onDelete}
userCanCrud={userCanCrud}
commentMarkdown={commentMarkdown}
/>
</EuiFlexItem>

View file

@ -27,7 +27,6 @@ type GetDescriptionUserActionArgs = Pick<
| 'caseData'
| 'commentRefs'
| 'manageMarkdownEditIds'
| 'userCanCrud'
| 'handleManageMarkdownEditId'
| 'handleManageQuote'
> &
@ -38,7 +37,6 @@ export const getDescriptionUserAction = ({
commentRefs,
manageMarkdownEditIds,
isLoadingDescription,
userCanCrud,
onUpdateField,
handleManageMarkdownEditId,
handleManageQuote,
@ -85,7 +83,6 @@ export const getDescriptionUserAction = ({
isLoading={isLoadingDescription}
onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)}
onQuote={handleManageQuote.bind(null, caseData.description)}
userCanCrud={userCanCrud}
/>
),
};

View file

@ -44,7 +44,6 @@ const defaultProps = {
selectedAlertPatterns: ['some-test-pattern'],
statusActionButton: null,
updateCase,
userCanCrud: true,
useFetchAlertData: (): [boolean, Record<string, unknown>] => [
false,
{ 'some-id': { _id: 'some-id' } },

View file

@ -91,7 +91,6 @@ export const UserActions = React.memo(
onUpdateField,
statusActionButton,
useFetchAlertData,
userCanCrud,
}: UserActionTreeProps) => {
const { detailName: caseId, commentId } = useCaseViewParams();
const [initLoading, setInitLoading] = useState(true);
@ -123,7 +122,6 @@ export const UserActions = React.memo(
<AddComment
id={NEW_COMMENT_ID}
caseId={caseId}
userCanCrud={userCanCrud}
ref={(element) => (commentRefs.current[NEW_COMMENT_ID] = element)}
onCommentPosted={handleUpdate}
onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_COMMENT_ID)}
@ -131,14 +129,7 @@ export const UserActions = React.memo(
statusActionButton={statusActionButton}
/>
),
[
caseId,
userCanCrud,
handleUpdate,
handleManageMarkdownEditId,
statusActionButton,
commentRefs,
]
[caseId, handleUpdate, handleManageMarkdownEditId, statusActionButton, commentRefs]
);
useEffect(() => {
@ -157,7 +148,6 @@ export const UserActions = React.memo(
commentRefs,
manageMarkdownEditIds,
isLoadingDescription,
userCanCrud,
onUpdateField,
handleManageMarkdownEditId,
handleManageQuote,
@ -167,7 +157,6 @@ export const UserActions = React.memo(
commentRefs,
manageMarkdownEditIds,
isLoadingDescription,
userCanCrud,
onUpdateField,
handleManageMarkdownEditId,
handleManageQuote,
@ -195,7 +184,6 @@ export const UserActions = React.memo(
caseServices,
comments: caseData.comments,
index,
userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
@ -222,7 +210,6 @@ export const UserActions = React.memo(
descriptionCommentListObj,
caseData,
caseServices,
userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
@ -241,7 +228,9 @@ export const UserActions = React.memo(
]
);
const bottomActions = userCanCrud
const { permissions } = useCasesContext();
const bottomActions = permissions.all
? [
{
username: (

View file

@ -67,7 +67,6 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => {
caseServices,
index: 0,
alertData,
userCanCrud: true,
commentRefs,
manageMarkdownEditIds: [],
selectedOutlineCommentId: '',

View file

@ -10,6 +10,7 @@ import { mount, ReactWrapper } from 'enzyme';
import { UserActionPropertyActions } from './property_actions';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProviders } from '../../common/mock';
jest.mock('../../common/lib/kibana');
@ -24,14 +25,17 @@ const props = {
isLoading: false,
onEdit,
onQuote,
userCanCrud: true,
};
describe('UserActionPropertyActions ', () => {
let wrapper: ReactWrapper;
beforeAll(() => {
wrapper = mount(<UserActionPropertyActions {...props} />);
wrapper = mount(
<TestProviders>
<UserActionPropertyActions {...props} />
</TestProviders>
);
});
beforeEach(() => {
@ -65,7 +69,11 @@ describe('UserActionPropertyActions ', () => {
});
it('shows the spinner when loading', async () => {
wrapper = mount(<UserActionPropertyActions {...props} isLoading={true} />);
wrapper = mount(
<TestProviders>
<UserActionPropertyActions {...props} isLoading={true} />
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
).toBeTruthy();
@ -82,14 +90,22 @@ describe('UserActionPropertyActions ', () => {
deleteConfirmlabel: 'confirm delete me',
};
it('shows the delete button', () => {
const renderResult = render(<UserActionPropertyActions {...deleteProps} />);
const renderResult = render(
<TestProviders>
<UserActionPropertyActions {...deleteProps} />
</TestProviders>
);
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
expect(renderResult.getByTestId('property-actions-trash')).toBeTruthy();
});
it('shows a confirm dialog when the delete button is clicked', () => {
const renderResult = render(<UserActionPropertyActions {...deleteProps} />);
const renderResult = render(
<TestProviders>
<UserActionPropertyActions {...deleteProps} />
</TestProviders>
);
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
userEvent.click(renderResult.getByTestId('property-actions-trash'));
@ -98,7 +114,11 @@ describe('UserActionPropertyActions ', () => {
});
it('closes the confirm dialog when the cancel button is clicked', () => {
const renderResult = render(<UserActionPropertyActions {...deleteProps} />);
const renderResult = render(
<TestProviders>
<UserActionPropertyActions {...deleteProps} />
</TestProviders>
);
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
userEvent.click(renderResult.getByTestId('property-actions-trash'));
@ -109,7 +129,11 @@ describe('UserActionPropertyActions ', () => {
});
it('calls onDelete when the confirm is pressed', () => {
const renderResult = render(<UserActionPropertyActions {...deleteProps} />);
const renderResult = render(
<TestProviders>
<UserActionPropertyActions {...deleteProps} />
</TestProviders>
);
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
userEvent.click(renderResult.getByTestId('property-actions-trash'));

View file

@ -11,6 +11,7 @@ import { EuiConfirmModal, EuiLoadingSpinner } from '@elastic/eui';
import { PropertyActions } from '../property_actions';
import { useLensOpenVisualization } from '../markdown_editor/plugins/lens/use_lens_open_visualization';
import { CANCEL_BUTTON, CONFIRM_BUTTON } from './translations';
import { useCasesContext } from '../cases_context/use_cases_context';
interface UserActionPropertyActionsProps {
id: string;
@ -22,7 +23,6 @@ interface UserActionPropertyActionsProps {
onEdit: (id: string) => void;
onDelete?: (id: string) => void;
onQuote: (id: string) => void;
userCanCrud: boolean;
commentMarkdown: string;
}
@ -36,9 +36,9 @@ const UserActionPropertyActionsComponent = ({
onEdit,
onDelete,
onQuote,
userCanCrud,
commentMarkdown,
}: UserActionPropertyActionsProps) => {
const { permissions } = useCasesContext();
const { canUseEditor, actionConfig } = useLensOpenVisualization({ comment: commentMarkdown });
const onEditClick = useCallback(() => onEdit(id), [id, onEdit]);
const onQuoteClick = useCallback(() => onQuote(id), [id, onQuote]);
@ -62,7 +62,7 @@ const UserActionPropertyActionsComponent = ({
const propertyActions = useMemo(
() =>
[
userCanCrud
permissions.all
? [
{
iconType: 'pencil',
@ -88,7 +88,7 @@ const UserActionPropertyActionsComponent = ({
canUseEditor && actionConfig ? [actionConfig] : [],
].flat(),
[
userCanCrud,
permissions.all,
editLabel,
onEditClick,
deleteLabel,

View file

@ -30,7 +30,6 @@ export interface UserActionTreeProps {
onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void;
statusActionButton: JSX.Element | null;
useFetchAlertData: UseFetchAlertData;
userCanCrud: boolean;
}
type UnsupportedUserActionTypes = typeof UNSUPPORTED_ACTION_TYPES[number];
@ -43,7 +42,6 @@ export interface UserActionBuilderArgs {
caseServices: CaseServices;
comments: Comment[];
index: number;
userCanCrud: boolean;
commentRefs: React.MutableRefObject<
Record<string, AddCommentRefObject | UserActionMarkdownRefObject | null | undefined>
>;

View file

@ -11,6 +11,7 @@ import { fireEvent } from '@testing-library/dom';
import { AddToCaseAction } from './add_to_case_action';
import * as useCaseHook from '../hooks/use_add_to_case';
import * as datePicker from '../components/date_range_picker';
import * as useGetUserCasesPermissionsModule from '../../../../hooks/use_get_user_cases_permissions';
import moment from 'moment';
describe('AddToCaseAction', function () {
@ -81,6 +82,10 @@ describe('AddToCaseAction', function () {
});
it('should be able to click add to case button', async function () {
const mockUseGetUserCasesPermissions = jest
.spyOn(useGetUserCasesPermissionsModule, 'useGetUserCasesPermissions')
.mockImplementation(() => ({ crud: false, read: false }));
const initSeries = {
data: [
{
@ -106,8 +111,13 @@ describe('AddToCaseAction', function () {
expect(core?.cases?.ui.getAllCasesSelectorModal).toHaveBeenCalledWith(
expect.objectContaining({
owner: ['observability'],
userCanCrud: true,
permissions: {
all: false,
read: false,
},
})
);
mockUseGetUserCasesPermissions.mockRestore();
});
});

View file

@ -15,6 +15,7 @@ import {
GetAllCasesSelectorModalProps,
} from '@kbn/cases-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions';
import { ObservabilityAppServices } from '../../../../application/types';
import { useAddToCase } from '../hooks/use_add_to_case';
import { observabilityFeatureId, observabilityAppId } from '../../../../../common';
@ -38,6 +39,8 @@ export function AddToCaseAction({
timeRange,
}: AddToCaseProps) {
const kServices = useKibana<ObservabilityAppServices>().services;
const userPermissions = useGetUserCasesPermissions();
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
const {
cases,
@ -74,8 +77,8 @@ export function AddToCaseAction({
});
const getAllCasesSelectorModalProps: GetAllCasesSelectorModalProps = {
permissions: casesPermissions,
onRowClick: onCaseClicked,
userCanCrud: true,
owner: [owner],
onClose: () => {
setIsCasesOpen(false);

View file

@ -15,7 +15,10 @@ export interface UseGetUserCasesPermissions {
}
export function useGetUserCasesPermissions() {
const [casesPermissions, setCasesPermissions] = useState<UseGetUserCasesPermissions | null>(null);
const [casesPermissions, setCasesPermissions] = useState<UseGetUserCasesPermissions>({
crud: false,
read: false,
});
const uiCapabilities = useKibana().services.application.capabilities;
useEffect(() => {

View file

@ -220,6 +220,7 @@ function AlertsPage() {
const CasesContext = cases.ui.getCasesContext();
const userPermissions = useGetUserCasesPermissions();
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
if (!hasAnyData && !isAllRequestsComplete) {
return <LoadingObservability />;
@ -265,7 +266,7 @@ function AlertsPage() {
<EuiFlexItem>
<CasesContext
owner={[observabilityFeatureId]}
userCanCrud={userPermissions?.crud ?? false}
permissions={casesPermissions}
features={{ alerts: { sync: false } }}
>
<AlertsTableTGrid

View file

@ -201,7 +201,7 @@ function ObservabilityActions({
const actionsMenuItems = useMemo(() => {
return [
...(casePermissions?.crud
...(casePermissions.crud
? [
<EuiContextMenuItem
data-test-subj="add-to-existing-case-action"
@ -246,7 +246,7 @@ function ObservabilityActions({
],
];
}, [
casePermissions?.crud,
casePermissions.crud,
handleAddToExistingCaseClick,
handleAddToNewCaseClick,
linkToRule,

View file

@ -13,17 +13,19 @@ import { usePluginContext } from '../../hooks/use_plugin_context';
import { LazyAlertsFlyout } from '../..';
import { useFetchAlertDetail } from './use_fetch_alert_detail';
import { useFetchAlertData } from './use_fetch_alert_data';
import { UseGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
interface CasesProps {
userCanCrud: boolean;
permissions: UseGetUserCasesPermissions;
}
export const Cases = React.memo<CasesProps>(({ userCanCrud }) => {
export const Cases = React.memo<CasesProps>(({ permissions }) => {
const {
cases,
application: { getUrlForApp, navigateToApp },
} = useKibana().services;
const { observabilityRuleTypeRegistry } = usePluginContext();
const [selectedAlertId, setSelectedAlertId] = useState<string>('');
const casesPermissions = { all: permissions.crud, read: permissions.read };
const handleFlyoutClose = useCallback(() => {
setSelectedAlertId('');
@ -44,7 +46,7 @@ export const Cases = React.memo<CasesProps>(({ userCanCrud }) => {
)}
{cases.ui.getCases({
basePath: CASES_PATH,
userCanCrud,
permissions: casesPermissions,
owner: [CASES_OWNER],
features: { alerts: { sync: false } },
useFetchAlertData,

View file

@ -38,13 +38,13 @@ export const CasesPage = React.memo(() => {
docsLink: docLinks.links.observability.guide,
});
return userPermissions == null || userPermissions?.read ? (
return userPermissions.read ? (
<ObservabilityPageTemplate
isPageDataLoaded={Boolean(hasAnyData || isAllRequestsComplete)}
data-test-subj={noDataConfig ? 'noDataPage' : undefined}
noDataConfig={noDataConfig}
>
<Cases userCanCrud={userPermissions?.crud ?? false} />
<Cases permissions={userPermissions} />
</ObservabilityPageTemplate>
) : (
<CaseFeatureNoPermissions />

View file

@ -128,6 +128,7 @@ export function OverviewPage({ routeParams }: Props) {
const CasesContext = cases.ui.getCasesContext();
const userPermissions = useGetUserCasesPermissions();
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
useEffect(() => {
if (hasAnyData !== true) {
@ -199,7 +200,7 @@ export function OverviewPage({ routeParams }: Props) {
>
<CasesContext
owner={[observabilityFeatureId]}
userCanCrud={userPermissions?.crud ?? false}
permissions={casesPermissions}
features={{ alerts: { sync: false } }}
>
<AlertsTableTGrid

View file

@ -56,7 +56,8 @@ const StartAppComponent: FC<StartAppComponent> = ({
cases,
} = useKibana().services;
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
const casesPermissions = useGetUserCasesPermissions();
const userPermissions = useGetUserCasesPermissions();
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
const CasesContext = cases.ui.getCasesContext();
return (
<EuiErrorBoundary>
@ -69,10 +70,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<ManageUserInfo>
<ReactQueryClientProvider>
<CasesContext
owner={[APP_ID]}
userCanCrud={casesPermissions?.crud ?? false}
>
<CasesContext owner={[APP_ID]} permissions={casesPermissions}>
<PageRouter
history={history}
onAppLeave={onAppLeave}

View file

@ -46,6 +46,7 @@ const CaseContainerComponent: React.FC = () => {
const { cases } = useKibana().services;
const { getAppUrl, navigateTo } = useNavigation();
const userPermissions = useGetUserCasesPermissions();
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
const dispatch = useDispatch();
const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl(
SecurityPageName.rules
@ -147,7 +148,7 @@ const CaseContainerComponent: React.FC = () => {
},
},
useFetchAlertData,
userCanCrud: userPermissions?.crud ?? false,
permissions: casesPermissions,
})}
</CaseDetailsRefreshContext.Provider>
<SpyRoute pageName={SecurityPageName.case} />

View file

@ -20,8 +20,16 @@ import { mockAlertDetailsData } from './__mocks__';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { TimelineTabs } from '../../../../common/types/timeline';
import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment';
import { useGetUserCasesPermissions } from '../../lib/kibana';
jest.mock('../../lib/kibana');
const originalKibanaLib = jest.requireActual('../../lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../containers/cti/event_enrichment');
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_with_fallback', () => {

View file

@ -29,7 +29,7 @@ export const RelatedCases: React.FC<Props> = React.memo(({ eventId, isReadOnly }
const [relatedCases, setRelatedCases] = useState<RelatedCaseList>([]);
const [areCasesLoading, setAreCasesLoading] = useState(true);
const [hasError, setHasError] = useState<boolean>(false);
const hasCasesReadPermissions = casePermissions?.read ?? false;
const hasCasesReadPermissions = casePermissions.read;
const getRelatedCases = useCallback(async () => {
let relatedCaseList: RelatedCaseList = [];

View file

@ -24,9 +24,17 @@ import { getDefaultControlColumn } from '../../../timelines/components/timeline/
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
import { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser';
import { useGetUserCasesPermissions } from '../../lib/kibana';
jest.mock('../../lib/kibana');
const originalKibanaLib = jest.requireActual('../../lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
}));

View file

@ -131,6 +131,16 @@ Object {
"name": "Timelines",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-cases",
"disabled": false,
"href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "cases",
"isSelected": false,
"name": "Cases",
"onClick": [Function],
},
],
"name": "Investigate",
},

View file

@ -23,6 +23,13 @@ import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pag
jest.mock('../../../lib/kibana/kibana_react');
jest.mock('../../../lib/kibana');
const originalKibanaLib = jest.requireActual('../../../lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../../hooks/use_selector');
jest.mock('../../../hooks/use_experimental_features');
jest.mock('../../../utils/route/use_route_spy');
@ -132,7 +139,7 @@ describe('useSecuritySolutionNavigation', () => {
});
it('should omit host isolation exceptions if hook reports false', () => {
(useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValueOnce(false);
(useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(false);
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
() => useSecuritySolutionNavigation(),
{ wrapper: TestProviders }

View file

@ -68,7 +68,7 @@ export const usePrimaryNavigationItems = ({
};
function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
const hasCasesReadPermissions = useGetUserCasesPermissions().read;
const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu();
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
const uiCapabilities = useKibana().services.application.capabilities;

View file

@ -12,9 +12,17 @@ import { TEST_ID, SessionsView, defaultSessionsFilter } from '.';
import { EntityType, TimelineId } from '@kbn/timelines-plugin/common';
import { SessionsComponentsProps } from './types';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { useGetUserCasesPermissions } from '../../lib/kibana';
jest.mock('../../lib/kibana');
const originalKibanaLib = jest.requireActual('../../lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../url_state/normalize_time_range');
const startDate = '2022-03-22T22:10:56.794Z';

View file

@ -14,7 +14,6 @@ import { ModalInspectQuery } from '../inspect/modal';
import { useInspect } from '../inspect/use_inspect';
import { useLensAttributes } from './use_lens_attributes';
import { useAddToExistingCase } from './use_add_to_existing_case';
import { useGetUserCasesPermissions } from '../../lib/kibana';
import { useAddToNewCase } from './use_add_to_new_case';
import { VisualizationActionsProps } from './types';
import {
@ -54,8 +53,6 @@ const VisualizationActionsComponent: React.FC<VisualizationActionsProps> = ({
stackByField,
}) => {
const { lens } = useKibana().services;
const userPermissions = useGetUserCasesPermissions();
const userCanCrud = userPermissions?.crud ?? false;
const { canUseEditor, navigateToPrefilledEditor } = lens;
const [isPopoverOpen, setPopover] = useState(false);
@ -82,14 +79,12 @@ const VisualizationActionsComponent: React.FC<VisualizationActionsProps> = ({
onAddToCaseClicked: closePopover,
lensAttributes: attributes,
timeRange: timerange,
userCanCrud,
});
const { onAddToNewCaseClicked, disabled: isAddToNewCaseDisabled } = useAddToNewCase({
onClick: closePopover,
timeRange: timerange,
lensAttributes: attributes,
userCanCrud,
});
const onOpenInLens = useCallback(() => {

View file

@ -5,25 +5,45 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { useKibana } from '../../lib/kibana';
import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
import { useAddToExistingCase } from './use_add_to_existing_case';
import { useGetUserCasesPermissions } from '../../lib/kibana';
jest.mock('../../lib/kibana/kibana_react');
const mockedUseKibana = mockUseKibana();
const mockGetUseCasesAddToExistingCaseModal = jest.fn();
jest.mock('../../lib/kibana', () => {
const original = jest.requireActual('../../lib/kibana');
return {
...original,
useGetUserCasesPermissions: jest.fn(),
useKibana: () => ({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
cases: {
hooks: {
getUseCasesAddToExistingCaseModal: mockGetUseCasesAddToExistingCaseModal,
},
},
},
}),
};
});
describe('useAddToExistingCase', () => {
const mockCases = mockCasesContract();
const mockOnAddToCaseClicked = jest.fn();
const timeRange = {
from: '2022-03-06T16:00:00.000Z',
to: '2022-03-07T15:59:59.999Z',
};
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
services: {
cases: mockCases,
},
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: true,
read: true,
});
});
@ -32,47 +52,48 @@ describe('useAddToExistingCase', () => {
useAddToExistingCase({
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
userCanCrud: true,
onAddToCaseClicked: mockOnAddToCaseClicked,
})
);
expect(mockCases.hooks.getUseCasesAddToExistingCaseModal).toHaveBeenCalledWith({
expect(mockGetUseCasesAddToExistingCaseModal).toHaveBeenCalledWith({
onClose: mockOnAddToCaseClicked,
toastContent: 'Successfully added visualization to the case',
});
expect(result.current.disabled).toEqual(false);
});
it("button disalbled if user Can't Crud", () => {
it("button disabled if user Can't Crud", () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: false,
read: true,
});
const { result } = renderHook(() =>
useAddToExistingCase({
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
userCanCrud: false,
onAddToCaseClicked: mockOnAddToCaseClicked,
})
);
expect(result.current.disabled).toEqual(true);
});
it('button disalbled if no lensAttributes', () => {
it('button disabled if no lensAttributes', () => {
const { result } = renderHook(() =>
useAddToExistingCase({
lensAttributes: null,
timeRange,
userCanCrud: true,
onAddToCaseClicked: mockOnAddToCaseClicked,
})
);
expect(result.current.disabled).toEqual(true);
});
it('button disalbled if no timeRange', () => {
it('button disabled if no timeRange', () => {
const { result } = renderHook(() =>
useAddToExistingCase({
lensAttributes: kpiHostMetricLensAttributes,
timeRange: null,
userCanCrud: true,
onAddToCaseClicked: mockOnAddToCaseClicked,
})
);

View file

@ -8,7 +8,7 @@ import { useCallback, useMemo } from 'react';
import { CommentType } from '@kbn/cases-plugin/common';
import { APP_ID } from '../../../../common/constants';
import { useKibana } from '../../lib/kibana/kibana_react';
import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana';
import { ADD_TO_CASE_SUCCESS } from './translations';
import { LensAttributes } from './types';
@ -19,13 +19,12 @@ export const useAddToExistingCase = ({
onAddToCaseClicked,
lensAttributes,
timeRange,
userCanCrud,
}: {
onAddToCaseClicked?: () => void;
lensAttributes: LensAttributes | null;
timeRange: { from: string; to: string } | null;
userCanCrud: boolean;
}) => {
const userPermissions = useGetUserCasesPermissions();
const { cases } = useKibana().services;
const attachments = useMemo(() => {
return [
@ -54,6 +53,6 @@ export const useAddToExistingCase = ({
return {
onAddToExistingCaseClicked,
disabled: lensAttributes == null || timeRange == null || !userCanCrud,
disabled: lensAttributes == null || timeRange == null || !userPermissions.crud,
};
};

View file

@ -5,24 +5,45 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { useKibana } from '../../lib/kibana';
import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
import { useAddToNewCase } from './use_add_to_new_case';
import { useGetUserCasesPermissions } from '../../lib/kibana';
jest.mock('../../lib/kibana/kibana_react');
const mockedUseKibana = mockUseKibana();
const mockGetUseCasesAddToNewCaseFlyout = jest.fn();
jest.mock('../../lib/kibana', () => {
const original = jest.requireActual('../../lib/kibana');
return {
...original,
useGetUserCasesPermissions: jest.fn(),
useKibana: () => ({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
cases: {
hooks: {
getUseCasesAddToNewCaseFlyout: mockGetUseCasesAddToNewCaseFlyout,
},
},
},
}),
};
});
describe('useAddToNewCase', () => {
const mockCases = mockCasesContract();
const timeRange = {
from: '2022-03-06T16:00:00.000Z',
to: '2022-03-07T15:59:59.999Z',
};
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
services: {
cases: mockCases,
},
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: true,
read: true,
});
});
@ -31,43 +52,44 @@ describe('useAddToNewCase', () => {
useAddToNewCase({
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
userCanCrud: true,
})
);
expect(mockCases.hooks.getUseCasesAddToNewCaseFlyout).toHaveBeenCalledWith({
expect(mockGetUseCasesAddToNewCaseFlyout).toHaveBeenCalledWith({
toastContent: 'Successfully added visualization to the case',
});
expect(result.current.disabled).toEqual(false);
});
it("button disalbled if user Can't Crud", () => {
it("button disabled if user Can't Crud", () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: false,
read: true,
});
const { result } = renderHook(() =>
useAddToNewCase({
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
userCanCrud: false,
})
);
expect(result.current.disabled).toEqual(true);
});
it('button disalbled if no lensAttributes', () => {
it('button disabled if no lensAttributes', () => {
const { result } = renderHook(() =>
useAddToNewCase({
lensAttributes: null,
timeRange,
userCanCrud: true,
})
);
expect(result.current.disabled).toEqual(true);
});
it('button disalbled if no timeRange', () => {
it('button disabled if no timeRange', () => {
const { result } = renderHook(() =>
useAddToNewCase({
lensAttributes: kpiHostMetricLensAttributes,
timeRange: null,
userCanCrud: true,
})
);
expect(result.current.disabled).toEqual(true);

View file

@ -9,7 +9,7 @@ import { useCallback, useMemo } from 'react';
import { CommentType } from '@kbn/cases-plugin/common';
import { APP_ID } from '../../../../common/constants';
import { useKibana } from '../../lib/kibana/kibana_react';
import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana';
import { ADD_TO_CASE_SUCCESS } from './translations';
import { LensAttributes } from './types';
@ -18,17 +18,12 @@ export interface UseAddToNewCaseProps {
onClick?: () => void;
timeRange: { from: string; to: string } | null;
lensAttributes: LensAttributes | null;
userCanCrud: boolean;
}
const owner = APP_ID;
export const useAddToNewCase = ({
onClick,
timeRange,
lensAttributes,
userCanCrud,
}: UseAddToNewCaseProps) => {
export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddToNewCaseProps) => {
const userPermissions = useGetUserCasesPermissions();
const { cases } = useKibana().services;
const attachments = useMemo(() => {
return [
@ -57,6 +52,6 @@ export const useAddToNewCase = ({
return {
onAddToNewCaseClicked,
disabled: lensAttributes == null || timeRange == null || !userCanCrud,
disabled: lensAttributes == null || timeRange == null || !userPermissions.crud,
};
};

View file

@ -152,7 +152,10 @@ export interface UseGetUserCasesPermissions {
}
export const useGetUserCasesPermissions = () => {
const [casesPermissions, setCasesPermissions] = useState<UseGetUserCasesPermissions | null>(null);
const [casesPermissions, setCasesPermissions] = useState<UseGetUserCasesPermissions>({
crud: false,
read: false,
});
const uiCapabilities = useKibana().services.application.capabilities;
useEffect(() => {

View file

@ -35,7 +35,7 @@ export const useAddToCaseActions = ({
}: UseAddToCaseActions) => {
const { cases: casesUi } = useKibana().services;
const casePermissions = useGetUserCasesPermissions();
const hasWritePermissions = casePermissions?.crud ?? false;
const hasWritePermissions = casePermissions.crud;
const isAlert = useMemo(() => {
return ecsData?.event?.kind?.includes('signal');

View file

@ -20,7 +20,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
const { cases: casesUi } = useKibana().services;
const casePermissions = useGetUserCasesPermissions();
const hasWritePermissions = casePermissions?.crud ?? false;
const hasWritePermissions = casePermissions.crud;
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
onClose,

View file

@ -14,10 +14,11 @@ const MAX_CASES_TO_SHOW = 3;
const RecentCasesComponent = () => {
const { cases } = useKibana().services;
const userCanCrud = useGetUserCasesPermissions()?.crud ?? false;
const permissions = useGetUserCasesPermissions();
const casesPermissions = { all: permissions.crud, read: permissions.read };
return cases.ui.getRecentCases({
userCanCrud,
permissions: casesPermissions,
maxCasesToShow: MAX_CASES_TO_SHOW,
owner: [APP_ID],
});

View file

@ -41,7 +41,7 @@ export const Sidebar = React.memo<{
);
// only render the recently created cases view if the user has at least read permissions
const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
const hasCasesReadPermissions = useGetUserCasesPermissions().read;
return (
<EuiFlexGroup direction="column" responsive={false} gutterSize="l">

View file

@ -53,7 +53,7 @@ const DetectionResponseComponent = () => {
const { indicesExist, indexPattern, loading: isSourcererLoading } = useSourcererDataView();
const { signalIndexName } = useSignalIndex();
const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges();
const canReadCases = useGetUserCasesPermissions()?.read;
const canReadCases = useGetUserCasesPermissions().read;
const canReadAlerts = hasKibanaREAD && hasIndexRead;
if (!canReadAlerts && !canReadCases) {

View file

@ -9,7 +9,7 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useKibana } from '../../../../common/lib/kibana';
import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { mockTimelineModel, TestProviders } from '../../../../common/mock';
import { AddToCaseButton } from '.';
@ -35,6 +35,13 @@ jest.mock('react-redux', () => {
});
jest.mock('../../../../common/lib/kibana');
const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../../../common/hooks/use_selector');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;

View file

@ -68,6 +68,7 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
);
const userPermissions = useGetUserCasesPermissions();
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
const handleButtonClick = useCallback(() => {
setPopover((currentIsOpen) => !currentIsOpen);
@ -163,8 +164,8 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
{isCaseModalOpen &&
cases.ui.getAllCasesSelectorModal({
onRowClick,
userCanCrud: userPermissions?.crud ?? false,
owner: [APP_ID],
permissions: casesPermissions,
})}
</>
);

View file

@ -13,7 +13,11 @@ import { TimelineId } from '../../../../../../common/types/timeline';
import { Ecs } from '../../../../../../common/ecs';
import { mockAlertDetailsData } from '../../../../../common/components/event_details/__mocks__';
import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy';
import { KibanaServices, useKibana } from '../../../../../common/lib/kibana';
import {
KibanaServices,
useKibana,
useGetUserCasesPermissions,
} from '../../../../../common/lib/kibana';
import { coreMock } from '@kbn/core/public/mocks';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
@ -64,7 +68,15 @@ jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
jest.mock('../../../../../detections/components/user_info', () => ({
useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]),
}));
jest.mock('../../../../../common/lib/kibana');
const originalKibanaLib = jest.requireActual('../../../../../common/lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock(
'../../../../../detections/containers/detection_engine/alerts/use_alerts_privileges',
() => ({

View file

@ -33,33 +33,37 @@ jest.mock(
})
);
jest.mock('../../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
application: {
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
jest.mock('../../../../../common/lib/kibana', () => {
const originalKibanaLib = jest.requireActual('../../../../../common/lib/kibana');
return {
useKibana: () => ({
services: {
application: {
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
},
},
cases: mockCasesContract(),
uiSettings: {
get: jest.fn(),
},
savedObjects: {
client: {},
},
timelines: { ...mockTimelines },
},
cases: mockCasesContract(),
uiSettings: {
get: jest.fn(),
},
savedObjects: {
client: {},
},
timelines: { ...mockTimelines },
},
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useGetUserCasesPermissions: jest.fn(),
}));
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useGetUserCasesPermissions: originalKibanaLib.useGetUserCasesPermissions,
};
});
const defaultProps = {
ariaRowindex: 2,

View file

@ -39,29 +39,33 @@ jest.mock('../../../../../common/components/user_privileges', () => {
};
});
jest.mock('../../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
timelines: { ...mockTimelines },
data: {
search: jest.fn(),
query: jest.fn(),
},
application: {
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
jest.mock('../../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../../common/lib/kibana');
return {
useKibana: () => ({
services: {
timelines: { ...mockTimelines },
data: {
search: jest.fn(),
query: jest.fn(),
},
application: {
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
},
},
cases: mockCasesContract(),
},
cases: mockCasesContract(),
},
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useGetUserCasesPermissions: jest.fn(),
}));
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useGetUserCasesPermissions: originalModule.useGetUserCasesPermissions,
};
});
describe('EventColumnView', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);

View file

@ -34,7 +34,6 @@ import { TimelineTabs } from '../../../../../common/types/timeline';
import { defaultRowRenderers } from './renderers';
import { createStore, State } from '../../../../common/store';
jest.mock('../../../../common/lib/kibana/hooks');
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('../../../../common/components/user_privileges', () => {
return {
@ -84,7 +83,6 @@ jest.mock('../../../../common/lib/kibana', () => {
},
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
};
});