mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Cases] Filter user activities pagination (#152702)
## Summary Implements #130227 https://user-images.githubusercontent.com/117571355/224038340-6b1c8cc7-3795-4412-8e67-a026dfe10776.mov ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Flaky Test Runner https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2059 ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8b68ce8558
commit
6eb2c5ed91
38 changed files with 2360 additions and 741 deletions
|
@ -31,6 +31,7 @@ import {
|
|||
caseViewProps,
|
||||
defaultGetCase,
|
||||
defaultGetCaseMetrics,
|
||||
defaultInfiniteUseFindCaseUserActions,
|
||||
defaultUpdateCaseState,
|
||||
defaultUseFindCaseUserActions,
|
||||
} from './mocks';
|
||||
|
@ -39,6 +40,8 @@ import { userProfiles } from '../../containers/user_profiles/api.mock';
|
|||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
import { CASE_VIEW_PAGE_TABS } from '../../../common/types';
|
||||
import { getCaseConnectorsMockResponse } from '../../common/mock/connectors';
|
||||
import { useInfiniteFindCaseUserActions } from '../../containers/use_infinite_find_case_user_actions';
|
||||
import { useGetCaseUserActionsStats } from '../../containers/use_get_case_user_actions_stats';
|
||||
|
||||
const mockSetTitle = jest.fn();
|
||||
|
||||
|
@ -46,6 +49,8 @@ jest.mock('../../containers/use_get_action_license');
|
|||
jest.mock('../../containers/use_update_case');
|
||||
jest.mock('../../containers/use_get_case_metrics');
|
||||
jest.mock('../../containers/use_find_case_user_actions');
|
||||
jest.mock('../../containers/use_infinite_find_case_user_actions');
|
||||
jest.mock('../../containers/use_get_case_user_actions_stats');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/use_get_case');
|
||||
jest.mock('../../containers/configure/use_get_supported_action_connectors');
|
||||
|
@ -80,6 +85,8 @@ const useUrlParamsMock = useUrlParams as jest.Mock;
|
|||
const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock;
|
||||
const useUpdateCaseMock = useUpdateCase as jest.Mock;
|
||||
const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock;
|
||||
const useInfiniteFindCaseUserActionsMock = useInfiniteFindCaseUserActions as jest.Mock;
|
||||
const useGetCaseUserActionsStatsMock = useGetCaseUserActionsStats as jest.Mock;
|
||||
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
|
||||
const usePostPushToServiceMock = usePostPushToService as jest.Mock;
|
||||
const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock;
|
||||
|
@ -111,6 +118,12 @@ export const caseClosedProps: CaseViewPageProps = {
|
|||
caseData: basicCaseClosed,
|
||||
};
|
||||
|
||||
const userActionsStats = {
|
||||
total: 21,
|
||||
totalComments: 9,
|
||||
totalOtherActions: 11,
|
||||
};
|
||||
|
||||
describe('CaseViewPage', () => {
|
||||
const updateCaseProperty = defaultUpdateCaseState.updateCaseProperty;
|
||||
const pushCaseToExternalService = jest.fn();
|
||||
|
@ -118,14 +131,15 @@ describe('CaseViewPage', () => {
|
|||
let appMockRenderer: AppMockRenderer;
|
||||
const caseConnectors = getCaseConnectorsMockResponse();
|
||||
const caseUsers = getCaseUsersMockResponse();
|
||||
const refetchFindCaseUserActions = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetCase();
|
||||
jest.clearAllMocks();
|
||||
mockGetCase();
|
||||
useUpdateCaseMock.mockReturnValue(defaultUpdateCaseState);
|
||||
useGetCaseMetricsMock.mockReturnValue(defaultGetCaseMetrics);
|
||||
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue(defaultInfiniteUseFindCaseUserActions);
|
||||
useGetCaseUserActionsStatsMock.mockReturnValue({ data: userActionsStats, isLoading: false });
|
||||
usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService });
|
||||
useGetCaseConnectorsMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
|
@ -349,22 +363,16 @@ describe('CaseViewPage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show loading content when loading user actions', async () => {
|
||||
it('should show loading content when loading user actions stats', async () => {
|
||||
const useFetchAlertData = jest.fn().mockReturnValue([true]);
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
isFetching: true,
|
||||
refetch: refetchFindCaseUserActions,
|
||||
});
|
||||
useGetCaseUserActionsStatsMock.mockReturnValue({ isLoading: true });
|
||||
|
||||
const result = appMockRenderer.render(
|
||||
<CaseViewPage {...caseProps} useFetchAlertData={useFetchAlertData} />
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.getByTestId('case-view-loading-content')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('user-actions')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('user-actions-list')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -374,7 +382,7 @@ describe('CaseViewPage', () => {
|
|||
<CaseViewPage {...caseProps} showAlertDetails={showAlertDetails} />
|
||||
);
|
||||
|
||||
userEvent.click(result.getByTestId('comment-action-show-alert-alert-action-id'));
|
||||
userEvent.click(result.getAllByTestId('comment-action-show-alert-alert-action-id')[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showAlertDetails).toHaveBeenCalledWith('alert-id-1', 'alert-index-1');
|
||||
|
@ -387,7 +395,7 @@ describe('CaseViewPage', () => {
|
|||
await waitFor(() => {
|
||||
expect(
|
||||
result
|
||||
.getByTestId('user-action-alert-comment-create-action-alert-action-id')
|
||||
.getAllByTestId('user-action-alert-comment-create-action-alert-action-id')[1]
|
||||
.querySelector('.euiCommentEvent__headerEvent')
|
||||
).toHaveTextContent('added an alert from Awesome rule');
|
||||
});
|
||||
|
|
|
@ -32,10 +32,12 @@ import { useGetCaseConnectors } from '../../../containers/use_get_case_connector
|
|||
import { useGetCaseUsers } from '../../../containers/use_get_case_users';
|
||||
import { waitForComponentToUpdate } from '../../../common/test_utils';
|
||||
import { getCaseConnectorsMockResponse } from '../../../common/mock/connectors';
|
||||
import { defaultUseFindCaseUserActions } from '../mocks';
|
||||
import { defaultInfiniteUseFindCaseUserActions, defaultUseFindCaseUserActions } from '../mocks';
|
||||
import { ActionTypes } from '../../../../common/api';
|
||||
import { useGetCaseUserActionsStats } from '../../../containers/use_get_case_user_actions_stats';
|
||||
import { useInfiniteFindCaseUserActions } from '../../../containers/use_infinite_find_case_user_actions';
|
||||
|
||||
jest.mock('../../../containers/use_infinite_find_case_user_actions');
|
||||
jest.mock('../../../containers/use_find_case_user_actions');
|
||||
jest.mock('../../../containers/use_get_case_user_actions_stats');
|
||||
jest.mock('../../../containers/configure/use_get_supported_action_connectors');
|
||||
|
@ -82,8 +84,13 @@ const caseViewProps: CaseViewProps = {
|
|||
},
|
||||
],
|
||||
};
|
||||
const filterActionType = 'all';
|
||||
const sortOrder = 'asc';
|
||||
|
||||
const userActivityQueryParams = {
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
const pushCaseToExternalService = jest.fn();
|
||||
|
||||
|
@ -105,6 +112,7 @@ export const caseProps = {
|
|||
const caseUsers = getCaseUsersMockResponse();
|
||||
|
||||
const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock;
|
||||
const useInfiniteFindCaseUserActionsMock = useInfiniteFindCaseUserActions as jest.Mock;
|
||||
const useGetCaseUserActionsStatsMock = useGetCaseUserActionsStats as jest.Mock;
|
||||
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
|
||||
const usePostPushToServiceMock = usePostPushToService as jest.Mock;
|
||||
|
@ -119,6 +127,7 @@ describe.skip('Case View Page activity tab', () => {
|
|||
|
||||
beforeAll(() => {
|
||||
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue(defaultInfiniteUseFindCaseUserActions);
|
||||
useGetCaseUserActionsStatsMock.mockReturnValue({ data: userActionsStats, isLoading: false });
|
||||
useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false });
|
||||
usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService });
|
||||
|
@ -138,24 +147,40 @@ describe.skip('Case View Page activity tab', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRender = createAppMockRenderer();
|
||||
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
|
||||
useGetCaseUsersMock.mockReturnValue({ isLoading: false, data: caseUsers });
|
||||
});
|
||||
|
||||
it('should render the activity content and main components', async () => {
|
||||
appMockRender = createAppMockRenderer({ license: platinumLicense });
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
expect(result.getByTestId('case-view-activity')).toBeInTheDocument();
|
||||
expect(result.getByTestId('user-actions')).toBeInTheDocument();
|
||||
expect(result.getByTestId('case-tags')).toBeInTheDocument();
|
||||
expect(result.getByTestId('connector-edit-header')).toBeInTheDocument();
|
||||
expect(result.getByTestId('case-view-status-action-button')).toBeInTheDocument();
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
expect(screen.getByTestId('case-view-activity')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
expect(screen.getByTestId('case-tags')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-view-status-action-button')).toBeInTheDocument();
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
});
|
||||
|
||||
it('should call use get user actions as per top and bottom actions list', async () => {
|
||||
appMockRender = createAppMockRenderer({ license: platinumLicense });
|
||||
appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
const lastPageForAll = Math.ceil(userActionsStats.total / userActivityQueryParams.perPage);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
{ ...userActivityQueryParams, page: lastPageForAll },
|
||||
true
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
});
|
||||
|
@ -168,14 +193,10 @@ describe.skip('Case View Page activity tab', () => {
|
|||
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
expect(result.getByTestId('case-view-activity')).toBeInTheDocument();
|
||||
expect(result.getByTestId('user-actions')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
expect(result.getByTestId('case-tags')).toBeInTheDocument();
|
||||
expect(result.getByTestId('connector-edit-header')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('case-view-status-action-button')).not.toBeInTheDocument();
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
});
|
||||
|
@ -188,32 +209,20 @@ describe.skip('Case View Page activity tab', () => {
|
|||
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
expect(result.getByTestId('case-view-activity')).toBeInTheDocument();
|
||||
expect(result.getByTestId('user-actions')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
expect(result.getByTestId('case-tags')).toBeInTheDocument();
|
||||
expect(result.getByTestId('connector-edit-header')).toBeInTheDocument();
|
||||
expect(result.getByTestId('case-severity-selection')).toBeDisabled();
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
});
|
||||
|
||||
it('should show a loading when is fetching data is true and hide the user actions activity', () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
isFetching: true,
|
||||
isLoading: true,
|
||||
});
|
||||
it('should show a loading when loading user actions stats', () => {
|
||||
useGetCaseUserActionsStatsMock.mockReturnValue({ isLoading: true });
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
expect(result.getByTestId('case-view-loading-content')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('case-view-activity')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('user-actions')).not.toBeInTheDocument();
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
expect(result.queryByTestId('user-actions-list')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render the assignees on basic license', () => {
|
||||
|
@ -251,104 +260,114 @@ describe.skip('Case View Page activity tab', () => {
|
|||
|
||||
describe('filter activity', () => {
|
||||
beforeEach(() => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue(defaultInfiniteUseFindCaseUserActions);
|
||||
useGetCaseUserActionsStatsMock.mockReturnValue({ data: userActionsStats, isLoading: false });
|
||||
});
|
||||
|
||||
it('should show all filter as active', async () => {
|
||||
appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
const lastPageForAll = Math.ceil(userActionsStats.total / userActivityQueryParams.perPage);
|
||||
|
||||
userEvent.click(screen.getByTestId('user-actions-filter-activity-button-all'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: 'all',
|
||||
sortOrder,
|
||||
});
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
expect(screen.getByLabelText(`${userActionsStats.total - 1} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalComments} available filters`));
|
||||
expect(
|
||||
screen.getByLabelText(`${userActionsStats.totalOtherActions - 1} available filters`)
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
{ ...userActivityQueryParams, page: lastPageForAll },
|
||||
true
|
||||
);
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
expect(screen.getByLabelText(`${userActionsStats.total} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalComments} available filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalOtherActions} available filters`));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show comment filter as active', async () => {
|
||||
appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
const lastPageForComment = Math.ceil(
|
||||
userActionsStats.totalComments / userActivityQueryParams.perPage
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('user-actions-filter-activity-button-comments'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: 'user',
|
||||
sortOrder,
|
||||
});
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalComments} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.total - 1} available filters`));
|
||||
expect(
|
||||
screen.getByLabelText(`${userActionsStats.totalOtherActions - 1} available filters`)
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
{ ...userActivityQueryParams, type: 'user' },
|
||||
true
|
||||
);
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
{ ...userActivityQueryParams, type: 'user', page: lastPageForComment },
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalComments} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.total} available filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalOtherActions} available filters`));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show history filter as active', async () => {
|
||||
appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
const lastPageForHistory = Math.ceil(
|
||||
userActionsStats.totalOtherActions / userActivityQueryParams.perPage
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('user-actions-filter-activity-button-history'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: 'action',
|
||||
sortOrder,
|
||||
});
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalOtherActions - 1} active filters`));
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
{ ...userActivityQueryParams, type: 'action' },
|
||||
true
|
||||
);
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
caseData.id,
|
||||
{ ...userActivityQueryParams, type: 'action', page: lastPageForHistory },
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalOtherActions} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalComments} available filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.total - 1} available filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.total} available filters`));
|
||||
});
|
||||
});
|
||||
|
||||
it('should render by desc sort order', async () => {
|
||||
appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
const sortSelect = screen.getByTestId('user-actions-sort-select');
|
||||
|
||||
fireEvent.change(sortSelect, { target: { value: 'desc' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, {
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(useGetCaseUserActionsStatsMock).toHaveBeenCalledWith(caseData.id);
|
||||
expect(screen.getByLabelText(`${userActionsStats.total - 1} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.total} active filters`));
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalComments} available filters`));
|
||||
expect(
|
||||
screen.getByLabelText(`${userActionsStats.totalOtherActions - 1} available filters`)
|
||||
);
|
||||
expect(screen.getByLabelText(`${userActionsStats.totalOtherActions} available filters`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -496,7 +515,7 @@ describe.skip('Case View Page activity tab', () => {
|
|||
appMockRender = createAppMockRenderer();
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
const userActions = within(result.getByTestId('user-actions'));
|
||||
const userActions = within(result.getAllByTestId('user-actions-list')[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(userActions.getByText('cases_no_connectors')).toBeInTheDocument();
|
||||
|
@ -524,7 +543,7 @@ describe.skip('Case View Page activity tab', () => {
|
|||
appMockRender = createAppMockRenderer();
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
const userActions = within(result.getByTestId('user-actions'));
|
||||
const userActions = within(result.getAllByTestId('user-actions-list')[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(userActions.getByText('Fuzzy Marten')).toBeInTheDocument();
|
||||
|
@ -580,7 +599,7 @@ describe.skip('Case View Page activity tab', () => {
|
|||
appMockRender = createAppMockRenderer();
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
|
||||
const userActions = within(result.getByTestId('user-actions'));
|
||||
const userActions = within(result.getAllByTestId('user-actions-list')[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(userActions.getByText('Participant 1')).toBeInTheDocument();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
/* eslint-disable complexity */
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
|
||||
|
@ -29,7 +29,6 @@ import { useOnUpdateField } from '../use_on_update_field';
|
|||
import { useCasesContext } from '../../cases_context/use_cases_context';
|
||||
import * as i18n from '../translations';
|
||||
import { SeveritySidebarSelector } from '../../severity/sidebar_selector';
|
||||
import { useFindCaseUserActions } from '../../../containers/use_find_case_user_actions';
|
||||
import { useGetCaseUserActionsStats } from '../../../containers/use_get_case_user_actions_stats';
|
||||
import { AssignUsers } from './assign_users';
|
||||
import { UserActionsActivityBar } from '../../user_actions_activity_bar';
|
||||
|
@ -90,6 +89,8 @@ export const CaseViewActivity = ({
|
|||
const [userActivityQueryParams, setUserActivityQueryParams] = useState<UserActivityParams>({
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
});
|
||||
|
||||
const { permissions } = useCasesContext();
|
||||
|
@ -100,11 +101,6 @@ export const CaseViewActivity = ({
|
|||
caseData.id
|
||||
);
|
||||
|
||||
const { data: userActionsData, isLoading: isLoadingUserActions } = useFindCaseUserActions(
|
||||
caseData.id,
|
||||
userActivityQueryParams
|
||||
);
|
||||
|
||||
const { data: userActionsStats, isLoading: isLoadingUserActionsStats } =
|
||||
useGetCaseUserActionsStats(caseData.id);
|
||||
|
||||
|
@ -180,25 +176,11 @@ export const CaseViewActivity = ({
|
|||
[onUpdateField]
|
||||
);
|
||||
|
||||
const showUserActions =
|
||||
!isLoadingUserActions &&
|
||||
!isLoadingCaseConnectors &&
|
||||
userActionsData &&
|
||||
caseConnectors &&
|
||||
caseUsers;
|
||||
|
||||
const showConnectorSidebar =
|
||||
pushToServiceAuthorized && userActionsData && caseConnectors && supportedActionConnectors;
|
||||
|
||||
const reporterAsArray =
|
||||
caseUsers?.reporter != null
|
||||
? [caseUsers.reporter]
|
||||
: [convertToCaseUserWithProfileInfo(caseData.createdBy)];
|
||||
|
||||
const handleUserActionsActivityChanged = useCallback(
|
||||
(params: UserActivityParams) => {
|
||||
setUserActivityQueryParams((oldParams) => ({
|
||||
...oldParams,
|
||||
page: 1,
|
||||
type: params.type,
|
||||
sortOrder: params.sortOrder,
|
||||
}));
|
||||
|
@ -206,6 +188,22 @@ export const CaseViewActivity = ({
|
|||
[setUserActivityQueryParams]
|
||||
);
|
||||
|
||||
const showUserActions =
|
||||
!isLoadingUserActionsStats &&
|
||||
!isLoadingCaseConnectors &&
|
||||
!isLoadingCaseUsers &&
|
||||
caseConnectors &&
|
||||
caseUsers &&
|
||||
userActionsStats;
|
||||
|
||||
const showConnectorSidebar =
|
||||
pushToServiceAuthorized && caseConnectors && supportedActionConnectors;
|
||||
|
||||
const reporterAsArray =
|
||||
caseUsers?.reporter != null
|
||||
? [caseUsers.reporter]
|
||||
: [convertToCaseUserWithProfileInfo(caseData.createdBy)];
|
||||
|
||||
const isLoadingDescription = isLoading && loadingKey === 'description';
|
||||
|
||||
return (
|
||||
|
@ -228,43 +226,38 @@ export const CaseViewActivity = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiSkeletonText
|
||||
lines={8}
|
||||
data-test-subj="case-view-loading-content"
|
||||
isLoading={isLoadingUserActions || isLoadingCaseConnectors}
|
||||
>
|
||||
{(isLoadingUserActionsStats || isLoadingCaseConnectors || isLoadingCaseUsers) && (
|
||||
<EuiLoadingSpinner data-test-subj="case-view-loading-content" size="l" />
|
||||
)}
|
||||
{showUserActions ? (
|
||||
<EuiFlexGroup direction="column" responsive={false} data-test-subj="case-view-activity">
|
||||
<EuiFlexItem>
|
||||
{showUserActions && (
|
||||
<UserActions
|
||||
userProfiles={userProfiles}
|
||||
currentUserProfile={currentUserProfile}
|
||||
getRuleDetailsHref={ruleDetailsNavigation?.href}
|
||||
onRuleDetailsClick={ruleDetailsNavigation?.onClick}
|
||||
caseConnectors={caseConnectors}
|
||||
caseUserActions={userActionsData.userActions}
|
||||
data={caseData}
|
||||
actionsNavigation={actionsNavigation}
|
||||
isLoadingUserActions={isLoadingUserActions}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
onUpdateField={onUpdateField}
|
||||
statusActionButton={
|
||||
permissions.update ? (
|
||||
<StatusActionButton
|
||||
status={caseData.status}
|
||||
onStatusChanged={changeStatus}
|
||||
isLoading={isLoading && loadingKey === 'status'}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
filterOptions={userActivityQueryParams.type}
|
||||
useFetchAlertData={useFetchAlertData}
|
||||
/>
|
||||
)}
|
||||
<UserActions
|
||||
userProfiles={userProfiles}
|
||||
currentUserProfile={currentUserProfile}
|
||||
getRuleDetailsHref={ruleDetailsNavigation?.href}
|
||||
onRuleDetailsClick={ruleDetailsNavigation?.onClick}
|
||||
caseConnectors={caseConnectors}
|
||||
data={caseData}
|
||||
actionsNavigation={actionsNavigation}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
onUpdateField={onUpdateField}
|
||||
statusActionButton={
|
||||
permissions.update ? (
|
||||
<StatusActionButton
|
||||
status={caseData.status}
|
||||
onStatusChanged={changeStatus}
|
||||
isLoading={isLoading && loadingKey === 'status'}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
useFetchAlertData={useFetchAlertData}
|
||||
userActivityQueryParams={userActivityQueryParams}
|
||||
userActionsStats={userActionsStats}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSkeletonText>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFlexGroup direction="column" responsive={false} gutterSize="xl">
|
||||
|
@ -297,7 +290,7 @@ export const CaseViewActivity = ({
|
|||
dataTestSubj="case-view-user-list-participants"
|
||||
theCase={caseData}
|
||||
headline={i18n.PARTICIPANTS}
|
||||
loading={isLoadingUserActions}
|
||||
loading={isLoadingCaseUsers}
|
||||
users={[...caseUsers.participants, ...caseUsers.assignees]}
|
||||
userProfiles={userProfiles}
|
||||
/>
|
||||
|
|
|
@ -100,9 +100,22 @@ export const defaultUpdateCaseState = {
|
|||
};
|
||||
|
||||
export const defaultUseFindCaseUserActions = {
|
||||
data: { userActions: [...caseUserActions, getAlertUserAction()] },
|
||||
data: { total: 4, perPage: 10, page: 1, userActions: [...caseUserActions, getAlertUserAction()] },
|
||||
refetch: jest.fn(),
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
};
|
||||
|
||||
export const defaultInfiniteUseFindCaseUserActions = {
|
||||
data: {
|
||||
pages: [
|
||||
{ total: 4, perPage: 10, page: 1, userActions: [...caseUserActions, getAlertUserAction()] },
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
hasNextPage: false,
|
||||
fetchNextPage: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -15,9 +15,11 @@ import { createSeverityUserActionBuilder } from './severity';
|
|||
import { createStatusUserActionBuilder } from './status';
|
||||
import { createTagsUserActionBuilder } from './tags';
|
||||
import { createTitleUserActionBuilder } from './title';
|
||||
import { createCaseUserActionBuilder } from './create_case';
|
||||
import type { UserActionBuilderMap } from './types';
|
||||
|
||||
export const builderMap: UserActionBuilderMap = {
|
||||
create_case: createCaseUserActionBuilder,
|
||||
connector: createConnectorUserActionBuilder,
|
||||
tags: createTagsUserActionBuilder,
|
||||
title: createTitleUserActionBuilder,
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { SupportedUserActionTypes } from './types';
|
|||
|
||||
export const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft';
|
||||
|
||||
export const UNSUPPORTED_ACTION_TYPES = ['create_case', 'delete_case'] as const;
|
||||
export const UNSUPPORTED_ACTION_TYPES = ['delete_case'] as const;
|
||||
export const SUPPORTED_ACTION_TYPES: SupportedUserActionTypes[] = Object.keys(
|
||||
omit(ActionTypes, UNSUPPORTED_ACTION_TYPES)
|
||||
) as SupportedUserActionTypes[];
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCommentList } from '@elastic/eui';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Actions } from '../../../common/api';
|
||||
import { getUserAction } from '../../containers/mock';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { createCaseUserActionBuilder } from './create_case';
|
||||
import { getMockBuilderArgs } from './mock';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../common/navigation/hooks');
|
||||
|
||||
describe('createCaseUserActionBuilder ', () => {
|
||||
const builderArgs = getMockBuilderArgs();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const userAction = getUserAction('create_case', Actions.create);
|
||||
// @ts-ignore no need to pass all the arguments
|
||||
const builder = createCaseUserActionBuilder({
|
||||
...builderArgs,
|
||||
userAction,
|
||||
});
|
||||
|
||||
const createdUserAction = builder.build();
|
||||
render(
|
||||
<TestProviders>
|
||||
<EuiCommentList comments={createdUserAction} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByText('created case "a title"')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CreateCaseUserAction } from '../../../common/api';
|
||||
import type { UserActionBuilder, UserActionResponse } from './types';
|
||||
import { createCommonUpdateUserActionBuilder } from './common';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const getLabelTitle = (userAction: UserActionResponse<CreateCaseUserAction>) =>
|
||||
`${i18n.CREATE_CASE.toLowerCase()} "${userAction.payload.title}"`;
|
||||
|
||||
export const createCaseUserActionBuilder: UserActionBuilder = ({
|
||||
userAction,
|
||||
userProfiles,
|
||||
handleOutlineComment,
|
||||
}) => ({
|
||||
build: () => {
|
||||
const createCaseUserAction = userAction as UserActionResponse<CreateCaseUserAction>;
|
||||
const label = getLabelTitle(createCaseUserAction);
|
||||
const commonBuilder = createCommonUpdateUserActionBuilder({
|
||||
userAction,
|
||||
userProfiles,
|
||||
handleOutlineComment,
|
||||
label,
|
||||
icon: 'dot',
|
||||
});
|
||||
|
||||
return commonBuilder.build();
|
||||
},
|
||||
});
|
|
@ -62,7 +62,7 @@ describe('helpers', () => {
|
|||
['title', true],
|
||||
['status', true],
|
||||
['settings', true],
|
||||
['create_case', false],
|
||||
['create_case', true],
|
||||
['delete_case', false],
|
||||
];
|
||||
|
||||
|
|
|
@ -6,64 +6,79 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import { waitFor, screen, within, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import routeData from 'react-router';
|
||||
|
||||
import { useUpdateComment } from '../../containers/use_update_comment';
|
||||
import {
|
||||
basicCase,
|
||||
getUserAction,
|
||||
caseUserActions,
|
||||
getHostIsolationUserAction,
|
||||
getUserAction,
|
||||
hostIsolationComment,
|
||||
} from '../../containers/mock';
|
||||
import { UserActions } from '.';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { createAppMockRenderer, TestProviders } from '../../common/mock';
|
||||
import { createAppMockRenderer } from '../../common/mock';
|
||||
import { Actions } from '../../../common/api';
|
||||
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
|
||||
import { connectorsMock, getCaseConnectorsMockResponse } from '../../common/mock/connectors';
|
||||
import type { UserActivityFilter } from '../user_actions_activity_bar/types';
|
||||
import { getCaseConnectorsMockResponse } from '../../common/mock/connectors';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions';
|
||||
import {
|
||||
defaultInfiniteUseFindCaseUserActions,
|
||||
defaultUseFindCaseUserActions,
|
||||
} from '../case_view/mocks';
|
||||
import { waitForComponentToUpdate } from '../../common/test_utils';
|
||||
import { useInfiniteFindCaseUserActions } from '../../containers/use_infinite_find_case_user_actions';
|
||||
import { getMockBuilderArgs } from './mock';
|
||||
|
||||
const fetchUserActions = jest.fn();
|
||||
const onUpdateField = jest.fn();
|
||||
const updateCase = jest.fn();
|
||||
const onShowAlertDetails = jest.fn();
|
||||
|
||||
const filterOptions: UserActivityFilter = 'all';
|
||||
const userActionsStats = {
|
||||
total: 25,
|
||||
totalComments: 9,
|
||||
totalOtherActions: 16,
|
||||
};
|
||||
|
||||
const userActivityQueryParams: UserActivityParams = {
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
const builderArgs = getMockBuilderArgs();
|
||||
|
||||
const defaultProps = {
|
||||
caseUserActions,
|
||||
...builderArgs,
|
||||
caseConnectors: getCaseConnectorsMockResponse(),
|
||||
caseUserActions: [],
|
||||
userProfiles: new Map(),
|
||||
currentUserProfile: undefined,
|
||||
connectors: connectorsMock,
|
||||
actionsNavigation: { href: jest.fn(), onClick: jest.fn() },
|
||||
getRuleDetailsHref: jest.fn(),
|
||||
onRuleDetailsClick: jest.fn(),
|
||||
data: basicCase,
|
||||
fetchUserActions,
|
||||
isLoadingUserActions: false,
|
||||
manualAlertsData: { 'some-id': { _id: 'some-id' } },
|
||||
onUpdateField,
|
||||
selectedAlertPatterns: ['some-test-pattern'],
|
||||
userActivityQueryParams,
|
||||
userActionsStats,
|
||||
statusActionButton: null,
|
||||
updateCase,
|
||||
useFetchAlertData: (): [boolean, Record<string, unknown>] => [
|
||||
false,
|
||||
{ 'some-id': { _id: 'some-id' } },
|
||||
],
|
||||
alerts: {},
|
||||
onShowAlertDetails,
|
||||
filterOptions,
|
||||
};
|
||||
|
||||
jest.mock('../../containers/use_infinite_find_case_user_actions');
|
||||
jest.mock('../../containers/use_find_case_user_actions');
|
||||
jest.mock('../../containers/use_update_comment');
|
||||
jest.mock('./timestamp', () => ({
|
||||
UserActionTimestamp: () => <></>,
|
||||
}));
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const useInfiniteFindCaseUserActionsMock = useInfiniteFindCaseUserActions as jest.Mock;
|
||||
const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock;
|
||||
const useUpdateCommentMock = useUpdateComment as jest.Mock;
|
||||
const patchComment = jest.fn();
|
||||
|
||||
|
@ -79,47 +94,38 @@ describe(`UserActions`, () => {
|
|||
isLoadingIds: [],
|
||||
patchComment,
|
||||
});
|
||||
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue(defaultInfiniteUseFindCaseUserActions);
|
||||
|
||||
jest.spyOn(routeData, 'useParams').mockReturnValue({ detailName: 'case-id' });
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('Loading spinner when user actions loading and displays fullName/username', () => {
|
||||
appMockRender.render(
|
||||
<UserActions
|
||||
{...{ ...defaultProps, currentUserProfile: userProfiles[0], isLoadingUserActions: true }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('user-actions-loading')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-user-profile-avatar-damaged_raccoon')).toBeInTheDocument();
|
||||
expect(screen.getByText('DR')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders service now update line with top and bottom when push is required', async () => {
|
||||
const caseConnectors = getCaseConnectorsMockResponse({ 'push.needsToBePushed': true });
|
||||
|
||||
const ourActions = [
|
||||
getUserAction('pushed', 'push_to_service', {
|
||||
createdAt: '2023-01-17T09:46:29.813Z',
|
||||
}),
|
||||
];
|
||||
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: ourActions },
|
||||
});
|
||||
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseConnectors,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toEqual(true);
|
||||
expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toEqual(true);
|
||||
expect(screen.getByTestId('top-footer')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('bottom-footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -130,300 +136,190 @@ describe(`UserActions`, () => {
|
|||
}),
|
||||
];
|
||||
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: ourActions },
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
appMockRender.render(<UserActions {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toEqual(true);
|
||||
expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toEqual(false);
|
||||
expect(screen.getByTestId('top-footer')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('bottom-footer')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Outlines comment when update move to link is clicked', async () => {
|
||||
const ourActions = [
|
||||
getUserAction('comment', Actions.create),
|
||||
getUserAction('comment', Actions.update),
|
||||
];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`)
|
||||
.first()
|
||||
.hasClass('outlined')
|
||||
).toEqual(false);
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-update-action-${ourActions[1].id}"] [data-test-subj="move-to-link-${props.data.comments[0].id}"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`)
|
||||
.first()
|
||||
.hasClass('outlined')
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
it('Switches to markdown when edit is clicked and back to panel when canceled', async () => {
|
||||
const ourActions = [getUserAction('comment', Actions.create)];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: ourActions },
|
||||
});
|
||||
|
||||
appMockRender.render(<UserActions {...defaultProps} />);
|
||||
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('property-actions-user-action-ellipses')
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-ellipses"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-pencil"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
userEvent.click(screen.getByTestId('property-actions-user-action-pencil'));
|
||||
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('user-action-cancel-markdown')
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
|
||||
)
|
||||
.exists()
|
||||
).toEqual(false);
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).queryByTestId('user-action-markdown-form')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls update comment when comment markdown is saved', async () => {
|
||||
const ourActions = [getUserAction('comment', Actions.create)];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: ourActions },
|
||||
});
|
||||
|
||||
appMockRender.render(<UserActions {...defaultProps} />);
|
||||
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('property-actions-user-action-ellipses')
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-ellipses"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-pencil"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
userEvent.click(screen.getByTestId('property-actions-user-action-pencil'));
|
||||
|
||||
wrapper
|
||||
.find(`.euiMarkdownEditorTextArea`)
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: sampleData.content },
|
||||
});
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] button[data-test-subj="user-action-save-markdown"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
fireEvent.change(screen.getAllByTestId(`euiMarkdownEditorTextArea`)[0], {
|
||||
target: { value: sampleData.content },
|
||||
});
|
||||
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('user-action-save-markdown')
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
|
||||
)
|
||||
.exists()
|
||||
).toEqual(false);
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).queryByTestId('user-action-markdown-form')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(patchComment).toBeCalledWith({
|
||||
commentUpdate: sampleData.content,
|
||||
caseId: 'case-id',
|
||||
commentId: props.data.comments[0].id,
|
||||
version: props.data.comments[0].version,
|
||||
commentId: defaultProps.data.comments[0].id,
|
||||
version: defaultProps.data.comments[0].version,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows quoted text in last MarkdownEditorTextArea', async () => {
|
||||
const quoteableText = `> Solve this fast! \n\n`;
|
||||
|
||||
const ourActions = [getUserAction('comment', Actions.create)];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: ourActions },
|
||||
});
|
||||
|
||||
appMockRender.render(<UserActions {...defaultProps} />);
|
||||
|
||||
expect((await screen.findByTestId(`euiMarkdownEditorTextArea`)).textContent).not.toContain(
|
||||
quoteableText
|
||||
);
|
||||
|
||||
expect(wrapper.find(`.euiMarkdownEditorTextArea`).text()).not.toContain(quoteableText);
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('property-actions-user-action-ellipses')
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-ellipses"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-quote"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
userEvent.click(screen.getByTestId('property-actions-user-action-quote'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).first().text()).toContain(
|
||||
quoteableText
|
||||
);
|
||||
expect(screen.getAllByTestId('add-comment')[0].textContent).toContain(quoteableText);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show add comment markdown when history filter is selected', async () => {
|
||||
appMockRender.render(<UserActions {...defaultProps} filterOptions="action" />);
|
||||
appMockRender.render(
|
||||
<UserActions
|
||||
{...defaultProps}
|
||||
userActivityQueryParams={{ ...userActivityQueryParams, type: 'action' }}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('add-comment')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Outlines comment when url param is provided', async () => {
|
||||
const commentId = 'basic-comment-id';
|
||||
jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId });
|
||||
|
||||
const ourActions = [getUserAction('comment', Actions.create)];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="comment-create-action-${commentId}"]`)
|
||||
.first()
|
||||
.hasClass('outlined')
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('it should persist the draft of new comment while existing old comment is updated', async () => {
|
||||
const editedComment = 'it is an edited comment';
|
||||
const newComment = 'another cool comment';
|
||||
const ourActions = [getUserAction('comment', Actions.create)];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
// type new comment in text area
|
||||
wrapper
|
||||
.find(`[data-test-subj="add-comment"] textarea`)
|
||||
.first()
|
||||
.simulate('change', { target: { value: newComment } });
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-ellipses"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-user-action-pencil"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
wrapper
|
||||
.find(`.euiMarkdownEditorTextArea`)
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: editedComment },
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] button[data-test-subj="user-action-save-markdown"]`
|
||||
)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find(
|
||||
`[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
|
||||
)
|
||||
.exists()
|
||||
).toEqual(false);
|
||||
expect(patchComment).toBeCalledWith({
|
||||
commentUpdate: editedComment,
|
||||
caseId: 'case-id',
|
||||
commentId: props.data.comments[0].id,
|
||||
version: props.data.comments[0].version,
|
||||
});
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: ourActions },
|
||||
});
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment);
|
||||
appMockRender.render(<UserActions {...defaultProps} />);
|
||||
|
||||
userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea'));
|
||||
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), newComment);
|
||||
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('property-actions-user-action-ellipses')
|
||||
);
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
userEvent.click(screen.getByTestId('property-actions-user-action-pencil'));
|
||||
|
||||
fireEvent.change(screen.getAllByTestId('euiMarkdownEditorTextArea')[0], {
|
||||
target: { value: editedComment },
|
||||
});
|
||||
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).getByTestId('user-action-save-markdown')
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(
|
||||
screen.getAllByTestId(`comment-create-action-${defaultProps.data.comments[0].id}`)[1]
|
||||
).queryByTestId('user-action-markdown-form')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('add-comment')[1].textContent).toContain(newComment);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Host isolation action', () => {
|
||||
|
@ -431,17 +327,17 @@ describe(`UserActions`, () => {
|
|||
const isolateAction = [getHostIsolationUserAction()];
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: isolateAction,
|
||||
data: { ...defaultProps.data, comments: [...basicCase.comments, hostIsolationComment()] },
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: isolateAction },
|
||||
});
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="endpoint-action"]`).exists()).toBe(true);
|
||||
expect(screen.getByTestId('endpoint-action')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -452,18 +348,263 @@ describe(`UserActions`, () => {
|
|||
const props = {
|
||||
...defaultProps,
|
||||
userProfiles: userProfilesMap,
|
||||
caseUserActions: isolateAction,
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
comments: [hostIsolationComment({ createdBy: { profileUid: userProfiles[0].uid } })],
|
||||
},
|
||||
};
|
||||
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: isolateAction },
|
||||
});
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
expect(screen.getByTestId('case-user-profile-avatar-damaged_raccoon')).toBeInTheDocument();
|
||||
expect(screen.getByText('DR')).toBeInTheDocument();
|
||||
expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByTestId('case-user-profile-avatar-damaged_raccoon')[0]
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText('DR')[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Damaged Raccoon')[0]).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Loading spinner when user actions loading', () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({ isLoading: true });
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({ isLoading: true });
|
||||
appMockRender.render(
|
||||
<UserActions {...{ ...defaultProps, currentUserProfile: userProfiles[0] }} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('user-actions-loading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders two user actions list when user actions are more than 10', () => {
|
||||
appMockRender.render(<UserActions {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders only one user actions list when last page is 0', async () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: [] },
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userActionsStats: {
|
||||
total: 0,
|
||||
totalComments: 0,
|
||||
totalOtherActions: 0,
|
||||
},
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders only one user actions list when last page is 1', async () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: [] },
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userActionsStats: {
|
||||
total: 1,
|
||||
totalComments: 0,
|
||||
totalOtherActions: 1,
|
||||
},
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders only one action list when user actions are less than or equal to 10', async () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: { userActions: [] },
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userActionsStats: {
|
||||
total: 10,
|
||||
totalComments: 6,
|
||||
totalOtherActions: 4,
|
||||
},
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows more button visible when hasNext page is true', async () => {
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultInfiniteUseFindCaseUserActions,
|
||||
hasNextPage: true,
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userActionsStats: {
|
||||
total: 25,
|
||||
totalComments: 10,
|
||||
totalOtherActions: 15,
|
||||
},
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
expect(screen.getByTestId('cases-show-more-user-actions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('call fetchNextPage on showMore button click', async () => {
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultInfiniteUseFindCaseUserActions,
|
||||
hasNextPage: true,
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userActionsStats: {
|
||||
total: 25,
|
||||
totalComments: 10,
|
||||
totalOtherActions: 15,
|
||||
},
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
|
||||
const showMore = screen.getByTestId('cases-show-more-user-actions');
|
||||
|
||||
expect(showMore).toBeInTheDocument();
|
||||
|
||||
userEvent.click(showMore);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultInfiniteUseFindCaseUserActions.fetchNextPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows more button visible 21st user action added', async () => {
|
||||
const mockUserActions = [
|
||||
...caseUserActions,
|
||||
getUserAction('comment', Actions.create),
|
||||
getUserAction('comment', Actions.update),
|
||||
getUserAction('comment', Actions.create),
|
||||
getUserAction('comment', Actions.update),
|
||||
getUserAction('comment', Actions.create),
|
||||
getUserAction('comment', Actions.update),
|
||||
getUserAction('comment', Actions.create),
|
||||
];
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultInfiniteUseFindCaseUserActions,
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
total: 20,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
userActions: mockUserActions,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: {
|
||||
total: 20,
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
userActions: mockUserActions,
|
||||
},
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userActionsStats: {
|
||||
total: 20,
|
||||
totalComments: 10,
|
||||
totalOtherActions: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const { rerender } = appMockRender.render(<UserActions {...props} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
expect(screen.queryByTestId('cases-show-more-user-actions')).not.toBeInTheDocument();
|
||||
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultInfiniteUseFindCaseUserActions,
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
total: 21,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
userActions: mockUserActions,
|
||||
},
|
||||
{
|
||||
total: 21,
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
userActions: [getUserAction('comment', Actions.create)],
|
||||
},
|
||||
],
|
||||
},
|
||||
hasNextPage: true,
|
||||
});
|
||||
useFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseFindCaseUserActions,
|
||||
data: {
|
||||
total: 21,
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
userActions: mockUserActions,
|
||||
},
|
||||
});
|
||||
|
||||
const newProps = {
|
||||
...props,
|
||||
userActionsStats: {
|
||||
total: 21,
|
||||
totalComments: 11,
|
||||
totalOtherActions: 10,
|
||||
},
|
||||
};
|
||||
|
||||
rerender(<UserActions {...newProps} />);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(screen.getAllByTestId('user-actions-list')).toHaveLength(2);
|
||||
|
||||
const firstUserActionsList = screen.getAllByTestId('user-actions-list')[0];
|
||||
|
||||
expect(firstUserActionsList.getElementsByTagName('li')).toHaveLength(11);
|
||||
|
||||
expect(screen.getByTestId('cases-show-more-user-actions')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,16 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiCommentProps } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiCommentList } from '@elastic/eui';
|
||||
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexItem, EuiSkeletonText, useEuiTheme } from '@elastic/eui';
|
||||
import type { EuiThemeComputed } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { AddComment } from '../add_comment';
|
||||
import { useCaseViewParams } from '../../common/navigation';
|
||||
import { builderMap } from './builder';
|
||||
import { isUserActionTypeSupported, getManualAlertIdsWithNoRuleId } from './helpers';
|
||||
import { getManualAlertIdsWithNoRuleId } from './helpers';
|
||||
import type { UserActionTreeProps } from './types';
|
||||
import { useUserActionsHandler } from './use_user_actions_handler';
|
||||
import { NEW_COMMENT_ID } from './constants';
|
||||
|
@ -22,234 +20,200 @@ import { useCasesContext } from '../cases_context/use_cases_context';
|
|||
import { UserToolTip } from '../user_profiles/user_tooltip';
|
||||
import { Username } from '../user_profiles/username';
|
||||
import { HoverableAvatar } from '../user_profiles/hoverable_avatar';
|
||||
import { UserActionsList } from './user_actions_list';
|
||||
import { useUserActionsPagination } from './use_user_actions_pagination';
|
||||
import { useLastPageUserActions } from './use_user_actions_last_page';
|
||||
import { ShowMoreButton } from './show_more_button';
|
||||
import { useLastPage } from './use_last_page';
|
||||
|
||||
const MyEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const MyEuiCommentList = styled(EuiCommentList)`
|
||||
${({ theme }) => `
|
||||
& .userAction__comment.outlined .euiCommentEvent {
|
||||
outline: solid 5px ${theme.eui.euiColorVis1_behindText};
|
||||
margin: 0.5em;
|
||||
transition: 0.8s;
|
||||
}
|
||||
|
||||
& .draftFooter {
|
||||
& .euiCommentEvent__body {
|
||||
padding: 0;
|
||||
const getIconsCss = (hasNextPage: boolean | undefined, euiTheme: EuiThemeComputed<{}>): string => {
|
||||
const customSize = hasNextPage
|
||||
? {
|
||||
showMoreSectionSize: euiTheme.size.xxxl,
|
||||
marginTopShowMoreSectionSize: euiTheme.size.xxl,
|
||||
marginBottomShowMoreSectionSize: euiTheme.size.xxl,
|
||||
}
|
||||
}
|
||||
: {
|
||||
showMoreSectionSize: euiTheme.size.s,
|
||||
marginTopShowMoreSectionSize: euiTheme.size.m,
|
||||
marginBottomShowMoreSectionSize: euiTheme.size.m,
|
||||
};
|
||||
|
||||
& .euiComment.isEdit {
|
||||
& .euiCommentEvent {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
const blockSize = `${customSize.showMoreSectionSize} + ${customSize.marginTopShowMoreSectionSize} +
|
||||
${customSize.marginBottomShowMoreSectionSize}`;
|
||||
return `
|
||||
.commentList--hasShowMore
|
||||
[class*='euiTimelineItem-center']:last-child:not(:only-child)
|
||||
> [class*='euiTimelineItemIcon-']::before {
|
||||
block-size: calc(
|
||||
100% + ${blockSize}
|
||||
);
|
||||
}
|
||||
.commentList--hasShowMore
|
||||
[class*='euiTimelineItem-center']:first-child
|
||||
> [class*='euiTimelineItemIcon-']::before {
|
||||
inset-block-start: 0%;
|
||||
block-size: calc(
|
||||
100% + ${blockSize}
|
||||
);
|
||||
}
|
||||
.commentList--hasShowMore
|
||||
[class*='euiTimelineItem-']
|
||||
> [class*='euiTimelineItemIcon-']::before {
|
||||
block-size: calc(
|
||||
100% + ${blockSize}
|
||||
);
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
& .euiCommentEvent__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& .euiCommentEvent__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .comment-alert .euiCommentEvent {
|
||||
background-color: ${theme.eui.euiColorLightestShade};
|
||||
border: ${theme.eui.euiBorderThin};
|
||||
padding: ${theme.eui.euiSizeS};
|
||||
border-radius: ${theme.eui.euiSizeXS};
|
||||
}
|
||||
|
||||
& .comment-alert .euiCommentEvent__headerData {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& .comment-action.empty-comment [class*="euiCommentEvent-regular"] {
|
||||
box-shadow: none;
|
||||
.euiCommentEvent__header {
|
||||
padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeS};
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const UserActions = React.memo(
|
||||
({
|
||||
caseConnectors,
|
||||
caseUserActions,
|
||||
userProfiles,
|
||||
export const UserActions = React.memo((props: UserActionTreeProps) => {
|
||||
const {
|
||||
currentUserProfile,
|
||||
data: caseData,
|
||||
getRuleDetailsHref,
|
||||
actionsNavigation,
|
||||
isLoadingUserActions,
|
||||
onRuleDetailsClick,
|
||||
onShowAlertDetails,
|
||||
onUpdateField,
|
||||
statusActionButton,
|
||||
useFetchAlertData,
|
||||
filterOptions,
|
||||
}: UserActionTreeProps) => {
|
||||
const { detailName: caseId, commentId } = useCaseViewParams();
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const {
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
appId,
|
||||
} = useCasesContext();
|
||||
userActivityQueryParams,
|
||||
userActionsStats,
|
||||
} = props;
|
||||
const { detailName: caseId } = useCaseViewParams();
|
||||
|
||||
const alertIdsWithoutRuleInfo = useMemo(
|
||||
() => getManualAlertIdsWithNoRuleId(caseData.comments),
|
||||
[caseData.comments]
|
||||
);
|
||||
const { lastPage } = useLastPage({ userActivityQueryParams, userActionsStats });
|
||||
|
||||
const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo);
|
||||
const {
|
||||
infiniteCaseUserActions,
|
||||
isLoadingInfiniteUserActions,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
showBottomList,
|
||||
isFetchingNextPage,
|
||||
} = useUserActionsPagination({
|
||||
userActivityQueryParams,
|
||||
caseId: caseData.id,
|
||||
lastPage,
|
||||
});
|
||||
|
||||
const {
|
||||
loadingCommentIds,
|
||||
commentRefs,
|
||||
selectedOutlineCommentId,
|
||||
manageMarkdownEditIds,
|
||||
handleManageMarkdownEditId,
|
||||
handleOutlineComment,
|
||||
handleSaveComment,
|
||||
handleManageQuote,
|
||||
handleDeleteComment,
|
||||
handleUpdate,
|
||||
} = useUserActionsHandler();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const MarkdownNewComment = useMemo(
|
||||
() => (
|
||||
<AddComment
|
||||
id={NEW_COMMENT_ID}
|
||||
caseId={caseId}
|
||||
ref={(element) => (commentRefs.current[NEW_COMMENT_ID] = element)}
|
||||
onCommentPosted={handleUpdate}
|
||||
onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_COMMENT_ID)}
|
||||
showLoading={false}
|
||||
statusActionButton={statusActionButton}
|
||||
/>
|
||||
),
|
||||
[caseId, handleUpdate, handleManageMarkdownEditId, statusActionButton, commentRefs]
|
||||
);
|
||||
const { isLoadingLastPageUserActions, lastPageUserActions } = useLastPageUserActions({
|
||||
userActivityQueryParams,
|
||||
caseId: caseData.id,
|
||||
lastPage,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initLoading && !isLoadingUserActions && loadingCommentIds.length === 0) {
|
||||
setInitLoading(false);
|
||||
if (commentId != null) {
|
||||
handleOutlineComment(commentId);
|
||||
}
|
||||
}
|
||||
}, [commentId, initLoading, isLoadingUserActions, loadingCommentIds, handleOutlineComment]);
|
||||
const alertIdsWithoutRuleInfo = useMemo(
|
||||
() => getManualAlertIdsWithNoRuleId(caseData.comments),
|
||||
[caseData.comments]
|
||||
);
|
||||
|
||||
const userActions: EuiCommentProps[] = useMemo(
|
||||
() =>
|
||||
caseUserActions.reduce<EuiCommentProps[]>((comments, userAction, index) => {
|
||||
if (!isUserActionTypeSupported(userAction.type)) {
|
||||
return comments;
|
||||
}
|
||||
const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo);
|
||||
|
||||
const builder = builderMap[userAction.type];
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
if (builder == null) {
|
||||
return comments;
|
||||
}
|
||||
// add-comment markdown is not visible in History filter
|
||||
const showCommentEditor = permissions.create && userActivityQueryParams.type !== 'action';
|
||||
|
||||
const userActionBuilder = builder({
|
||||
appId,
|
||||
caseData,
|
||||
caseConnectors,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
userAction,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
comments: caseData.comments,
|
||||
index,
|
||||
commentRefs,
|
||||
manageMarkdownEditIds,
|
||||
selectedOutlineCommentId,
|
||||
loadingCommentIds,
|
||||
loadingAlertData,
|
||||
alertData: manualAlertsData,
|
||||
handleOutlineComment,
|
||||
handleManageMarkdownEditId,
|
||||
handleDeleteComment,
|
||||
handleSaveComment,
|
||||
handleManageQuote,
|
||||
onShowAlertDetails,
|
||||
actionsNavigation,
|
||||
getRuleDetailsHref,
|
||||
onRuleDetailsClick,
|
||||
});
|
||||
return [...comments, ...userActionBuilder.build()];
|
||||
}, []),
|
||||
[
|
||||
appId,
|
||||
caseConnectors,
|
||||
caseUserActions,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
caseData,
|
||||
commentRefs,
|
||||
manageMarkdownEditIds,
|
||||
selectedOutlineCommentId,
|
||||
loadingCommentIds,
|
||||
loadingAlertData,
|
||||
manualAlertsData,
|
||||
handleOutlineComment,
|
||||
handleManageMarkdownEditId,
|
||||
handleDeleteComment,
|
||||
handleSaveComment,
|
||||
handleManageQuote,
|
||||
onShowAlertDetails,
|
||||
actionsNavigation,
|
||||
getRuleDetailsHref,
|
||||
onRuleDetailsClick,
|
||||
const {
|
||||
commentRefs,
|
||||
handleManageMarkdownEditId,
|
||||
handleManageQuote,
|
||||
handleUpdate,
|
||||
loadingCommentIds,
|
||||
} = useUserActionsHandler();
|
||||
|
||||
const MarkdownNewComment = useMemo(
|
||||
() => (
|
||||
<AddComment
|
||||
id={NEW_COMMENT_ID}
|
||||
caseId={caseId}
|
||||
ref={(element) => (commentRefs.current[NEW_COMMENT_ID] = element)}
|
||||
onCommentPosted={handleUpdate}
|
||||
onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_COMMENT_ID)}
|
||||
showLoading={false}
|
||||
statusActionButton={statusActionButton}
|
||||
/>
|
||||
),
|
||||
[caseId, handleUpdate, handleManageMarkdownEditId, statusActionButton, commentRefs]
|
||||
);
|
||||
|
||||
const bottomActions = showCommentEditor
|
||||
? [
|
||||
{
|
||||
username: (
|
||||
<UserToolTip userInfo={currentUserProfile}>
|
||||
<Username userInfo={currentUserProfile} />
|
||||
</UserToolTip>
|
||||
),
|
||||
'data-test-subj': 'add-comment',
|
||||
timelineAvatar: <HoverableAvatar userInfo={currentUserProfile} />,
|
||||
className: 'isEdit',
|
||||
children: MarkdownNewComment,
|
||||
},
|
||||
]
|
||||
);
|
||||
: [];
|
||||
|
||||
const { permissions } = useCasesContext();
|
||||
const handleShowMore = useCallback(() => {
|
||||
if (fetchNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [fetchNextPage]);
|
||||
|
||||
const showCommentEditor = permissions.create && filterOptions !== 'action'; // add-comment markdown is not visible in History filter
|
||||
|
||||
const bottomActions = showCommentEditor
|
||||
? [
|
||||
{
|
||||
username: (
|
||||
<UserToolTip userInfo={currentUserProfile}>
|
||||
<Username userInfo={currentUserProfile} />
|
||||
</UserToolTip>
|
||||
),
|
||||
'data-test-subj': 'add-comment',
|
||||
timelineAvatar: <HoverableAvatar userInfo={currentUserProfile} />,
|
||||
className: 'isEdit',
|
||||
children: MarkdownNewComment,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const comments = [...userActions, ...bottomActions];
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyEuiCommentList comments={comments} data-test-subj="user-actions" />
|
||||
{(isLoadingUserActions || loadingCommentIds.includes(NEW_COMMENT_ID)) && (
|
||||
<MyEuiFlexGroup justifyContent="center" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner data-test-subj="user-actions-loading" size="l" />
|
||||
</EuiFlexItem>
|
||||
</MyEuiFlexGroup>
|
||||
return (
|
||||
<EuiSkeletonText
|
||||
lines={8}
|
||||
data-test-subj="user-actions-loading"
|
||||
isLoading={
|
||||
isLoadingLastPageUserActions ||
|
||||
loadingCommentIds.includes(NEW_COMMENT_ID) ||
|
||||
isLoadingInfiniteUserActions
|
||||
}
|
||||
>
|
||||
<EuiFlexItem
|
||||
{...(showBottomList
|
||||
? {
|
||||
css: css`
|
||||
${getIconsCss(hasNextPage, euiTheme)}
|
||||
`,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<UserActionsList
|
||||
{...props}
|
||||
caseUserActions={infiniteCaseUserActions}
|
||||
loadingAlertData={loadingAlertData}
|
||||
manualAlertsData={manualAlertsData}
|
||||
commentRefs={commentRefs}
|
||||
handleManageQuote={handleManageQuote}
|
||||
bottomActions={lastPage <= 1 ? bottomActions : []}
|
||||
isExpandable
|
||||
/>
|
||||
{hasNextPage && (
|
||||
<ShowMoreButton onShowMoreClick={handleShowMore} isLoading={isFetchingNextPage} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
{lastPageUserActions?.length ? (
|
||||
<EuiFlexItem
|
||||
{...(!hasNextPage
|
||||
? {
|
||||
css: css`
|
||||
margin-top: 24px;
|
||||
`,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<UserActionsList
|
||||
{...props}
|
||||
caseUserActions={lastPageUserActions}
|
||||
loadingAlertData={loadingAlertData}
|
||||
manualAlertsData={manualAlertsData}
|
||||
bottomActions={bottomActions}
|
||||
commentRefs={commentRefs}
|
||||
handleManageQuote={handleManageQuote}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiSkeletonText>
|
||||
);
|
||||
});
|
||||
|
||||
UserActions.displayName = 'UserActions';
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ShowMoreButton } from './show_more_button';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { createAppMockRenderer } from '../../common/mock';
|
||||
|
||||
const showMoreClickMock = jest.fn();
|
||||
|
||||
describe('ShowMoreButton', () => {
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
appMockRender.render(<ShowMoreButton onShowMoreClick={showMoreClickMock} />);
|
||||
|
||||
expect(screen.getByTestId('cases-show-more-user-actions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state and is disabled when isLoading is true', () => {
|
||||
appMockRender.render(<ShowMoreButton onShowMoreClick={showMoreClickMock} isLoading={true} />);
|
||||
|
||||
const btn = screen.getByTestId('cases-show-more-user-actions');
|
||||
|
||||
expect(btn).toBeInTheDocument();
|
||||
expect(btn).toHaveAttribute('disabled');
|
||||
expect(screen.getByRole('progressbar')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onShowMoreClick on button click', () => {
|
||||
appMockRender.render(<ShowMoreButton onShowMoreClick={showMoreClickMock} />);
|
||||
|
||||
userEvent.click(screen.getByTestId('cases-show-more-user-actions'));
|
||||
expect(showMoreClickMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiPanel, useEuiTheme } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ShowMoreButtonProps {
|
||||
onShowMoreClick: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const ShowMoreButton = React.memo<ShowMoreButtonProps>(
|
||||
({ onShowMoreClick, isLoading = false }) => {
|
||||
const handleShowMore = () => {
|
||||
onShowMoreClick();
|
||||
};
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
css={css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-block: ${euiTheme.size.xl};
|
||||
margin-inline-start: ${euiTheme.size.xxxl};
|
||||
`}
|
||||
>
|
||||
<EuiButton
|
||||
fill
|
||||
color="text"
|
||||
size="s"
|
||||
onClick={handleShowMore}
|
||||
data-test-subj="cases-show-more-user-actions"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{i18n.SHOW_MORE}
|
||||
</EuiButton>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ShowMoreButton.displayName = 'ShowMoreButton';
|
|
@ -101,3 +101,11 @@ export const UNSAVED_DRAFT_DESCRIPTION = i18n.translate(
|
|||
defaultMessage: 'You have unsaved edits for the description',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_MORE = i18n.translate('xpack.cases.caseView.userActions.showMore', {
|
||||
defaultMessage: 'Show more',
|
||||
});
|
||||
|
||||
export const CREATE_CASE = i18n.translate('xpack.cases.caseView.userActions.createCase', {
|
||||
defaultMessage: 'Created case',
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
CaseUserActions,
|
||||
Comment,
|
||||
UseFetchAlertData,
|
||||
CaseUserActionsStats,
|
||||
} from '../../containers/types';
|
||||
import type { AddCommentRefObject } from '../add_comment';
|
||||
import type { UserActionMarkdownRefObject } from './markdown_form';
|
||||
|
@ -24,23 +25,22 @@ import type { OnUpdateFields } from '../case_view/types';
|
|||
import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry';
|
||||
import type { CurrentUserProfile } from '../types';
|
||||
import type { UserActivityFilter } from '../user_actions_activity_bar/types';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
export interface UserActionTreeProps {
|
||||
caseConnectors: CaseConnectors;
|
||||
caseUserActions: CaseUserActions[];
|
||||
userProfiles: Map<string, UserProfileWithAvatar>;
|
||||
currentUserProfile: CurrentUserProfile;
|
||||
data: Case;
|
||||
getRuleDetailsHref?: RuleDetailsNavigation['href'];
|
||||
actionsNavigation?: ActionsNavigation;
|
||||
isLoadingUserActions: boolean;
|
||||
onRuleDetailsClick?: RuleDetailsNavigation['onClick'];
|
||||
onShowAlertDetails: (alertId: string, index: string) => void;
|
||||
onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void;
|
||||
statusActionButton: JSX.Element | null;
|
||||
useFetchAlertData: UseFetchAlertData;
|
||||
filterOptions: UserActivityFilter;
|
||||
userActivityQueryParams: UserActivityParams;
|
||||
userActionsStats: CaseUserActionsStats;
|
||||
}
|
||||
|
||||
type UnsupportedUserActionTypes = typeof UNSUPPORTED_ACTION_TYPES[number];
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useLastPage } from './use_last_page';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
const userActivityQueryParams: UserActivityParams = {
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
const userActionsStats = {
|
||||
total: 5,
|
||||
totalComments: 2,
|
||||
totalOtherActions: 3,
|
||||
};
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
describe('useLastPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns correctly', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLastPage({
|
||||
userActionsStats,
|
||||
userActivityQueryParams,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lastPage: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 1 when actions stats are 0', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLastPage({
|
||||
userActionsStats: { total: 0, totalComments: 0, totalOtherActions: 0 },
|
||||
userActivityQueryParams,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lastPage: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct last page when filter type is all', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLastPage({
|
||||
userActionsStats: { total: 38, totalComments: 17, totalOtherActions: 21 },
|
||||
userActivityQueryParams,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lastPage: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct last page when filter type is user', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLastPage({
|
||||
userActionsStats: { total: 38, totalComments: 17, totalOtherActions: 21 },
|
||||
userActivityQueryParams: {
|
||||
...userActivityQueryParams,
|
||||
type: 'user',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lastPage: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct last page when filter type is action', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLastPage({
|
||||
userActionsStats: { total: 38, totalComments: 17, totalOtherActions: 21 },
|
||||
userActivityQueryParams: {
|
||||
...userActivityQueryParams,
|
||||
type: 'action',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lastPage: 3,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { assertNever } from '@elastic/eui';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { CaseUserActionsStats } from '../../containers/types';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
export const useLastPage = ({
|
||||
userActivityQueryParams,
|
||||
userActionsStats,
|
||||
}: {
|
||||
userActivityQueryParams: UserActivityParams;
|
||||
userActionsStats: CaseUserActionsStats;
|
||||
}) => {
|
||||
const lastPage = useMemo(() => {
|
||||
if (!userActionsStats) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const perPage = userActivityQueryParams.perPage;
|
||||
let lastPageType = 1;
|
||||
|
||||
switch (userActivityQueryParams.type) {
|
||||
case 'action':
|
||||
lastPageType = Math.ceil(userActionsStats.totalOtherActions / perPage);
|
||||
break;
|
||||
case 'user':
|
||||
lastPageType = Math.ceil(userActionsStats.totalComments / perPage);
|
||||
break;
|
||||
case 'all':
|
||||
lastPageType = Math.ceil(userActionsStats.total / perPage);
|
||||
break;
|
||||
default:
|
||||
return assertNever(userActivityQueryParams.type);
|
||||
}
|
||||
|
||||
return Math.max(lastPageType, 1);
|
||||
}, [userActionsStats, userActivityQueryParams]);
|
||||
|
||||
return { lastPage };
|
||||
};
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useLastPageUserActions } from './use_user_actions_last_page';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions';
|
||||
import { defaultUseFindCaseUserActions } from '../case_view/mocks';
|
||||
import { basicCase } from '../../containers/mock';
|
||||
|
||||
const userActivityQueryParams: UserActivityParams = {
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
jest.mock('../../containers/use_find_case_user_actions');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock;
|
||||
|
||||
describe('useLastPageUserActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useLastPageUserActions({
|
||||
lastPage: 5,
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ ...userActivityQueryParams, page: 5 },
|
||||
true
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
isLoadingLastPageUserActions: false,
|
||||
lastPageUserActions: defaultUseFindCaseUserActions.data.userActions,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls find API hook with enabled as false when last page is 1', async () => {
|
||||
renderHook(() =>
|
||||
useLastPageUserActions({
|
||||
lastPage: 1,
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ ...userActivityQueryParams, page: 1 },
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('returns loading state correctly', async () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({ isLoading: true });
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useLastPageUserActions({
|
||||
lastPage: 2,
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ ...userActivityQueryParams, page: 2 },
|
||||
true
|
||||
);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
isLoadingLastPageUserActions: true,
|
||||
lastPageUserActions: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array when data is undefined', async () => {
|
||||
useFindCaseUserActionsMock.mockReturnValue({ isLoading: false, data: undefined });
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useLastPageUserActions({
|
||||
lastPage: 2,
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ ...userActivityQueryParams, page: 2 },
|
||||
true
|
||||
);
|
||||
|
||||
expect(useFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
isLoadingLastPageUserActions: false,
|
||||
lastPageUserActions: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { CaseUserActions } from '../../containers/types';
|
||||
import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
interface LastPageUserActions {
|
||||
userActivityQueryParams: UserActivityParams;
|
||||
caseId: string;
|
||||
lastPage: number;
|
||||
}
|
||||
|
||||
export const useLastPageUserActions = ({
|
||||
userActivityQueryParams,
|
||||
caseId,
|
||||
lastPage,
|
||||
}: LastPageUserActions) => {
|
||||
const { data: lastPageUserActionsData, isLoading: isLoadingLastPageUserActions } =
|
||||
useFindCaseUserActions(caseId, { ...userActivityQueryParams, page: lastPage }, lastPage > 1);
|
||||
|
||||
const lastPageUserActions = useMemo<CaseUserActions[]>(() => {
|
||||
if (isLoadingLastPageUserActions || !lastPageUserActionsData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return lastPageUserActionsData.userActions;
|
||||
}, [lastPageUserActionsData, isLoadingLastPageUserActions]);
|
||||
|
||||
return {
|
||||
isLoadingLastPageUserActions,
|
||||
lastPageUserActions,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useUserActionsPagination } from './use_user_actions_pagination';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
import { defaultInfiniteUseFindCaseUserActions } from '../case_view/mocks';
|
||||
import { basicCase } from '../../containers/mock';
|
||||
import { useInfiniteFindCaseUserActions } from '../../containers/use_infinite_find_case_user_actions';
|
||||
|
||||
const userActivityQueryParams: UserActivityParams = {
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
jest.mock('../../containers/use_infinite_find_case_user_actions');
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const useInfiniteFindCaseUserActionsMock = useInfiniteFindCaseUserActions as jest.Mock;
|
||||
|
||||
describe('useUserActionsPagination', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue(defaultInfiniteUseFindCaseUserActions);
|
||||
});
|
||||
|
||||
it('renders expandable option correctly when user actions are more than 10', async () => {
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useUserActionsPagination({
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
lastPage: 3,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
showBottomList: true,
|
||||
isLoadingInfiniteUserActions: defaultInfiniteUseFindCaseUserActions.isLoading,
|
||||
infiniteCaseUserActions: defaultInfiniteUseFindCaseUserActions.data.pages[0].userActions,
|
||||
hasNextPage: defaultInfiniteUseFindCaseUserActions.hasNextPage,
|
||||
fetchNextPage: defaultInfiniteUseFindCaseUserActions.fetchNextPage,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders less than 10 user actions correctly', async () => {
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useUserActionsPagination({
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
lastPage: 1,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
showBottomList: false,
|
||||
isLoadingInfiniteUserActions: false,
|
||||
infiniteCaseUserActions: defaultInfiniteUseFindCaseUserActions.data.pages[0].userActions,
|
||||
hasNextPage: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns loading state correctly', async () => {
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({ isLoading: true });
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useUserActionsPagination({
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
lastPage: 3,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
showBottomList: true,
|
||||
isLoadingInfiniteUserActions: true,
|
||||
infiniteCaseUserActions: [],
|
||||
hasNextPage: undefined,
|
||||
fetchNextPage: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array when data is undefined', async () => {
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({ isLoading: false, data: undefined });
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useUserActionsPagination({
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
lastPage: 3,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
showBottomList: true,
|
||||
isLoadingInfiniteUserActions: false,
|
||||
infiniteCaseUserActions: [],
|
||||
hasNextPage: undefined,
|
||||
fetchNextPage: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('return hasNextPage as false when it has less than 10 user actions', async () => {
|
||||
useInfiniteFindCaseUserActionsMock.mockReturnValue({
|
||||
...defaultInfiniteUseFindCaseUserActions,
|
||||
data: {
|
||||
pages: { total: 25, perPage: 10, page: 1, userActions: [] },
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useUserActionsPagination({
|
||||
userActivityQueryParams,
|
||||
caseId: basicCase.id,
|
||||
lastPage: 1,
|
||||
})
|
||||
);
|
||||
|
||||
expect(useInfiniteFindCaseUserActionsMock).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
userActivityQueryParams,
|
||||
true
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
showBottomList: false,
|
||||
isLoadingInfiniteUserActions: defaultInfiniteUseFindCaseUserActions.isLoading,
|
||||
infiniteCaseUserActions: [],
|
||||
hasNextPage: false,
|
||||
fetchNextPage: defaultInfiniteUseFindCaseUserActions.fetchNextPage,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useInfiniteFindCaseUserActions } from '../../containers/use_infinite_find_case_user_actions';
|
||||
import type { CaseUserActions } from '../../containers/types';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
interface UserActionsPagination {
|
||||
userActivityQueryParams: UserActivityParams;
|
||||
caseId: string;
|
||||
lastPage: number;
|
||||
}
|
||||
|
||||
export const useUserActionsPagination = ({
|
||||
userActivityQueryParams,
|
||||
caseId,
|
||||
lastPage,
|
||||
}: UserActionsPagination) => {
|
||||
const {
|
||||
data: caseInfiniteUserActionsData,
|
||||
isLoading: isLoadingInfiniteUserActions,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useInfiniteFindCaseUserActions(caseId, userActivityQueryParams, true);
|
||||
|
||||
const showBottomList = lastPage > 1;
|
||||
|
||||
const infiniteCaseUserActions = useMemo<CaseUserActions[]>(() => {
|
||||
if (!caseInfiniteUserActionsData?.pages?.length || isLoadingInfiniteUserActions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userActionsData: CaseUserActions[] = [];
|
||||
|
||||
caseInfiniteUserActionsData.pages.forEach((page) => userActionsData.push(...page.userActions));
|
||||
|
||||
return userActionsData;
|
||||
}, [caseInfiniteUserActionsData, isLoadingInfiniteUserActions]);
|
||||
|
||||
return {
|
||||
lastPage,
|
||||
showBottomList,
|
||||
isLoadingInfiniteUserActions,
|
||||
infiniteCaseUserActions,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import routeData from 'react-router';
|
||||
|
||||
import { basicCase, caseUserActions, getUserAction } from '../../containers/mock';
|
||||
import { UserActionsList } from './user_actions_list';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { createAppMockRenderer } from '../../common/mock';
|
||||
import { Actions } from '../../../common/api';
|
||||
import { getCaseConnectorsMockResponse } from '../../common/mock/connectors';
|
||||
import { getMockBuilderArgs } from './mock';
|
||||
|
||||
const builderArgs = getMockBuilderArgs();
|
||||
|
||||
const defaultProps = {
|
||||
caseUserActions,
|
||||
...builderArgs,
|
||||
caseConnectors: getCaseConnectorsMockResponse(),
|
||||
data: basicCase,
|
||||
manualAlertsData: { 'some-id': { _id: 'some-id' } },
|
||||
};
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
describe(`UserActionsList`, () => {
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
jest.spyOn(routeData, 'useParams').mockReturnValue({ detailName: 'case-id' });
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders list correctly with isExpandable option', async () => {
|
||||
appMockRender.render(<UserActionsList {...defaultProps} isExpandable />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user-actions-list')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders list correctly with isExpandable=false option', async () => {
|
||||
appMockRender.render(<UserActionsList {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user-actions-list')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders user actions correctly', async () => {
|
||||
appMockRender.render(<UserActionsList {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(`description-create-action-${caseUserActions[0].id}`));
|
||||
expect(screen.getByTestId(`comment-create-action-${caseUserActions[1].commentId}`));
|
||||
expect(screen.getByTestId(`description-update-action-${caseUserActions[2].id}`));
|
||||
});
|
||||
});
|
||||
|
||||
it('renders bottom actions correctly', async () => {
|
||||
const userName = 'Username';
|
||||
const sample = 'This is an add comment bottom actions';
|
||||
|
||||
const bottomActions = [
|
||||
{
|
||||
username: <div>{userName}</div>,
|
||||
'data-test-subj': 'add-comment',
|
||||
timelineAvatar: null,
|
||||
className: 'isEdit',
|
||||
children: <span>{sample}</span>,
|
||||
},
|
||||
];
|
||||
appMockRender.render(<UserActionsList {...defaultProps} bottomActions={bottomActions} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user-actions-list')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('add-comment')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Outlines comment when url param is provided', async () => {
|
||||
const commentId = 'basic-comment-id';
|
||||
jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId });
|
||||
|
||||
const ourActions = [getUserAction('comment', Actions.create)];
|
||||
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActionsList {...props} />);
|
||||
|
||||
expect(
|
||||
await screen.findAllByTestId(`comment-create-action-${commentId}`)
|
||||
)[0]?.classList.contains('outlined');
|
||||
});
|
||||
|
||||
it('Outlines comment when update move to link is clicked', async () => {
|
||||
const ourActions = [
|
||||
getUserAction('comment', Actions.create),
|
||||
getUserAction('comment', Actions.update),
|
||||
];
|
||||
|
||||
const props = {
|
||||
...defaultProps,
|
||||
caseUserActions: ourActions,
|
||||
};
|
||||
|
||||
appMockRender.render(<UserActionsList {...props} />);
|
||||
expect(
|
||||
screen
|
||||
.queryAllByTestId(`comment-create-action-${props.data.comments[0].id}`)[0]
|
||||
?.classList.contains('outlined')
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
screen
|
||||
.queryAllByTestId(`comment-create-action-${props.data.comments[0].id}`)[0]
|
||||
?.classList.contains('outlined')
|
||||
).toBe(false);
|
||||
|
||||
userEvent.click(screen.getByTestId(`comment-update-action-${ourActions[1].id}`));
|
||||
|
||||
expect(
|
||||
await screen.findAllByTestId(`comment-create-action-${props.data.comments[0].id}`)
|
||||
)[0]?.classList.contains('outlined');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiCommentProps } from '@elastic/eui';
|
||||
import { EuiCommentList } from '@elastic/eui';
|
||||
|
||||
import React, { useMemo, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { CaseUserActions } from '../../containers/types';
|
||||
import type { UserActionBuilderArgs, UserActionTreeProps } from './types';
|
||||
import { isUserActionTypeSupported } from './helpers';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { builderMap } from './builder';
|
||||
import { useCaseViewParams } from '../../common/navigation';
|
||||
import { useUserActionsHandler } from './use_user_actions_handler';
|
||||
|
||||
const MyEuiCommentList = styled(EuiCommentList)`
|
||||
${({ theme }) => `
|
||||
& .userAction__comment.outlined .euiCommentEvent {
|
||||
outline: solid 5px ${theme.eui.euiColorVis1_behindText};
|
||||
margin: 0.5em;
|
||||
transition: 0.8s;
|
||||
}
|
||||
|
||||
& .draftFooter {
|
||||
& .euiCommentEvent__body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& .euiComment.isEdit {
|
||||
& .euiCommentEvent {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
& .euiCommentEvent__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& .euiCommentEvent__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .comment-alert .euiCommentEvent {
|
||||
background-color: ${theme.eui.euiColorLightestShade};
|
||||
border: ${theme.eui.euiBorderThin};
|
||||
padding: ${theme.eui.euiSizeS};
|
||||
border-radius: ${theme.eui.euiSizeXS};
|
||||
}
|
||||
|
||||
& .comment-alert .euiCommentEvent__headerData {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& .comment-action.empty-comment [class*="euiCommentEvent-regular"] {
|
||||
box-shadow: none;
|
||||
.euiCommentEvent__header {
|
||||
padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeS};
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export type UserActionListProps = Omit<
|
||||
UserActionTreeProps,
|
||||
| 'userActivityQueryParams'
|
||||
| 'userActionsStats'
|
||||
| 'useFetchAlertData'
|
||||
| 'onUpdateField'
|
||||
| 'statusActionButton'
|
||||
> &
|
||||
Pick<UserActionBuilderArgs, 'commentRefs' | 'handleManageQuote'> & {
|
||||
caseUserActions: CaseUserActions[];
|
||||
loadingAlertData: boolean;
|
||||
manualAlertsData: Record<string, unknown>;
|
||||
bottomActions?: EuiCommentProps[];
|
||||
isExpandable?: boolean;
|
||||
};
|
||||
|
||||
export const UserActionsList = React.memo(
|
||||
({
|
||||
caseUserActions,
|
||||
caseConnectors,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
data: caseData,
|
||||
getRuleDetailsHref,
|
||||
actionsNavigation,
|
||||
onRuleDetailsClick,
|
||||
onShowAlertDetails,
|
||||
loadingAlertData,
|
||||
manualAlertsData,
|
||||
commentRefs,
|
||||
handleManageQuote,
|
||||
bottomActions = [],
|
||||
isExpandable = false,
|
||||
}: UserActionListProps) => {
|
||||
const {
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
appId,
|
||||
} = useCasesContext();
|
||||
const { commentId } = useCaseViewParams();
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
|
||||
const {
|
||||
loadingCommentIds,
|
||||
selectedOutlineCommentId,
|
||||
manageMarkdownEditIds,
|
||||
handleManageMarkdownEditId,
|
||||
handleOutlineComment,
|
||||
handleSaveComment,
|
||||
handleDeleteComment,
|
||||
} = useUserActionsHandler();
|
||||
|
||||
const builtUserActions: EuiCommentProps[] = useMemo(() => {
|
||||
if (!caseUserActions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return caseUserActions.reduce<EuiCommentProps[]>((comments, userAction, index) => {
|
||||
if (!isUserActionTypeSupported(userAction.type)) {
|
||||
return comments;
|
||||
}
|
||||
|
||||
const builder = builderMap[userAction.type];
|
||||
|
||||
if (builder == null) {
|
||||
return comments;
|
||||
}
|
||||
|
||||
const userActionBuilder = builder({
|
||||
appId,
|
||||
caseData,
|
||||
caseConnectors,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
userAction,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
comments: caseData?.comments,
|
||||
index,
|
||||
commentRefs,
|
||||
manageMarkdownEditIds,
|
||||
selectedOutlineCommentId,
|
||||
loadingCommentIds,
|
||||
loadingAlertData,
|
||||
alertData: manualAlertsData,
|
||||
handleOutlineComment,
|
||||
handleManageMarkdownEditId,
|
||||
handleDeleteComment,
|
||||
handleSaveComment,
|
||||
handleManageQuote,
|
||||
onShowAlertDetails,
|
||||
actionsNavigation,
|
||||
getRuleDetailsHref,
|
||||
onRuleDetailsClick,
|
||||
});
|
||||
return [...comments, ...userActionBuilder.build()];
|
||||
}, []);
|
||||
}, [
|
||||
appId,
|
||||
caseConnectors,
|
||||
caseUserActions,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
caseData,
|
||||
commentRefs,
|
||||
manageMarkdownEditIds,
|
||||
selectedOutlineCommentId,
|
||||
loadingCommentIds,
|
||||
loadingAlertData,
|
||||
manualAlertsData,
|
||||
handleOutlineComment,
|
||||
handleManageMarkdownEditId,
|
||||
handleDeleteComment,
|
||||
handleSaveComment,
|
||||
handleManageQuote,
|
||||
onShowAlertDetails,
|
||||
actionsNavigation,
|
||||
getRuleDetailsHref,
|
||||
onRuleDetailsClick,
|
||||
]);
|
||||
|
||||
const comments = bottomActions?.length
|
||||
? [...builtUserActions, ...bottomActions]
|
||||
: [...builtUserActions];
|
||||
|
||||
useEffect(() => {
|
||||
if (commentId != null && initLoading) {
|
||||
setInitLoading(false);
|
||||
handleOutlineComment(commentId);
|
||||
}
|
||||
}, [commentId, initLoading, handleOutlineComment]);
|
||||
|
||||
return (
|
||||
<MyEuiCommentList
|
||||
className={isExpandable ? 'commentList--hasShowMore' : ''}
|
||||
comments={comments}
|
||||
data-test-subj="user-actions-list"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
UserActionsList.displayName = 'UserActionsList';
|
|
@ -76,14 +76,12 @@ describe('FilterActivity ', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByLabelText(`${userActionsStats.total - 1} active filters`)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(`${userActionsStats.total} active filters`)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText(`${userActionsStats.totalComments} available filters`)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText(`${userActionsStats.totalOtherActions - 1} available filters`)
|
||||
screen.getByLabelText(`${userActionsStats.totalOtherActions} available filters`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
@ -50,9 +50,7 @@ export const FilterActivity = React.memo<FilterActivityProps>(
|
|||
grow={false}
|
||||
onClick={() => handleFilterChange('all')}
|
||||
hasActiveFilters={type === 'all'}
|
||||
numFilters={
|
||||
userActionsStats && userActionsStats.total > 0 ? userActionsStats.total - 1 : 0
|
||||
} // subtracting user action of description from total
|
||||
numFilters={userActionsStats && userActionsStats.total > 0 ? userActionsStats.total : 0}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
data-test-subj="user-actions-filter-activity-button-all"
|
||||
|
@ -77,10 +75,10 @@ export const FilterActivity = React.memo<FilterActivityProps>(
|
|||
{i18n.COMMENTS}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={type === 'action'} // subtracting user action of description other actions
|
||||
hasActiveFilters={type === 'action'}
|
||||
numFilters={
|
||||
userActionsStats && userActionsStats.totalOtherActions > 0
|
||||
? userActionsStats.totalOtherActions - 1
|
||||
? userActionsStats.totalOtherActions
|
||||
: 0
|
||||
}
|
||||
onClick={() => handleFilterChange('action')}
|
||||
|
|
|
@ -22,6 +22,8 @@ describe('UserActionsActivityBar ', () => {
|
|||
const params: UserActivityParams = {
|
||||
type: 'all',
|
||||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -120,6 +122,7 @@ describe('UserActionsActivityBar ', () => {
|
|||
|
||||
await waitFor(() =>
|
||||
expect(onUserActionsActivityChanged).toHaveBeenCalledWith({
|
||||
...params,
|
||||
type: 'action',
|
||||
sortOrder: 'asc',
|
||||
})
|
||||
|
|
|
@ -12,4 +12,6 @@ export type UserActivitySortOrder = 'asc' | 'desc';
|
|||
export interface UserActivityParams {
|
||||
type: UserActivityFilter;
|
||||
sortOrder: UserActivitySortOrder;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
CASES_URL,
|
||||
INTERNAL_BULK_CREATE_ATTACHMENTS_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL,
|
||||
} from '../../common/constants';
|
||||
|
||||
|
@ -469,8 +468,8 @@ describe('Cases API', () => {
|
|||
describe('findCaseUserActions', () => {
|
||||
const findCaseUserActionsSnake = {
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
total: 20,
|
||||
perPage: 10,
|
||||
total: 30,
|
||||
userActions: [...caseUserActionsWithRegisteredAttachmentsSnake],
|
||||
};
|
||||
const filterActionType: CaseUserActionTypeWithAll = 'all';
|
||||
|
@ -478,6 +477,8 @@ describe('Cases API', () => {
|
|||
const params = {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -493,7 +494,8 @@ describe('Cases API', () => {
|
|||
query: {
|
||||
types: [],
|
||||
sortOrder: 'asc',
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -501,7 +503,7 @@ describe('Cases API', () => {
|
|||
it('should be called with action type user action and desc sort order', async () => {
|
||||
await findCaseUserActions(
|
||||
basicCase.id,
|
||||
{ type: 'action', sortOrder: 'desc' },
|
||||
{ type: 'action', sortOrder: 'desc', page: 2, perPage: 15 },
|
||||
abortCtrl.signal
|
||||
);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions/_find`, {
|
||||
|
@ -510,7 +512,8 @@ describe('Cases API', () => {
|
|||
query: {
|
||||
types: ['action'],
|
||||
sortOrder: 'desc',
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
page: 2,
|
||||
perPage: 15,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -523,7 +526,8 @@ describe('Cases API', () => {
|
|||
query: {
|
||||
types: ['user'],
|
||||
sortOrder: 'asc',
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,7 +51,6 @@ import {
|
|||
CASE_TAGS_URL,
|
||||
CASES_URL,
|
||||
INTERNAL_BULK_CREATE_ATTACHMENTS_URL,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
} from '../../common/constants';
|
||||
import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api';
|
||||
|
||||
|
@ -161,13 +160,16 @@ export const findCaseUserActions = async (
|
|||
params: {
|
||||
type: CaseUserActionTypeWithAll;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
page: number;
|
||||
perPage: number;
|
||||
},
|
||||
signal: AbortSignal
|
||||
): Promise<FindCaseUserActions> => {
|
||||
const query = {
|
||||
types: params.type !== 'all' ? [params.type] : [],
|
||||
sortOrder: params.sortOrder ?? 'asc',
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
sortOrder: params.sortOrder,
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
};
|
||||
|
||||
const response = await KibanaServices.get().http.fetch<UserActionFindResponse>(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SingleCaseMetricsFeature, CaseUserActionTypeWithAll } from './types';
|
||||
import type { SingleCaseMetricsFeature } from './types';
|
||||
|
||||
export const DEFAULT_TABLE_ACTIVE_PAGE = 1;
|
||||
export const DEFAULT_TABLE_LIMIT = 10;
|
||||
|
@ -27,17 +27,8 @@ export const casesQueriesKeys = {
|
|||
[...casesQueriesKeys.case(id), 'metrics', features] as const,
|
||||
caseConnectors: (id: string) => [...casesQueriesKeys.case(id), 'connectors'],
|
||||
caseUsers: (id: string) => [...casesQueriesKeys.case(id), 'users'],
|
||||
caseUserActions: (
|
||||
id: string,
|
||||
filterActionType: CaseUserActionTypeWithAll,
|
||||
sortOrder: 'asc' | 'desc'
|
||||
) =>
|
||||
[
|
||||
...casesQueriesKeys.case(id),
|
||||
...casesQueriesKeys.userActions,
|
||||
filterActionType,
|
||||
sortOrder,
|
||||
] as const,
|
||||
caseUserActions: (id: string, params: unknown) =>
|
||||
[...casesQueriesKeys.case(id), ...casesQueriesKeys.userActions, params] as const,
|
||||
caseUserActionsStats: (id: string) => [
|
||||
...casesQueriesKeys.case(id),
|
||||
...casesQueriesKeys.userActions,
|
||||
|
|
|
@ -877,8 +877,8 @@ export const caseUserActionsWithRegisteredAttachments: CaseUserActions[] = [
|
|||
|
||||
export const findCaseUserActionsResponse: FindCaseUserActions = {
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
total: 20,
|
||||
perPage: 10,
|
||||
total: 30,
|
||||
userActions: [...caseUserActionsWithRegisteredAttachments],
|
||||
};
|
||||
|
||||
|
|
|
@ -29,8 +29,12 @@ describe('UseFindCaseUserActions', () => {
|
|||
const params = {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
};
|
||||
|
||||
const isEnabled = true;
|
||||
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -41,7 +45,7 @@ describe('UseFindCaseUserActions', () => {
|
|||
|
||||
it('returns proper state on findCaseUserActions', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useFindCaseUserActions(basicCase.id, params),
|
||||
() => useFindCaseUserActions(basicCase.id, params, isEnabled),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
|
@ -52,8 +56,8 @@ describe('UseFindCaseUserActions', () => {
|
|||
...initialData,
|
||||
data: {
|
||||
userActions: [...findCaseUserActionsResponse.userActions],
|
||||
total: 20,
|
||||
perPage: 1000,
|
||||
total: 30,
|
||||
perPage: 10,
|
||||
page: 1,
|
||||
},
|
||||
isError: false,
|
||||
|
@ -67,7 +71,17 @@ describe('UseFindCaseUserActions', () => {
|
|||
const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData);
|
||||
|
||||
const { waitForNextUpdate } = renderHook(
|
||||
() => useFindCaseUserActions(basicCase.id, { type: 'user', sortOrder: 'desc' }),
|
||||
() =>
|
||||
useFindCaseUserActions(
|
||||
basicCase.id,
|
||||
{
|
||||
type: 'user',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
},
|
||||
isEnabled
|
||||
),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
|
@ -75,26 +89,50 @@ describe('UseFindCaseUserActions', () => {
|
|||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ type: 'user', sortOrder: 'desc' },
|
||||
{ type: 'user', sortOrder: 'desc', page: 1, perPage: 5 },
|
||||
expect.any(AbortSignal)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not call API when not enabled', async () => {
|
||||
const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useFindCaseUserActions(
|
||||
basicCase.id,
|
||||
{
|
||||
type: 'user',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
},
|
||||
false
|
||||
),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a toast error when the API returns an error', async () => {
|
||||
const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(new Error("C'est la vie"));
|
||||
|
||||
const addError = jest.fn();
|
||||
(useToasts as jest.Mock).mockReturnValue({ addError });
|
||||
|
||||
const { waitForNextUpdate } = renderHook(() => useFindCaseUserActions(basicCase.id, params), {
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
});
|
||||
const { waitForNextUpdate } = renderHook(
|
||||
() => useFindCaseUserActions(basicCase.id, params, isEnabled),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ type: filterActionType, sortOrder },
|
||||
{ type: filterActionType, sortOrder, page: 1, perPage: 10 },
|
||||
expect.any(AbortSignal)
|
||||
);
|
||||
expect(addError).toHaveBeenCalled();
|
||||
|
|
|
@ -18,17 +18,21 @@ export const useFindCaseUserActions = (
|
|||
params: {
|
||||
type: CaseUserActionTypeWithAll;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
page: number;
|
||||
perPage: number;
|
||||
},
|
||||
isEnabled: boolean
|
||||
) => {
|
||||
const { showErrorToast } = useCasesToast();
|
||||
const abortCtrlRef = new AbortController();
|
||||
|
||||
return useQuery<FindCaseUserActions, ServerError>(
|
||||
casesQueriesKeys.caseUserActions(caseId, params.type, params.sortOrder),
|
||||
casesQueriesKeys.caseUserActions(caseId, params),
|
||||
async () => {
|
||||
return findCaseUserActions(caseId, params, abortCtrlRef.signal);
|
||||
},
|
||||
{
|
||||
enabled: isEnabled,
|
||||
onError: (error: ServerError) => {
|
||||
showErrorToast(error, { title: ERROR_TITLE });
|
||||
},
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useInfiniteFindCaseUserActions } from './use_infinite_find_case_user_actions';
|
||||
import type { CaseUserActionTypeWithAll } from '../../common/ui/types';
|
||||
import { basicCase, findCaseUserActionsResponse } from './mock';
|
||||
import * as api from './api';
|
||||
import { useToasts } from '../common/lib/kibana';
|
||||
import type { AppMockRenderer } from '../common/mock';
|
||||
import { createAppMockRenderer } from '../common/mock';
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../common/lib/kibana');
|
||||
|
||||
const initialData = {
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
describe('UseInfiniteFindCaseUserActions', () => {
|
||||
const filterActionType: CaseUserActionTypeWithAll = 'all';
|
||||
const sortOrder: 'asc' | 'desc' = 'asc';
|
||||
const params = {
|
||||
type: filterActionType,
|
||||
sortOrder,
|
||||
perPage: 10,
|
||||
};
|
||||
const isEnabled = true;
|
||||
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
appMockRender = createAppMockRenderer();
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns proper state on findCaseUserActions', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
...initialData,
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
userActions: [...findCaseUserActionsResponse.userActions],
|
||||
total: 30,
|
||||
perPage: 10,
|
||||
page: 1,
|
||||
},
|
||||
],
|
||||
pageParams: [undefined],
|
||||
},
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the API with correct parameters', async () => {
|
||||
const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData);
|
||||
|
||||
const { waitForNextUpdate } = renderHook(
|
||||
() =>
|
||||
useInfiniteFindCaseUserActions(
|
||||
basicCase.id,
|
||||
{
|
||||
type: 'user',
|
||||
sortOrder: 'desc',
|
||||
perPage: 5,
|
||||
},
|
||||
isEnabled
|
||||
),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ type: 'user', sortOrder: 'desc', page: 1, perPage: 5 },
|
||||
expect.any(AbortSignal)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not call API when not enabled', async () => {
|
||||
const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useInfiniteFindCaseUserActions(
|
||||
basicCase.id,
|
||||
{
|
||||
type: 'user',
|
||||
sortOrder: 'desc',
|
||||
perPage: 5,
|
||||
},
|
||||
false
|
||||
),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a toast error when the API returns an error', async () => {
|
||||
const spy = jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(new Error("C'est la vie"));
|
||||
|
||||
const addError = jest.fn();
|
||||
(useToasts as jest.Mock).mockReturnValue({ addError });
|
||||
|
||||
const { waitForNextUpdate } = renderHook(
|
||||
() => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ type: filterActionType, sortOrder, page: 1, perPage: 10 },
|
||||
expect.any(AbortSignal)
|
||||
);
|
||||
expect(addError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches next page with correct params', async () => {
|
||||
const spy = jest.spyOn(api, 'findCaseUserActions');
|
||||
|
||||
const { result, waitFor } = renderHook(
|
||||
() => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => result.current.isSuccess);
|
||||
|
||||
expect(result.current.data?.pages).toStrictEqual([findCaseUserActionsResponse]);
|
||||
|
||||
expect(result.current.hasNextPage).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.fetchNextPage();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
basicCase.id,
|
||||
{ type: 'all', sortOrder, page: 2, perPage: 10 },
|
||||
expect.any(AbortSignal)
|
||||
);
|
||||
});
|
||||
await waitFor(() => result.current.data?.pages.length === 2);
|
||||
});
|
||||
|
||||
it('returns hasNextPage correctly', async () => {
|
||||
jest.spyOn(api, 'findCaseUserActions').mockRejectedValue(initialData);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteFindCaseUserActions(basicCase.id, params, isEnabled),
|
||||
{ wrapper: appMockRender.AppWrapper }
|
||||
);
|
||||
|
||||
expect(result.current.hasNextPage).toBe(undefined);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import type { FindCaseUserActions, CaseUserActionTypeWithAll } from '../../common/ui/types';
|
||||
import { findCaseUserActions } from './api';
|
||||
import type { ServerError } from '../types';
|
||||
import { useCasesToast } from '../common/use_cases_toast';
|
||||
import { ERROR_TITLE } from './translations';
|
||||
import { casesQueriesKeys } from './constants';
|
||||
|
||||
export const useInfiniteFindCaseUserActions = (
|
||||
caseId: string,
|
||||
params: {
|
||||
type: CaseUserActionTypeWithAll;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
perPage: number;
|
||||
},
|
||||
isEnabled: boolean
|
||||
) => {
|
||||
const { showErrorToast } = useCasesToast();
|
||||
const abortCtrlRef = new AbortController();
|
||||
|
||||
return useInfiniteQuery<FindCaseUserActions, ServerError>(
|
||||
casesQueriesKeys.caseUserActions(caseId, params),
|
||||
async ({ pageParam = 1 }) => {
|
||||
return findCaseUserActions(caseId, { ...params, page: pageParam }, abortCtrlRef.signal);
|
||||
},
|
||||
{
|
||||
enabled: isEnabled,
|
||||
onError: (error: ServerError) => {
|
||||
showErrorToast(error, { title: ERROR_TITLE });
|
||||
},
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
const lastPageNumber = Math.ceil(lastPage.total / lastPage.perPage);
|
||||
// here last page fetching is skipped because last page is fetched separately using useQuery hook
|
||||
if (lastPage.page < lastPageNumber - 1) {
|
||||
return lastPage.page + 1;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export type UseInfiniteFindCaseUserActions = ReturnType<typeof useInfiniteFindCaseUserActions>;
|
|
@ -12,6 +12,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
|
|||
export function ObservabilityPageProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const textValue = 'Foobar';
|
||||
const PageObjects = getPageObjects(['common', 'header']);
|
||||
|
||||
return {
|
||||
async clickSolutionNavigationEntry(appId: string, navId: string) {
|
||||
|
@ -45,6 +46,7 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro
|
|||
},
|
||||
|
||||
async expectAddCommentButton() {
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.setValue('add-comment', textValue);
|
||||
const button = await testSubjects.find('submit-comment', 20000);
|
||||
const disabledAttr = await button.getAttribute('disabled');
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import pMap from 'p-map';
|
||||
import { CasePostRequest, CaseResponse, CaseStatuses } from '@kbn/cases-plugin/common/api';
|
||||
import {
|
||||
CasePostRequest,
|
||||
CaseResponse,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import {
|
||||
createCase as createCaseAPI,
|
||||
deleteAllCaseItems,
|
||||
|
@ -98,5 +103,43 @@ export function CasesAPIServiceProvider({ getService }: FtrProviderContext) {
|
|||
async getCase({ caseId }: OmitSupertest<Parameters<typeof getCase>[0]>): Promise<CaseResponse> {
|
||||
return getCase({ supertest: kbnSupertest, caseId });
|
||||
},
|
||||
|
||||
async generateUserActions({
|
||||
caseId,
|
||||
caseVersion,
|
||||
totalUpdates = 1,
|
||||
}: {
|
||||
caseId: string;
|
||||
caseVersion: string;
|
||||
totalUpdates: number;
|
||||
}) {
|
||||
let latestVersion = caseVersion;
|
||||
const severities = Object.values(CaseSeverity);
|
||||
const statuses = Object.values(CaseStatuses);
|
||||
|
||||
for (let index = 0; index < totalUpdates; index++) {
|
||||
const severity = severities[index % severities.length];
|
||||
const status = statuses[index % statuses.length];
|
||||
|
||||
const theCase = await updateCase({
|
||||
supertest: kbnSupertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: caseId,
|
||||
version: latestVersion,
|
||||
title: `Title update ${index}`,
|
||||
description: `Desc update ${index}`,
|
||||
severity,
|
||||
status,
|
||||
tags: [`tag-${index}`],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
latestVersion = theCase[0].version;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -359,26 +359,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
describe('filter activity', () => {
|
||||
createOneCaseBeforeDeleteAllAfter(getPageObject, getService);
|
||||
|
||||
it('filters by history successfully', async () => {
|
||||
await cases.common.selectSeverity(CaseSeverity.MEDIUM);
|
||||
|
||||
await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']);
|
||||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
await testSubjects.click('user-actions-filter-activity-button-history');
|
||||
|
||||
const historyBadge = await find.byCssSelector(
|
||||
'[data-test-subj="user-actions-filter-activity-button-history"] span.euiNotificationBadge'
|
||||
it('filters by comment successfully', async () => {
|
||||
const commentBadge = await find.byCssSelector(
|
||||
'[data-test-subj="user-actions-filter-activity-button-comments"] span.euiNotificationBadge'
|
||||
);
|
||||
|
||||
expect(await historyBadge.getVisibleText()).equal('2');
|
||||
});
|
||||
|
||||
it('filters by comment successfully', async () => {
|
||||
await testSubjects.click('user-actions-filter-activity-button-comments');
|
||||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
expect(await commentBadge.getVisibleText()).equal('0');
|
||||
|
||||
const commentArea = await find.byCssSelector(
|
||||
'[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea'
|
||||
|
@ -389,11 +375,25 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
const commentBadge = await find.byCssSelector(
|
||||
'[data-test-subj="user-actions-filter-activity-button-comments"] span.euiNotificationBadge'
|
||||
expect(await commentBadge.getVisibleText()).equal('1');
|
||||
});
|
||||
|
||||
it('filters by history successfully', async () => {
|
||||
const historyBadge = await find.byCssSelector(
|
||||
'[data-test-subj="user-actions-filter-activity-button-history"] span.euiNotificationBadge'
|
||||
);
|
||||
|
||||
expect(await commentBadge.getVisibleText()).equal('1');
|
||||
expect(await historyBadge.getVisibleText()).equal('1');
|
||||
|
||||
await cases.common.selectSeverity(CaseSeverity.MEDIUM);
|
||||
|
||||
await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']);
|
||||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
await testSubjects.click('user-actions-filter-activity-button-history');
|
||||
|
||||
expect(await historyBadge.getVisibleText()).equal('3');
|
||||
});
|
||||
|
||||
it('sorts by newest first successfully', async () => {
|
||||
|
@ -403,7 +403,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
'[data-test-subj="user-actions-filter-activity-button-all"] span.euiNotificationBadge'
|
||||
);
|
||||
|
||||
expect(await AllBadge.getVisibleText()).equal('3');
|
||||
expect(await AllBadge.getVisibleText()).equal('4');
|
||||
|
||||
const sortDesc = await find.byCssSelector(
|
||||
'[data-test-subj="user-actions-sort-select"] [value="desc"]'
|
||||
|
@ -413,13 +413,63 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
const userActions = await find.byCssSelector('[data-test-subj="user-actions"]');
|
||||
|
||||
const actionsList = await userActions.findAllByClassName('euiComment');
|
||||
|
||||
expect(await actionsList[0].getAttribute('data-test-subj')).contain(
|
||||
'comment-create-action'
|
||||
const userActionsLists = await find.allByCssSelector(
|
||||
'[data-test-subj="user-actions-list"]'
|
||||
);
|
||||
|
||||
const actionList = await userActionsLists[0].findAllByClassName('euiComment');
|
||||
|
||||
expect(await actionList[0].getAttribute('data-test-subj')).contain('status-update-action');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', async () => {
|
||||
let createdCase: any;
|
||||
|
||||
before(async () => {
|
||||
await cases.navigation.navigateToApp();
|
||||
createdCase = await cases.api.createCase({ title: 'Pagination feature' });
|
||||
await cases.casesTable.waitForCasesToBeListed();
|
||||
await cases.casesTable.goToFirstListedCase();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cases.api.deleteAllCases();
|
||||
});
|
||||
|
||||
it('shows more actions on button click', async () => {
|
||||
await cases.api.generateUserActions({
|
||||
caseId: createdCase.id,
|
||||
caseVersion: createdCase.version,
|
||||
totalUpdates: 4,
|
||||
});
|
||||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
await testSubjects.click('case-refresh');
|
||||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
expect(testSubjects.existOrFail('cases-show-more-user-actions'));
|
||||
|
||||
const userActionsLists = await find.allByCssSelector(
|
||||
'[data-test-subj="user-actions-list"]'
|
||||
);
|
||||
|
||||
expect(userActionsLists).length(2);
|
||||
|
||||
expect(await userActionsLists[0].findAllByClassName('euiComment')).length(10);
|
||||
|
||||
expect(await userActionsLists[1].findAllByClassName('euiComment')).length(4);
|
||||
|
||||
testSubjects.click('cases-show-more-user-actions');
|
||||
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
expect(await userActionsLists[0].findAllByClassName('euiComment')).length(20);
|
||||
|
||||
expect(await userActionsLists[1].findAllByClassName('euiComment')).length(4);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue