= ({
) : (
@@ -98,7 +93,7 @@ export const CasesTable: FunctionComponent = ({
className={classnames({ isSelectorView })}
columns={columns}
data-test-subj="cases-table"
- isSelectable={showActions}
+ isSelectable={!isSelectorView}
itemId="id"
items={data.cases}
loading={isCommentUpdating}
@@ -128,8 +123,9 @@ export const CasesTable: FunctionComponent = ({
pagination={pagination}
ref={tableRef}
rowProps={tableRowProps}
- selection={showActions ? selection : undefined}
+ selection={!isSelectorView ? selection : undefined}
sorting={sorting}
+ hasActions={false}
/>
);
diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts
index 96a683aee5077..332c0d493101b 100644
--- a/x-pack/plugins/cases/public/components/all_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts
@@ -130,40 +130,3 @@ export const TOTAL_ASSIGNEES_FILTERED = (total: number) =>
defaultMessage: '{total, plural, one {# assignee} other {# assignees}} filtered',
values: { total },
});
-
-export const CLOSED_CASES = ({
- totalCases,
- caseTitle,
-}: {
- totalCases: number;
- caseTitle?: string;
-}) =>
- i18n.translate('xpack.cases.containers.closedCases', {
- values: { caseTitle, totalCases },
- defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
- });
-
-export const REOPENED_CASES = ({
- totalCases,
- caseTitle,
-}: {
- totalCases: number;
- caseTitle?: string;
-}) =>
- i18n.translate('xpack.cases.containers.reopenedCases', {
- values: { caseTitle, totalCases },
- defaultMessage: 'Opened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
- });
-
-export const MARK_IN_PROGRESS_CASES = ({
- totalCases,
- caseTitle,
-}: {
- totalCases: number;
- caseTitle?: string;
-}) =>
- i18n.translate('xpack.cases.containers.markInProgressCases', {
- values: { caseTitle, totalCases },
- defaultMessage:
- 'Marked {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}} as in progress',
- });
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx
new file mode 100644
index 0000000000000..0d8bfab2a7a33
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/use_actions.test.tsx
@@ -0,0 +1,275 @@
+/*
+ * 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 userEvent from '@testing-library/user-event';
+import { waitFor } from '@testing-library/dom';
+import { act, renderHook } from '@testing-library/react-hooks';
+
+import { useActions } from './use_actions';
+import { basicCase } from '../../containers/mock';
+import * as api from '../../containers/api';
+import {
+ AppMockRenderer,
+ createAppMockRenderer,
+ noDeleteCasesPermissions,
+ onlyDeleteCasesPermission,
+ allCasesPermissions,
+ readCasesPermissions,
+} from '../../common/mock';
+
+jest.mock('../../containers/api');
+
+describe('useActions', () => {
+ let appMockRender: AppMockRenderer;
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('renders column actions', async () => {
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "actions": Object {
+ "align": "right",
+ "name": "Actions",
+ "render": [Function],
+ },
+ }
+ `);
+ });
+
+ it('renders the popover', async () => {
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ expect(res.getByTestId(`case-action-popover-${basicCase.id}`)).toBeInTheDocument();
+ });
+
+ it('open the action popover', async () => {
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByText('Actions')).toBeInTheDocument();
+ expect(res.getByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ });
+ });
+
+ it('change the status of the case', async () => {
+ const updateCasesSpy = jest.spyOn(api, 'updateCases');
+
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-status-panel-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId('cases-bulk-action-status-open')).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-status-in-progress')).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-status-closed')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('cases-bulk-action-status-in-progress'));
+ });
+
+ await waitFor(() => {
+ expect(updateCasesSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Modals', () => {
+ it('delete a case', async () => {
+ const deleteSpy = jest.spyOn(api, 'deleteCases');
+
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('cases-bulk-action-delete'), undefined, {
+ skipPointerEventsCheck: true,
+ });
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId('confirm-delete-case-modal')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('confirmModalConfirmButton'));
+ });
+
+ await waitFor(() => {
+ expect(deleteSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('closes the modal', async () => {
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('cases-bulk-action-delete'), undefined, {
+ skipPointerEventsCheck: true,
+ });
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId('confirm-delete-case-modal')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('confirmModalCancelButton'), undefined, {
+ skipPointerEventsCheck: true,
+ });
+ });
+
+ expect(res.queryByTestId('confirm-delete-case-modal')).toBeFalsy();
+ });
+ });
+
+ describe('Permissions', () => {
+ it('shows the correct actions with all permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: allCasesPermissions() });
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ expect(res.getByTestId(`actions-separator-${basicCase.id}`)).toBeInTheDocument();
+ });
+ });
+
+ it('shows the correct actions with no delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: noDeleteCasesPermissions() });
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument();
+ expect(res.queryByTestId('cases-bulk-action-delete')).toBeFalsy();
+ expect(res.queryByTestId(`actions-separator-${basicCase.id}`)).toBeFalsy();
+ });
+ });
+
+ it('shows the correct actions with only delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() });
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ act(() => {
+ userEvent.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
+ });
+
+ await waitFor(() => {
+ expect(res.queryByTestId(`case-action-status-panel-${basicCase.id}`)).toBeFalsy();
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ expect(res.queryByTestId(`actions-separator-${basicCase.id}`)).toBeFalsy();
+ });
+ });
+
+ it('returns null if the user does not have update or delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: readCasesPermissions() });
+ const { result } = renderHook(() => useActions({ disableActions: false }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ expect(result.current.actions).toBe(null);
+ });
+
+ it('disables the action correctly', async () => {
+ appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() });
+ const { result } = renderHook(() => useActions({ disableActions: true }), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const comp = result.current.actions!.render(basicCase) as React.ReactElement;
+ const res = appMockRender.render(comp);
+
+ await waitFor(() => {
+ expect(res.getByTestId(`case-action-popover-button-${basicCase.id}`)).toBeDisabled();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx b/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx
new file mode 100644
index 0000000000000..c397047ee98bf
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx
@@ -0,0 +1,166 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ EuiButtonIcon,
+ EuiContextMenu,
+ EuiContextMenuPanelDescriptor,
+ EuiContextMenuPanelItemDescriptor,
+ EuiPopover,
+ EuiTableComputedColumnType,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { Case } from '../../containers/types';
+import { useDeleteAction } from '../actions/delete/use_delete_action';
+import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
+import { useStatusAction } from '../actions/status/use_status_action';
+import { useRefreshCases } from './use_on_refresh_cases';
+import * as i18n from './translations';
+import { statuses } from '../status';
+import { useCasesContext } from '../cases_context/use_cases_context';
+
+const ActionColumnComponent: React.FC<{ theCase: Case; disableActions: boolean }> = ({
+ theCase,
+ disableActions,
+}) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const tooglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+ const refreshCases = useRefreshCases();
+
+ const deleteAction = useDeleteAction({
+ isDisabled: false,
+ onAction: closePopover,
+ onActionSuccess: refreshCases,
+ });
+
+ const statusAction = useStatusAction({
+ isDisabled: false,
+ onAction: closePopover,
+ onActionSuccess: refreshCases,
+ selectedStatus: theCase.status,
+ });
+
+ const canDelete = deleteAction.canDelete;
+ const canUpdate = statusAction.canUpdateStatus;
+
+ const panels = useMemo((): EuiContextMenuPanelDescriptor[] => {
+ const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [];
+ const panelsToBuild: EuiContextMenuPanelDescriptor[] = [
+ { id: 0, items: mainPanelItems, title: i18n.ACTIONS },
+ ];
+
+ if (canUpdate) {
+ mainPanelItems.push({
+ name: (
+ {statuses[theCase.status]?.label ?? '-'} }}
+ />
+ ),
+ panel: 1,
+ disabled: !canUpdate,
+ key: `case-action-status-panel-${theCase.id}`,
+ 'data-test-subj': `case-action-status-panel-${theCase.id}`,
+ });
+ }
+
+ /**
+ * A separator is added if a) there is one item above
+ * and b) there is an item below. For this to happen the
+ * user has to have delete and update permissions
+ */
+ if (canUpdate && canDelete) {
+ mainPanelItems.push({
+ isSeparator: true,
+ key: `actions-separator-${theCase.id}`,
+ 'data-test-subj': `actions-separator-${theCase.id}`,
+ });
+ }
+
+ if (canDelete) {
+ mainPanelItems.push(deleteAction.getAction([theCase]));
+ }
+
+ if (canUpdate) {
+ panelsToBuild.push({
+ id: 1,
+ title: i18n.STATUS,
+ items: statusAction.getActions([theCase]),
+ });
+ }
+
+ return panelsToBuild;
+ }, [canDelete, canUpdate, deleteAction, statusAction, theCase]);
+
+ return (
+ <>
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+
+
+ {deleteAction.isModalVisible ? (
+
+ ) : null}
+ >
+ );
+};
+
+ActionColumnComponent.displayName = 'ActionColumnComponent';
+
+const ActionColumn = React.memo(ActionColumnComponent);
+
+interface UseBulkActionsReturnValue {
+ actions: EuiTableComputedColumnType | null;
+}
+
+interface UseBulkActionsProps {
+ disableActions: boolean;
+}
+
+export const useActions = ({ disableActions }: UseBulkActionsProps): UseBulkActionsReturnValue => {
+ const { permissions } = useCasesContext();
+ const shouldShowActions = permissions.update || permissions.delete;
+
+ return {
+ actions: shouldShowActions
+ ? {
+ name: i18n.ACTIONS,
+ align: 'right',
+ render: (theCase: Case) => {
+ return (
+
+ );
+ },
+ }
+ : null,
+ };
+};
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx
new file mode 100644
index 0000000000000..88d596af41fdf
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx
@@ -0,0 +1,334 @@
+/*
+ * 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 { EuiContextMenu } from '@elastic/eui';
+import userEvent from '@testing-library/user-event';
+import { waitFor } from '@testing-library/react';
+import { act, renderHook } from '@testing-library/react-hooks';
+
+import {
+ allCasesPermissions,
+ AppMockRenderer,
+ createAppMockRenderer,
+ noDeleteCasesPermissions,
+ onlyDeleteCasesPermission,
+} from '../../common/mock';
+import { useBulkActions } from './use_bulk_actions';
+import * as api from '../../containers/api';
+import { basicCase } from '../../containers/mock';
+
+jest.mock('../../containers/api');
+
+describe('useBulkActions', () => {
+ let appMockRender: AppMockRenderer;
+ const onAction = jest.fn();
+ const onActionSuccess = jest.fn();
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ describe('Panels', () => {
+ it('renders bulk actions', async () => {
+ const { result } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "modals": ,
+ "panels": Array [
+ Object {
+ "id": 0,
+ "items": Array [
+ Object {
+ "data-test-subj": "case-bulk-action-status",
+ "disabled": false,
+ "key": "case-bulk-action-status",
+ "name": "Status",
+ "panel": 1,
+ },
+ Object {
+ "data-test-subj": "bulk-actions-separator",
+ "isSeparator": true,
+ "key": "bulk-actions-separator",
+ },
+ Object {
+ "data-test-subj": "cases-bulk-action-delete",
+ "disabled": false,
+ "icon": ,
+ "key": "cases-bulk-action-delete",
+ "name":
+ Delete case
+ ,
+ "onClick": [Function],
+ },
+ ],
+ "title": "Actions",
+ },
+ Object {
+ "id": 1,
+ "items": Array [
+ Object {
+ "data-test-subj": "cases-bulk-action-status-open",
+ "disabled": true,
+ "icon": "empty",
+ "key": "cases-bulk-action-status-open",
+ "name": "Open",
+ "onClick": [Function],
+ },
+ Object {
+ "data-test-subj": "cases-bulk-action-status-in-progress",
+ "disabled": false,
+ "icon": "empty",
+ "key": "cases-bulk-action-status-in-progress",
+ "name": "In progress",
+ "onClick": [Function],
+ },
+ Object {
+ "data-test-subj": "cases-bulk-action-status-closed",
+ "disabled": false,
+ "icon": "empty",
+ "key": "cases-bulk-status-action",
+ "name": "Closed",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Status",
+ },
+ ],
+ }
+ `);
+ });
+
+ it('change the status of cases', async () => {
+ const updateCasesSpy = jest.spyOn(api, 'updateCases');
+
+ const { result, waitFor: waitForHook } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ const modals = result.current.modals;
+ const panels = result.current.panels;
+
+ const res = appMockRender.render(
+ <>
+
+ {modals}
+ >
+ );
+
+ act(() => {
+ userEvent.click(res.getByTestId('case-bulk-action-status'));
+ });
+
+ await waitFor(() => {
+ expect(res.getByTestId('cases-bulk-action-status-open')).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-status-in-progress')).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-status-closed')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('cases-bulk-action-status-in-progress'));
+ });
+
+ await waitForHook(() => {
+ expect(updateCasesSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Modals', () => {
+ it('delete a case', async () => {
+ const deleteSpy = jest.spyOn(api, 'deleteCases');
+
+ const { result, waitFor: waitForHook } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ let modals = result.current.modals;
+ const panels = result.current.panels;
+
+ const res = appMockRender.render(
+ <>
+
+ {modals}
+ >
+ );
+
+ act(() => {
+ userEvent.click(res.getByTestId('cases-bulk-action-delete'));
+ });
+
+ modals = result.current.modals;
+ res.rerender(
+ <>
+
+ {modals}
+ >
+ );
+
+ await waitFor(() => {
+ expect(res.getByTestId('confirm-delete-case-modal')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('confirmModalConfirmButton'));
+ });
+
+ await waitForHook(() => {
+ expect(deleteSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('closes the modal', async () => {
+ const { result } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ let modals = result.current.modals;
+ const panels = result.current.panels;
+
+ const res = appMockRender.render(
+ <>
+
+ {modals}
+ >
+ );
+
+ act(() => {
+ userEvent.click(res.getByTestId('cases-bulk-action-delete'));
+ });
+
+ modals = result.current.modals;
+ res.rerender(
+ <>
+
+ {modals}
+ >
+ );
+
+ await waitFor(() => {
+ expect(res.getByTestId('confirm-delete-case-modal')).toBeInTheDocument();
+ });
+
+ act(() => {
+ userEvent.click(res.getByTestId('confirmModalCancelButton'));
+ });
+
+ modals = result.current.modals;
+ res.rerender(
+ <>
+
+ {modals}
+ >
+ );
+
+ expect(res.queryByTestId('confirm-delete-case-modal')).toBeFalsy();
+ });
+ });
+ });
+
+ describe('Permissions', () => {
+ it('shows the correct actions with all permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: allCasesPermissions() });
+ const { result, waitFor: waitForHook } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ const modals = result.current.modals;
+ const panels = result.current.panels;
+
+ const res = appMockRender.render(
+ <>
+
+ {modals}
+ >
+ );
+
+ await waitForHook(() => {
+ expect(res.getByTestId('case-bulk-action-status')).toBeInTheDocument();
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ expect(res.getByTestId('bulk-actions-separator')).toBeInTheDocument();
+ });
+ });
+
+ it('shows the correct actions with no delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: noDeleteCasesPermissions() });
+ const { result, waitFor: waitForHook } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ const modals = result.current.modals;
+ const panels = result.current.panels;
+
+ const res = appMockRender.render(
+ <>
+
+ {modals}
+ >
+ );
+
+ await waitForHook(() => {
+ expect(res.getByTestId('case-bulk-action-status')).toBeInTheDocument();
+ expect(res.queryByTestId('cases-bulk-action-delete')).toBeFalsy();
+ expect(res.queryByTestId('bulk-actions-separator')).toBeFalsy();
+ });
+ });
+
+ it('shows the correct actions with only delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() });
+ const { result, waitFor: waitForHook } = renderHook(
+ () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCase] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ const modals = result.current.modals;
+ const panels = result.current.panels;
+
+ const res = appMockRender.render(
+ <>
+
+ {modals}
+ >
+ );
+
+ await waitForHook(() => {
+ expect(res.queryByTestId('case-bulk-action-status')).toBeFalsy();
+ expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
+ expect(res.queryByTestId('bulk-actions-separator')).toBeFalsy();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx
new file mode 100644
index 0000000000000..bef085ce6d8a0
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
+import React, { useMemo } from 'react';
+
+import { Case } from '../../containers/types';
+import { useDeleteAction } from '../actions/delete/use_delete_action';
+import { useStatusAction } from '../actions/status/use_status_action';
+import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
+import * as i18n from './translations';
+
+interface UseBulkActionsProps {
+ selectedCases: Case[];
+ onAction: () => void;
+ onActionSuccess: () => void;
+}
+
+interface UseBulkActionsReturnValue {
+ panels: EuiContextMenuPanelDescriptor[];
+ modals: JSX.Element;
+}
+
+export const useBulkActions = ({
+ selectedCases,
+ onAction,
+ onActionSuccess,
+}: UseBulkActionsProps): UseBulkActionsReturnValue => {
+ const isDisabled = selectedCases.length === 0;
+
+ const deleteAction = useDeleteAction({
+ isDisabled,
+ onAction,
+ onActionSuccess,
+ });
+
+ const statusAction = useStatusAction({
+ isDisabled,
+ onAction,
+ onActionSuccess,
+ });
+
+ const canDelete = deleteAction.canDelete;
+ const canUpdate = statusAction.canUpdateStatus;
+
+ const panels = useMemo((): EuiContextMenuPanelDescriptor[] => {
+ const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [];
+ const panelsToBuild: EuiContextMenuPanelDescriptor[] = [
+ { id: 0, items: mainPanelItems, title: i18n.ACTIONS },
+ ];
+
+ if (canUpdate) {
+ mainPanelItems.push({
+ name: i18n.STATUS,
+ panel: 1,
+ disabled: isDisabled,
+ 'data-test-subj': 'case-bulk-action-status',
+ key: 'case-bulk-action-status',
+ });
+ }
+
+ /**
+ * A separator is added if a) there is one item above
+ * and b) there is an item below. For this to happen the
+ * user has to have delete and update permissions
+ */
+ if (canUpdate && canDelete) {
+ mainPanelItems.push({
+ isSeparator: true as const,
+ key: 'bulk-actions-separator',
+ 'data-test-subj': 'bulk-actions-separator',
+ });
+ }
+
+ if (canDelete) {
+ mainPanelItems.push(deleteAction.getAction(selectedCases));
+ }
+
+ if (canUpdate) {
+ panelsToBuild.push({
+ id: 1,
+ title: i18n.STATUS,
+ items: statusAction.getActions(selectedCases),
+ });
+ }
+
+ return panelsToBuild;
+ }, [canDelete, canUpdate, deleteAction, isDisabled, selectedCases, statusAction]);
+
+ return {
+ modals: (
+ <>
+ {deleteAction.isModalVisible ? (
+
+ ) : null}
+ >
+ ),
+ panels,
+ };
+};
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
new file mode 100644
index 0000000000000..85caa0b0348dc
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.test.tsx
@@ -0,0 +1,679 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
+
+import '../../common/mock/match_media';
+import { ExternalServiceColumn, GetCasesColumn, useCasesColumns } from './use_cases_columns';
+import { useGetCasesMockState } from '../../containers/mock';
+import { connectors } from '../configure_cases/__mock__';
+import {
+ AppMockRenderer,
+ createAppMockRenderer,
+ readCasesPermissions,
+ TestProviders,
+} from '../../common/mock';
+import { renderHook } from '@testing-library/react-hooks';
+import { CaseStatuses } from '../../../common';
+import { userProfilesMap, userProfiles } from '../../containers/user_profiles/api.mock';
+
+describe('useCasesColumns ', () => {
+ let appMockRender: AppMockRenderer;
+ const useCasesColumnsProps: GetCasesColumn = {
+ filterStatus: CaseStatuses.open,
+ userProfiles: userProfilesMap,
+ currentUserProfile: userProfiles[0],
+ isSelectorView: false,
+ showSolutionColumn: true,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRender = createAppMockRenderer();
+ });
+
+ it('return all columns correctly', async () => {
+ const license = licensingMock.createLicense({
+ license: { type: 'platinum' },
+ });
+
+ appMockRender = createAppMockRenderer({ license });
+
+ const { result } = renderHook(() => useCasesColumns(useCasesColumnsProps), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "assignees",
+ "name": "Assignees",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "name": "Actions",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('does not render the solution columns', async () => {
+ const license = licensingMock.createLicense({
+ license: { type: 'platinum' },
+ });
+
+ appMockRender = createAppMockRenderer({ license });
+
+ const { result } = renderHook(
+ () => useCasesColumns({ ...useCasesColumnsProps, showSolutionColumn: false }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "assignees",
+ "name": "Assignees",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "name": "Actions",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('does not return the alerts column', async () => {
+ const license = licensingMock.createLicense({
+ license: { type: 'platinum' },
+ });
+
+ appMockRender = createAppMockRenderer({ license, features: { alerts: { enabled: false } } });
+
+ const { result } = renderHook(() => useCasesColumns(useCasesColumnsProps), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "assignees",
+ "name": "Assignees",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "name": "Actions",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('does not return the assignees column', async () => {
+ const { result } = renderHook(() => useCasesColumns(useCasesColumnsProps), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "name": "Actions",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('shows the closedAt column if the filterStatus=closed', async () => {
+ appMockRender = createAppMockRenderer();
+
+ const { result } = renderHook(
+ () => useCasesColumns({ ...useCasesColumnsProps, filterStatus: CaseStatuses.closed }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "closedAt",
+ "name": "Closed on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "name": "Actions",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('shows the select button if isSelectorView=true', async () => {
+ const { result } = renderHook(
+ () => useCasesColumns({ ...useCasesColumnsProps, isSelectorView: true }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('does not shows the actions if isSelectorView=true', async () => {
+ const { result } = renderHook(
+ () => useCasesColumns({ ...useCasesColumnsProps, isSelectorView: true }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('does not shows the actions if the user does not have the right permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: readCasesPermissions() });
+
+ const { result } = renderHook(() => useCasesColumns(useCasesColumnsProps), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ Object {
+ "name": "Name",
+ "render": [Function],
+ },
+ Object {
+ "field": "tags",
+ "name": "Tags",
+ "render": [Function],
+ "truncateText": true,
+ },
+ Object {
+ "align": "right",
+ "field": "totalAlerts",
+ "name": "Alerts",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "owner",
+ "name": "Solution",
+ "render": [Function],
+ },
+ Object {
+ "align": "right",
+ "field": "totalComment",
+ "name": "Comments",
+ "render": [Function],
+ },
+ Object {
+ "field": "createdAt",
+ "name": "Created on",
+ "render": [Function],
+ "sortable": true,
+ },
+ Object {
+ "name": "External Incident",
+ "render": [Function],
+ },
+ Object {
+ "name": "Status",
+ "render": [Function],
+ },
+ Object {
+ "name": "Severity",
+ "render": [Function],
+ },
+ ],
+ }
+ `);
+ });
+
+ describe('ExternalServiceColumn ', () => {
+ it('Not pushed render', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(
+ wrapper.find(`[data-test-subj="case-table-column-external-notPushed"]`).last().exists()
+ ).toBeTruthy();
+ });
+
+ it('Up to date', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(
+ wrapper.find(`[data-test-subj="case-table-column-external-upToDate"]`).last().exists()
+ ).toBeTruthy();
+ });
+
+ it('Needs update', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(
+ wrapper.find(`[data-test-subj="case-table-column-external-requiresUpdate"]`).last().exists()
+ ).toBeTruthy();
+ });
+
+ it('it does not throw when accessing the icon if the connector type is not registered', () => {
+ // If the component throws the test will fail
+ expect(() =>
+ mount(
+
+
+
+ )
+ ).not.toThrowError();
+ });
+
+ it('shows the connectors icon if the user has read access to actions', async () => {
+ const result = appMockRender.render(
+
+ );
+
+ expect(result.getByTestId('cases-table-connector-icon')).toBeInTheDocument();
+ });
+
+ it('hides the connectors icon if the user does not have read access to actions', async () => {
+ appMockRender.coreStart.application.capabilities = {
+ ...appMockRender.coreStart.application.capabilities,
+ actions: { save: false, show: false },
+ };
+
+ const result = appMockRender.render(
+
+ );
+
+ expect(result.queryByTestId('cases-table-connector-icon')).toBe(null);
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
similarity index 51%
rename from x-pack/plugins/cases/public/components/all_cases/columns.tsx
rename to x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
index 948abdbbcd2f3..b996e219c17e7 100644
--- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback } from 'react';
import {
EuiBadgeGroup,
EuiBadge,
@@ -24,7 +24,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
import styled from 'styled-components';
import { UserProfileWithAvatar } from '@kbn/user-profile-components';
-import { Case, UpdateByKey } from '../../../common/ui/types';
+import { Case } from '../../../common/ui/types';
import { CaseStatuses, ActionConnector, CaseSeverity } from '../../../common/api';
import { OWNER_INFO } from '../../../common/constants';
import { getEmptyTagValue } from '../empty_value';
@@ -32,26 +32,21 @@ import { FormattedRelativePreferenceDate } from '../formatted_date';
import { CaseDetailsLink } from '../links';
import * as i18n from './translations';
import { ALERTS } from '../../common/translations';
-import { getActions } from './actions';
-import { useDeleteCases } from '../../containers/use_delete_cases';
-import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
+import { useActions } from './use_actions';
import { useApplicationCapabilities, useKibana } from '../../common/lib/kibana';
-import { StatusContextMenu } from '../case_action_bar/status_context_menu';
import { TruncatedText } from '../truncated_text';
import { getConnectorIcon } from '../utils';
import type { CasesOwners } from '../../client/helpers/can_use_cases';
import { severities } from '../severity/config';
-import { useUpdateCase } from '../../containers/use_update_case';
-import { useCasesContext } from '../cases_context/use_cases_context';
import { UserToolTip } from '../user_profiles/user_tooltip';
import { useAssignees } from '../../containers/user_profiles/use_assignees';
import { getUsernameDataTestSubj } from '../user_profiles/data_test_subject';
import { CurrentUserProfile } from '../types';
import { SmallUserAvatar } from '../user_profiles/small_user_avatar';
import { useCasesFeatures } from '../../common/use_cases_features';
-import { useRefreshCases } from './use_on_refresh_cases';
+import { Status } from '../status';
-export type CasesColumns =
+type CasesColumns =
| EuiTableActionsColumnType
| EuiTableComputedColumnType
| EuiTableFieldDataColumnType;
@@ -107,9 +102,14 @@ export interface GetCasesColumn {
isSelectorView: boolean;
connectors?: ActionConnector[];
onRowClick?: (theCase: Case) => void;
-
showSolutionColumn?: boolean;
+ disableActions?: boolean;
+}
+
+export interface UseCasesColumnsReturnValue {
+ columns: CasesColumns[];
}
+
export const useCasesColumns = ({
filterStatus,
userProfiles,
@@ -118,57 +118,10 @@ export const useCasesColumns = ({
connectors = [],
onRowClick,
showSolutionColumn,
-}: GetCasesColumn): CasesColumns[] => {
- const [isModalVisible, setIsModalVisible] = useState(false);
- const { mutate: deleteCases } = useDeleteCases();
- const refreshCases = useRefreshCases();
+ disableActions = false,
+}: GetCasesColumn): UseCasesColumnsReturnValue => {
const { isAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures();
- const { permissions } = useCasesContext();
- const [caseToBeDeleted, setCaseToBeDeleted] = useState();
- const { updateCaseProperty, isLoading: isLoadingUpdateCase } = useUpdateCase();
-
- const closeModal = useCallback(() => setIsModalVisible(false), []);
- const openModal = useCallback(() => setIsModalVisible(true), []);
-
- const onDeleteAction = useCallback(
- (theCase: Case) => {
- openModal();
- setCaseToBeDeleted(theCase.id);
- },
- [openModal]
- );
-
- const onConfirmDeletion = useCallback(() => {
- closeModal();
- if (caseToBeDeleted) {
- deleteCases({
- caseIds: [caseToBeDeleted],
- successToasterTitle: i18n.DELETED_CASES(1),
- });
- }
- }, [caseToBeDeleted, closeModal, deleteCases]);
-
- const handleDispatchUpdate = useCallback(
- ({ updateKey, updateValue, caseData }: UpdateByKey) => {
- updateCaseProperty({
- updateKey,
- updateValue,
- caseData,
- onSuccess: () => {
- refreshCases();
- },
- });
- },
- [refreshCases, updateCaseProperty]
- );
-
- const actions = useMemo(
- () =>
- getActions({
- deleteCaseOnClick: onDeleteAction,
- }),
- [onDeleteAction]
- );
+ const { actions } = useActions({ disableActions });
const assignCaseAction = useCallback(
async (theCase: Case) => {
@@ -179,7 +132,7 @@ export const useCasesColumns = ({
[onRowClick]
);
- return [
+ const columns: CasesColumns[] = [
{
name: i18n.NAME,
render: (theCase: Case) => {
@@ -205,129 +158,134 @@ export const useCasesColumns = ({
return getEmptyTagValue();
},
},
- ...(caseAssignmentAuthorized
- ? [
- {
- field: 'assignees',
- name: i18n.ASSIGNEES,
- render: (assignees: Case['assignees']) => (
-
- ),
- },
- ]
- : []),
- {
- field: 'tags',
- name: i18n.TAGS,
- render: (tags: Case['tags']) => {
- if (tags != null && tags.length > 0) {
- const badges = (
-
- {tags.map((tag: string, i: number) => (
-
- {tag}
-
- ))}
-
- );
+ ];
+
+ if (caseAssignmentAuthorized) {
+ columns.push({
+ field: 'assignees',
+ name: i18n.ASSIGNEES,
+ render: (assignees: Case['assignees']) => (
+
+ ),
+ });
+ }
+
+ columns.push({
+ field: 'tags',
+ name: i18n.TAGS,
+ render: (tags: Case['tags']) => {
+ if (tags != null && tags.length > 0) {
+ const badges = (
+
+ {tags.map((tag: string, i: number) => (
+
+ {tag}
+
+ ))}
+
+ );
+
+ return (
+
+ {badges}
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ truncateText: true,
+ });
+
+ if (isAlertsEnabled) {
+ columns.push({
+ align: RIGHT_ALIGNMENT,
+ field: 'totalAlerts',
+ name: ALERTS,
+ render: (totalAlerts: Case['totalAlerts']) =>
+ totalAlerts != null
+ ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`)
+ : getEmptyTagValue(),
+ });
+ }
+
+ if (showSolutionColumn) {
+ columns.push({
+ align: RIGHT_ALIGNMENT,
+ field: 'owner',
+ name: i18n.SOLUTION,
+ render: (caseOwner: CasesOwners) => {
+ const ownerInfo = OWNER_INFO[caseOwner];
+ return ownerInfo ? (
+
+ ) : (
+ getEmptyTagValue()
+ );
+ },
+ });
+ }
+
+ columns.push({
+ align: RIGHT_ALIGNMENT,
+ field: 'totalComment',
+ name: i18n.COMMENTS,
+ render: (totalComment: Case['totalComment']) =>
+ totalComment != null
+ ? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
+ : getEmptyTagValue(),
+ });
+ if (filterStatus === CaseStatuses.closed) {
+ columns.push({
+ field: 'closedAt',
+ name: i18n.CLOSED_ON,
+ sortable: true,
+ render: (closedAt: Case['closedAt']) => {
+ if (closedAt != null) {
return (
-
- {badges}
-
+
+
+
);
}
return getEmptyTagValue();
},
- truncateText: true,
- },
- ...(isAlertsEnabled
- ? [
- {
- align: RIGHT_ALIGNMENT,
- field: 'totalAlerts',
- name: ALERTS,
- render: (totalAlerts: Case['totalAlerts']) =>
- totalAlerts != null
- ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`)
- : getEmptyTagValue(),
- },
- ]
- : []),
- ...(showSolutionColumn
- ? [
- {
- align: RIGHT_ALIGNMENT,
- field: 'owner',
- name: i18n.SOLUTION,
- render: (caseOwner: CasesOwners) => {
- const ownerInfo = OWNER_INFO[caseOwner];
- return ownerInfo ? (
-
- ) : (
- getEmptyTagValue()
- );
- },
- },
- ]
- : []),
- {
- align: RIGHT_ALIGNMENT,
- field: 'totalComment',
- name: i18n.COMMENTS,
- render: (totalComment: Case['totalComment']) =>
- totalComment != null
- ? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
- : getEmptyTagValue(),
- },
- filterStatus === CaseStatuses.closed
- ? {
- field: 'closedAt',
- name: i18n.CLOSED_ON,
- sortable: true,
- render: (closedAt: Case['closedAt']) => {
- if (closedAt != null) {
- return (
-
-
-
- );
- }
- return getEmptyTagValue();
- },
+ });
+ } else {
+ columns.push({
+ field: 'createdAt',
+ name: i18n.CREATED_ON,
+ sortable: true,
+ render: (createdAt: Case['createdAt']) => {
+ if (createdAt != null) {
+ return (
+
+
+
+ );
}
- : {
- field: 'createdAt',
- name: i18n.CREATED_ON,
- sortable: true,
- render: (createdAt: Case['createdAt']) => {
- if (createdAt != null) {
- return (
-
-
-
- );
- }
- return getEmptyTagValue();
- },
- },
+ return getEmptyTagValue();
+ },
+ });
+ }
+
+ columns.push(
{
name: i18n.EXTERNAL_INCIDENT,
render: (theCase: Case) => {
@@ -337,32 +295,16 @@ export const useCasesColumns = ({
return getEmptyTagValue();
},
},
- ...(!isSelectorView
- ? [
- {
- name: i18n.STATUS,
- render: (theCase: Case) => {
- if (theCase.status === null || theCase.status === undefined) {
- return getEmptyTagValue();
- }
-
- return (
-
- handleDispatchUpdate({
- updateKey: 'status',
- updateValue: status,
- caseData: theCase,
- })
- }
- />
- );
- },
- },
- ]
- : []),
+ {
+ name: i18n.STATUS,
+ render: (theCase: Case) => {
+ if (theCase.status === null || theCase.status === undefined) {
+ return getEmptyTagValue();
+ }
+
+ return ;
+ },
+ },
{
name: i18n.SEVERITY,
render: (theCase: Case) => {
@@ -376,52 +318,37 @@ export const useCasesColumns = ({
}
return getEmptyTagValue();
},
- },
+ }
+ );
- ...(isSelectorView
- ? [
- {
- align: RIGHT_ALIGNMENT,
- render: (theCase: Case) => {
- if (theCase.id != null) {
- return (
- {
- assignCaseAction(theCase);
- }}
- size="s"
- fill={true}
- >
- {i18n.SELECT}
-
- );
- }
- return getEmptyTagValue();
- },
- },
- ]
- : []),
- ...(permissions.delete && !isSelectorView
- ? [
- {
- name: (
- <>
- {i18n.ACTIONS}
- {isModalVisible ? (
-
- ) : null}
- >
- ),
- actions,
- },
- ]
- : []),
- ];
+ if (isSelectorView) {
+ columns.push({
+ align: RIGHT_ALIGNMENT,
+ render: (theCase: Case) => {
+ if (theCase.id != null) {
+ return (
+ {
+ assignCaseAction(theCase);
+ }}
+ size="s"
+ fill={true}
+ >
+ {i18n.SELECT}
+
+ );
+ }
+ return getEmptyTagValue();
+ },
+ });
+ }
+
+ if (!isSelectorView && actions) {
+ columns.push(actions);
+ }
+
+ return { columns };
};
interface Props {
diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx
new file mode 100644
index 0000000000000..3a8769460656d
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import {
+ noCasesPermissions,
+ onlyDeleteCasesPermission,
+ AppMockRenderer,
+ createAppMockRenderer,
+ writeCasesPermissions,
+} from '../../common/mock';
+import { casesQueriesKeys } from '../../containers/constants';
+import { basicCase } from '../../containers/mock';
+import { CasesTableUtilityBar } from './utility_bar';
+
+describe('Severity form field', () => {
+ let appMockRender: AppMockRenderer;
+ const deselectCases = jest.fn();
+
+ const props = {
+ totalCases: 5,
+ selectedCases: [basicCase],
+ deselectCases,
+ };
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ });
+
+ it('renders', async () => {
+ const result = appMockRender.render();
+ expect(result.getByText('Showing 5 cases')).toBeInTheDocument();
+ expect(result.getByText('Selected 1 case')).toBeInTheDocument();
+ expect(result.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument();
+ expect(result.getByTestId('all-cases-refresh-link-icon')).toBeInTheDocument();
+ });
+
+ it('opens the bulk actions correctly', async () => {
+ const result = appMockRender.render();
+
+ act(() => {
+ userEvent.click(result.getByTestId('case-table-bulk-actions-link-icon'));
+ });
+
+ await waitFor(() => {
+ expect(result.getByTestId('case-table-bulk-actions-context-menu'));
+ });
+ });
+
+ it('closes the bulk actions correctly', async () => {
+ const result = appMockRender.render();
+
+ act(() => {
+ userEvent.click(result.getByTestId('case-table-bulk-actions-link-icon'));
+ });
+
+ await waitFor(() => {
+ expect(result.getByTestId('case-table-bulk-actions-context-menu'));
+ });
+
+ act(() => {
+ userEvent.click(result.getByTestId('case-table-bulk-actions-link-icon'));
+ });
+
+ await waitFor(() => {
+ expect(result.queryByTestId('case-table-bulk-actions-context-menu')).toBeFalsy();
+ });
+ });
+
+ it('refresh correctly', async () => {
+ const result = appMockRender.render();
+ const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries');
+
+ act(() => {
+ userEvent.click(result.getByTestId('all-cases-refresh-link-icon'));
+ });
+
+ await waitFor(() => {
+ expect(deselectCases).toHaveBeenCalled();
+ expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList());
+ expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags());
+ expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles());
+ });
+ });
+
+ it('does not show the bulk actions without update & delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: noCasesPermissions() });
+ const result = appMockRender.render();
+
+ expect(result.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy();
+ });
+
+ it('does show the bulk actions with only delete permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() });
+ const result = appMockRender.render();
+
+ expect(result.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument();
+ });
+
+ it('does show the bulk actions with update permissions', async () => {
+ appMockRender = createAppMockRenderer({ permissions: writeCasesPermissions() });
+ const result = appMockRender.render();
+
+ expect(result.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument();
+ });
+
+ it('does not show the bulk actions if there are not selected cases', async () => {
+ const result = appMockRender.render();
+
+ expect(result.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy();
+ expect(result.queryByText('Showing 0 cases')).toBeFalsy();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx
index fdfcdc17d472c..415472574f25b 100644
--- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx
@@ -6,8 +6,7 @@
*/
import React, { FunctionComponent, useCallback, useState } from 'react';
-import { EuiContextMenuPanel } from '@elastic/eui';
-import { CaseStatuses } from '../../../common';
+import { EuiContextMenu } from '@elastic/eui';
import {
UtilityBar,
UtilityBarAction,
@@ -16,142 +15,92 @@ import {
UtilityBarText,
} from '../utility_bar';
import * as i18n from './translations';
-import { Cases, Case, FilterOptions } from '../../../common/ui/types';
-import { getBulkItems } from '../bulk_actions';
-import { useDeleteCases } from '../../containers/use_delete_cases';
-import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
-import { useUpdateCases } from '../../containers/use_bulk_update_case';
+import { Case } from '../../../common/ui/types';
import { useRefreshCases } from './use_on_refresh_cases';
+import { UtilityBarBulkActions } from '../utility_bar/utility_bar_bulk_actions';
+import { useBulkActions } from './use_bulk_actions';
+import { useCasesContext } from '../cases_context/use_cases_context';
interface Props {
- data: Cases;
- enableBulkActions: boolean;
- filterOptions: FilterOptions;
+ isSelectorView?: boolean;
+ totalCases: number;
selectedCases: Case[];
deselectCases: () => void;
}
-export const getStatusToasterMessage = (status: CaseStatuses, cases: Case[]): string => {
- const totalCases = cases.length;
- const caseTitle = totalCases === 1 ? cases[0].title : '';
+export const CasesTableUtilityBar: FunctionComponent = React.memo(
+ ({ isSelectorView, totalCases, selectedCases, deselectCases }) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+ const refreshCases = useRefreshCases();
+ const { permissions } = useCasesContext();
- if (status === CaseStatuses.open) {
- return i18n.REOPENED_CASES({ totalCases, caseTitle });
- } else if (status === CaseStatuses['in-progress']) {
- return i18n.MARK_IN_PROGRESS_CASES({ totalCases, caseTitle });
- } else if (status === CaseStatuses.closed) {
- return i18n.CLOSED_CASES({ totalCases, caseTitle });
- }
-
- return '';
-};
-
-export const CasesTableUtilityBar: FunctionComponent = ({
- data,
- enableBulkActions = false,
- filterOptions,
- selectedCases,
- deselectCases,
-}) => {
- const [isModalVisible, setIsModalVisible] = useState(false);
- const onCloseModal = useCallback(() => setIsModalVisible(false), []);
- const refreshCases = useRefreshCases();
-
- const { mutate: deleteCases } = useDeleteCases();
- const { mutate: updateCases } = useUpdateCases();
-
- const toggleBulkDeleteModal = useCallback((cases: Case[]) => {
- setIsModalVisible(true);
- }, []);
-
- const handleUpdateCaseStatus = useCallback(
- (status: CaseStatuses) => {
- const casesToUpdate = selectedCases.map((theCase) => ({
- status,
- id: theCase.id,
- version: theCase.version,
- }));
+ const onRefresh = useCallback(() => {
+ deselectCases();
+ refreshCases();
+ }, [deselectCases, refreshCases]);
- updateCases({
- cases: casesToUpdate,
- successToasterTitle: getStatusToasterMessage(status, selectedCases),
- });
- },
- [selectedCases, updateCases]
- );
-
- const getBulkItemsPopoverContent = useCallback(
- (closePopover: () => void) => (
-
- ),
- [selectedCases, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus]
- );
-
- const onConfirmDeletion = useCallback(() => {
- setIsModalVisible(false);
- deleteCases({
- caseIds: selectedCases.map(({ id }) => id),
- successToasterTitle: i18n.DELETED_CASES(selectedCases.length),
+ const { panels, modals } = useBulkActions({
+ selectedCases,
+ onAction: closePopover,
+ onActionSuccess: onRefresh,
});
- }, [deleteCases, selectedCases]);
- const onRefresh = useCallback(() => {
- deselectCases();
- refreshCases();
- }, [deselectCases, refreshCases]);
+ /**
+ * At least update or delete permissions needed to show bulk actions.
+ * Granular permission check for each action is performed
+ * in the useBulkActions hook.
+ */
+ const showBulkActions = (permissions.update || permissions.delete) && selectedCases.length > 0;
- return (
-
-
-
-
- {i18n.SHOWING_CASES(data.total ?? 0)}
-
-
-
- {enableBulkActions && (
- <>
-
- {i18n.SHOWING_SELECTED_CASES(selectedCases.length)}
+ return (
+ <>
+
+
+
+
+ {i18n.SHOWING_CASES(totalCases)}
-
+
+
+ {!isSelectorView && showBulkActions && (
+ <>
+
+ {i18n.SHOWING_SELECTED_CASES(selectedCases.length)}
+
+
+
+
+ >
+ )}
- {i18n.BULK_ACTIONS}
+ {i18n.REFRESH}
- >
- )}
-
- {i18n.REFRESH}
-
-
-
- {isModalVisible ? (
-
- ) : null}
-
- );
-};
+
+
+
+ {modals}
+ >
+ );
+ }
+);
+
CasesTableUtilityBar.displayName = 'CasesTableUtilityBar';
diff --git a/x-pack/plugins/cases/public/components/bulk_actions/index.tsx b/x-pack/plugins/cases/public/components/bulk_actions/index.tsx
deleted file mode 100644
index fcf2002f8882c..0000000000000
--- a/x-pack/plugins/cases/public/components/bulk_actions/index.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiContextMenuItem } from '@elastic/eui';
-
-import { CaseStatusWithAllStatus } from '../../../common/ui/types';
-import { CaseStatuses } from '../../../common/api';
-import { statuses } from '../status';
-import * as i18n from './translations';
-import { Case } from '../../containers/types';
-
-interface GetBulkItems {
- caseStatus: CaseStatusWithAllStatus;
- closePopover: () => void;
- deleteCasesAction: (cases: Case[]) => void;
- selectedCases: Case[];
- updateCaseStatus: (status: CaseStatuses) => void;
-}
-
-export const getBulkItems = ({
- caseStatus,
- closePopover,
- deleteCasesAction,
- selectedCases,
- updateCaseStatus,
-}: GetBulkItems) => {
- let statusMenuItems: JSX.Element[] = [];
-
- const openMenuItem = (
- {
- closePopover();
- updateCaseStatus(CaseStatuses.open);
- }}
- >
- {statuses[CaseStatuses.open].actions.bulk.title}
-
- );
-
- const inProgressMenuItem = (
- {
- closePopover();
- updateCaseStatus(CaseStatuses['in-progress']);
- }}
- >
- {statuses[CaseStatuses['in-progress']].actions.bulk.title}
-
- );
-
- const closeMenuItem = (
- {
- closePopover();
- updateCaseStatus(CaseStatuses.closed);
- }}
- >
- {statuses[CaseStatuses.closed].actions.bulk.title}
-
- );
-
- switch (caseStatus) {
- case CaseStatuses.open:
- statusMenuItems = [inProgressMenuItem, closeMenuItem];
- break;
-
- case CaseStatuses['in-progress']:
- statusMenuItems = [openMenuItem, closeMenuItem];
- break;
-
- case CaseStatuses.closed:
- statusMenuItems = [openMenuItem, inProgressMenuItem];
- break;
-
- default:
- break;
- }
-
- return [
- ...statusMenuItems,
- {
- closePopover();
- deleteCasesAction(selectedCases);
- }}
- >
- {i18n.BULK_ACTION_DELETE_SELECTED}
- ,
- ];
-};
diff --git a/x-pack/plugins/cases/public/components/link_icon/index.tsx b/x-pack/plugins/cases/public/components/link_icon/index.tsx
index b33529399db90..6285eceed0dd4 100644
--- a/x-pack/plugins/cases/public/components/link_icon/index.tsx
+++ b/x-pack/plugins/cases/public/components/link_icon/index.tsx
@@ -79,6 +79,7 @@ export const LinkIcon = React.memo(
}
return theChild != null && Object.keys(theChild).length > 0 ? (theChild as string) : '';
}, []);
+
const aria = useMemo(() => {
if (ariaLabel) {
return ariaLabel;
diff --git a/x-pack/plugins/cases/public/components/status/config.ts b/x-pack/plugins/cases/public/components/status/config.ts
index 6c5ff18ad977a..520759991605b 100644
--- a/x-pack/plugins/cases/public/components/status/config.ts
+++ b/x-pack/plugins/cases/public/components/status/config.ts
@@ -19,9 +19,6 @@ export const statuses: Statuses = {
label: i18n.OPEN,
icon: 'folderOpen' as const,
actions: {
- bulk: {
- title: i18n.BULK_ACTION_OPEN_SELECTED,
- },
single: {
title: i18n.OPEN_CASE,
},
@@ -41,9 +38,6 @@ export const statuses: Statuses = {
label: i18n.IN_PROGRESS,
icon: 'folderExclamation' as const,
actions: {
- bulk: {
- title: i18n.BULK_ACTION_MARK_IN_PROGRESS,
- },
single: {
title: i18n.MARK_CASE_IN_PROGRESS,
},
@@ -63,9 +57,6 @@ export const statuses: Statuses = {
label: i18n.CLOSED,
icon: 'folderCheck' as const,
actions: {
- bulk: {
- title: i18n.BULK_ACTION_CLOSE_SELECTED,
- },
single: {
title: i18n.CLOSE_CASE,
},
diff --git a/x-pack/plugins/cases/public/components/status/translations.ts b/x-pack/plugins/cases/public/components/status/translations.ts
index 4fe75bbcfac7a..9401209c51c08 100644
--- a/x-pack/plugins/cases/public/components/status/translations.ts
+++ b/x-pack/plugins/cases/public/components/status/translations.ts
@@ -40,30 +40,9 @@ export const CASE_CLOSED = i18n.translate('xpack.cases.caseView.caseClosed', {
defaultMessage: 'Case closed',
});
-export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
- 'xpack.cases.caseTable.bulkActions.closeSelectedTitle',
- {
- defaultMessage: 'Close selected',
- }
-);
-
-export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
- 'xpack.cases.caseTable.bulkActions.openSelectedTitle',
- {
- defaultMessage: 'Open selected',
- }
-);
-
export const BULK_ACTION_DELETE_SELECTED = i18n.translate(
'xpack.cases.caseTable.bulkActions.deleteSelectedTitle',
{
defaultMessage: 'Delete selected',
}
);
-
-export const BULK_ACTION_MARK_IN_PROGRESS = i18n.translate(
- 'xpack.cases.caseTable.bulkActions.markInProgressTitle',
- {
- defaultMessage: 'Mark in progress',
- }
-);
diff --git a/x-pack/plugins/cases/public/components/status/types.ts b/x-pack/plugins/cases/public/components/status/types.ts
index 0b4a1184633e1..1df8eb781ecc0 100644
--- a/x-pack/plugins/cases/public/components/status/types.ts
+++ b/x-pack/plugins/cases/public/components/status/types.ts
@@ -18,9 +18,6 @@ export type Statuses = Record<
label: string;
icon: EuiIconType;
actions: {
- bulk: {
- title: string;
- };
single: {
title: string;
description?: string;
diff --git a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap
index f082dc4023e7a..83c8a16ea0290 100644
--- a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap
+++ b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap
@@ -11,7 +11,6 @@ exports[`UtilityBar it renders 1`] = `
Test action
diff --git a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap
deleted file mode 100644
index eb20ac217b300..0000000000000
--- a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UtilityBarAction it renders 1`] = `
-
- Test action
-
-`;
diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx
index 62988a7a9dd76..52486e32905db 100644
--- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx
+++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx
@@ -29,9 +29,7 @@ describe('UtilityBar', () => {
- {'Test popover'}
}>
- {'Test action'}
-
+ {'Test action'}
@@ -57,9 +55,7 @@ describe('UtilityBar', () => {
- {'Test popover'}
}>
- {'Test action'}
-
+ {'Test action'}
@@ -87,9 +83,7 @@ describe('UtilityBar', () => {
- {'Test popover'}
}>
- {'Test action'}
-
+ {'Test action'}
diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx
index 88977fa9bc587..881f4e922bcab 100644
--- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx
+++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx
@@ -5,32 +5,38 @@
* 2.0.
*/
-import { mount, shallow } from 'enzyme';
import React from 'react';
-import { TestProviders } from '../../common/mock';
+import { AppMockRenderer, createAppMockRenderer } from '../../common/mock';
import { UtilityBarAction } from '.';
describe('UtilityBarAction', () => {
+ let appMockRenderer: AppMockRenderer;
+ const dataTestSubj = 'test-bar-action';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRenderer = createAppMockRenderer();
+ });
+
test('it renders', () => {
- const wrapper = shallow(
-
- {'Test action'}
-
+ const res = appMockRenderer.render(
+
+ {'Test action'}
+
);
- expect(wrapper.find('UtilityBarAction')).toMatchSnapshot();
+ expect(res.getByTestId(dataTestSubj)).toBeInTheDocument();
+ expect(res.getByText('Test action')).toBeInTheDocument();
});
test('it renders a popover', () => {
- const wrapper = mount(
-
- {'Test popover'}
}>
- {'Test action'}
-
-
+ const res = appMockRenderer.render(
+
+ {'Test action'}
+
);
- expect(wrapper.find('.euiPopover').first().exists()).toBe(true);
+ expect(res.getByTestId(`${dataTestSubj}-link-icon`)).toBeInTheDocument();
});
});
diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx
index e5bed87021491..b0748f1dd7c9f 100644
--- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx
+++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx
@@ -5,79 +5,19 @@
* 2.0.
*/
-import { EuiPopover } from '@elastic/eui';
-import React, { useCallback, useState } from 'react';
+import React from 'react';
import { LinkIcon, LinkIconProps } from '../link_icon';
import { BarAction } from './styles';
-const Popover = React.memo(
- ({ children, color, iconSide, iconSize, iconType, popoverContent, disabled, ownFocus }) => {
- const [popoverState, setPopoverState] = useState(false);
-
- const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]);
-
- return (
- setPopoverState(!popoverState)}
- disabled={disabled}
- >
- {children}
-
- }
- closePopover={() => setPopoverState(false)}
- isOpen={popoverState}
- repositionOnScroll
- >
- {popoverContent?.(closePopover)}
-
- );
- }
-);
-
-Popover.displayName = 'Popover';
-
export interface UtilityBarActionProps extends LinkIconProps {
- popoverContent?: (closePopover: () => void) => React.ReactNode;
- ownFocus?: boolean;
dataTestSubj?: string;
}
export const UtilityBarAction = React.memo(
- ({
- dataTestSubj,
- children,
- color,
- disabled,
- href,
- iconSide,
- iconSize,
- iconType,
- ownFocus,
- onClick,
- popoverContent,
- }) => (
-
- {popoverContent ? (
-
- {children}
-
- ) : (
+ ({ dataTestSubj, children, color, disabled, href, iconSide, iconSize, iconType, onClick }) => {
+ return (
+
(
iconSize={iconSize}
iconType={iconType}
onClick={onClick}
+ dataTestSubj={dataTestSubj ? `${dataTestSubj}-link-icon` : 'utility-bar-action-link-icon'}
>
{children}
- )}
-
- )
+
+ );
+ }
);
UtilityBarAction.displayName = 'UtilityBarAction';
diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx
new file mode 100644
index 0000000000000..fa3372cf52331
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 userEvent from '@testing-library/user-event';
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
+import { UtilityBarBulkActions } from './utility_bar_bulk_actions';
+
+describe('UtilityBarBulkActions', () => {
+ let appMockRenderer: AppMockRenderer;
+ const closePopover = jest.fn();
+ const onButtonClick = jest.fn();
+ const dataTestSubj = 'test-bar-action';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRenderer = createAppMockRenderer();
+ });
+
+ it('renders', () => {
+ const res = appMockRenderer.render(
+
+
+ {'Test bulk actions'}
+
+
+ );
+
+ expect(res.getByTestId(dataTestSubj)).toBeInTheDocument();
+ expect(res.getByText('button title')).toBeInTheDocument();
+ });
+
+ it('renders a popover', async () => {
+ const res = appMockRenderer.render(
+
+
+ {'Test bulk actions'}
+
+
+ );
+
+ expect(res.getByText('Test bulk actions')).toBeInTheDocument();
+ });
+
+ it('calls onButtonClick', async () => {
+ const res = appMockRenderer.render(
+
+
+ {'Test bulk actions'}
+
+
+ );
+
+ expect(res.getByText('Test bulk actions')).toBeInTheDocument();
+
+ act(() => {
+ userEvent.click(res.getByText('button title'));
+ });
+
+ expect(onButtonClick).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx
new file mode 100644
index 0000000000000..afeb93cc221ea
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { EuiPopover } from '@elastic/eui';
+import React from 'react';
+import { LinkIcon, LinkIconProps } from '../link_icon';
+
+import { BarAction } from './styles';
+
+export interface UtilityBarActionProps extends Omit {
+ isPopoverOpen: boolean;
+ buttonTitle: string;
+ closePopover: () => void;
+ onButtonClick: () => void;
+ dataTestSubj?: string;
+}
+
+export const UtilityBarBulkActions = React.memo(
+ ({
+ dataTestSubj,
+ children,
+ color,
+ disabled,
+ href,
+ iconSide,
+ iconSize,
+ iconType,
+ isPopoverOpen,
+ onButtonClick,
+ buttonTitle,
+ closePopover,
+ }) => {
+ return (
+
+
+ {buttonTitle}
+
+ }
+ >
+ {children}
+
+
+ );
+ }
+);
+
+UtilityBarBulkActions.displayName = 'UtilityBarBulkActions';
diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts
index 2c0ee9bdc2b03..7f90187cd6075 100644
--- a/x-pack/plugins/cases/public/containers/mock.ts
+++ b/x-pack/plugins/cases/public/containers/mock.ts
@@ -399,7 +399,7 @@ const basicAction = {
export const cases: Case[] = [
basicCase,
- { ...pushedCase, id: '1', totalComment: 0, comments: [] },
+ { ...pushedCase, id: '1', totalComment: 0, comments: [], status: CaseStatuses['in-progress'] },
{ ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] },
{ ...basicCase, id: '3', totalComment: 0, comments: [] },
{ ...basicCase, id: '4', totalComment: 0, comments: [] },
@@ -557,7 +557,13 @@ export const pushedCaseSnake = {
export const casesSnake: CasesResponse = [
basicCaseSnake,
- { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] },
+ {
+ ...pushedCaseSnake,
+ id: '1',
+ totalComment: 0,
+ comments: [],
+ status: CaseStatuses['in-progress'],
+ },
{ ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] },
{ ...basicCaseSnake, id: '3', totalComment: 0, comments: [] },
{ ...basicCaseSnake, id: '4', totalComment: 0, comments: [] },
diff --git a/x-pack/plugins/cases/server/client/cases/types.ts b/x-pack/plugins/cases/server/client/cases/types.ts
index c14cc66210614..6d56fa28dca59 100644
--- a/x-pack/plugins/cases/server/client/cases/types.ts
+++ b/x-pack/plugins/cases/server/client/cases/types.ts
@@ -17,7 +17,7 @@ import {
PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams,
PushToServiceApiParamsSIR as ServiceNowSIRPushToServiceApiParams,
ServiceNowITSMIncident,
-} from '@kbn/stack-connectors-plugin/server/connector_types/cases/servicenow/types';
+} from '@kbn/stack-connectors-plugin/server/connector_types/lib/servicenow/types';
import { UserProfile } from '@kbn/security-plugin/common';
import { CaseResponse, ConnectorMappingsAttributes } from '../../../common/api';
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx
index c7a3872ce6e3e..9202e6690408c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx
@@ -85,7 +85,7 @@ export const IngestPipelinesCard: React.FC = () => {
- {pipelineState.name}
+ {pipelineName}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx
index bd895dcf45704..99be659cbac36 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx
@@ -22,11 +22,29 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
import { docLinks } from '../../../../../shared/doc_links';
import { MLInferenceLogic } from './ml_inference_logic';
+const NoSourceFieldsError: React.FC = () => (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.sourceField.error.docLink',
+ { defaultMessage: 'Learn more about field mapping' }
+ )}
+
+ ),
+ }}
+ />
+);
+
export const ConfigurePipeline: React.FC = () => {
const {
addInferencePipelineModal: { configuration },
@@ -39,6 +57,7 @@ export const ConfigurePipeline: React.FC = () => {
const { destinationField, modelID, pipelineName, sourceField } = configuration;
const models = supportedMLModels ?? [];
const nameError = formErrors.pipelineName !== undefined && pipelineName.length > 0;
+ const emptySourceFields = (sourceFields?.length ?? 0) === 0;
return (
<>
@@ -143,6 +162,8 @@ export const ConfigurePipeline: React.FC = () => {
defaultMessage: 'Source field',
}
)}
+ error={emptySourceFields && }
+ isInvalid={emptySourceFields}
>
HttpError;
@@ -76,6 +83,7 @@ interface MLInferenceProcessorsValues {
formErrors: AddInferencePipelineFormErrors;
isLoading: boolean;
isPipelineDataValid: boolean;
+ index: FetchIndexApiResponse;
mappingData: typeof MappingsApiLogic.values.data;
mappingStatus: Status;
mlInferencePipeline?: MlInferencePipeline;
@@ -113,6 +121,8 @@ export const MLInferenceLogic = kea<
],
],
values: [
+ FetchIndexApiLogic,
+ ['data as index'],
MappingsApiLogic,
['data as mappingData', 'status as mappingStatus'],
MLModelsApiLogic,
@@ -210,10 +220,19 @@ export const MLInferenceLogic = kea<
},
],
sourceFields: [
- () => [selectors.mappingStatus, selectors.mappingData],
- (status: Status, mapping: IndicesGetMappingIndexMappingRecord) => {
+ () => [selectors.mappingStatus, selectors.mappingData, selectors.index],
+ (
+ status: Status,
+ mapping: IndicesGetMappingIndexMappingRecord,
+ index: FetchIndexApiResponse
+ ) => {
if (status !== Status.SUCCESS) return;
- if (mapping?.mappings?.properties === undefined) return [];
+ if (mapping?.mappings?.properties === undefined) {
+ if (isConnectorIndex(index)) {
+ return DEFAULT_CONNECTOR_FIELDS;
+ }
+ return [];
+ }
return Object.entries(mapping.mappings.properties)
.reduce((fields, [key, value]) => {
if (value.type === 'text' || value.type === 'keyword') {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
index f695b7c541c5a..18190cef67ca3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
@@ -33,12 +33,8 @@ import { PipelinesJSONConfigurations } from './pipelines_json_configurations';
import { PipelinesLogic } from './pipelines_logic';
export const SearchIndexPipelines: React.FC = () => {
- const {
- showAddMlInferencePipelineModal,
- hasIndexIngestionPipeline,
- index,
- pipelineState: { name: pipelineName },
- } = useValues(PipelinesLogic);
+ const { showAddMlInferencePipelineModal, hasIndexIngestionPipeline, index, pipelineName } =
+ useValues(PipelinesLogic);
const { closeAddMlInferencePipelineModal, openAddMlInferencePipelineModal } =
useActions(PipelinesLogic);
const apiIndex = isApiIndex(index);
@@ -133,7 +129,7 @@ export const SearchIndexPipelines: React.FC = () => {
'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.subtitleAPIindex',
{
defaultMessage:
- "Inference pipelines will be run as processors from the Enterprise Search Ingest Pipeline. In order to use these pipeline on API-based indices you'll need to reference the {pipelineName} pipeline in your API requests.",
+ "Inference pipelines will be run as processors from the Enterprise Search Ingest Pipeline. In order to use these pipelines on API-based indices you'll need to reference the {pipelineName} pipeline in your API requests.",
values: {
pipelineName,
},
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts
index b847b2fdc6b8c..ff3b779d61e29 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts
@@ -4,11 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { LogicMounter, mockFlashMessageHelpers } from '../../../../__mocks__/kea_logic';
-import { connectorIndex } from '../../../__mocks__/view_index.mock';
+import { apiIndex, connectorIndex } from '../../../__mocks__/view_index.mock';
+
+import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
import { UpdatePipelineApiLogic } from '../../../api/connector/update_pipeline_api_logic';
+import { FetchCustomPipelineApiLogic } from '../../../api/index/fetch_custom_pipeline_api_logic';
import { FetchIndexApiLogic } from '../../../api/index/fetch_index_api_logic';
import { PipelinesLogic } from './pipelines_logic';
@@ -40,6 +42,7 @@ describe('PipelinesLogic', () => {
const { mount } = new LogicMounter(PipelinesLogic);
const { mount: mountFetchIndexApiLogic } = new LogicMounter(FetchIndexApiLogic);
const { mount: mountUpdatePipelineLogic } = new LogicMounter(UpdatePipelineApiLogic);
+ const { mount: mountFetchCustomPipelineApiLogic } = new LogicMounter(FetchCustomPipelineApiLogic);
const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
const newPipeline = {
@@ -51,6 +54,7 @@ describe('PipelinesLogic', () => {
beforeEach(() => {
jest.clearAllMocks();
mountFetchIndexApiLogic();
+ mountFetchCustomPipelineApiLogic();
mountUpdatePipelineLogic();
mount();
});
@@ -195,5 +199,41 @@ describe('PipelinesLogic', () => {
});
});
});
+ describe('fetchCustomPipelineSuccess', () => {
+ it('should support api indices with custom ingest pipelines', () => {
+ PipelinesLogic.actions.fetchIndexApiSuccess({
+ ...apiIndex,
+ });
+ const indexName = apiIndex.name;
+ const indexPipelines: Record = {
+ [indexName]: {
+ processors: [],
+ version: 1,
+ },
+ [`${indexName}@custom`]: {
+ processors: [],
+ version: 1,
+ },
+ [`${indexName}@ml-inference`]: {
+ processors: [],
+ version: 1,
+ },
+ };
+ PipelinesLogic.actions.fetchCustomPipelineSuccess(indexPipelines);
+
+ expect(PipelinesLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ customPipelineData: indexPipelines,
+ index: {
+ ...apiIndex,
+ },
+ indexName,
+ pipelineName: indexName,
+ canSetPipeline: false,
+ hasIndexIngestionPipeline: true,
+ canUseMlInferencePipeline: true,
+ });
+ });
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
index 952c5baf77553..dca18863cde02 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
@@ -90,6 +90,10 @@ type PipelinesActions = Pick<
FetchCustomPipelineApiLogicArgs,
FetchCustomPipelineApiLogicResponse
>['makeRequest'];
+ fetchCustomPipelineSuccess: Actions<
+ FetchCustomPipelineApiLogicArgs,
+ FetchCustomPipelineApiLogicResponse
+ >['apiSuccess'];
fetchDefaultPipeline: Actions['makeRequest'];
fetchDefaultPipelineSuccess: Actions['apiSuccess'];
fetchIndexApiSuccess: Actions['apiSuccess'];
@@ -143,7 +147,7 @@ export const PipelinesLogic = kea !isApiIndex(index),
],
canUseMlInferencePipeline: [
- () => [
- selectors.canSetPipeline,
- selectors.hasIndexIngestionPipeline,
- selectors.pipelineState,
- ],
+ () => [selectors.hasIndexIngestionPipeline, selectors.pipelineState, selectors.index],
(
- canSetPipeline: boolean,
hasIndexIngestionPipeline: boolean,
- pipelineState: IngestPipelineParams
- ) => canSetPipeline && hasIndexIngestionPipeline && pipelineState.run_ml_inference,
+ pipelineState: IngestPipelineParams,
+ index: ElasticsearchIndexWithIngestion
+ ) => hasIndexIngestionPipeline && (pipelineState.run_ml_inference || isApiIndex(index)),
],
defaultPipelineValues: [
() => [selectors.defaultPipelineValuesData],
diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx
index 1d5416cf0483d..411f0030ccb6c 100644
--- a/x-pack/plugins/fleet/.storybook/context/index.tsx
+++ b/x-pack/plugins/fleet/.storybook/context/index.tsx
@@ -15,6 +15,7 @@ import { I18nProvider } from '@kbn/i18n-react';
import { CoreScopedHistory } from '@kbn/core/public';
import { getStorybookContextProvider } from '@kbn/custom-integrations-plugin/storybook';
+import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
import { IntegrationsAppContext } from '../../public/applications/integrations/app';
import type { FleetConfigType, FleetStartServices } from '../../public/plugin';
@@ -110,6 +111,7 @@ export const StorybookContext: React.FC<{ storyContext?: Parameters
writeIntegrationPolicies: true,
},
},
+ guidedOnboarding: guidedOnboardingMock.createStart(),
}),
[isCloudEnabled]
);
diff --git a/x-pack/plugins/fleet/dev_docs/data_model.md b/x-pack/plugins/fleet/dev_docs/data_model.md
index 483eabb4ed569..a36cda76fffb6 100644
--- a/x-pack/plugins/fleet/dev_docs/data_model.md
+++ b/x-pack/plugins/fleet/dev_docs/data_model.md
@@ -117,6 +117,15 @@ Contains configuration for ingest outputs that can be shared across multiple `in
only exposes a single Elasticsearch output that will be used for all package policies, but in the future this may be
used for other types of outputs like separate monitoring clusters, Logstash, etc.
+### `ingest-download-sources`
+- Constant in code: `DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE`
+- Introduced in ?
+- [Code Link](../server/saved_objects/index.ts#329)
+- Migrations: 8.4.0, 8.5.0
+
+Contains configuration for the download source objects that allow users to configure a custom registry
+for downloading the Elastic Agent. The default value is for the registry is `https://artifacts.elastic.co/downloads/`. The UI exposes this configuration in Settings.
+
### `epm-packages`
- Constant in code: `PACKAGES_SAVED_OBJECT_TYPE`
diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json
index 79d8bbd40644e..c11cd0d9cdaed 100644
--- a/x-pack/plugins/fleet/kibana.json
+++ b/x-pack/plugins/fleet/kibana.json
@@ -8,7 +8,7 @@
"server": true,
"ui": true,
"configPath": ["xpack", "fleet"],
- "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager"],
+ "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager", "guidedOnboarding"],
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry", "discover", "ingestPipelines"],
"extraPublicDirs": ["common"],
"requiredBundles": ["kibanaReact", "cloudChat", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx
index 011134151cbd7..772d90ada7dc1 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx
@@ -27,6 +27,8 @@ import type { SearchHit } from '@kbn/es-types';
import styled from 'styled-components';
+import { useStartServices, useIsGuidedOnboardingActive } from '../../../../../../../hooks';
+
import type { PackageInfo } from '../../../../../../../../common';
import {
@@ -136,8 +138,15 @@ export const ConfirmIncomingDataWithPreview: React.FunctionComponent = ({
);
const { enrolledAgents, numAgentsWithData } = useGetAgentIncomingData(incomingData, packageInfo);
+ const isGuidedOnboardingActive = useIsGuidedOnboardingActive(packageInfo?.name);
+ const { guidedOnboarding } = useStartServices();
if (!isLoading && enrolledAgents > 0 && numAgentsWithData > 0) {
setAgentDataConfirmed(true);
+ if (isGuidedOnboardingActive) {
+ guidedOnboarding.guidedOnboardingApi?.completeGuidedOnboardingForIntegration(
+ packageInfo?.name
+ );
+ }
}
if (!agentDataConfirmed) {
return (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.test.tsx
index a2e9c7c7c55fa..d8c320a3c101c 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.test.tsx
@@ -82,7 +82,23 @@ describe('useActionStatus', () => {
});
expect(mockSendPostCancelAction).toHaveBeenCalledWith('action1');
expect(mockOnAbortSuccess).toHaveBeenCalled();
- expect(mockOpenConfirm).toHaveBeenCalledWith('This action will abort upgrade of 1 agents', {
+ expect(mockOpenConfirm).toHaveBeenCalledWith('This action will abort upgrade of 1 agent', {
+ title: 'Abort upgrade?',
+ });
+ });
+
+ it('should post abort and invoke callback on abort upgrade - plural', async () => {
+ mockSendPostCancelAction.mockResolvedValue({});
+ let result: any | undefined;
+ await act(async () => {
+ ({ result } = renderHook(() => useActionStatus(mockOnAbortSuccess, false)));
+ });
+ await act(async () => {
+ await result.current.abortUpgrade({ ...mockActionStatuses[0], nbAgentsAck: 0 });
+ });
+ expect(mockSendPostCancelAction).toHaveBeenCalledWith('action1');
+ expect(mockOnAbortSuccess).toHaveBeenCalled();
+ expect(mockOpenConfirm).toHaveBeenCalledWith('This action will abort upgrade of 2 agents', {
title: 'Abort upgrade?',
});
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.tsx
index bc8e20b17ee00..3a6e1b2e54292 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_action_status.tsx
@@ -54,7 +54,8 @@ export function useActionStatus(onAbortSuccess: () => void, refreshAgentActivity
try {
const confirmRes = await overlays.openConfirm(
i18n.translate('xpack.fleet.currentUpgrade.confirmDescription', {
- defaultMessage: 'This action will abort upgrade of {nbAgents} agents',
+ defaultMessage:
+ 'This action will abort upgrade of {nbAgents, plural, one {# agent} other {# agents}}',
values: {
nbAgents: action.nbAgentsActioned - action.nbAgentsAck,
},
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.test.tsx
index d5d72c3deaeeb..89059d5a17971 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.test.tsx
@@ -45,7 +45,7 @@ describe('useUpdateTags', () => {
await act(() => result.current.updateTags('agent1', ['tag1'], mockOnSuccess));
expect(mockOnSuccess).toHaveBeenCalled();
expect(useStartServices().notifications.toasts.addSuccess as jest.Mock).toHaveBeenCalledWith(
- 'Tags updated'
+ 'Tag(s) updated'
);
});
@@ -57,7 +57,7 @@ describe('useUpdateTags', () => {
expect(mockOnSuccess).not.toHaveBeenCalled();
expect(useStartServices().notifications.toasts.addError as jest.Mock).toHaveBeenCalledWith(
'error',
- { title: 'Tags update failed' }
+ { title: 'Tag(s) update failed' }
);
});
@@ -68,7 +68,7 @@ describe('useUpdateTags', () => {
await act(() => result.current.bulkUpdateTags('query', ['tag1'], [], mockOnSuccess));
expect(mockOnSuccess).toHaveBeenCalled();
expect(useStartServices().notifications.toasts.addSuccess as jest.Mock).toHaveBeenCalledWith(
- 'Tags updated'
+ 'Tag(s) updated'
);
});
@@ -80,7 +80,7 @@ describe('useUpdateTags', () => {
expect(mockOnSuccess).not.toHaveBeenCalled();
expect(useStartServices().notifications.toasts.addError as jest.Mock).toHaveBeenCalledWith(
'error',
- { title: 'Tags update failed' }
+ { title: 'Tag(s) update failed' }
);
});
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx
index 969b9caa4d02f..96e619db12f09 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx
@@ -34,7 +34,7 @@ export const useUpdateTags = () => {
const message =
successMessage ??
i18n.translate('xpack.fleet.updateAgentTags.successNotificationTitle', {
- defaultMessage: 'Tags updated',
+ defaultMessage: 'Tag(s) updated',
});
notifications.toasts.addSuccess(message);
@@ -43,7 +43,7 @@ export const useUpdateTags = () => {
const errorTitle =
errorMessage ??
i18n.translate('xpack.fleet.updateAgentTags.errorNotificationTitle', {
- defaultMessage: 'Tags update failed',
+ defaultMessage: 'Tag(s) update failed',
});
notifications.toasts.addError(error, { title: errorTitle });
}
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/with_guided_onboarding_tour.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/with_guided_onboarding_tour.tsx
new file mode 100644
index 0000000000000..2ea4a6775d6e6
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/with_guided_onboarding_tour.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import type { FunctionComponent, ReactElement } from 'react';
+import { EuiButton, EuiText, EuiTourStep } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+type TourType = 'addIntegrationButton' | 'integrationsList';
+const getTourConfig = (packageKey: string, tourType: TourType) => {
+ if (packageKey.startsWith('endpoint') && tourType === 'addIntegrationButton') {
+ return {
+ title: i18n.translate('xpack.fleet.guidedOnboardingTour.endpointButton.title', {
+ defaultMessage: 'Add Elastic Defend',
+ }),
+ description: i18n.translate('xpack.fleet.guidedOnboardingTour.endpointButton.description', {
+ defaultMessage:
+ 'In just a few steps, configure your data with our recommended defaults. You can change this later.',
+ }),
+ };
+ }
+ return null;
+};
+export const WithGuidedOnboardingTour: FunctionComponent<{
+ packageKey: string;
+ isGuidedOnboardingActive: boolean;
+ tourType: TourType;
+ children: ReactElement;
+}> = ({ packageKey, isGuidedOnboardingActive, tourType, children }) => {
+ const [isGuidedOnboardingTourOpen, setIsGuidedOnboardingTourOpen] =
+ useState(isGuidedOnboardingActive);
+ useEffect(() => {
+ setIsGuidedOnboardingTourOpen(isGuidedOnboardingActive);
+ }, [isGuidedOnboardingActive]);
+ const config = getTourConfig(packageKey, tourType);
+
+ return config ? (
+ {config.description}}
+ isStepOpen={isGuidedOnboardingTourOpen}
+ maxWidth={350}
+ onFinish={() => setIsGuidedOnboardingTourOpen(false)}
+ step={1}
+ stepsTotal={1}
+ title={config.title}
+ anchorPosition="rightUp"
+ footerAction={
+ setIsGuidedOnboardingTourOpen(false)} size="s" color="success">
+ {i18n.translate('xpack.fleet.guidedOnboardingTour.nextButtonLabel', {
+ defaultMessage: 'Next',
+ })}
+
+ }
+ >
+ {children}
+
+ ) : (
+ <>{children}>
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
index d43afbb28835c..caefad75ad7a1 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
@@ -39,7 +39,12 @@ import {
} from '../../../../hooks';
import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants';
import { ExperimentalFeaturesService } from '../../../../services';
-import { useGetPackageInfoByKey, useLink, useAgentPolicyContext } from '../../../../hooks';
+import {
+ useGetPackageInfoByKey,
+ useLink,
+ useAgentPolicyContext,
+ useIsGuidedOnboardingActive,
+} from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services';
import type { DetailViewPanelName, PackageInfo } from '../../../../types';
import { InstallStatus } from '../../../../types';
@@ -47,6 +52,8 @@ import { Error, Loading, HeaderReleaseBadge } from '../../../../components';
import type { WithHeaderLayoutProps } from '../../../../layouts';
import { WithHeaderLayout } from '../../../../layouts';
+import { WithGuidedOnboardingTour } from './components/with_guided_onboarding_tour';
+
import { useIsFirstTimeAgentUser } from './hooks';
import { getInstallPkgRouteOptions } from './utils';
import {
@@ -154,6 +161,7 @@ export function Detail() {
const { isFirstTimeAgentUser = false, isLoading: firstTimeUserLoading } =
useIsFirstTimeAgentUser();
+ const isGuidedOnboardingActive = useIsGuidedOnboardingActive(pkgName);
// Refresh package info when status change
const [oldPackageInstallStatus, setOldPackageStatus] = useState(packageInstallStatus);
@@ -292,6 +300,7 @@ export function Detail() {
isCloud,
isExperimentalAddIntegrationPageEnabled,
isFirstTimeAgentUser,
+ isGuidedOnboardingActive,
pkgkey,
});
@@ -305,6 +314,7 @@ export function Detail() {
isCloud,
isExperimentalAddIntegrationPageEnabled,
isFirstTimeAgentUser,
+ isGuidedOnboardingActive,
pathname,
pkgkey,
search,
@@ -349,19 +359,25 @@ export function Detail() {
{ isDivider: true },
{
content: (
-
+
+
+
),
},
].map((item, index) => (
@@ -385,6 +401,7 @@ export function Detail() {
packageInfo,
updateAvailable,
isInstalled,
+ isGuidedOnboardingActive,
userCanInstallPackages,
getHref,
pkgkey,
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.test.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.test.ts
index e085b9034235b..7d233f0977b8a 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.test.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.test.ts
@@ -22,6 +22,7 @@ describe('getInstallPkgRouteOptions', () => {
integration: 'myintegration',
pkgkey: 'myintegration-1.0.0',
isFirstTimeAgentUser: false,
+ isGuidedOnboardingActive: false,
isCloud: false,
isExperimentalAddIntegrationPageEnabled: false,
};
@@ -51,6 +52,7 @@ describe('getInstallPkgRouteOptions', () => {
pkgkey: 'myintegration-1.0.0',
agentPolicyId: '12345',
isFirstTimeAgentUser: false,
+ isGuidedOnboardingActive: false,
isCloud: false,
isExperimentalAddIntegrationPageEnabled: false,
};
@@ -78,6 +80,7 @@ describe('getInstallPkgRouteOptions', () => {
integration: 'myintegration',
pkgkey: 'myintegration-1.0.0',
isFirstTimeAgentUser: true,
+ isGuidedOnboardingActive: false,
isCloud: true,
isExperimentalAddIntegrationPageEnabled: true,
};
@@ -105,6 +108,7 @@ describe('getInstallPkgRouteOptions', () => {
integration: 'myintegration',
pkgkey: 'apm-1.0.0',
isFirstTimeAgentUser: true,
+ isGuidedOnboardingActive: false,
isCloud: true,
isExperimentalAddIntegrationPageEnabled: true,
};
@@ -137,6 +141,7 @@ describe('getInstallPkgRouteOptions', () => {
integration: 'myintegration',
pkgkey: 'endpoint-1.0.0',
isFirstTimeAgentUser: true,
+ isGuidedOnboardingActive: false,
isCloud: true,
isExperimentalAddIntegrationPageEnabled: true,
};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts
index 6a8612a44f42f..f4ac18057dbbf 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts
@@ -29,6 +29,7 @@ interface GetInstallPkgRouteOptionsParams {
isCloud: boolean;
isExperimentalAddIntegrationPageEnabled: boolean;
isFirstTimeAgentUser: boolean;
+ isGuidedOnboardingActive: boolean;
}
const isPackageExemptFromStepsLayout = (pkgkey: string) =>
@@ -45,13 +46,14 @@ export const getInstallPkgRouteOptions = ({
isFirstTimeAgentUser,
isCloud,
isExperimentalAddIntegrationPageEnabled,
+ isGuidedOnboardingActive,
}: GetInstallPkgRouteOptionsParams): [string, { path: string; state: unknown }] => {
const integrationOpts: { integration?: string } = integration ? { integration } : {};
const packageExemptFromStepsLayout = isPackageExemptFromStepsLayout(pkgkey);
const useMultiPageLayout =
isExperimentalAddIntegrationPageEnabled &&
isCloud &&
- isFirstTimeAgentUser &&
+ (isFirstTimeAgentUser || isGuidedOnboardingActive) &&
!packageExemptFromStepsLayout;
const path = pagePathGetters.add_integration_to_policy({
pkgkey,
diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts
index 579d1ab5bc3de..b155ccf63a0db 100644
--- a/x-pack/plugins/fleet/public/hooks/index.ts
+++ b/x-pack/plugins/fleet/public/hooks/index.ts
@@ -28,3 +28,4 @@ export * from './use_agent_policy_refresh';
export * from './use_package_installations';
export * from './use_agent_enrollment_flyout_data';
export * from './use_flyout_context';
+export * from './use_is_guided_onboarding_active';
diff --git a/x-pack/plugins/fleet/public/hooks/use_is_guided_onboarding_active.ts b/x-pack/plugins/fleet/public/hooks/use_is_guided_onboarding_active.ts
new file mode 100644
index 0000000000000..22b8ab9b1a231
--- /dev/null
+++ b/x-pack/plugins/fleet/public/hooks/use_is_guided_onboarding_active.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { useEffect, useState } from 'react';
+import useObservable from 'react-use/lib/useObservable';
+
+import { of } from 'rxjs';
+
+import { useStartServices } from '.';
+
+export const useIsGuidedOnboardingActive = (packageName?: string): boolean => {
+ const [result, setResult] = useState(false);
+ const { guidedOnboarding } = useStartServices();
+ const isGuidedOnboardingActiveForIntegration = useObservable(
+ // if guided onboarding is not available, return false
+ guidedOnboarding.guidedOnboardingApi
+ ? guidedOnboarding.guidedOnboardingApi.isGuidedOnboardingActiveForIntegration$(packageName)
+ : of(false)
+ );
+ useEffect(() => {
+ setResult(!!isGuidedOnboardingActiveForIntegration);
+ }, [isGuidedOnboardingActiveForIntegration]);
+
+ return result;
+};
diff --git a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx
index c5d9b50111569..86816e296dde3 100644
--- a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx
+++ b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx
@@ -13,6 +13,8 @@ import { coreMock } from '@kbn/core/public/mocks';
import type { IStorage } from '@kbn/kibana-utils-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
+import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
+
import { setHttpClient } from '../hooks/use_request';
import type { FleetAuthz } from '../../common';
@@ -90,6 +92,7 @@ export const createStartServices = (basePath: string = '/mock'): MockedFleetStar
},
storage: new Storage(createMockStore()) as jest.Mocked,
authz: fleetAuthzMock,
+ guidedOnboarding: guidedOnboardingMock.createStart(),
};
configureStartServices(startServices);
diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts
index b1d845aa9e52f..3590a80037b1f 100644
--- a/x-pack/plugins/fleet/public/plugin.ts
+++ b/x-pack/plugins/fleet/public/plugin.ts
@@ -44,6 +44,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
+import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, setupRouteService, appRoutesService } from '../common';
import { calculateAuthz, calculatePackagePrivilegesFromCapabilities } from '../common/authz';
@@ -101,6 +102,7 @@ export interface FleetStartDeps {
share: SharePluginStart;
cloud?: CloudStart;
usageCollection?: UsageCollectionStart;
+ guidedOnboarding: GuidedOnboardingPluginStart;
}
export interface FleetStartServices extends CoreStart, Exclude {
@@ -110,6 +112,7 @@ export interface FleetStartServices extends CoreStart, Exclude {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
index 8c908ecc9ef87..4999c07a2aec1 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
@@ -261,9 +261,10 @@ const installTransformsAssets = async (
await Promise.all(
destinationIndexTemplates
.map((destinationIndexTemplate) => {
- const customMappings = transformsSpecifications
- .get(destinationIndexTemplate.transformModuleId)
- ?.get('mappings');
+ const customMappings =
+ transformsSpecifications
+ .get(destinationIndexTemplate.transformModuleId)
+ ?.get('mappings') ?? {};
const registryElasticsearch: RegistryElasticsearch = {
'index_template.settings': destinationIndexTemplate.template.settings,
'index_template.mappings': destinationIndexTemplate.template.mappings,
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/legacy_transforms.test.ts
similarity index 99%
rename from x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts
rename to x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/legacy_transforms.test.ts
index 97fa1e94ca218..124004dee94ac 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/legacy_transforms.test.ts
@@ -37,7 +37,7 @@ import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../constants';
import { getAsset } from './common';
import { installTransforms } from './install';
-describe('test transform install', () => {
+describe('test transform install with legacy schema', () => {
let esClient: ReturnType;
let savedObjectsClient: jest.Mocked;
beforeEach(() => {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts
new file mode 100644
index 0000000000000..aeeeb59e12b38
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts
@@ -0,0 +1,672 @@
+/*
+ * 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.
+ */
+
+// eslint-disable-next-line import/order
+import { createAppContextStartContractMock } from '../../../../mocks';
+
+jest.mock('../../packages/get', () => {
+ return { getInstallation: jest.fn(), getInstallationObject: jest.fn() };
+});
+
+jest.mock('./common', () => {
+ return {
+ getAsset: jest.fn(),
+ };
+});
+
+import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
+import { loggerMock } from '@kbn/logging-mocks';
+
+import { savedObjectsClientMock } from '@kbn/core/server/mocks';
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+
+import { getInstallation, getInstallationObject } from '../../packages';
+import type { Installation, RegistryPackage } from '../../../../types';
+import { ElasticsearchAssetType } from '../../../../types';
+import { appContextService } from '../../../app_context';
+
+import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../constants';
+
+import { getESAssetMetadata } from '../meta';
+
+import { installTransforms } from './install';
+import { getAsset } from './common';
+
+const meta = getESAssetMetadata({ packageName: 'endpoint' });
+
+describe('test transform install', () => {
+ let esClient: ReturnType;
+ let savedObjectsClient: jest.Mocked;
+
+ const getYamlTestData = (autoStart: boolean | undefined = undefined) => {
+ const start =
+ autoStart === undefined
+ ? ''
+ : `
+start: ${autoStart}`;
+ return {
+ MANIFEST:
+ `destination_index_template:
+ settings:
+ index:
+ codec: best_compression
+ refresh_interval: 5s
+ number_of_shards: 1
+ number_of_routing_shards: 30
+ hidden: true
+ mappings:
+ dynamic: false
+ _meta: {}
+ dynamic_templates:
+ - strings_as_keyword:
+ match_mapping_type: string
+ mapping:
+ ignore_above: 1024
+ type: keyword
+ date_detection: false` + start,
+ TRANSFORM: `source:
+ index:
+ - metrics-endpoint.metadata_current_default*
+ - ".fleet-agents*"
+dest:
+ index: ".metrics-endpoint.metadata_united_default"
+frequency: 1s
+sync:
+ time:
+ delay: 4s
+ field: updated_at
+pivot:
+ aggs:
+ united:
+ scripted_metric:
+ init_script: state.docs = []
+ map_script: state.docs.add(new HashMap(params['_source']))
+ combine_script: return state.docs
+ reduce_script: def ret = new HashMap(); for (s in states) { for (d in s) { if (d.containsKey('Endpoint')) { ret.endpoint = d } else { ret.agent = d } }} return ret
+ group_by:
+ agent.id:
+ terms:
+ field: agent.id
+description: Merges latest endpoint and Agent metadata documents.
+_meta:
+ managed: true`,
+ FIELDS: `- name: '@timestamp'
+ type: date
+- name: updated_at
+ type: alias
+ path: event.ingested`,
+ };
+ };
+ const getExpectedData = () => {
+ return {
+ TRANSFORM: {
+ transform_id: 'logs-endpoint.metadata_current-default-0.16.0-dev.0',
+ defer_validation: true,
+ body: {
+ description: 'Merges latest endpoint and Agent metadata documents.',
+ dest: {
+ index: '.metrics-endpoint.metadata_united_default',
+ },
+ frequency: '1s',
+ pivot: {
+ aggs: {
+ united: {
+ scripted_metric: {
+ combine_script: 'return state.docs',
+ init_script: 'state.docs = []',
+ map_script: "state.docs.add(new HashMap(params['_source']))",
+ reduce_script:
+ "def ret = new HashMap(); for (s in states) { for (d in s) { if (d.containsKey('Endpoint')) { ret.endpoint = d } else { ret.agent = d } }} return ret",
+ },
+ },
+ },
+ group_by: {
+ 'agent.id': {
+ terms: {
+ field: 'agent.id',
+ },
+ },
+ },
+ },
+ source: {
+ index: ['metrics-endpoint.metadata_current_default*', '.fleet-agents*'],
+ },
+ sync: {
+ time: {
+ delay: '4s',
+ field: 'updated_at',
+ },
+ },
+ _meta: meta,
+ },
+ },
+ };
+ };
+
+ beforeEach(() => {
+ appContextService.start(createAppContextStartContractMock());
+ esClient = elasticsearchClientMock.createClusterClient().asInternalUser;
+ (getInstallation as jest.MockedFunction).mockReset();
+ (getInstallationObject as jest.MockedFunction).mockReset();
+ savedObjectsClient = savedObjectsClientMock.create();
+ savedObjectsClient.update.mockImplementation(async (type, id, attributes) => ({
+ type: PACKAGES_SAVED_OBJECT_TYPE,
+ id: 'endpoint',
+ attributes,
+ references: [],
+ }));
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('can install new versions and removes older version when start is not defined', async () => {
+ const sourceData = getYamlTestData();
+ const expectedData = getExpectedData();
+
+ const previousInstallation: Installation = {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'endpoint.metadata_current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown as Installation;
+
+ const currentInstallation: Installation = {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'endpoint.metadata_current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'endpoint.metadata_current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'endpoint.metadata-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown as Installation;
+ (getAsset as jest.MockedFunction)
+ .mockReturnValueOnce(Buffer.from(sourceData.MANIFEST, 'utf8'))
+ .mockReturnValueOnce(Buffer.from(sourceData.TRANSFORM, 'utf8'));
+
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (
+ getInstallationObject as jest.MockedFunction
+ ).mockReturnValueOnce(
+ Promise.resolve({
+ attributes: {
+ installed_es: previousInstallation.installed_es,
+ },
+ } as unknown as SavedObject)
+ );
+
+ // Mock transform from old version
+ esClient.transform.getTransform.mockResponseOnce({
+ count: 1,
+ transforms: [
+ // @ts-expect-error incomplete data
+ {
+ dest: {
+ index: 'mock-old-destination-index',
+ },
+ },
+ ],
+ });
+
+ await installTransforms(
+ {
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ } as unknown as RegistryPackage,
+ [
+ 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/manifest.yml',
+ 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/transform.yml',
+ ],
+ esClient,
+ savedObjectsClient,
+ loggerMock.create(),
+ previousInstallation.installed_es
+ );
+
+ expect(esClient.transform.getTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'endpoint.metadata_current-default-0.15.0-dev.0',
+ },
+ { ignore: [404] },
+ ],
+ ]);
+ // Stop and delete previously installed transforms
+ expect(esClient.transform.stopTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'endpoint.metadata_current-default-0.15.0-dev.0',
+ force: true,
+ },
+ { ignore: [404] },
+ ],
+ ]);
+ expect(esClient.transform.deleteTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'endpoint.metadata_current-default-0.15.0-dev.0',
+ force: true,
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Delete destination index
+ expect(esClient.transport.request.mock.calls).toEqual([
+ [
+ {
+ method: 'DELETE',
+ path: '/mock-old-destination-index',
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Create a @package component template and an empty @custom component template
+ expect(esClient.cluster.putComponentTemplate.mock.calls).toEqual([
+ [
+ {
+ body: {
+ _meta: meta,
+ template: {
+ mappings: {
+ _meta: {},
+ date_detection: false,
+ dynamic: false,
+ dynamic_templates: [
+ {
+ strings_as_keyword: {
+ mapping: { ignore_above: 1024, type: 'keyword' },
+ match_mapping_type: 'string',
+ },
+ },
+ ],
+ properties: {},
+ },
+ settings: {
+ index: {
+ codec: 'best_compression',
+ hidden: true,
+ mapping: { total_fields: { limit: '10000' } },
+ number_of_routing_shards: 30,
+ number_of_shards: 1,
+ refresh_interval: '5s',
+ },
+ },
+ },
+ },
+ create: false,
+ name: 'logs-endpoint.metadata_current-template@package',
+ },
+ { ignore: [404] },
+ ],
+ [
+ {
+ body: {
+ _meta: meta,
+ template: { settings: {} },
+ },
+ create: true,
+ name: 'logs-endpoint.metadata_current-template@custom',
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Index template composed of the two component templates created
+ // with index pattern matching the destination index
+ expect(esClient.indices.putIndexTemplate.mock.calls).toEqual([
+ [
+ {
+ body: {
+ _meta: meta,
+ composed_of: [
+ 'logs-endpoint.metadata_current-template@package',
+ 'logs-endpoint.metadata_current-template@custom',
+ ],
+ index_patterns: ['.metrics-endpoint.metadata_united_default'],
+ priority: 250,
+ template: { mappings: undefined, settings: undefined },
+ },
+ name: 'logs-endpoint.metadata_current-template',
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Destination index is created before transform is created
+ expect(esClient.indices.create.mock.calls).toEqual([
+ [{ index: '.metrics-endpoint.metadata_united_default' }, { ignore: [400] }],
+ ]);
+
+ expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
+ expect(esClient.transform.startTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'logs-endpoint.metadata_current-default-0.16.0-dev.0',
+ },
+ { ignore: [409] },
+ ],
+ ]);
+
+ // Saved object is updated with newly created index templates, component templates, transform
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template',
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@custom',
+ type: ElasticsearchAssetType.componentTemplate,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@package',
+ type: ElasticsearchAssetType.componentTemplate,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ },
+ {
+ refresh: false,
+ },
+ ],
+ ]);
+ });
+
+ test('can install new version when no older version', async () => {
+ const sourceData = getYamlTestData(true);
+ const expectedData = getExpectedData();
+
+ const previousInstallation: Installation = {
+ installed_es: [],
+ } as unknown as Installation;
+
+ const currentInstallation: Installation = {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown as Installation;
+ (getAsset as jest.MockedFunction).mockReturnValueOnce(
+ Buffer.from(sourceData.TRANSFORM, 'utf8')
+ );
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (
+ getInstallationObject as jest.MockedFunction
+ ).mockReturnValueOnce(
+ Promise.resolve({
+ attributes: { installed_es: [] },
+ } as unknown as SavedObject)
+ );
+
+ await installTransforms(
+ {
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ } as unknown as RegistryPackage,
+ ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/transform.yml'],
+ esClient,
+ savedObjectsClient,
+ loggerMock.create(),
+ previousInstallation.installed_es
+ );
+
+ expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
+ expect(esClient.transform.startTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'logs-endpoint.metadata_current-default-0.16.0-dev.0',
+ },
+ { ignore: [409] },
+ ],
+ ]);
+
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'logs-endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ {
+ refresh: false,
+ },
+ ],
+ ]);
+ });
+
+ test('can combine settings fields.yml & manifest.yml and not start transform automatically', async () => {
+ const sourceData = getYamlTestData(false);
+ const expectedData = getExpectedData();
+
+ const previousInstallation: Installation = {
+ installed_es: [
+ {
+ id: 'endpoint.metadata-current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template',
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@custom',
+ type: ElasticsearchAssetType.componentTemplate,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@package',
+ type: ElasticsearchAssetType.componentTemplate,
+ },
+ ],
+ } as unknown as Installation;
+
+ const currentInstallation: Installation = {
+ installed_es: [],
+ } as unknown as Installation;
+
+ (getAsset as jest.MockedFunction)
+ .mockReturnValueOnce(Buffer.from(sourceData.FIELDS, 'utf8'))
+ .mockReturnValueOnce(Buffer.from(sourceData.MANIFEST, 'utf8'))
+ .mockReturnValueOnce(Buffer.from(sourceData.TRANSFORM, 'utf8'));
+
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (
+ getInstallationObject as jest.MockedFunction
+ ).mockReturnValueOnce(
+ Promise.resolve({
+ attributes: { installed_es: currentInstallation.installed_es },
+ } as unknown as SavedObject)
+ );
+
+ esClient.transform.getTransform.mockResponseOnce({
+ count: 1,
+ transforms: [
+ // @ts-expect-error incomplete data
+ {
+ dest: {
+ index: 'mock-old-destination-index',
+ },
+ },
+ ],
+ });
+
+ await installTransforms(
+ {
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ } as unknown as RegistryPackage,
+ [
+ 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/fields/fields.yml',
+ 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/manifest.yml',
+ 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/transform.yml',
+ ],
+ esClient,
+ savedObjectsClient,
+ loggerMock.create(),
+ previousInstallation.installed_es
+ );
+
+ expect(esClient.transform.getTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'endpoint.metadata-current-default-0.15.0-dev.0',
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Transform from old version is stopped & deleted
+ expect(esClient.transform.stopTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'endpoint.metadata-current-default-0.15.0-dev.0',
+ force: true,
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ expect(esClient.transform.deleteTransform.mock.calls).toEqual([
+ [
+ {
+ transform_id: 'endpoint.metadata-current-default-0.15.0-dev.0',
+ force: true,
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Destination index from old version is also deleted
+ expect(esClient.transport.request.mock.calls).toEqual([
+ [{ method: 'DELETE', path: '/mock-old-destination-index' }, { ignore: [404] }],
+ ]);
+
+ // Component templates are created with mappings from fields.yml
+ // and template from manifest
+ expect(esClient.cluster.putComponentTemplate.mock.calls).toEqual([
+ [
+ {
+ name: 'logs-endpoint.metadata_current-template@package',
+ body: {
+ template: {
+ settings: {
+ index: {
+ codec: 'best_compression',
+ refresh_interval: '5s',
+ number_of_shards: 1,
+ number_of_routing_shards: 30,
+ hidden: true,
+ mapping: { total_fields: { limit: '10000' } },
+ },
+ },
+ mappings: {
+ properties: { '@timestamp': { type: 'date' } },
+ dynamic_templates: [
+ {
+ strings_as_keyword: {
+ match_mapping_type: 'string',
+ mapping: { ignore_above: 1024, type: 'keyword' },
+ },
+ },
+ ],
+ dynamic: false,
+ _meta: {},
+ date_detection: false,
+ },
+ },
+ _meta: meta,
+ },
+ create: false,
+ },
+ { ignore: [404] },
+ ],
+ [
+ {
+ name: 'logs-endpoint.metadata_current-template@custom',
+ body: {
+ template: { settings: {} },
+ _meta: meta,
+ },
+ create: true,
+ },
+ { ignore: [404] },
+ ],
+ ]);
+ // Index template composed of the two component templates created
+ // with index pattern matching the destination index
+ expect(esClient.indices.putIndexTemplate.mock.calls).toEqual([
+ [
+ {
+ body: {
+ _meta: meta,
+ composed_of: [
+ 'logs-endpoint.metadata_current-template@package',
+ 'logs-endpoint.metadata_current-template@custom',
+ ],
+ index_patterns: ['.metrics-endpoint.metadata_united_default'],
+ priority: 250,
+ template: { mappings: undefined, settings: undefined },
+ },
+ name: 'logs-endpoint.metadata_current-template',
+ },
+ { ignore: [404] },
+ ],
+ ]);
+
+ // Destination index is created before transform is created
+ expect(esClient.indices.create.mock.calls).toEqual([
+ [{ index: '.metrics-endpoint.metadata_united_default' }, { ignore: [400] }],
+ ]);
+
+ // New transform created but not not started automatically if start: false in manifest.yml
+ expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
+ expect(esClient.transform.startTransform.mock.calls).toEqual([]);
+ });
+});
diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json
index 320843546a305..c9c730b6a170c 100644
--- a/x-pack/plugins/fleet/tsconfig.json
+++ b/x-pack/plugins/fleet/tsconfig.json
@@ -27,6 +27,7 @@
{ "path": "../licensing/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../encrypted_saved_objects/tsconfig.json" },
+ {"path": "../../../src/plugins/guided_onboarding/tsconfig.json"},
// optionalPlugins from ./kibana.json
{ "path": "../security/tsconfig.json" },
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
index cd74119bbe315..9d71e74eff473 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
@@ -64,17 +64,20 @@ export function DimensionContainer({
}, [handleClose]);
useEffect(() => {
- if (isOpen) {
- document.body.classList.add('lnsBody--overflowHidden');
- } else {
- document.body.classList.remove('lnsBody--overflowHidden');
- }
+ document.body.classList.toggle('lnsBody--overflowHidden', isOpen);
return () => {
+ if (isOpen) {
+ setFocusTrapIsEnabled(false);
+ }
document.body.classList.remove('lnsBody--overflowHidden');
};
- });
+ }, [isOpen]);
+
+ if (!isOpen) {
+ return null;
+ }
- return isOpen ? (
+ return (
- ) : null;
+ );
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index af653425e88e4..84c56b0f914fc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -605,7 +605,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
...services,
};
- const helpButton = ;
+ const helpButton = (
+
+ );
const columnsSidebar = [
{
diff --git a/x-pack/plugins/profiling/common/callee.test.ts b/x-pack/plugins/profiling/common/callee.test.ts
index 0ae26e6d848e7..4e3ef4b286e31 100644
--- a/x-pack/plugins/profiling/common/callee.test.ts
+++ b/x-pack/plugins/profiling/common/callee.test.ts
@@ -10,15 +10,28 @@ import { createCalleeTree } from './callee';
import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces';
+const totalSamples = sum([...events.values()]);
+const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
+const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
+
describe('Callee operations', () => {
- test('1', () => {
- const totalSamples = sum([...events.values()]);
- const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
+ test('inclusive count of root equals total sampled stacktraces', () => {
+ expect(tree.CountInclusive[0]).toEqual(totalSamples);
+ });
- const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
+ test('inclusive count for each node should be greater than or equal to its children', () => {
+ const allGreaterThanOrEqual = tree.Edges.map(
+ (children, i) =>
+ tree.CountInclusive[i] >= sum([...children.values()].map((j) => tree.CountInclusive[j]))
+ );
+ expect(allGreaterThanOrEqual).toBeTruthy();
+ });
- expect(tree.Samples[0]).toEqual(totalSamples);
- expect(tree.CountInclusive[0]).toEqual(totalSamples);
+ test('exclusive count of root is zero', () => {
expect(tree.CountExclusive[0]).toEqual(0);
});
+
+ test('tree de-duplicates sibling nodes', () => {
+ expect(tree.Size).toEqual(totalFrames - 2);
+ });
});
diff --git a/x-pack/plugins/profiling/common/callee.ts b/x-pack/plugins/profiling/common/callee.ts
index 63db0640513c3..5c347f034760d 100644
--- a/x-pack/plugins/profiling/common/callee.ts
+++ b/x-pack/plugins/profiling/common/callee.ts
@@ -5,20 +5,15 @@
* 2.0.
*/
-import fnv from 'fnv-plus';
-
import { createFrameGroupID, FrameGroupID } from './frame_group';
import {
- createStackFrameMetadata,
emptyExecutable,
emptyStackFrame,
emptyStackTrace,
Executable,
FileID,
- getCalleeLabel,
StackFrame,
StackFrameID,
- StackFrameMetadata,
StackTrace,
StackTraceID,
} from './profiling';
@@ -29,93 +24,56 @@ export interface CalleeTree {
Size: number;
Edges: Array
- {status === AsyncStatus.Loading ? (
-
-
-
- ) : undefined}
@@ -101,16 +91,10 @@ function FlamegraphFrameInformationPanel({
);
}
-export function FlamegraphInformationWindow({
- onClose,
- frame,
- totalSamples,
- totalSeconds,
- status,
-}: Props) {
+export function FlamegraphInformationWindow({ onClose, frame, totalSamples, totalSeconds }: Props) {
if (!frame) {
return (
-
+
{i18n.translate('xpack.profiling.flamegraphInformationWindow.selectFrame', {
defaultMessage: 'Click on a frame to display more information',
@@ -130,7 +114,7 @@ export function FlamegraphInformationWindow({
});
return (
-
+
= ({
}) => {
const theme = useEuiTheme();
- const {
- services: { fetchFrameInformation },
- } = useProfilingDependencies();
-
const columnarData = useMemo(() => {
return getFlamegraphModel({
primaryFlamegraph,
@@ -193,38 +187,13 @@ export const FlameGraph: React.FC = ({
const [highlightedVmIndex, setHighlightedVmIndex] = useState(undefined);
- const highlightedFrameQueryParams = useMemo(() => {
- if (!primaryFlamegraph || highlightedVmIndex === undefined || highlightedVmIndex === 0) {
- return undefined;
- }
-
- const frameID = primaryFlamegraph.FrameID[highlightedVmIndex];
- const executableID = primaryFlamegraph.ExecutableID[highlightedVmIndex];
-
- return {
- frameID,
- executableID,
- };
- }, [primaryFlamegraph, highlightedVmIndex]);
-
- const { data: highlightedFrame, status: highlightedFrameStatus } = useAsync(() => {
- if (!highlightedFrameQueryParams) {
- return Promise.resolve(undefined);
- }
-
- return fetchFrameInformation({
- frameID: highlightedFrameQueryParams.frameID,
- executableID: highlightedFrameQueryParams.executableID,
- });
- }, [highlightedFrameQueryParams, fetchFrameInformation]);
-
const selected: undefined | React.ComponentProps['frame'] =
- primaryFlamegraph && highlightedFrame && highlightedVmIndex !== undefined
+ primaryFlamegraph && highlightedVmIndex !== undefined
? {
- exeFileName: highlightedFrame.ExeFileName,
- sourceFileName: highlightedFrame.SourceFilename,
- functionName: highlightedFrame.FunctionName,
- countInclusive: primaryFlamegraph.Samples[highlightedVmIndex],
+ exeFileName: primaryFlamegraph.ExeFilename[highlightedVmIndex],
+ sourceFileName: primaryFlamegraph.SourceFilename[highlightedVmIndex],
+ functionName: primaryFlamegraph.FunctionName[highlightedVmIndex],
+ countInclusive: primaryFlamegraph.CountInclusive[highlightedVmIndex],
countExclusive: primaryFlamegraph.CountExclusive[highlightedVmIndex],
}
: undefined;
@@ -271,7 +240,7 @@ export const FlameGraph: React.FC = ({
const valueIndex = props.values[0].valueAccessor as number;
const label = primaryFlamegraph.Label[valueIndex];
- const samples = primaryFlamegraph.Samples[valueIndex];
+ const samples = primaryFlamegraph.CountInclusive[valueIndex];
const countInclusive = primaryFlamegraph.CountInclusive[valueIndex];
const countExclusive = primaryFlamegraph.CountExclusive[valueIndex];
const nodeID = primaryFlamegraph.ID[valueIndex];
@@ -287,8 +256,8 @@ export const FlameGraph: React.FC = ({
comparisonCountInclusive={comparisonNode?.CountInclusive}
comparisonCountExclusive={comparisonNode?.CountExclusive}
totalSamples={totalSamples}
- comparisonTotalSamples={comparisonFlamegraph?.Samples[0]}
- comparisonSamples={comparisonNode?.Samples}
+ comparisonTotalSamples={comparisonFlamegraph?.CountInclusive[0]}
+ comparisonSamples={comparisonNode?.CountInclusive}
/>
);
},
@@ -309,7 +278,6 @@ export const FlameGraph: React.FC = ({
{
diff --git a/x-pack/plugins/profiling/public/services.ts b/x-pack/plugins/profiling/public/services.ts
index 07234ca124b36..a1f345dc96a2c 100644
--- a/x-pack/plugins/profiling/public/services.ts
+++ b/x-pack/plugins/profiling/public/services.ts
@@ -7,9 +7,8 @@
import { CoreStart, HttpFetchQuery } from '@kbn/core/public';
import { getRoutePaths } from '../common';
-import { ElasticFlameGraph } from '../common/flamegraph';
+import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph';
import { TopNFunctions } from '../common/functions';
-import { StackFrameMetadata } from '../common/profiling';
import { TopNResponse } from '../common/topn';
export interface Services {
@@ -31,10 +30,6 @@ export interface Services {
timeTo: number;
kuery: string;
}) => Promise;
- fetchFrameInformation: (params: {
- frameID: string;
- executableID: string;
- }) => Promise;
}
export function getServices(core: CoreStart): Services {
@@ -96,24 +91,8 @@ export function getServices(core: CoreStart): Services {
timeTo,
kuery,
};
- return await core.http.get(paths.Flamechart, { query });
- } catch (e) {
- return e;
- }
- },
- fetchFrameInformation: async ({
- frameID,
- executableID,
- }: {
- frameID: string;
- executableID: string;
- }) => {
- try {
- const query: HttpFetchQuery = {
- frameID,
- executableID,
- };
- return await core.http.get(paths.FrameInformation, { query });
+ const baseFlamegraph: BaseFlameGraph = await core.http.get(paths.Flamechart, { query });
+ return createFlameGraph(baseFlamegraph);
} catch (e) {
return e;
}
diff --git a/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts b/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts
index c63a73185d26b..4cda7befe44c9 100644
--- a/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts
+++ b/x-pack/plugins/profiling/public/utils/get_flamegraph_model/index.ts
@@ -6,12 +6,8 @@
*/
import d3 from 'd3';
import { sum, uniqueId } from 'lodash';
-import {
- createColumnarViewModel,
- ElasticFlameGraph,
- FlameGraphComparisonMode,
- rgbToRGBA,
-} from '../../../common/flamegraph';
+import { createColumnarViewModel, rgbToRGBA } from '../../../common/columnar_view_model';
+import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph';
import { getInterpolationValue } from './get_interpolation_value';
const nullColumnarViewModel = {
@@ -39,10 +35,8 @@ export function getFlamegraphModel({
colorNeutral: string;
comparisonMode: FlameGraphComparisonMode;
}) {
- const comparisonNodesById: Record<
- string,
- { Samples: number; CountInclusive: number; CountExclusive: number }
- > = {};
+ const comparisonNodesById: Record =
+ {};
if (!primaryFlamegraph || !primaryFlamegraph.Label || primaryFlamegraph.Label.length === 0) {
return { key: uniqueId(), viewModel: nullColumnarViewModel, comparisonNodesById };
@@ -53,7 +47,6 @@ export function getFlamegraphModel({
if (comparisonFlamegraph) {
comparisonFlamegraph.ID.forEach((nodeID, index) => {
comparisonNodesById[nodeID] = {
- Samples: comparisonFlamegraph.Samples[index],
CountInclusive: comparisonFlamegraph.CountInclusive[index],
CountExclusive: comparisonFlamegraph.CountExclusive[index],
};
@@ -88,8 +81,8 @@ export function getFlamegraphModel({
: primaryFlamegraph.TotalSeconds / comparisonFlamegraph.TotalSeconds;
primaryFlamegraph.ID.forEach((nodeID, index) => {
- const samples = primaryFlamegraph.Samples[index];
- const comparisonSamples = comparisonNodesById[nodeID]?.Samples as number | undefined;
+ const samples = primaryFlamegraph.CountInclusive[index];
+ const comparisonSamples = comparisonNodesById[nodeID]?.CountInclusive as number | undefined;
const foreground =
comparisonMode === FlameGraphComparisonMode.Absolute ? samples : samples / totalSamples;
diff --git a/x-pack/plugins/profiling/server/routes/flamechart.ts b/x-pack/plugins/profiling/server/routes/flamechart.ts
index 6d27305a82c69..772d505817484 100644
--- a/x-pack/plugins/profiling/server/routes/flamechart.ts
+++ b/x-pack/plugins/profiling/server/routes/flamechart.ts
@@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { createCalleeTree } from '../../common/callee';
-import { createFlameGraph } from '../../common/flamegraph';
+import { createBaseFlameGraph } from '../../common/flamegraph';
import { createProfilingEsClient } from '../utils/create_profiling_es_client';
import { withProfilingSpan } from '../utils/with_profiling_span';
import { getClient } from './compat';
@@ -42,20 +42,13 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
});
const totalSeconds = timeTo - timeFrom;
- const {
- stackTraces,
- executables,
- stackFrames,
- eventsIndex,
- totalCount,
- totalFrames,
- stackTraceEvents,
- } = await getExecutablesAndStackTraces({
- logger,
- client: createProfilingEsClient({ request, esClient }),
- filter,
- sampleSize: targetSampleSize,
- });
+ const { stackTraceEvents, stackTraces, executables, stackFrames, totalFrames } =
+ await getExecutablesAndStackTraces({
+ logger,
+ client: createProfilingEsClient({ request, esClient }),
+ filter,
+ sampleSize: targetSampleSize,
+ });
const flamegraph = await withProfilingSpan('create_flamegraph', async () => {
const t0 = Date.now();
@@ -68,23 +61,8 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
);
logger.info(`creating callee tree took ${Date.now() - t0} ms`);
- // sampleRate is 1/5^N, with N being the downsampled index the events were fetched from.
- // N=0: full events table (sampleRate is 1)
- // N=1: downsampled by 5 (sampleRate is 0.2)
- // ...
-
- // totalCount is the sum(Count) of all events in the filter range in the
- // downsampled index we were looking at.
- // To estimate how many events we have in the full events index: totalCount / sampleRate.
- // Do the same for single entries in the events array.
-
const t1 = Date.now();
- const fg = createFlameGraph(
- tree,
- totalSeconds,
- Math.floor(totalCount / eventsIndex.sampleRate),
- totalCount
- );
+ const fg = createBaseFlameGraph(tree, totalSeconds);
logger.info(`creating flamegraph took ${Date.now() - t1} ms`);
return fg;
diff --git a/x-pack/plugins/profiling/server/routes/frames.ts b/x-pack/plugins/profiling/server/routes/frames.ts
deleted file mode 100644
index 4a0ce745c7246..0000000000000
--- a/x-pack/plugins/profiling/server/routes/frames.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { schema } from '@kbn/config-schema';
-import { Logger } from '@kbn/logging';
-import { RouteRegisterParameters } from '.';
-import { getRoutePaths } from '../../common';
-import {
- createStackFrameMetadata,
- Executable,
- StackFrame,
- StackFrameMetadata,
-} from '../../common/profiling';
-import { createProfilingEsClient, ProfilingESClient } from '../utils/create_profiling_es_client';
-import { mgetStackFrames, mgetExecutables } from './stacktrace';
-
-async function getFrameInformation({
- frameID,
- executableID,
- logger,
- client,
-}: {
- frameID: string;
- executableID: string;
- logger: Logger;
- client: ProfilingESClient;
-}): Promise {
- const [stackFrames, executables] = await Promise.all([
- mgetStackFrames({
- logger,
- client,
- stackFrameIDs: new Set([frameID]),
- }),
- mgetExecutables({
- logger,
- client,
- executableIDs: new Set([executableID]),
- }),
- ]);
-
- const frame = Array.from(stackFrames.values())[0] as StackFrame | undefined;
- const executable = Array.from(executables.values())[0] as Executable | undefined;
-
- if (frame) {
- return createStackFrameMetadata({
- FrameID: frameID,
- FileID: executableID,
- SourceFilename: frame.FileName,
- FunctionName: frame.FunctionName,
- ExeFileName: executable?.FileName,
- });
- }
-}
-
-export function registerFrameInformationRoute(params: RouteRegisterParameters) {
- const { logger, router } = params;
-
- const routePaths = getRoutePaths();
-
- router.get(
- {
- path: routePaths.FrameInformation,
- validate: {
- query: schema.object({
- frameID: schema.string(),
- executableID: schema.string(),
- }),
- },
- },
- async (context, request, response) => {
- const { frameID, executableID } = request.query;
-
- const client = createProfilingEsClient({
- request,
- esClient: (await context.core).elasticsearch.client.asCurrentUser,
- });
-
- try {
- const frame = await getFrameInformation({
- frameID,
- executableID,
- logger,
- client,
- });
-
- return response.ok({ body: frame });
- } catch (error: any) {
- logger.error(error);
- return response.custom({
- statusCode: error.statusCode ?? 500,
- body: {
- message: error.message ?? 'An internal server error occured',
- },
- });
- }
- }
- );
-}
diff --git a/x-pack/plugins/profiling/server/routes/index.ts b/x-pack/plugins/profiling/server/routes/index.ts
index 6e44bf6909585..b6bd705ba0e07 100644
--- a/x-pack/plugins/profiling/server/routes/index.ts
+++ b/x-pack/plugins/profiling/server/routes/index.ts
@@ -13,7 +13,6 @@ import {
} from '../types';
import { registerFlameChartSearchRoute } from './flamechart';
-import { registerFrameInformationRoute } from './frames';
import { registerTopNFunctionsSearchRoute } from './functions';
import {
@@ -41,5 +40,4 @@ export function registerRoutes(params: RouteRegisterParameters) {
registerTraceEventsTopNHostsSearchRoute(params);
registerTraceEventsTopNStackTracesSearchRoute(params);
registerTraceEventsTopNThreadsSearchRoute(params);
- registerFrameInformationRoute(params);
}
diff --git a/x-pack/plugins/profiling/server/routes/stacktrace.test.ts b/x-pack/plugins/profiling/server/routes/stacktrace.test.ts
index a40f72d25f2d8..5dd3f1985a35a 100644
--- a/x-pack/plugins/profiling/server/routes/stacktrace.test.ts
+++ b/x-pack/plugins/profiling/server/routes/stacktrace.test.ts
@@ -118,6 +118,14 @@ describe('Stack trace operations', () => {
}
});
+ test('runLengthDecode with larger output than available input', () => {
+ const bytes = Buffer.from([0x5, 0x0, 0x2, 0x2]);
+ const decoded = [0, 0, 0, 0, 0, 2, 2];
+ const expected = decoded.concat(Array(decoded.length).fill(0));
+
+ expect(runLengthDecode(bytes, expected.length)).toEqual(expected);
+ });
+
test('runLengthDecode without optional parameter', () => {
const tests: Array<{
bytes: Buffer;
diff --git a/x-pack/plugins/profiling/server/routes/stacktrace.ts b/x-pack/plugins/profiling/server/routes/stacktrace.ts
index 4ae7d91596f10..6dbe063e2c4f9 100644
--- a/x-pack/plugins/profiling/server/routes/stacktrace.ts
+++ b/x-pack/plugins/profiling/server/routes/stacktrace.ts
@@ -122,6 +122,17 @@ export function runLengthDecode(input: Buffer, outputSize?: number): number[] {
}
}
+ // Due to truncation of the frame types for stacktraces longer than 255,
+ // the expected output size and the actual decoded size can be different.
+ // Ordinarily, these two values should be the same.
+ //
+ // We have decided to fill in the remainder of the output array with zeroes
+ // as a reasonable default. Without this step, the output array would have
+ // undefined values.
+ for (let i = idx; i < size; i++) {
+ output[i] = 0;
+ }
+
return output;
}
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 7358fbb33b4b2..6357dfd766a4c 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -22,6 +22,7 @@ export const APP_ICON = 'securityAnalyticsApp' as const;
export const APP_ICON_SOLUTION = 'logoSecurity' as const;
export const APP_PATH = `/app/security` as const;
export const ADD_DATA_PATH = `/app/integrations/browse/security`;
+export const ADD_THREAT_INTELLIGENCE_DATA_PATH = `/app/integrations/browse/threat_intel`;
export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern' as const;
export const DEFAULT_DATE_FORMAT = 'dateFormat' as const;
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const;
@@ -152,6 +153,7 @@ export const USERS_PATH = '/users' as const;
export const KUBERNETES_PATH = '/kubernetes' as const;
export const NETWORK_PATH = '/network' as const;
export const MANAGEMENT_PATH = '/administration' as const;
+export const THREAT_INTELLIGENCE_PATH = '/threat_intelligence' as const;
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints` as const;
export const POLICIES_PATH = `${MANAGEMENT_PATH}/policy` as const;
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps` as const;
diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts
index 8f0bd47c204cb..19df6122044a4 100644
--- a/x-pack/plugins/security_solution/common/endpoint/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts
@@ -41,6 +41,11 @@ export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default'
export const policyIndexPattern = 'metrics-endpoint.policy-*';
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
+// File storage indexes supporting endpoint Upload/download
+export const FILE_STORAGE_METADATA_INDEX = '.fleet-files';
+export const FILE_STORAGE_DATA_INDEX = '.fleet-file_data';
+
+// Endpoint API routes
export const BASE_ENDPOINT_ROUTE = '/api/endpoint';
export const HOST_METADATA_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata`;
export const HOST_METADATA_GET_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/{id}`;
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts
index 9f1c0495f162d..971f096300e66 100644
--- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts
@@ -18,6 +18,8 @@ import type {
LogsEndpointAction,
LogsEndpointActionResponse,
ProcessesEntry,
+ EndpointActionDataParameterTypes,
+ ActionResponseOutput,
} from '../types';
import { ActivityLogItemTypes } from '../types';
import { RESPONSE_ACTION_COMMANDS } from '../service/response_actions/constants';
@@ -75,6 +77,32 @@ export class EndpointActionGenerator extends BaseDataGenerator {
);
});
+ const command = overrides?.EndpointActions?.data?.command ?? this.randomResponseActionCommand();
+ let parameters: EndpointActionDataParameterTypes = overrides?.EndpointActions?.data?.parameters;
+ let output: ActionResponseOutput = overrides?.EndpointActions?.data
+ ?.output as ActionResponseOutput;
+
+ if (command === 'get-file') {
+ if (!parameters) {
+ parameters = {
+ file: '/some/path/bad_file.txt',
+ };
+ }
+
+ if (!output) {
+ output = {
+ type: 'json',
+ content: {
+ file: {
+ name: 'bad_file.txt',
+ path: '/some/path/bad_file.txt',
+ size: 221,
+ },
+ },
+ };
+ }
+ }
+
return merge(
{
'@timestamp': timeStamp.toISOString(),
@@ -84,14 +112,14 @@ export class EndpointActionGenerator extends BaseDataGenerator {
EndpointActions: {
action_id: this.seededUUIDv4(),
completed_at: timeStamp.toISOString(),
+ // randomly before a few hours/minutes/seconds later
+ started_at: new Date(startedAtTimes[this.randomN(startedAtTimes.length)]).toISOString(),
data: {
- command: this.randomResponseActionCommand(),
+ command,
comment: '',
- parameters: undefined,
+ parameters,
+ output,
},
- // randomly before a few hours/minutes/seconds later
- started_at: new Date(startedAtTimes[this.randomN(startedAtTimes.length)]).toISOString(),
- output: undefined,
},
error: undefined,
},
@@ -160,7 +188,7 @@ export class EndpointActionGenerator extends BaseDataGenerator {
type: ActivityLogItemTypes.RESPONSE,
item: {
id: this.seededUUIDv4(),
- data: this.generateResponse(),
+ data: this.generateResponse({ ...(overrides?.item?.data ?? {}) }),
},
},
overrides
diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts
index dee5d94b4e62b..5a22f37e0e006 100644
--- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts
@@ -13,5 +13,6 @@ export const RESPONSE_ACTION_COMMANDS = [
'kill-process',
'suspend-process',
'running-processes',
+ 'get-file',
] as const;
export type ResponseActions = typeof RESPONSE_ACTION_COMMANDS[number];
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
index 91eb10c5f45a2..40a65e886fb6d 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
@@ -62,9 +62,12 @@ interface EcsError {
type?: string;
}
-interface EndpointActionFields {
+interface EndpointActionFields<
+ TParameters extends EndpointActionDataParameterTypes = never,
+ TOutputContent extends object = object
+> {
action_id: string;
- data: EndpointActionData;
+ data: EndpointActionData;
}
interface ActionRequestFields {
@@ -98,12 +101,15 @@ export interface LogsEndpointAction {
* An Action response written by the endpoint to the Endpoint `.logs-endpoint.action.responses` datastream
* @since v7.16
*/
-export interface LogsEndpointActionResponse {
+export interface LogsEndpointActionResponse<
+ TParameters extends EndpointActionDataParameterTypes = never,
+ TOutputContent extends object = object
+> {
'@timestamp': string;
agent: {
id: string | string[];
};
- EndpointActions: EndpointActionFields & ActionResponseFields;
+ EndpointActions: EndpointActionFields & ActionResponseFields;
error?: EcsError;
}
@@ -121,9 +127,14 @@ export type ResponseActionParametersWithPidOrEntityId =
| ResponseActionParametersWithPid
| ResponseActionParametersWithEntityId;
+export interface ResponseActionGetFileParameters {
+ file: string;
+}
+
export type EndpointActionDataParameterTypes =
| undefined
- | ResponseActionParametersWithPidOrEntityId;
+ | ResponseActionParametersWithPidOrEntityId
+ | ResponseActionGetFileParameters;
export interface EndpointActionData<
T extends EndpointActionDataParameterTypes = never,
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/file_storage.ts b/x-pack/plugins/security_solution/common/endpoint/types/file_storage.ts
new file mode 100644
index 0000000000000..9ed8065197921
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/types/file_storage.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/**
+ * The Metadata information about a file that was uploaded by Endpoint
+ * as a result of a `get-file` response action
+ */
+export interface UploadedFile {
+ file: {
+ /** The chunk size used for each chunk in this file */
+ ChunkSize?: number;
+ /**
+ * - `AWAITING_UPLOAD`: file metadata has been created. File is ready to be uploaded.
+ * - `UPLOADING`: file contents are being uploaded.
+ * - `READY`: file has been uploaded, successfully, without errors.
+ * - `UPLOAD_ERROR`: an error happened while the file was being uploaded, file contents
+ * are most likely corrupted.
+ * - `DELETED`: file is deleted. Files can be marked as deleted before the actual deletion
+ * of the contents and metadata happens. Deleted files should be treated as if they don’t
+ * exist. Only files in READY state can transition into DELETED state.
+ */
+ Status: 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERROR' | 'DELETED';
+ /** File extension (if any) */
+ extension?: string;
+ hash?: {
+ md5?: string;
+ sha1?: string;
+ sha256?: string;
+ sha384?: string;
+ sha512?: string;
+ ssdeep?: string;
+ tlsh?: string;
+ };
+ mime_type?: string;
+ mode?: string;
+ /** File name */
+ name: string;
+ /** The full path to the file on the host machine */
+ path: string;
+ /** The total size in bytes */
+ size: number;
+ created?: string;
+ type: string;
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
index 87b0b5a7da6ed..bd5049909c95d 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
@@ -9,7 +9,12 @@ import { render } from '@testing-library/react';
import { useLocation } from 'react-router-dom';
import { useVariationMock } from '../../../common/components/utils.mocks';
import { GlobalHeader } from '.';
-import { ADD_DATA_PATH, SecurityPageName } from '../../../../common/constants';
+import {
+ ADD_DATA_PATH,
+ ADD_THREAT_INTELLIGENCE_DATA_PATH,
+ SecurityPageName,
+ THREAT_INTELLIGENCE_PATH,
+} from '../../../../common/constants';
import {
createSecuritySolutionStorageMock,
mockGlobalState,
@@ -98,6 +103,17 @@ describe('global header', () => {
expect(link?.getAttribute('href')).toBe(ADD_DATA_PATH);
});
+ it('points to the threat_intel Add data URL for threat_intelligence url', () => {
+ (useLocation as jest.Mock).mockReturnValue({ pathname: THREAT_INTELLIGENCE_PATH });
+ const { queryByTestId } = render(
+
+
+
+ );
+ const link = queryByTestId('add-data');
+ expect(link?.getAttribute('href')).toBe(ADD_THREAT_INTELLIGENCE_DATA_PATH);
+ });
+
it('points to the resolved Add data URL by useVariation', () => {
(useLocation as jest.Mock).mockReturnValue([
{ pageName: SecurityPageName.overview, detailName: undefined },
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
index 3b405df246601..37efdce430317 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
@@ -20,8 +20,8 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { useVariation } from '../../../common/components/utils';
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
import { useKibana } from '../../../common/lib/kibana';
-import { ADD_DATA_PATH } from '../../../../common/constants';
-import { isDetectionsPath } from '../../../helpers';
+import { ADD_DATA_PATH, ADD_THREAT_INTELLIGENCE_DATA_PATH } from '../../../../common/constants';
+import { isDetectionsPath, isThreatIntelligencePath } from '../../../helpers';
import { Sourcerer } from '../../../common/components/sourcerer';
import { TimelineId } from '../../../../common/types/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
@@ -58,7 +58,10 @@ export const GlobalHeader = React.memo(
const sourcererScope = getScopeFromPath(pathname);
const showSourcerer = showSourcererByPath(pathname);
- const [addIntegrationsUrl, setAddIntegrationsUrl] = useState(ADD_DATA_PATH);
+ const integrationsUrl = isThreatIntelligencePath(pathname)
+ ? ADD_THREAT_INTELLIGENCE_DATA_PATH
+ : ADD_DATA_PATH;
+ const [addIntegrationsUrl, setAddIntegrationsUrl] = useState(integrationsUrl);
useVariation(
cloudExperiments,
'security-solutions.add-integrations-url',
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx
index b31cf1d1252bc..5ab2a172bc6e1 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx
@@ -146,7 +146,7 @@ describe('Insights', () => {
expect(screen.getByTestId('related-alerts-by-ancestry')).toBeInTheDocument();
expect(
- screen.queryByRole('link', { name: new RegExp(i18n.ALERT_UPSELL) })
+ screen.queryByRole('button', { name: new RegExp(i18n.INSIGHTS_UPSELL) })
).not.toBeInTheDocument();
});
@@ -179,7 +179,7 @@ describe('Insights', () => {
);
expect(
- screen.getByRole('link', { name: new RegExp(i18n.ALERT_UPSELL) })
+ screen.getByRole('button', { name: new RegExp(i18n.INSIGHTS_UPSELL) })
).toBeInTheDocument();
expect(screen.queryByTestId('related-alerts-by-ancestry')).not.toBeInTheDocument();
});
@@ -198,7 +198,7 @@ describe('Insights', () => {
expect(screen.queryByTestId('related-alerts-by-ancestry')).not.toBeInTheDocument();
expect(
- screen.queryByRole('link', { name: new RegExp(i18n.ALERT_UPSELL) })
+ screen.queryByRole('button', { name: new RegExp(i18n.INSIGHTS_UPSELL) })
).not.toBeInTheDocument();
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx
index 37ac07e3c03d0..ba225495a032b 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx
@@ -5,11 +5,12 @@
* 2.0.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiIcon, EuiText } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
-import { ALERT_UPSELL } from './translations';
+import { INSIGHTS_UPSELL } from './translations';
+import { useNavigation } from '../../../lib/kibana';
const UpsellContainer = euiStyled.div`
border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
@@ -22,6 +23,15 @@ const StyledIcon = euiStyled(EuiIcon)`
`;
export const RelatedAlertsUpsell = React.memo(() => {
+ const { getAppUrl, navigateTo } = useNavigation();
+ const subscriptionUrl = getAppUrl({
+ appId: 'management',
+ path: 'stack/license_management',
+ });
+ const goToSubscription = useCallback(() => {
+ navigateTo({ url: subscriptionUrl });
+ }, [navigateTo, subscriptionUrl]);
+
return (
@@ -30,13 +40,8 @@ export const RelatedAlertsUpsell = React.memo(() => {
-
- {ALERT_UPSELL}
+
+ {INSIGHTS_UPSELL}
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts
index e839003e1f7f9..10d9efca4682d 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts
@@ -136,9 +136,9 @@ export const SIMPLE_ALERT_TABLE_LIMITED = i18n.translate(
}
);
-export const ALERT_UPSELL = i18n.translate(
+export const INSIGHTS_UPSELL = i18n.translate(
'xpack.securitySolution.alertDetails.overview.insights.alertUpsellTitle',
{
- defaultMessage: 'Get more insights with a subscription',
+ defaultMessage: 'Get more insights with a platinum subscription',
}
);
diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx
index 4520f70e3ab76..df0a660f4def4 100644
--- a/x-pack/plugins/security_solution/public/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/helpers.tsx
@@ -5,8 +5,8 @@
* 2.0.
*/
-import { ALERT_RULE_UUID, ALERT_RULE_NAME, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
-import { has, get, isEmpty } from 'lodash/fp';
+import { ALERT_RULE_NAME, ALERT_RULE_PARAMETERS, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
+import { get, has, isEmpty } from 'lodash/fp';
import React from 'react';
import type { RouteProps } from 'react-router-dom';
import { matchPath, Redirect } from 'react-router-dom';
@@ -15,12 +15,13 @@ import type { Capabilities, CoreStart } from '@kbn/core/public';
import {
ALERTS_PATH,
APP_UI_ID,
+ CASES_FEATURE_ID,
+ CASES_PATH,
EXCEPTIONS_PATH,
+ LANDING_PATH,
RULES_PATH,
SERVER_APP_ID,
- CASES_FEATURE_ID,
- LANDING_PATH,
- CASES_PATH,
+ THREAT_INTELLIGENCE_PATH,
} from '../common/constants';
import type { Ecs } from '../common/ecs';
import type {
@@ -164,6 +165,13 @@ export const isDetectionsPath = (pathname: string): boolean => {
});
};
+export const isThreatIntelligencePath = (pathname: string): boolean => {
+ return !!matchPath(pathname, {
+ path: `(${THREAT_INTELLIGENCE_PATH})`,
+ strict: false,
+ });
+};
+
export const getSubPluginRoutesByCapabilities = (
subPlugins: StartedSubPlugins,
capabilities: Capabilities
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx
index 0133e09ac7202..ae59f3ceaa8b3 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx
@@ -20,6 +20,7 @@ import { MANAGEMENT_PATH } from '../../../../common/constants';
import { getActionListMock } from './mocks';
import { useGetEndpointsList } from '../../hooks/endpoint/use_get_endpoints_list';
import uuid from 'uuid';
+import { RESPONSE_ACTION_COMMANDS } from '../../../../common/endpoint/service/response_actions/constants';
let mockUseGetEndpointActionList: {
isFetched?: boolean;
@@ -556,10 +557,10 @@ describe('Response actions history', () => {
userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);
expect(filterList).toBeTruthy();
- expect(filterList.querySelectorAll('ul>li').length).toEqual(5);
+ expect(filterList.querySelectorAll('ul>li').length).toEqual(RESPONSE_ACTION_COMMANDS.length);
expect(
Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent)
- ).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes']);
+ ).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes', 'get-file']);
});
it('should have `clear all` button `disabled` when no selected values', () => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx
index 7b0132a4b4be4..d42585485bb94 100644
--- a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx
@@ -375,7 +375,7 @@ describe('Response actions history page', () => {
});
expect(history.location.search).toEqual(
- '?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses'
+ '?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses%2Cget-file'
);
});
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/action_responder/utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/action_responder/utils.ts
index 59bf6e6da303f..0853500b83c73 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/action_responder/utils.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/action_responder/utils.ts
@@ -8,11 +8,14 @@
import type { KbnClient } from '@kbn/test';
import type { Client } from '@elastic/elasticsearch';
import { AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
+import type { UploadedFile } from '../../../common/endpoint/types/file_storage';
import { sendEndpointMetadataUpdate } from '../common/endpoint_metadata_services';
import { FleetActionGenerator } from '../../../common/endpoint/data_generators/fleet_action_generator';
import {
ENDPOINT_ACTION_RESPONSES_INDEX,
ENDPOINTS_ACTION_LIST_ROUTE,
+ FILE_STORAGE_DATA_INDEX,
+ FILE_STORAGE_METADATA_INDEX,
} from '../../../common/endpoint/constants';
import type {
ActionDetails,
@@ -144,6 +147,40 @@ export const sendEndpointActionResponse = async (
}
}
+ // For `get-file`, upload a file to ES
+ if (action.command === 'get-file' && !endpointResponse.error) {
+ // Add the file's metadata
+ const fileMeta = await esClient.index({
+ index: FILE_STORAGE_METADATA_INDEX,
+ id: `${action.id}.${action.hosts[0]}`,
+ body: {
+ file: {
+ created: new Date().toISOString(),
+ extension: 'zip',
+ path: '/some/path/bad_file.txt',
+ type: 'file',
+ size: 221,
+ name: 'bad_file.txt.zip',
+ mime_type: 'application/zip',
+ Status: 'READY',
+ ChunkSize: 4194304,
+ },
+ },
+ refresh: 'wait_for',
+ });
+
+ await esClient.index({
+ index: FILE_STORAGE_DATA_INDEX,
+ id: `${fileMeta._id}.0`,
+ body: {
+ bid: fileMeta._id,
+ last: true,
+ data: 'UEsDBBQACAAIAFVeRFUAAAAAAAAAABMAAAAMACAAYmFkX2ZpbGUudHh0VVQNAAdTVjxjU1Y8Y1NWPGN1eAsAAQT1AQAABBQAAAArycgsVgCiRIWkxBSFtMycVC4AUEsHCKkCwMsTAAAAEwAAAFBLAQIUAxQACAAIAFVeRFWpAsDLEwAAABMAAAAMACAAAAAAAAAAAACkgQAAAABiYWRfZmlsZS50eHRVVA0AB1NWPGNTVjxjU1Y8Y3V4CwABBPUBAAAEFAAAAFBLBQYAAAAAAQABAFoAAABtAAAAAAA=',
+ },
+ refresh: 'wait_for',
+ });
+ }
+
return endpointResponse;
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts
index 23705d6bc43be..aec86fe776663 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/mocks.ts
@@ -46,8 +46,7 @@ export const createActionResponsesEsSearchResultsMock = (
const fleetActionGenerator = new FleetActionGenerator('seed');
let hitSource: Array<
- | estypes.SearchHit
- | estypes.SearchHit>
+ estypes.SearchHit | estypes.SearchHit
> = [
fleetActionGenerator.generateResponseEsHit({
action_id: '123',
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts
index 8f5fc01c7f2d7..8cc2f72f150d2 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts
@@ -441,7 +441,18 @@ describe('When using Actions service utilities', () => {
completedAt: COMPLETED_AT,
wasSuccessful: true,
errors: undefined,
- outputs: {},
+ outputs: {
+ '456': {
+ content: {
+ file: {
+ name: 'bad_file.txt',
+ path: '/some/path/bad_file.txt',
+ size: 221,
+ },
+ },
+ type: 'json',
+ },
+ },
agentState: {
'123': {
completedAt: '2022-01-05T19:27:23.816Z',
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases/index.ts
index 22f32cf636036..d7a42c3350c82 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/index.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/index.ts
@@ -8,10 +8,6 @@
export { getCasesWebhookConnectorType } from './cases_webhook';
export { getJiraConnectorType } from './jira';
export { getResilientConnectorType } from './resilient';
-export {
- getServiceNowITSMConnectorType,
- getServiceNowSIRConnectorType,
- getServiceNowITOMConnectorType,
-} from './servicenow';
+export { getServiceNowITSMConnectorType } from './servicenow_itsm';
+export { getServiceNowSIRConnectorType } from './servicenow_sir';
export { getSwimlaneConnectorType } from './swimlane';
-export { getXmattersConnectorType } from './xmatters';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow.tsx
deleted file mode 100644
index 932d244e852f8..0000000000000
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { lazy } from 'react';
-import { i18n } from '@kbn/i18n';
-import type {
- ActionTypeModel as ConnectorTypeModel,
- GenericValidationResult,
-} from '@kbn/triggers-actions-ui-plugin/public';
-import {
- ServiceNowConfig,
- ServiceNowITOMActionParams,
- ServiceNowITSMActionParams,
- ServiceNowSecrets,
- ServiceNowSIRActionParams,
-} from './types';
-import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from './helpers';
-
-export const SERVICENOW_ITOM_TITLE = i18n.translate(
- 'xpack.stackConnectors.components.serviceNowITOM.connectorTypeTitle',
- {
- defaultMessage: 'ServiceNow ITOM',
- }
-);
-
-export const SERVICENOW_ITOM_DESC = i18n.translate(
- 'xpack.stackConnectors.components.serviceNowITOM.selectMessageText',
- {
- defaultMessage: 'Create an event in ServiceNow ITOM.',
- }
-);
-
-export const SERVICENOW_ITSM_DESC = i18n.translate(
- 'xpack.stackConnectors.components.serviceNowITSM.selectMessageText',
- {
- defaultMessage: 'Create an incident in ServiceNow ITSM.',
- }
-);
-
-export const SERVICENOW_SIR_DESC = i18n.translate(
- 'xpack.stackConnectors.components.serviceNowSIR.selectMessageText',
- {
- defaultMessage: 'Create an incident in ServiceNow SecOps.',
- }
-);
-
-export const SERVICENOW_ITSM_TITLE = i18n.translate(
- 'xpack.stackConnectors.components.serviceNowITSM.connectorTypeTitle',
- {
- defaultMessage: 'ServiceNow ITSM',
- }
-);
-
-export const SERVICENOW_SIR_TITLE = i18n.translate(
- 'xpack.stackConnectors.components.serviceNowSIR.connectorTypeTitle',
- {
- defaultMessage: 'ServiceNow SecOps',
- }
-);
-
-export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
- ServiceNowConfig,
- ServiceNowSecrets,
- ServiceNowITSMActionParams
-> {
- return {
- id: '.servicenow',
- iconClass: lazy(() => import('./logo')),
- selectMessage: SERVICENOW_ITSM_DESC,
- actionTypeTitle: SERVICENOW_ITSM_TITLE,
- actionConnectorFields: lazy(() => import('./servicenow_connectors')),
- validateParams: async (
- actionParams: ServiceNowITSMActionParams
- ): Promise> => {
- const translations = await import('./translations');
- const errors = {
- 'subActionParams.incident.short_description': new Array(),
- };
- const validationResult = {
- errors,
- };
- if (
- actionParams.subActionParams &&
- actionParams.subActionParams.incident &&
- !actionParams.subActionParams.incident.short_description?.length
- ) {
- errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
- }
- return validationResult;
- },
- actionParamsFields: lazy(() => import('./servicenow_itsm_params')),
- customConnectorSelectItem: {
- getText: getConnectorDescriptiveTitle,
- getComponent: getSelectedConnectorIcon,
- },
- };
-}
-
-export function getServiceNowSIRConnectorType(): ConnectorTypeModel<
- ServiceNowConfig,
- ServiceNowSecrets,
- ServiceNowSIRActionParams
-> {
- return {
- id: '.servicenow-sir',
- iconClass: lazy(() => import('./logo')),
- selectMessage: SERVICENOW_SIR_DESC,
- actionTypeTitle: SERVICENOW_SIR_TITLE,
- actionConnectorFields: lazy(() => import('./servicenow_connectors')),
- validateParams: async (
- actionParams: ServiceNowSIRActionParams
- ): Promise> => {
- const translations = await import('./translations');
- const errors = {
- 'subActionParams.incident.short_description': new Array(),
- };
- const validationResult = {
- errors,
- };
- if (
- actionParams.subActionParams &&
- actionParams.subActionParams.incident &&
- !actionParams.subActionParams.incident.short_description?.length
- ) {
- errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
- }
- return validationResult;
- },
- actionParamsFields: lazy(() => import('./servicenow_sir_params')),
- customConnectorSelectItem: {
- getText: getConnectorDescriptiveTitle,
- getComponent: getSelectedConnectorIcon,
- },
- };
-}
-
-export function getServiceNowITOMConnectorType(): ConnectorTypeModel<
- ServiceNowConfig,
- ServiceNowSecrets,
- ServiceNowITOMActionParams
-> {
- return {
- id: '.servicenow-itom',
- iconClass: lazy(() => import('./logo')),
- selectMessage: SERVICENOW_ITOM_DESC,
- actionTypeTitle: SERVICENOW_ITOM_TITLE,
- actionConnectorFields: lazy(() => import('./servicenow_connectors_no_app')),
- validateParams: async (
- actionParams: ServiceNowITOMActionParams
- ): Promise> => {
- const translations = await import('./translations');
- const errors = {
- severity: new Array(),
- };
- const validationResult = { errors };
-
- if (actionParams?.subActionParams?.severity == null) {
- errors.severity.push(translations.SEVERITY_REQUIRED);
- }
-
- return validationResult;
- },
- actionParamsFields: lazy(() => import('./servicenow_itom_params')),
- };
-}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/index.ts
similarity index 65%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/index.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/index.ts
index 553cf2edde846..703987149c50f 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/index.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/index.ts
@@ -5,8 +5,4 @@
* 2.0.
*/
-export {
- getServiceNowITSMConnectorType,
- getServiceNowSIRConnectorType,
- getServiceNowITOMConnectorType,
-} from './servicenow';
+export { getServiceNowITSMConnectorType } from './servicenow_itsm';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/logo.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/logo.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/logo.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm.test.tsx
new file mode 100644
index 0000000000000..fe70d5f06046b
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm.test.tsx
@@ -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 { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
+import { registerConnectorTypes } from '../..';
+import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
+import { registrationServicesMock } from '../../../mocks';
+
+const SERVICENOW_ITSM_CONNECTOR_TYPE_ID = '.servicenow';
+let connectorTypeRegistry: TypeRegistry;
+
+beforeAll(() => {
+ connectorTypeRegistry = new TypeRegistry();
+ registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock });
+});
+
+describe('connectorTypeRegistry.get() works', () => {
+ test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: connector type static data is as expected`, () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
+ expect(connectorTypeModel.id).toEqual(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
+ });
+});
+
+describe('servicenow action params validation', () => {
+ test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: action params validation succeeds when action params is valid`, async () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
+ const actionParams = {
+ subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
+ };
+
+ expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
+ errors: { ['subActionParams.incident.short_description']: [] },
+ });
+ });
+
+ test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: params validation fails when short_description is not valid`, async () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
+ const actionParams = {
+ subActionParams: { incident: { short_description: '' }, comments: [] },
+ };
+
+ expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
+ errors: {
+ ['subActionParams.incident.short_description']: ['Short description is required.'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm.tsx
new file mode 100644
index 0000000000000..67c689f971e6e
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import type {
+ ActionTypeModel as ConnectorTypeModel,
+ GenericValidationResult,
+} from '@kbn/triggers-actions-ui-plugin/public';
+import { ServiceNowConfig, ServiceNowSecrets } from '../../lib/servicenow/types';
+import { ServiceNowITSMActionParams } from './types';
+import {
+ getConnectorDescriptiveTitle,
+ getSelectedConnectorIcon,
+} from '../../lib/servicenow/helpers';
+
+export const SERVICENOW_ITSM_DESC = i18n.translate(
+ 'xpack.stackConnectors.components.serviceNowITSM.selectMessageText',
+ {
+ defaultMessage: 'Create an incident in ServiceNow ITSM.',
+ }
+);
+
+export const SERVICENOW_ITSM_TITLE = i18n.translate(
+ 'xpack.stackConnectors.components.serviceNowITSM.connectorTypeTitle',
+ {
+ defaultMessage: 'ServiceNow ITSM',
+ }
+);
+
+export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
+ ServiceNowConfig,
+ ServiceNowSecrets,
+ ServiceNowITSMActionParams
+> {
+ return {
+ id: '.servicenow',
+ iconClass: lazy(() => import('./logo')),
+ selectMessage: SERVICENOW_ITSM_DESC,
+ actionTypeTitle: SERVICENOW_ITSM_TITLE,
+ actionConnectorFields: lazy(() => import('../../lib/servicenow/servicenow_connectors')),
+ validateParams: async (
+ actionParams: ServiceNowITSMActionParams
+ ): Promise> => {
+ const translations = await import('../../lib/servicenow/translations');
+ const errors = {
+ 'subActionParams.incident.short_description': new Array(),
+ };
+ const validationResult = {
+ errors,
+ };
+ if (
+ actionParams.subActionParams &&
+ actionParams.subActionParams.incident &&
+ !actionParams.subActionParams.incident.short_description?.length
+ ) {
+ errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
+ }
+ return validationResult;
+ },
+ actionParamsFields: lazy(() => import('./servicenow_itsm_params')),
+ customConnectorSelectItem: {
+ getText: getConnectorDescriptiveTitle,
+ getComponent: getSelectedConnectorIcon,
+ },
+ };
+}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm_params.test.tsx
similarity index 98%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itsm_params.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm_params.test.tsx
index aa6cb6c71278d..39157f1a25f7a 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itsm_params.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm_params.test.tsx
@@ -10,12 +10,12 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from '@testing-library/react';
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
-import { useGetChoices } from './use_get_choices';
+import { useGetChoices } from '../../lib/servicenow/use_get_choices';
import ServiceNowITSMParamsFields from './servicenow_itsm_params';
-import { Choice } from './types';
+import { Choice } from '../../lib/servicenow/types';
import { merge } from 'lodash';
-jest.mock('./use_get_choices');
+jest.mock('../../lib/servicenow/use_get_choices');
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
const useGetChoicesMock = useGetChoices as jest.Mock;
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm_params.tsx
similarity index 96%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itsm_params.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm_params.tsx
index a585ee48864e8..56afb35086a64 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itsm_params.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/servicenow_itsm_params.tsx
@@ -22,11 +22,12 @@ import {
TextFieldWithMessageVariables,
useKibana,
} from '@kbn/triggers-actions-ui-plugin/public';
-import { ServiceNowITSMActionParams, Choice, Fields } from './types';
-import { useGetChoices } from './use_get_choices';
-import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from './helpers';
+import { Choice, Fields } from '../../lib/servicenow/types';
+import { ServiceNowITSMActionParams } from './types';
+import { useGetChoices } from '../../lib/servicenow/use_get_choices';
+import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from '../../lib/servicenow/helpers';
-import * as i18n from './translations';
+import * as i18n from '../../lib/servicenow/translations';
const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory'];
const defaultFields: Fields = {
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/types.ts
new file mode 100644
index 0000000000000..6c3b558f22a04
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_itsm/types.ts
@@ -0,0 +1,13 @@
+/*
+ * 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 { ExecutorSubActionPushParamsITSM } from '../../../../server/connector_types/lib/servicenow/types';
+
+export interface ServiceNowITSMActionParams {
+ subAction: string;
+ subActionParams: ExecutorSubActionPushParamsITSM;
+}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/index.ts
new file mode 100644
index 0000000000000..ec1076d47219c
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { getServiceNowSIRConnectorType } from './servicenow_sir';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/logo.tsx
new file mode 100644
index 0000000000000..f97b07247569d
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/logo.tsx
@@ -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 React from 'react';
+import { LogoProps } from '../../types';
+
+function Logo(props: LogoProps) {
+ return (
+
+ );
+}
+
+// eslint-disable-next-line import/no-default-export
+export default Logo;
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir.test.tsx
new file mode 100644
index 0000000000000..e9ac99d210df8
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir.test.tsx
@@ -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 { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
+import { registerConnectorTypes } from '../..';
+import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
+import { registrationServicesMock } from '../../../mocks';
+
+const SERVICENOW_SIR_CONNECTOR_TYPE_ID = '.servicenow-sir';
+let connectorTypeRegistry: TypeRegistry;
+
+beforeAll(() => {
+ connectorTypeRegistry = new TypeRegistry();
+ registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock });
+});
+
+describe('connectorTypeRegistry.get() works', () => {
+ test(`${SERVICENOW_SIR_CONNECTOR_TYPE_ID}: connector type static data is as expected`, () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_SIR_CONNECTOR_TYPE_ID);
+ expect(connectorTypeModel.id).toEqual(SERVICENOW_SIR_CONNECTOR_TYPE_ID);
+ });
+});
+
+describe('servicenow action params validation', () => {
+ test(`${SERVICENOW_SIR_CONNECTOR_TYPE_ID}: action params validation succeeds when action params is valid`, async () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_SIR_CONNECTOR_TYPE_ID);
+ const actionParams = {
+ subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
+ };
+
+ expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
+ errors: { ['subActionParams.incident.short_description']: [] },
+ });
+ });
+
+ test(`${SERVICENOW_SIR_CONNECTOR_TYPE_ID}: params validation fails when short_description is not valid`, async () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_SIR_CONNECTOR_TYPE_ID);
+ const actionParams = {
+ subActionParams: { incident: { short_description: '' }, comments: [] },
+ };
+
+ expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
+ errors: {
+ ['subActionParams.incident.short_description']: ['Short description is required.'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir.tsx
new file mode 100644
index 0000000000000..ff6ebaa913941
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import type {
+ ActionTypeModel as ConnectorTypeModel,
+ GenericValidationResult,
+} from '@kbn/triggers-actions-ui-plugin/public';
+import { ServiceNowConfig, ServiceNowSecrets } from '../../lib/servicenow/types';
+import { ServiceNowSIRActionParams } from './types';
+import {
+ getConnectorDescriptiveTitle,
+ getSelectedConnectorIcon,
+} from '../../lib/servicenow/helpers';
+
+export const SERVICENOW_SIR_DESC = i18n.translate(
+ 'xpack.stackConnectors.components.serviceNowSIR.selectMessageText',
+ {
+ defaultMessage: 'Create an incident in ServiceNow SecOps.',
+ }
+);
+
+export const SERVICENOW_SIR_TITLE = i18n.translate(
+ 'xpack.stackConnectors.components.serviceNowSIR.connectorTypeTitle',
+ {
+ defaultMessage: 'ServiceNow SecOps',
+ }
+);
+
+export function getServiceNowSIRConnectorType(): ConnectorTypeModel<
+ ServiceNowConfig,
+ ServiceNowSecrets,
+ ServiceNowSIRActionParams
+> {
+ return {
+ id: '.servicenow-sir',
+ iconClass: lazy(() => import('./logo')),
+ selectMessage: SERVICENOW_SIR_DESC,
+ actionTypeTitle: SERVICENOW_SIR_TITLE,
+ actionConnectorFields: lazy(() => import('../../lib/servicenow/servicenow_connectors')),
+ validateParams: async (
+ actionParams: ServiceNowSIRActionParams
+ ): Promise> => {
+ const translations = await import('../../lib/servicenow/translations');
+ const errors = {
+ 'subActionParams.incident.short_description': new Array(),
+ };
+ const validationResult = {
+ errors,
+ };
+ if (
+ actionParams.subActionParams &&
+ actionParams.subActionParams.incident &&
+ !actionParams.subActionParams.incident.short_description?.length
+ ) {
+ errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
+ }
+ return validationResult;
+ },
+ actionParamsFields: lazy(() => import('./servicenow_sir_params')),
+ customConnectorSelectItem: {
+ getText: getConnectorDescriptiveTitle,
+ getComponent: getSelectedConnectorIcon,
+ },
+ };
+}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_sir_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir_params.test.tsx
similarity index 98%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_sir_params.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir_params.test.tsx
index 8739938891625..49916b350c6a7 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_sir_params.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir_params.test.tsx
@@ -10,12 +10,12 @@ import { act } from '@testing-library/react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
-import { useGetChoices } from './use_get_choices';
+import { useGetChoices } from '../../lib/servicenow/use_get_choices';
import ServiceNowSIRParamsFields from './servicenow_sir_params';
-import { Choice } from './types';
+import { Choice } from '../../lib/servicenow/types';
import { merge } from 'lodash';
-jest.mock('./use_get_choices');
+jest.mock('../../lib/servicenow/use_get_choices');
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
const useGetChoicesMock = useGetChoices as jest.Mock;
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir_params.tsx
similarity index 95%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_sir_params.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir_params.tsx
index e58d635f9ef2d..e1b28425887fd 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_sir_params.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/servicenow_sir_params.tsx
@@ -23,11 +23,12 @@ import {
useKibana,
} from '@kbn/triggers-actions-ui-plugin/public';
-import * as i18n from './translations';
-import { useGetChoices } from './use_get_choices';
-import { ServiceNowSIRActionParams, Fields, Choice } from './types';
-import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from './helpers';
-import { DeprecatedCallout } from './deprecated_callout';
+import * as i18n from '../../lib/servicenow/translations';
+import { useGetChoices } from '../../lib/servicenow/use_get_choices';
+import { ServiceNowSIRActionParams } from './types';
+import { Fields, Choice } from '../../lib/servicenow/types';
+import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from '../../lib/servicenow/helpers';
+import { DeprecatedCallout } from '../../lib/servicenow/deprecated_callout';
const useGetChoicesFields = ['category', 'subcategory', 'priority'];
const defaultFields: Fields = {
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/types.ts
new file mode 100644
index 0000000000000..889bd527a4a6a
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow_sir/types.ts
@@ -0,0 +1,13 @@
+/*
+ * 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 { ExecutorSubActionPushParamsSIR } from '../../../../server/connector_types/lib/servicenow/types';
+
+export interface ServiceNowSIRActionParams {
+ subAction: string;
+ subActionParams: ExecutorSubActionPushParamsSIR;
+}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/index.ts
index fe24d311c4d65..a2684c0b20734 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/index.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/index.ts
@@ -12,20 +12,20 @@ import {
getIndexConnectorType,
getPagerDutyConnectorType,
getServerLogConnectorType,
+ getServiceNowITOMConnectorType,
getSlackConnectorType,
getTeamsConnectorType,
getWebhookConnectorType,
+ getXmattersConnectorType,
} from './stack';
import {
getCasesWebhookConnectorType,
getJiraConnectorType,
getResilientConnectorType,
- getServiceNowITOMConnectorType,
getServiceNowITSMConnectorType,
getServiceNowSIRConnectorType,
getSwimlaneConnectorType,
- getXmattersConnectorType,
} from './cases';
export interface RegistrationServices {
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/api.test.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/api.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/api.test.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/api.test.ts
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/api.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/api.ts
similarity index 96%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/api.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/api.ts
index 4cf46d57eb7f4..38ae8727f635d 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/api.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/api.ts
@@ -15,10 +15,7 @@ import {
import { snExternalServiceConfig } from '../../../../common/servicenow_config';
import { API_INFO_ERROR } from './translations';
import { AppInfo, RESTApiError, ServiceNowActionConnector } from './types';
-import {
- ConnectorExecutorResult,
- rewriteResponseToCamelCase,
-} from '../../lib/rewrite_response_body';
+import { ConnectorExecutorResult, rewriteResponseToCamelCase } from '../rewrite_response_body';
import { Choice } from './types';
export async function getChoices({
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/application_required_callout.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/application_required_callout.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/application_required_callout.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/application_required_callout.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/application_required_callout.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/application_required_callout.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/application_required_callout.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/application_required_callout.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/auth_types/credentials_auth.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/auth_types/credentials_auth.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/auth_types/credentials_auth.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/auth_types/credentials_auth.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/auth_types/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/auth_types/index.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/auth_types/index.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/auth_types/index.ts
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/auth_types/oauth.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/auth_types/oauth.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/auth_types/oauth.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/auth_types/oauth.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials.test.tsx
similarity index 97%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials.test.tsx
index aab2ab0fb21c6..2e1ae98a10886 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { Credentials } from './credentials';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
-import { ConnectorFormTestProvider } from '../../lib/test_utils';
+import { ConnectorFormTestProvider } from '../test_utils';
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials_api_url.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials_api_url.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/credentials_api_url.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/credentials_api_url.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/deprecated_callout.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/deprecated_callout.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/deprecated_callout.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/deprecated_callout.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/deprecated_callout.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/deprecated_callout.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/deprecated_callout.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/deprecated_callout.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/helpers.test.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/helpers.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/helpers.test.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/helpers.test.ts
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/helpers.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/helpers.ts
similarity index 96%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/helpers.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/helpers.ts
index def683edbdd33..f3e71b0e9fc7f 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/helpers.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/helpers.ts
@@ -42,6 +42,6 @@ export const getSelectedConnectorIcon = (
actionConnector: ActionConnector
): React.LazyExoticComponent> | undefined => {
if (actionConnector.isDeprecated) {
- return lazy(() => import('./servicenow_selection_row'));
+ return lazy(() => import('./selection_row'));
}
};
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/installation_callout.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/installation_callout.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/installation_callout.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/installation_callout.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/installation_callout.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/installation_callout.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/installation_callout.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/installation_callout.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_selection_row.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/selection_row.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_selection_row.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/selection_row.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors.test.tsx
similarity index 99%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors.test.tsx
index 6bf81f5aeae74..51b0f759c75bd 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors.test.tsx
@@ -15,7 +15,7 @@ import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
import { updateActionConnector } from '@kbn/triggers-actions-ui-plugin/public/application/lib/action_connector_api';
import ServiceNowConnectorFields from './servicenow_connectors';
import { getAppInfo } from './api';
-import { ConnectorFormTestProvider } from '../../lib/test_utils';
+import { ConnectorFormTestProvider } from '../test_utils';
import { mount } from 'enzyme';
import userEvent from '@testing-library/user-event';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors_no_app.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors_no_app.test.tsx
similarity index 97%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors_no_app.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors_no_app.test.tsx
index e70005f8c7e1b..a0dda6edf76e0 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors_no_app.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors_no_app.test.tsx
@@ -8,11 +8,7 @@
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
-import {
- AppMockRenderer,
- ConnectorFormTestProvider,
- createAppMockRenderer,
-} from '../../lib/test_utils';
+import { AppMockRenderer, ConnectorFormTestProvider, createAppMockRenderer } from '../test_utils';
import ServiceNowConnectorFieldsNoApp from './servicenow_connectors_no_app';
describe('ServiceNowActionConnectorFields renders', () => {
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors_no_app.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors_no_app.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_connectors_no_app.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/servicenow_connectors_no_app.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/sn_store_button.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/sn_store_button.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/sn_store_button.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/sn_store_button.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/sn_store_button.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/sn_store_button.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/sn_store_button.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/sn_store_button.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/translations.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/translations.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/translations.ts
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/types.ts
similarity index 73%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/types.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/types.ts
index f10de69252f9d..862eb2165b7b2 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/types.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/types.ts
@@ -6,32 +6,12 @@
*/
import { UserConfiguredActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
-import type {
- ExecutorSubActionPushParamsITSM,
- ExecutorSubActionPushParamsSIR,
- ExecutorSubActionAddEventParams,
-} from '../../../../server/connector_types/cases/servicenow/types';
export type ServiceNowActionConnector = UserConfiguredActionConnector<
ServiceNowConfig,
ServiceNowSecrets
>;
-export interface ServiceNowITSMActionParams {
- subAction: string;
- subActionParams: ExecutorSubActionPushParamsITSM;
-}
-
-export interface ServiceNowSIRActionParams {
- subAction: string;
- subActionParams: ExecutorSubActionPushParamsSIR;
-}
-
-export interface ServiceNowITOMActionParams {
- subAction: string;
- subActionParams: ExecutorSubActionAddEventParams;
-}
-
// Config
export interface ServiceNowCommonConfig {
isOAuth: boolean;
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/update_connector.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/update_connector.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/update_connector.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/update_connector.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/update_connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/update_connector.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/update_connector.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/update_connector.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_choices.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_choices.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_choices.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_choices.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_choices.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_app_info.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_app_info.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_app_info.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_app_info.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_app_info.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_choices.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_choices.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_choices.tsx b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/use_get_choices.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/use_get_choices.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts
index 93d444d20204d..fec0283a799ab 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts
@@ -9,6 +9,8 @@ export { getEmailConnectorType } from './email';
export { getIndexConnectorType } from './es_index';
export { getPagerDutyConnectorType } from './pagerduty';
export { getServerLogConnectorType } from './server_log';
+export { getServiceNowITOMConnectorType } from './servicenow_itom';
export { getSlackConnectorType } from './slack';
export { getTeamsConnectorType } from './teams';
export { getWebhookConnectorType } from './webhook';
+export { getXmattersConnectorType } from './xmatters';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/index.ts
new file mode 100644
index 0000000000000..bfcd0f888f855
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { getServiceNowITOMConnectorType } from './servicenow_itom';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/logo.tsx
new file mode 100644
index 0000000000000..f97b07247569d
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/logo.tsx
@@ -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 React from 'react';
+import { LogoProps } from '../../types';
+
+function Logo(props: LogoProps) {
+ return (
+
+ );
+}
+
+// eslint-disable-next-line import/no-default-export
+export default Logo;
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom.test.tsx
similarity index 54%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom.test.tsx
index 9427623f0de8a..b86616ccde5e1 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom.test.tsx
@@ -10,8 +10,6 @@ import { registerConnectorTypes } from '../..';
import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
import { registrationServicesMock } from '../../../mocks';
-const SERVICENOW_ITSM_CONNECTOR_TYPE_ID = '.servicenow';
-const SERVICENOW_SIR_CONNECTOR_TYPE_ID = '.servicenow-sir';
const SERVICENOW_ITOM_CONNECTOR_TYPE_ID = '.servicenow-itom';
let connectorTypeRegistry: TypeRegistry;
@@ -21,45 +19,13 @@ beforeAll(() => {
});
describe('connectorTypeRegistry.get() works', () => {
- [
- SERVICENOW_ITSM_CONNECTOR_TYPE_ID,
- SERVICENOW_SIR_CONNECTOR_TYPE_ID,
- SERVICENOW_ITOM_CONNECTOR_TYPE_ID,
- ].forEach((id) => {
- test(`${id}: connector type static data is as expected`, () => {
- const connectorTypeModel = connectorTypeRegistry.get(id);
- expect(connectorTypeModel.id).toEqual(id);
- });
+ test(`${SERVICENOW_ITOM_CONNECTOR_TYPE_ID}: connector type static data is as expected`, () => {
+ const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITOM_CONNECTOR_TYPE_ID);
+ expect(connectorTypeModel.id).toEqual(SERVICENOW_ITOM_CONNECTOR_TYPE_ID);
});
});
describe('servicenow action params validation', () => {
- [SERVICENOW_ITSM_CONNECTOR_TYPE_ID, SERVICENOW_SIR_CONNECTOR_TYPE_ID].forEach((id) => {
- test(`${id}: action params validation succeeds when action params is valid`, async () => {
- const connectorTypeModel = connectorTypeRegistry.get(id);
- const actionParams = {
- subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
- };
-
- expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
- errors: { ['subActionParams.incident.short_description']: [] },
- });
- });
-
- test(`${id}: params validation fails when short_description is not valid`, async () => {
- const connectorTypeModel = connectorTypeRegistry.get(id);
- const actionParams = {
- subActionParams: { incident: { short_description: '' }, comments: [] },
- };
-
- expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
- errors: {
- ['subActionParams.incident.short_description']: ['Short description is required.'],
- },
- });
- });
- });
-
test(`${SERVICENOW_ITOM_CONNECTOR_TYPE_ID}: action params validation succeeds when action params is valid`, async () => {
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITOM_CONNECTOR_TYPE_ID);
const actionParams = { subActionParams: { severity: 'Critical' } };
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom.tsx
new file mode 100644
index 0000000000000..09f4002774f67
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import type {
+ ActionTypeModel as ConnectorTypeModel,
+ GenericValidationResult,
+} from '@kbn/triggers-actions-ui-plugin/public';
+import { ServiceNowConfig, ServiceNowSecrets } from '../../lib/servicenow/types';
+import { ServiceNowITOMActionParams } from './types';
+
+export const SERVICENOW_ITOM_TITLE = i18n.translate(
+ 'xpack.stackConnectors.components.serviceNowITOM.connectorTypeTitle',
+ {
+ defaultMessage: 'ServiceNow ITOM',
+ }
+);
+
+export const SERVICENOW_ITOM_DESC = i18n.translate(
+ 'xpack.stackConnectors.components.serviceNowITOM.selectMessageText',
+ {
+ defaultMessage: 'Create an event in ServiceNow ITOM.',
+ }
+);
+
+export function getServiceNowITOMConnectorType(): ConnectorTypeModel<
+ ServiceNowConfig,
+ ServiceNowSecrets,
+ ServiceNowITOMActionParams
+> {
+ return {
+ id: '.servicenow-itom',
+ iconClass: lazy(() => import('./logo')),
+ selectMessage: SERVICENOW_ITOM_DESC,
+ actionTypeTitle: SERVICENOW_ITOM_TITLE,
+ actionConnectorFields: lazy(() => import('../../lib/servicenow/servicenow_connectors_no_app')),
+ validateParams: async (
+ actionParams: ServiceNowITOMActionParams
+ ): Promise> => {
+ const translations = await import('../../lib/servicenow/translations');
+ const errors = {
+ severity: new Array(),
+ };
+ const validationResult = { errors };
+
+ if (actionParams?.subActionParams?.severity == null) {
+ errors.severity.push(translations.SEVERITY_REQUIRED);
+ }
+
+ return validationResult;
+ },
+ actionParamsFields: lazy(() => import('./servicenow_itom_params')),
+ };
+}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itom_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom_params.test.tsx
similarity index 98%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itom_params.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom_params.test.tsx
index 60531c7a7104d..d2e9880058277 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itom_params.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom_params.test.tsx
@@ -9,10 +9,10 @@ import React from 'react';
import { mount } from 'enzyme';
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
-import { useChoices } from './use_choices';
+import { useChoices } from '../../lib/servicenow/use_choices';
import ServiceNowITOMParamsFields from './servicenow_itom_params';
-jest.mock('./use_choices');
+jest.mock('../../lib/servicenow/use_choices');
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
const useChoicesMock = useChoices as jest.Mock;
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itom_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom_params.tsx
similarity index 96%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itom_params.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom_params.tsx
index caa2f40bac2c1..0086c6713e074 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases/servicenow/servicenow_itom_params.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/servicenow_itom_params.tsx
@@ -14,10 +14,10 @@ import {
useKibana,
} from '@kbn/triggers-actions-ui-plugin/public';
-import * as i18n from './translations';
-import { useChoices } from './use_choices';
+import * as i18n from '../../lib/servicenow/translations';
+import { useChoices } from '../../lib/servicenow/use_choices';
import { ServiceNowITOMActionParams } from './types';
-import { choicesToEuiOptions, isFieldInvalid } from './helpers';
+import { choicesToEuiOptions, isFieldInvalid } from '../../lib/servicenow/helpers';
const choicesFields = ['severity'];
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/types.ts
new file mode 100644
index 0000000000000..5155997c996c8
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/servicenow_itom/types.ts
@@ -0,0 +1,13 @@
+/*
+ * 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 { ExecutorSubActionAddEventParams } from '../../../../server/connector_types/lib/servicenow/types';
+
+export interface ServiceNowITOMActionParams {
+ subAction: string;
+ subActionParams: ExecutorSubActionAddEventParams;
+}
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/index.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/index.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/index.ts
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/logo.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/logo.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/logo.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/translations.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/translations.ts
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/translations.ts
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_connectors.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_connectors.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_connectors.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_connectors.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_connectors.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_connectors.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_params.test.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_params.test.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_params.test.tsx
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_params.tsx
similarity index 100%
rename from x-pack/plugins/stack_connectors/public/connector_types/cases/xmatters/xmatters_params.tsx
rename to x-pack/plugins/stack_connectors/public/connector_types/stack/xmatters/xmatters_params.tsx
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/index.ts
index abc6c0557a780..ad706fff3b8e7 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/index.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/index.ts
@@ -22,20 +22,10 @@ export {
} from './resilient';
export type { ActionParamsType as ResilientActionParams } from './resilient';
-export {
- getServiceNowITSMConnectorType,
- getServiceNowSIRConnectorType,
- getServiceNowITOMConnectorType,
- ServiceNowITSMConnectorTypeId,
- ServiceNowSIRConnectorTypeId,
- ServiceNowITOMConnectorTypeId,
-} from './servicenow';
-export type { ActionParamsType as ServiceNowActionParams } from './servicenow';
+export { getServiceNowITSMConnectorType, ServiceNowITSMConnectorTypeId } from './servicenow_itsm';
+import type { ActionParamsType as ServiceNowITSMActionParams } from './servicenow_itsm';
+export { getServiceNowSIRConnectorType, ServiceNowSIRConnectorTypeId } from './servicenow_sir';
+import type { ActionParamsType as ServiceNowSIRActionParams } from './servicenow_sir';
+export type ServiceNowActionParams = ServiceNowITSMActionParams | ServiceNowSIRActionParams;
export { getConnectorType as getSwimlaneConnectorType } from './swimlane';
-
-export {
- getConnectorType as getXmattersConnectorType,
- ConnectorTypeId as XmattersConnectorTypeId,
-} from './xmatters';
-export type { ActionParamsType as XmattersActionParams } from './xmatters';
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/api.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/api.ts
new file mode 100644
index 0000000000000..4eee89cb7de98
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/api.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { api } from '../../lib/servicenow/api';
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts
similarity index 70%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/index.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts
index 02cc32111bff4..50ff4d8e0f1c9 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/index.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts
@@ -8,12 +8,11 @@
import { Logger } from '@kbn/core/server';
import { loggerMock } from '@kbn/logging-mocks';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
-import { ExecutorParams, ServiceNowPublicConfigurationType } from './types';
+import { ExecutorParams, ServiceNowPublicConfigurationType } from '../../lib/servicenow/types';
import {
ServiceNowConnectorType,
ServiceNowConnectorTypeExecutorOptions,
getServiceNowITSMConnectorType,
- getServiceNowSIRConnectorType,
} from '.';
import { api } from './api';
@@ -79,38 +78,4 @@ describe('ServiceNow', () => {
});
});
});
-
- describe('ServiceNow SIR', () => {
- let connectorType: ServiceNowConnectorType;
-
- beforeAll(() => {
- connectorType = getServiceNowSIRConnectorType({
- logger: mockedLogger,
- });
- });
-
- describe('execute()', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- test('it pass the correct comment field key', async () => {
- const actionId = 'some-action-id';
- const executorOptions = {
- actionId,
- config,
- secrets,
- params,
- services,
- } as unknown as ServiceNowConnectorTypeExecutorOptions<
- ServiceNowPublicConfigurationType,
- ExecutorParams
- >;
- await connectorType.executor(executorOptions);
- expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe(
- 'work_notes'
- );
- });
- });
- });
});
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts
similarity index 51%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/index.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts
index 493c7024bb15f..40e9d1470950e 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/index.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts
@@ -20,62 +20,40 @@ import {
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} from '@kbn/actions-plugin/common/types';
-import { validate } from './validators';
+import { validate } from '../../lib/servicenow/validators';
import {
+ ExecutorParamsSchemaITSM,
ExternalIncidentServiceConfigurationSchema,
- ExternalIncidentServiceConfigurationBaseSchema,
ExternalIncidentServiceSecretConfigurationSchema,
- ExecutorParamsSchemaITSM,
- ExecutorParamsSchemaSIR,
- ExecutorParamsSchemaITOM,
-} from './schema';
+} from '../../lib/servicenow/schema';
import { createExternalService } from './service';
-import { api as commonAPI } from './api';
-import * as i18n from './translations';
+import { api as apiITSM } from './api';
+import * as i18n from '../../lib/servicenow/translations';
import {
ExecutorParams,
ExecutorSubActionPushParams,
- ServiceNowPublicConfigurationType,
- ServiceNowSecretConfigurationType,
- PushToServiceResponse,
- ExecutorSubActionCommonFieldsParams,
- ServiceNowExecutorResultData,
- ExecutorSubActionGetChoicesParams,
ServiceFactory,
ExternalServiceAPI,
- ExecutorParamsITOM,
- ExecutorSubActionAddEventParams,
- ExternalServiceApiITOM,
- ExternalServiceITOM,
ServiceNowPublicConfigurationBaseType,
ExternalService,
-} from './types';
+ ExecutorSubActionCommonFieldsParams,
+ ExecutorSubActionGetChoicesParams,
+ PushToServiceResponse,
+ ServiceNowExecutorResultData,
+ ServiceNowPublicConfigurationType,
+ ServiceNowSecretConfigurationType,
+} from '../../lib/servicenow/types';
import {
- ServiceNowITOMConnectorTypeId,
ServiceNowITSMConnectorTypeId,
serviceNowITSMTable,
- ServiceNowSIRConnectorTypeId,
- serviceNowSIRTable,
snExternalServiceConfig,
-} from './config';
-import { createExternalServiceSIR } from './service_sir';
-import { apiSIR } from './api_sir';
-import { throwIfSubActionIsNotSupported } from './utils';
-import { createExternalServiceITOM } from './service_itom';
-import { apiITOM } from './api_itom';
-import { createServiceWrapper } from './create_service_wrapper';
+} from '../../lib/servicenow/config';
+import { throwIfSubActionIsNotSupported } from '../../lib/servicenow/utils';
+import { createServiceWrapper } from '../../lib/servicenow/create_service_wrapper';
-export {
- ServiceNowITSMConnectorTypeId,
- serviceNowITSMTable,
- ServiceNowSIRConnectorTypeId,
- serviceNowSIRTable,
- ServiceNowITOMConnectorTypeId,
-};
+export { ServiceNowITSMConnectorTypeId, serviceNowITSMTable };
-export type ActionParamsType =
- | TypeOf
- | TypeOf;
+export type ActionParamsType = TypeOf;
interface GetConnectorTypeParams {
logger: Logger;
@@ -124,75 +102,7 @@ export function getServiceNowITSMConnectorType(
logger,
actionTypeId: ServiceNowITSMConnectorTypeId,
createService: createExternalService,
- api: commonAPI,
- }),
- };
-}
-
-export function getServiceNowSIRConnectorType(
- params: GetConnectorTypeParams
-): ServiceNowConnectorType {
- const { logger } = params;
- return {
- id: ServiceNowSIRConnectorTypeId,
- minimumLicenseRequired: 'platinum',
- name: i18n.SERVICENOW_SIR,
- supportedFeatureIds: [
- AlertingConnectorFeatureId,
- CasesConnectorFeatureId,
- SecurityConnectorFeatureId,
- ],
- validate: {
- config: {
- schema: ExternalIncidentServiceConfigurationSchema,
- customValidator: validate.config,
- },
- secrets: {
- schema: ExternalIncidentServiceSecretConfigurationSchema,
- customValidator: validate.secrets,
- },
- connector: validate.connector,
- params: {
- schema: ExecutorParamsSchemaSIR,
- },
- },
- executor: curry(executor)({
- logger,
- actionTypeId: ServiceNowSIRConnectorTypeId,
- createService: createExternalServiceSIR,
- api: apiSIR,
- }),
- };
-}
-
-export function getServiceNowITOMConnectorType(
- params: GetConnectorTypeParams
-): ServiceNowConnectorType {
- const { logger } = params;
- return {
- id: ServiceNowITOMConnectorTypeId,
- minimumLicenseRequired: 'platinum',
- name: i18n.SERVICENOW_ITOM,
- supportedFeatureIds: [AlertingConnectorFeatureId, SecurityConnectorFeatureId],
- validate: {
- config: {
- schema: ExternalIncidentServiceConfigurationBaseSchema,
- customValidator: validate.config,
- },
- secrets: {
- schema: ExternalIncidentServiceSecretConfigurationSchema,
- customValidator: validate.secrets,
- },
- connector: validate.connector,
- params: {
- schema: ExecutorParamsSchemaITOM,
- },
- },
- executor: curry(executorITOM)({
- logger,
- actionTypeId: ServiceNowITOMConnectorTypeId,
- createService: createExternalServiceITOM,
- api: apiITOM,
+ api: apiITSM,
}),
};
}
@@ -272,71 +182,3 @@ async function executor(
return { status: 'ok', data: data ?? {}, actionId };
}
-
-const supportedSubActionsITOM = ['addEvent', 'getChoices'];
-
-async function executorITOM(
- {
- logger,
- actionTypeId,
- createService,
- api,
- }: {
- logger: Logger;
- actionTypeId: string;
- createService: ServiceFactory;
- api: ExternalServiceApiITOM;
- },
- execOptions: ServiceNowConnectorTypeExecutorOptions<
- ServiceNowPublicConfigurationBaseType,
- ExecutorParamsITOM
- >
-): Promise> {
- const { actionId, config, params, secrets, configurationUtilities } = execOptions;
- const { subAction, subActionParams } = params;
- const connectorTokenClient = execOptions.services.connectorTokenClient;
- const externalServiceConfig = snExternalServiceConfig[actionTypeId];
- let data: ServiceNowExecutorResultData | null = null;
-
- const externalService = createServiceWrapper({
- connectorId: actionId,
- credentials: {
- config,
- secrets,
- },
- logger,
- configurationUtilities,
- serviceConfig: externalServiceConfig,
- connectorTokenClient,
- createServiceFn: createService,
- });
-
- const apiAsRecord = api as unknown as Record;
-
- throwIfSubActionIsNotSupported({
- api: apiAsRecord,
- subAction,
- supportedSubActions: supportedSubActionsITOM,
- logger,
- });
-
- if (subAction === 'addEvent') {
- const eventParams = subActionParams as ExecutorSubActionAddEventParams;
- await api.addEvent({
- externalService,
- params: eventParams,
- logger,
- });
- }
-
- if (subAction === 'getChoices') {
- const getChoicesParams = subActionParams as ExecutorSubActionGetChoicesParams;
- data = await api.getChoices({
- externalService,
- params: getChoicesParams,
- logger,
- });
- }
-
- return { status: 'ok', data: data ?? {}, actionId };
-}
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/service.test.ts
new file mode 100644
index 0000000000000..f2bb94bd60756
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/service.test.ts
@@ -0,0 +1,1022 @@
+/*
+ * 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 axios, { AxiosResponse } from 'axios';
+
+import { createExternalService } from './service';
+import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
+import { ExternalService, ServiceNowITSMIncident } from '../../lib/servicenow/types';
+import { Logger } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
+import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
+import { serviceNowCommonFields, serviceNowChoices } from '../../lib/servicenow/mocks';
+import { snExternalServiceConfig } from '../../lib/servicenow/config';
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+
+jest.mock('axios');
+jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
+ const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
+ return {
+ ...originalUtils,
+ request: jest.fn(),
+ patch: jest.fn(),
+ };
+});
+
+axios.create = jest.fn(() => axios);
+const requestMock = utils.request as jest.Mock;
+const configurationUtilities = actionsConfigMock.create();
+
+const getImportSetAPIResponse = (update = false) => ({
+ import_set: 'ISET01',
+ staging_table: 'x_elas2_inc_int_elastic_incident',
+ result: [
+ {
+ transform_map: 'Elastic Incident',
+ table: 'incident',
+ display_name: 'number',
+ display_value: 'INC01',
+ record_link: 'https://example.com/api/now/table/incident/1',
+ status: update ? 'updated' : 'inserted',
+ sys_id: '1',
+ },
+ ],
+});
+
+const getImportSetAPIError = () => ({
+ import_set: 'ISET01',
+ staging_table: 'x_elas2_inc_int_elastic_incident',
+ result: [
+ {
+ transform_map: 'Elastic Incident',
+ status: 'error',
+ error_message: 'An error has occurred while importing the incident',
+ status_message: 'failure',
+ },
+ ],
+});
+
+const mockApplicationVersion = () =>
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ result: { name: 'Elastic', scope: 'x_elas2_inc_int', version: '1.0.0' },
+ },
+ }));
+
+const mockImportIncident = (update: boolean) =>
+ requestMock.mockImplementationOnce(() => ({
+ data: getImportSetAPIResponse(update),
+ }));
+
+const mockIncidentResponse = (update: boolean) =>
+ requestMock.mockImplementation(() => ({
+ data: {
+ result: {
+ sys_id: '1',
+ number: 'INC01',
+ ...(update
+ ? { sys_updated_on: '2020-03-10 12:24:20' }
+ : { sys_created_on: '2020-03-10 12:24:20' }),
+ },
+ },
+ }));
+
+const createIncident = async (service: ExternalService) => {
+ // Get application version
+ mockApplicationVersion();
+ // Import set api response
+ mockImportIncident(false);
+ // Get incident response
+ mockIncidentResponse(false);
+
+ return await service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+};
+
+const updateIncident = async (service: ExternalService) => {
+ // Get application version
+ mockApplicationVersion();
+ // Import set api response
+ mockImportIncident(true);
+ // Get incident response
+ mockIncidentResponse(true);
+
+ return await service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+};
+
+const expectImportedIncident = (update: boolean) => {
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
+ method: 'get',
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
+ method: 'post',
+ data: {
+ u_short_description: 'title',
+ u_description: 'desc',
+ ...(update ? { elastic_incident_id: '1' } : {}),
+ },
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(3, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident/1',
+ method: 'get',
+ });
+};
+
+describe('ServiceNow service', () => {
+ let service: ExternalService;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ service = createExternalService({
+ credentials: {
+ // The trailing slash at the end of the url is intended.
+ // All API calls need to have the trailing slash removed.
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: snExternalServiceConfig['.servicenow'],
+ axiosInstance: axios,
+ });
+ });
+
+ describe('createExternalService', () => {
+ test('throws without url', () => {
+ expect(() =>
+ createExternalService({
+ credentials: {
+ config: { apiUrl: null, isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: snExternalServiceConfig['.servicenow'],
+ axiosInstance: axios,
+ })
+ ).toThrow();
+ });
+
+ test('throws when isOAuth is false and basic auth required values are falsy', () => {
+ const badBasicCredentials = [
+ {
+ config: { apiUrl: 'test.com', isOAuth: false },
+ secrets: { username: '', password: 'admin' },
+ },
+ {
+ config: { apiUrl: 'test.com', isOAuth: false },
+ secrets: { username: null, password: 'admin' },
+ },
+ {
+ config: { apiUrl: 'test.com', isOAuth: false },
+ secrets: { password: 'admin' },
+ },
+ {
+ config: { apiUrl: 'test.com', isOAuth: false },
+ secrets: { username: 'admin', password: '' },
+ },
+ {
+ config: { apiUrl: 'test.com', isOAuth: false },
+ secrets: { username: 'admin', password: null },
+ },
+ {
+ config: { apiUrl: 'test.com', isOAuth: false },
+ secrets: { username: 'admin' },
+ },
+ ];
+
+ badBasicCredentials.forEach((badCredentials) => {
+ expect(() =>
+ createExternalService({
+ credentials: badCredentials,
+ logger,
+ configurationUtilities,
+ serviceConfig: snExternalServiceConfig['.servicenow'],
+ axiosInstance: axios,
+ })
+ ).toThrow();
+ });
+ });
+
+ test('throws when isOAuth is true and OAuth required values are falsy', () => {
+ const badOAuthCredentials = [
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: '',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: null,
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: '',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: null,
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: '',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: null,
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: '', privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: null, privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { privateKey: 'privateKey' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: '' },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret', privateKey: null },
+ },
+ {
+ config: {
+ apiUrl: 'test.com',
+ isOAuth: true,
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'user@email.com',
+ },
+ secrets: { clientSecret: 'clientSecret' },
+ },
+ ];
+
+ badOAuthCredentials.forEach((badCredentials) => {
+ expect(() =>
+ createExternalService({
+ credentials: badCredentials,
+ logger,
+ configurationUtilities,
+ serviceConfig: snExternalServiceConfig['.servicenow'],
+ axiosInstance: axios,
+ })
+ ).toThrow();
+ });
+ });
+ });
+
+ describe('getIncident', () => {
+ test('it returns the incident correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { result: { sys_id: '1', number: 'INC01' } },
+ }));
+ const res = await service.getIncident('1');
+ expect(res).toEqual({ sys_id: '1', number: 'INC01' });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { result: { sys_id: '1', number: 'INC01' } },
+ }));
+
+ await service.getIncident('1');
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident/1',
+ method: 'get',
+ });
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
+ axiosInstance: axios,
+ });
+
+ requestMock.mockImplementation(() => ({
+ data: { result: { sys_id: '1', number: 'INC01' } },
+ }));
+
+ await service.getIncident('1');
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'get',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ await expect(service.getIncident('1')).rejects.toThrow(
+ 'Unable to get incident with id 1. Error: An error has occurred'
+ );
+ });
+
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(service.getIncident('1')).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
+ });
+ });
+
+ describe('createIncident', () => {
+ // new connectors
+ describe('import set table', () => {
+ test('it creates the incident correctly', async () => {
+ const res = await createIncident(service);
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ await createIncident(service);
+ expect(requestMock).toHaveBeenCalledTimes(3);
+ expectImportedIncident(false);
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: snExternalServiceConfig['.servicenow-sir'],
+ axiosInstance: axios,
+ });
+
+ const res = await createIncident(service);
+
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
+ method: 'get',
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
+ method: 'post',
+ data: { u_short_description: 'title', u_description: 'desc' },
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(3, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'get',
+ });
+
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
+ });
+
+ test('it should throw an error when the application is not installed', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(
+ service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to create incident. Error: [Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown: errorResponse was null Reason: unknown: errorResponse was null'
+ );
+ });
+
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(
+ service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
+ });
+
+ test('it should throw an error when there is an import set api error', async () => {
+ requestMock.mockImplementation(() => ({ data: getImportSetAPIError() }));
+ await expect(
+ service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred while importing the incident Reason: unknown'
+ );
+ });
+ });
+
+ // old connectors
+ describe('table API', () => {
+ beforeEach(() => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
+ axiosInstance: axios,
+ });
+ });
+
+ test('it creates the incident correctly', async () => {
+ mockIncidentResponse(false);
+ const res = await service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
+
+ expect(requestMock).toHaveBeenCalledTimes(2);
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident',
+ method: 'post',
+ data: { short_description: 'title', description: 'desc' },
+ });
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
+ axiosInstance: axios,
+ });
+
+ mockIncidentResponse(false);
+
+ const res = await service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident',
+ method: 'post',
+ data: { short_description: 'title', description: 'desc' },
+ });
+
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
+ });
+ });
+ });
+
+ describe('updateIncident', () => {
+ // new connectors
+ describe('import set table', () => {
+ test('it updates the incident correctly', async () => {
+ const res = await updateIncident(service);
+
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ await updateIncident(service);
+ expectImportedIncident(true);
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: snExternalServiceConfig['.servicenow-sir'],
+ axiosInstance: axios,
+ });
+
+ const res = await updateIncident(service);
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
+ method: 'get',
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
+ method: 'post',
+ data: { u_short_description: 'title', u_description: 'desc', elastic_incident_id: '1' },
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(3, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'get',
+ });
+
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
+ });
+
+ test('it should throw an error when the application is not installed', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to update incident with id 1. Error: [Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown: errorResponse was null Reason: unknown: errorResponse was null'
+ );
+ });
+
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
+ });
+
+ test('it should throw an error when there is an import set api error', async () => {
+ requestMock.mockImplementation(() => ({ data: getImportSetAPIError() }));
+ await expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred while importing the incident Reason: unknown'
+ );
+ });
+ });
+
+ // old connectors
+ describe('table API', () => {
+ beforeEach(() => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
+ axiosInstance: axios,
+ });
+ });
+
+ test('it updates the incident correctly', async () => {
+ mockIncidentResponse(true);
+ const res = await service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
+
+ expect(requestMock).toHaveBeenCalledTimes(2);
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident/1',
+ method: 'patch',
+ data: { short_description: 'title', description: 'desc' },
+ });
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
+ axiosInstance: axios,
+ });
+
+ mockIncidentResponse(false);
+
+ const res = await service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'patch',
+ data: { short_description: 'title', description: 'desc' },
+ });
+
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
+ });
+ });
+ });
+
+ describe('getFields', () => {
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { result: serviceNowCommonFields },
+ }));
+ await service.getFields();
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
+ });
+ });
+
+ test('it returns common fields correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { result: serviceNowCommonFields },
+ }));
+ const res = await service.getFields();
+ expect(res).toEqual(serviceNowCommonFields);
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
+ axiosInstance: axios,
+ });
+
+ requestMock.mockImplementation(() => ({
+ data: { result: serviceNowCommonFields },
+ }));
+ await service.getFields();
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ await expect(service.getFields()).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to get fields. Error: An error has occurred'
+ );
+ });
+
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(service.getIncident('1')).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
+ });
+ });
+
+ describe('getChoices', () => {
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { result: serviceNowChoices },
+ }));
+ await service.getChoices(['priority', 'category']);
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category^language=en&sysparm_fields=label,value,dependent_value,element',
+ });
+ });
+
+ test('it returns common fields correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { result: serviceNowChoices },
+ }));
+ const res = await service.getChoices(['priority']);
+ expect(res).toEqual(serviceNowChoices);
+ });
+
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
+ axiosInstance: axios,
+ });
+
+ requestMock.mockImplementation(() => ({
+ data: { result: serviceNowChoices },
+ }));
+
+ await service.getChoices(['priority', 'category']);
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category^language=en&sysparm_fields=label,value,dependent_value,element',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ await expect(service.getChoices(['priority'])).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to get choices. Error: An error has occurred'
+ );
+ });
+
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(service.getIncident('1')).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
+ });
+ });
+
+ describe('getUrl', () => {
+ test('it returns the instance url', async () => {
+ expect(service.getUrl()).toBe('https://example.com');
+ });
+ });
+
+ describe('checkInstance', () => {
+ test('it throws an error if there is no result on data', () => {
+ const res = { status: 200, data: {} } as AxiosResponse;
+ expect(() => service.checkInstance(res)).toThrow();
+ });
+
+ test('it does NOT throws an error if the status > 400', () => {
+ const res = { status: 500, data: {} } as AxiosResponse;
+ expect(() => service.checkInstance(res)).not.toThrow();
+ });
+
+ test('it shows the servername', () => {
+ const res = {
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'https://example.com' } },
+ } as AxiosResponse;
+ expect(() => service.checkInstance(res)).toThrow(
+ 'There is an issue with your Service Now Instance. Please check https://example.com.'
+ );
+ });
+
+ describe('getApplicationInformation', () => {
+ test('it returns the application information', async () => {
+ mockApplicationVersion();
+ const res = await service.getApplicationInformation();
+ expect(res).toEqual({
+ name: 'Elastic',
+ scope: 'x_elas2_inc_int',
+ version: '1.0.0',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ await expect(service.getApplicationInformation()).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown'
+ );
+ });
+ });
+
+ describe('checkIfApplicationIsInstalled', () => {
+ test('it logs the application information', async () => {
+ mockApplicationVersion();
+ await service.checkIfApplicationIsInstalled();
+ expect(logger.debug).toHaveBeenCalledWith(
+ 'Create incident: Application scope: x_elas2_inc_int: Application version1.0.0'
+ );
+ });
+
+ test('it does not log if useOldApi = true', async () => {
+ service = createExternalService({
+ credentials: {
+ config: { apiUrl: 'https://example.com/', isOAuth: false },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
+ axiosInstance: axios,
+ });
+ await service.checkIfApplicationIsInstalled();
+ expect(requestMock).not.toHaveBeenCalled();
+ expect(logger.debug).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/bulk_actions/translations.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/service.ts
similarity index 55%
rename from x-pack/plugins/cases/public/components/bulk_actions/translations.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/service.ts
index c5bc5d7cde66b..f4ac61632722a 100644
--- a/x-pack/plugins/cases/public/components/bulk_actions/translations.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/service.ts
@@ -5,11 +5,6 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
+export const SYS_DICTIONARY_ENDPOINT = `api/now/table/sys_dictionary`;
-export const BULK_ACTION_DELETE_SELECTED = i18n.translate(
- 'xpack.cases.caseTable.bulkActions.deleteSelectedTitle',
- {
- defaultMessage: 'Delete selected',
- }
-);
+export { createExternalService } from '../../lib/servicenow/service';
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_sir.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/api.test.ts
similarity index 96%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_sir.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/api.test.ts
index 0eab1bf7fe089..5947b317c9e27 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_sir.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/api.test.ts
@@ -6,9 +6,9 @@
*/
import { Logger } from '@kbn/core/server';
-import { externalServiceSIRMock, sirParams } from './mocks';
-import { ExternalServiceSIR, ObservableTypes } from './types';
-import { apiSIR, combineObservables, formatObservables, prepareParams } from './api_sir';
+import { externalServiceSIRMock, sirParams } from '../../lib/servicenow/mocks';
+import { ExternalServiceSIR, ObservableTypes } from '../../lib/servicenow/types';
+import { api, combineObservables, formatObservables, prepareParams } from './api';
let mockedLogger: jest.Mocked;
describe('api_sir', () => {
@@ -189,7 +189,7 @@ describe('api_sir', () => {
describe('pushToService', () => {
test('it creates an incident correctly', async () => {
const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } };
- const res = await apiSIR.pushToService({
+ const res = await api.pushToService({
externalService,
params,
config: { usesTableApi: false },
@@ -218,7 +218,7 @@ describe('api_sir', () => {
test('it adds observables correctly', async () => {
const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } };
- await apiSIR.pushToService({
+ await api.pushToService({
externalService,
params,
config: { usesTableApi: false },
@@ -246,7 +246,7 @@ describe('api_sir', () => {
test('it does not call bulkAddObservableToIncident if the connector uses the old API', async () => {
const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } };
- await apiSIR.pushToService({
+ await api.pushToService({
externalService,
params,
config: { usesTableApi: true },
@@ -271,7 +271,7 @@ describe('api_sir', () => {
},
};
- await apiSIR.pushToService({
+ await api.pushToService({
externalService,
params,
config: { usesTableApi: false },
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_sir.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/api.ts
similarity index 95%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_sir.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/api.ts
index 4e74d79c6f4a0..3d81f4782b94b 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_sir.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/api.ts
@@ -6,7 +6,7 @@
*/
import { isEmpty, isString } from 'lodash';
-
+import { api as commonApi } from '../../lib/servicenow/api';
import {
ExecutorSubActionPushParamsSIR,
ExternalServiceAPI,
@@ -15,9 +15,7 @@ import {
PushToServiceApiHandlerArgs,
PushToServiceApiParamsSIR,
PushToServiceResponse,
-} from './types';
-
-import { api } from './api';
+} from '../../lib/servicenow/types';
const SPLIT_REGEX = /[ ,|\r\n\t]+/;
@@ -106,7 +104,7 @@ const pushToServiceHandler = async ({
commentFieldKey,
logger,
}: PushToServiceApiHandlerArgs): Promise => {
- const res = await api.pushToService({
+ const res = await commonApi.pushToService({
externalService,
params: prepareParams(!!config.usesTableApi, params as PushToServiceApiParamsSIR),
config,
@@ -148,7 +146,7 @@ const pushToServiceHandler = async ({
return res;
};
-export const apiSIR: ExternalServiceAPI = {
- ...api,
+export const api: ExternalServiceAPI = {
+ ...commonApi,
pushToService: pushToServiceHandler,
};
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts
new file mode 100644
index 0000000000000..3fff7ae0e389d
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { Logger } from '@kbn/core/server';
+import { loggerMock } from '@kbn/logging-mocks';
+import { actionsMock } from '@kbn/actions-plugin/server/mocks';
+import { ExecutorParams, ServiceNowPublicConfigurationType } from '../../lib/servicenow/types';
+import {
+ ServiceNowConnectorType,
+ ServiceNowConnectorTypeExecutorOptions,
+ getServiceNowSIRConnectorType,
+} from '.';
+import { api } from './api';
+
+jest.mock('./api', () => ({
+ api: {
+ getChoices: jest.fn(),
+ getFields: jest.fn(),
+ getIncident: jest.fn(),
+ handshake: jest.fn(),
+ pushToService: jest.fn(),
+ },
+}));
+
+const services = actionsMock.createServices();
+const mockedLogger: jest.Mocked = loggerMock.create();
+
+describe('ServiceNow', () => {
+ const config = { apiUrl: 'https://instance.com' };
+ const secrets = { username: 'username', password: 'password' };
+ const params = {
+ subAction: 'pushToService',
+ subActionParams: {
+ incident: {
+ short_description: 'An incident',
+ description: 'This is serious',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ (api.pushToService as jest.Mock).mockResolvedValue({ id: 'some-id' });
+ });
+
+ describe('ServiceNow SIR', () => {
+ let connectorType: ServiceNowConnectorType;
+
+ beforeAll(() => {
+ connectorType = getServiceNowSIRConnectorType({
+ logger: mockedLogger,
+ });
+ });
+
+ describe('execute()', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it pass the correct comment field key', async () => {
+ const actionId = 'some-action-id';
+ const executorOptions = {
+ actionId,
+ config,
+ secrets,
+ params,
+ services,
+ } as unknown as ServiceNowConnectorTypeExecutorOptions<
+ ServiceNowPublicConfigurationType,
+ ExecutorParams
+ >;
+ await connectorType.executor(executorOptions);
+ expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe(
+ 'work_notes'
+ );
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts
new file mode 100644
index 0000000000000..16db999af8a1e
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts
@@ -0,0 +1,182 @@
+/*
+ * 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 { curry } from 'lodash';
+import { TypeOf } from '@kbn/config-schema';
+
+import { Logger } from '@kbn/core/server';
+import type {
+ ActionType as ConnectorType,
+ ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
+ ActionTypeExecutorResult as ConnectorTypeExecutorResult,
+} from '@kbn/actions-plugin/server/types';
+import {
+ AlertingConnectorFeatureId,
+ CasesConnectorFeatureId,
+ SecurityConnectorFeatureId,
+} from '@kbn/actions-plugin/common/types';
+import { validate } from '../../lib/servicenow/validators';
+import {
+ ExecutorParamsSchemaSIR,
+ ExternalIncidentServiceConfigurationSchema,
+ ExternalIncidentServiceSecretConfigurationSchema,
+} from '../../lib/servicenow/schema';
+import * as i18n from '../../lib/servicenow/translations';
+import {
+ ExecutorParams,
+ ExecutorSubActionPushParams,
+ ServiceFactory,
+ ExternalServiceAPI,
+ ServiceNowPublicConfigurationBaseType,
+ ExternalService,
+ ExecutorSubActionCommonFieldsParams,
+ ExecutorSubActionGetChoicesParams,
+ PushToServiceResponse,
+ ServiceNowExecutorResultData,
+ ServiceNowPublicConfigurationType,
+ ServiceNowSecretConfigurationType,
+} from '../../lib/servicenow/types';
+import {
+ ServiceNowSIRConnectorTypeId,
+ serviceNowSIRTable,
+ snExternalServiceConfig,
+} from '../../lib/servicenow/config';
+import { createExternalService } from './service';
+import { api as apiSIR } from './api';
+import { throwIfSubActionIsNotSupported } from '../../lib/servicenow/utils';
+import { createServiceWrapper } from '../../lib/servicenow/create_service_wrapper';
+
+export { ServiceNowSIRConnectorTypeId, serviceNowSIRTable };
+
+export type ActionParamsType = TypeOf;
+
+interface GetConnectorTypeParams {
+ logger: Logger;
+}
+
+export type ServiceNowConnectorType<
+ C extends Record = ServiceNowPublicConfigurationBaseType,
+ T extends Record = ExecutorParams
+> = ConnectorType;
+
+export type ServiceNowConnectorTypeExecutorOptions<
+ C extends Record = ServiceNowPublicConfigurationBaseType,
+ T extends Record = ExecutorParams
+> = ConnectorTypeExecutorOptions;
+
+// connector type definition
+export function getServiceNowSIRConnectorType(
+ params: GetConnectorTypeParams
+): ServiceNowConnectorType {
+ const { logger } = params;
+ return {
+ id: ServiceNowSIRConnectorTypeId,
+ minimumLicenseRequired: 'platinum',
+ name: i18n.SERVICENOW_SIR,
+ supportedFeatureIds: [
+ AlertingConnectorFeatureId,
+ CasesConnectorFeatureId,
+ SecurityConnectorFeatureId,
+ ],
+ validate: {
+ config: {
+ schema: ExternalIncidentServiceConfigurationSchema,
+ customValidator: validate.config,
+ },
+ secrets: {
+ schema: ExternalIncidentServiceSecretConfigurationSchema,
+ customValidator: validate.secrets,
+ },
+ connector: validate.connector,
+ params: {
+ schema: ExecutorParamsSchemaSIR,
+ },
+ },
+ executor: curry(executor)({
+ logger,
+ actionTypeId: ServiceNowSIRConnectorTypeId,
+ createService: createExternalService,
+ api: apiSIR,
+ }),
+ };
+}
+
+// action executor
+const supportedSubActions: string[] = ['getFields', 'pushToService', 'getChoices', 'getIncident'];
+async function executor(
+ {
+ logger,
+ actionTypeId,
+ createService,
+ api,
+ }: {
+ logger: Logger;
+ actionTypeId: string;
+ createService: ServiceFactory;
+ api: ExternalServiceAPI;
+ },
+ execOptions: ServiceNowConnectorTypeExecutorOptions<
+ ServiceNowPublicConfigurationType,
+ ExecutorParams
+ >
+): Promise> {
+ const { actionId, config, params, secrets, services, configurationUtilities } = execOptions;
+ const { subAction, subActionParams } = params;
+ const connectorTokenClient = services.connectorTokenClient;
+ const externalServiceConfig = snExternalServiceConfig[actionTypeId];
+ let data: ServiceNowExecutorResultData | null = null;
+
+ const externalService = createServiceWrapper({
+ connectorId: actionId,
+ credentials: {
+ config,
+ secrets,
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: externalServiceConfig,
+ connectorTokenClient,
+ createServiceFn: createService,
+ });
+
+ const apiAsRecord = api as unknown as Record;
+ throwIfSubActionIsNotSupported({ api: apiAsRecord, subAction, supportedSubActions, logger });
+
+ if (subAction === 'pushToService') {
+ const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
+ data = await api.pushToService({
+ externalService,
+ params: pushToServiceParams,
+ config,
+ secrets,
+ logger,
+ commentFieldKey: externalServiceConfig.commentFieldKey,
+ });
+
+ logger.debug(`response push to service for incident id: ${data.id}`);
+ }
+
+ if (subAction === 'getFields') {
+ const getFieldsParams = subActionParams as ExecutorSubActionCommonFieldsParams;
+ data = await api.getFields({
+ externalService,
+ params: getFieldsParams,
+ logger,
+ });
+ }
+
+ if (subAction === 'getChoices') {
+ const getChoicesParams = subActionParams as ExecutorSubActionGetChoicesParams;
+ data = await api.getChoices({
+ externalService,
+ params: getChoicesParams,
+ logger,
+ });
+ }
+
+ return { status: 'ok', data: data ?? {}, actionId };
+}
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_sir.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/service.test.ts
similarity index 92%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_sir.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/service.test.ts
index 0cf30d358b47f..f8033b7e3a60d 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_sir.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/service.test.ts
@@ -7,14 +7,14 @@
import axios from 'axios';
-import { createExternalServiceSIR } from './service_sir';
+import { createExternalService } from './service';
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
-import { ExternalServiceSIR } from './types';
+import { ExternalServiceSIR } from '../../lib/servicenow/types';
import { Logger } from '@kbn/core/server';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
-import { observables } from './mocks';
-import { snExternalServiceConfig } from './config';
+import { observables } from '../../lib/servicenow/mocks';
+import { snExternalServiceConfig } from '../../lib/servicenow/config';
const logger = loggingSystemMock.create().get() as jest.Mocked;
@@ -92,7 +92,7 @@ describe('ServiceNow SIR service', () => {
let service: ExternalServiceSIR;
beforeEach(() => {
- service = createExternalServiceSIR({
+ service = createExternalService({
credentials: {
config: { apiUrl: 'https://example.com/', isOAuth: false },
secrets: { username: 'admin', password: 'admin' },
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_sir.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/service.ts
similarity index 84%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_sir.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/service.ts
index de3220c36bd4c..918710d5b8229 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_sir.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/service.ts
@@ -6,10 +6,15 @@
*/
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
-import { Observable, ExternalServiceSIR, ObservableResponse, ServiceFactory } from './types';
+import {
+ Observable,
+ ExternalServiceSIR,
+ ObservableResponse,
+ ServiceFactory,
+} from '../../lib/servicenow/types';
-import { createExternalService } from './service';
-import { createServiceError } from './utils';
+import { createExternalService as createExternalServiceCommon } from '../../lib/servicenow/service';
+import { createServiceError } from '../../lib/servicenow/utils';
const getAddObservableToIncidentURL = (url: string, incidentID: string) =>
`${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables`;
@@ -17,14 +22,14 @@ const getAddObservableToIncidentURL = (url: string, incidentID: string) =>
const getBulkAddObservableToIncidentURL = (url: string, incidentID: string) =>
`${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables/bulk`;
-export const createExternalServiceSIR: ServiceFactory = ({
+export const createExternalService: ServiceFactory = ({
credentials,
logger,
configurationUtilities,
serviceConfig,
axiosInstance,
}): ExternalServiceSIR => {
- const snService = createExternalService({
+ const snService = createExternalServiceCommon({
credentials,
logger,
configurationUtilities,
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/index.ts
index db227eb96109a..67a34dd285fe3 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/index.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/index.ts
@@ -12,19 +12,19 @@ import {
getIndexConnectorType,
getPagerDutyConnectorType,
getServerLogConnectorType,
+ getServiceNowITOMConnectorType,
getSlackConnectorType,
getTeamsConnectorType,
getWebhookConnectorType,
+ getXmattersConnectorType,
} from './stack';
import {
getCasesWebhookConnectorType,
getJiraConnectorType,
getResilientConnectorType,
- getServiceNowITOMConnectorType,
getServiceNowITSMConnectorType,
getServiceNowSIRConnectorType,
getSwimlaneConnectorType,
- getXmattersConnectorType,
} from './cases';
export type {
@@ -35,31 +35,31 @@ export type {
SlackActionParams,
TeamsActionParams,
WebhookActionParams,
+ XmattersActionParams,
} from './stack';
export {
EmailConnectorTypeId,
IndexConnectorTypeId,
PagerDutyConnectorTypeId,
+ ServiceNowITOMConnectorTypeId,
ServerLogConnectorTypeId,
SlackConnectorTypeId,
TeamsConnectorTypeId,
WebhookConnectorTypeId,
+ XmattersConnectorTypeId,
} from './stack';
export type {
CasesWebhookActionParams,
JiraActionParams,
ResilientActionParams,
ServiceNowActionParams,
- XmattersActionParams,
} from './cases';
export {
CasesWebhookConnectorTypeId,
JiraConnectorTypeId,
ResilientConnectorTypeId,
- ServiceNowITOMConnectorTypeId,
ServiceNowITSMConnectorTypeId,
ServiceNowSIRConnectorTypeId,
- XmattersConnectorTypeId,
} from './cases';
export function registerConnectorTypes({
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/api.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/api.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/api.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/api.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/config.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/config.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/config.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/config.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/config.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/config.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/config.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/config.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/create_service_wrapper.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/create_service_wrapper.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/create_service_wrapper.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/create_service_wrapper.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/create_service_wrapper.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/create_service_wrapper.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/create_service_wrapper.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/create_service_wrapper.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/mocks.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/mocks.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/mocks.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/mocks.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/schema.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/schema.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/schema.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/schema.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/service.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/service.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/service.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/service.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/translations.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/translations.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/translations.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/translations.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/types.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/types.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/types.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/types.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/utils.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/utils.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/validators.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/validators.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/validators.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/validators.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/validators.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/validators.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/validators.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/validators.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts
index 18fc54872261e..6fd7faeaf1729 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts
@@ -29,6 +29,8 @@ export {
} from './server_log';
export type { ActionParamsType as ServerLogActionParams } from './server_log';
+export { getServiceNowITOMConnectorType, ServiceNowITOMConnectorTypeId } from './servicenow_itom';
+
export {
getConnectorType as getSlackConnectorType,
ConnectorTypeId as SlackConnectorTypeId,
@@ -46,3 +48,9 @@ export {
ConnectorTypeId as WebhookConnectorTypeId,
} from './webhook';
export type { ActionParamsType as WebhookActionParams } from './webhook';
+
+export {
+ getConnectorType as getXmattersConnectorType,
+ ConnectorTypeId as XmattersConnectorTypeId,
+} from './xmatters';
+export type { ActionParamsType as XmattersActionParams } from './xmatters';
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_itom.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/api.test.ts
similarity index 87%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_itom.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/api.test.ts
index be10a10dfb819..981a8953750d9 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_itom.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/api.test.ts
@@ -6,9 +6,9 @@
*/
import { Logger } from '@kbn/core/server';
-import { externalServiceITOMMock, itomEventParams } from './mocks';
-import { ExternalServiceITOM } from './types';
-import { apiITOM, prepareParams } from './api_itom';
+import { externalServiceITOMMock, itomEventParams } from '../../lib/servicenow/mocks';
+import { ExternalServiceITOM } from '../../lib/servicenow/types';
+import { api, prepareParams } from './api';
let mockedLogger: jest.Mocked;
describe('api_itom', () => {
@@ -41,7 +41,7 @@ describe('api_itom', () => {
describe('addEvent', () => {
test('it adds an event correctly', async () => {
- await apiITOM.addEvent({
+ await api.addEvent({
externalService,
params: itomEventParams,
logger: mockedLogger,
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_itom.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/api.ts
similarity index 91%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_itom.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/api.ts
index 668e17a042718..102e8e4191667 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/api_itom.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/api.ts
@@ -5,12 +5,12 @@
* 2.0.
*/
-import { api } from './api';
+import { api as commonApi } from '../../lib/servicenow/api';
import {
ExecutorSubActionAddEventParams,
AddEventApiHandlerArgs,
ExternalServiceApiITOM,
-} from './types';
+} from '../../lib/servicenow/types';
const isValidDate = (d: Date) => !isNaN(d.valueOf());
@@ -64,7 +64,7 @@ const addEventServiceHandler = async ({
await itomExternalService.addEvent(preparedParams);
};
-export const apiITOM: ExternalServiceApiITOM = {
- getChoices: api.getChoices,
+export const api: ExternalServiceApiITOM = {
+ getChoices: commonApi.getChoices,
addEvent: addEventServiceHandler,
};
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts
new file mode 100644
index 0000000000000..c6d1c5d772899
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts
@@ -0,0 +1,163 @@
+/*
+ * 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 { curry } from 'lodash';
+
+import { Logger } from '@kbn/core/server';
+import type {
+ ActionType as ConnectorType,
+ ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
+ ActionTypeExecutorResult as ConnectorTypeExecutorResult,
+} from '@kbn/actions-plugin/server/types';
+import {
+ AlertingConnectorFeatureId,
+ SecurityConnectorFeatureId,
+} from '@kbn/actions-plugin/common/types';
+import { validate } from '../../lib/servicenow/validators';
+import {
+ ExecutorParamsSchemaITOM,
+ ExternalIncidentServiceSecretConfigurationSchema,
+ ExternalIncidentServiceConfigurationBaseSchema,
+} from '../../lib/servicenow/schema';
+import * as i18n from '../../lib/servicenow/translations';
+import {
+ ExecutorSubActionGetChoicesParams,
+ PushToServiceResponse,
+ ServiceNowExecutorResultData,
+ ServiceNowSecretConfigurationType,
+ ServiceFactory,
+ ExecutorParamsITOM,
+ ExecutorSubActionAddEventParams,
+ ExternalServiceApiITOM,
+ ExternalServiceITOM,
+ ServiceNowPublicConfigurationBaseType,
+} from '../../lib/servicenow/types';
+import {
+ ServiceNowITOMConnectorTypeId,
+ snExternalServiceConfig,
+} from '../../lib/servicenow/config';
+import { throwIfSubActionIsNotSupported } from '../../lib/servicenow/utils';
+import { createExternalService } from './service';
+import { api as apiITOM } from './api';
+import { createServiceWrapper } from '../../lib/servicenow/create_service_wrapper';
+
+export { ServiceNowITOMConnectorTypeId };
+
+interface GetConnectorTypeParams {
+ logger: Logger;
+}
+
+export type ServiceNowConnectorType<
+ C extends Record = ServiceNowPublicConfigurationBaseType,
+ T extends Record = ExecutorParamsITOM
+> = ConnectorType;
+
+export type ServiceNowConnectorTypeExecutorOptions<
+ C extends Record = ServiceNowPublicConfigurationBaseType,
+ T extends Record = ExecutorParamsITOM
+> = ConnectorTypeExecutorOptions;
+
+// connector type definition
+export function getServiceNowITOMConnectorType(
+ params: GetConnectorTypeParams
+): ServiceNowConnectorType {
+ const { logger } = params;
+ return {
+ id: ServiceNowITOMConnectorTypeId,
+ minimumLicenseRequired: 'platinum',
+ name: i18n.SERVICENOW_ITOM,
+ supportedFeatureIds: [AlertingConnectorFeatureId, SecurityConnectorFeatureId],
+ validate: {
+ config: {
+ schema: ExternalIncidentServiceConfigurationBaseSchema,
+ customValidator: validate.config,
+ },
+ secrets: {
+ schema: ExternalIncidentServiceSecretConfigurationSchema,
+ customValidator: validate.secrets,
+ },
+ connector: validate.connector,
+ params: {
+ schema: ExecutorParamsSchemaITOM,
+ },
+ },
+ executor: curry(executorITOM)({
+ logger,
+ actionTypeId: ServiceNowITOMConnectorTypeId,
+ createService: createExternalService,
+ api: apiITOM,
+ }),
+ };
+}
+
+// action executor
+const supportedSubActionsITOM = ['addEvent', 'getChoices'];
+async function executorITOM(
+ {
+ logger,
+ actionTypeId,
+ createService,
+ api,
+ }: {
+ logger: Logger;
+ actionTypeId: string;
+ createService: ServiceFactory;
+ api: ExternalServiceApiITOM;
+ },
+ execOptions: ServiceNowConnectorTypeExecutorOptions<
+ ServiceNowPublicConfigurationBaseType,
+ ExecutorParamsITOM
+ >
+): Promise> {
+ const { actionId, config, params, secrets, configurationUtilities } = execOptions;
+ const { subAction, subActionParams } = params;
+ const connectorTokenClient = execOptions.services.connectorTokenClient;
+ const externalServiceConfig = snExternalServiceConfig[actionTypeId];
+ let data: ServiceNowExecutorResultData | null = null;
+
+ const externalService = createServiceWrapper({
+ connectorId: actionId,
+ credentials: {
+ config,
+ secrets,
+ },
+ logger,
+ configurationUtilities,
+ serviceConfig: externalServiceConfig,
+ connectorTokenClient,
+ createServiceFn: createService,
+ });
+
+ const apiAsRecord = api as unknown as Record;
+
+ throwIfSubActionIsNotSupported({
+ api: apiAsRecord,
+ subAction,
+ supportedSubActions: supportedSubActionsITOM,
+ logger,
+ });
+
+ if (subAction === 'addEvent') {
+ const eventParams = subActionParams as ExecutorSubActionAddEventParams;
+ await api.addEvent({
+ externalService,
+ params: eventParams,
+ logger,
+ });
+ }
+
+ if (subAction === 'getChoices') {
+ const getChoicesParams = subActionParams as ExecutorSubActionGetChoicesParams;
+ data = await api.getChoices({
+ externalService,
+ params: getChoicesParams,
+ logger,
+ });
+ }
+
+ return { status: 'ok', data: data ?? {}, actionId };
+}
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_itom.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/service.test.ts
similarity index 89%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_itom.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/service.test.ts
index ddc347f49dc76..13d03df0cd972 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_itom.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/service.test.ts
@@ -7,14 +7,14 @@
import axios from 'axios';
-import { createExternalServiceITOM } from './service_itom';
+import { createExternalService } from './service';
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
-import { ExternalServiceITOM } from './types';
+import { ExternalServiceITOM } from '../../lib/servicenow/types';
import { Logger } from '@kbn/core/server';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
-import { snExternalServiceConfig } from './config';
-import { itomEventParams, serviceNowChoices } from './mocks';
+import { snExternalServiceConfig } from '../../lib/servicenow/config';
+import { itomEventParams, serviceNowChoices } from '../../lib/servicenow/mocks';
const logger = loggingSystemMock.create().get() as jest.Mocked;
@@ -35,7 +35,7 @@ describe('ServiceNow SIR service', () => {
let service: ExternalServiceITOM;
beforeEach(() => {
- service = createExternalServiceITOM({
+ service = createExternalService({
credentials: {
config: { apiUrl: 'https://example.com/', isOAuth: false },
secrets: { username: 'admin', password: 'admin' },
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_itom.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/service.ts
similarity index 73%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_itom.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/service.ts
index 74e76e4aedc1d..74d704a116016 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow/service_itom.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/service.ts
@@ -6,21 +6,25 @@
*/
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
-import { ServiceFactory, ExternalServiceITOM, ExecutorSubActionAddEventParams } from './types';
+import {
+ ServiceFactory,
+ ExternalServiceITOM,
+ ExecutorSubActionAddEventParams,
+} from '../../lib/servicenow/types';
-import { createExternalService } from './service';
-import { createServiceError } from './utils';
+import { createExternalService as createExternalServiceCommon } from '../../lib/servicenow/service';
+import { createServiceError } from '../../lib/servicenow/utils';
const getAddEventURL = (url: string) => `${url}/api/global/em/jsonv2`;
-export const createExternalServiceITOM: ServiceFactory = ({
+export const createExternalService: ServiceFactory = ({
credentials,
logger,
configurationUtilities,
serviceConfig,
axiosInstance,
}): ExternalServiceITOM => {
- const snService = createExternalService({
+ const snService = createExternalServiceCommon({
credentials,
logger,
configurationUtilities,
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/xmatters/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.test.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/xmatters/index.test.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.test.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/xmatters/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/xmatters/index.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.ts
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/xmatters/post_xmatters.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/post_xmatters.ts
similarity index 100%
rename from x-pack/plugins/stack_connectors/server/connector_types/cases/xmatters/post_xmatters.ts
rename to x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/post_xmatters.ts
diff --git a/x-pack/plugins/stack_connectors/server/types.ts b/x-pack/plugins/stack_connectors/server/types.ts
index 01f7768bfac10..1da81e3dc5436 100644
--- a/x-pack/plugins/stack_connectors/server/types.ts
+++ b/x-pack/plugins/stack_connectors/server/types.ts
@@ -6,7 +6,7 @@
*/
export type { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './connector_types/cases/jira/types';
-export type { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './connector_types/cases/servicenow/types';
+export type { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './connector_types/lib/servicenow/types';
export type { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './connector_types/cases/resilient/types';
export type { SwimlanePublicConfigurationType } from './connector_types/cases/swimlane/types';
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx
index c1da7e0035097..89fb255dc80ef 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx
@@ -18,8 +18,8 @@ import { formatTestDuration } from '../../../utils/monitor_test_result/test_time
export const SinglePingResult = ({ ping, loading }: { ping: Ping; loading: boolean }) => {
const ip = !loading ? ping?.resolve?.ip : undefined;
- const durationUs = !loading ? ping?.monitor?.duration?.us : undefined;
- const rtt = !loading ? ping?.resolve?.rtt?.us : undefined;
+ const durationUs = !loading ? ping?.monitor?.duration?.us ?? NaN : NaN;
+ const rtt = !loading ? ping?.resolve?.rtt?.us ?? NaN : NaN;
const url = !loading ? ping?.url?.full : undefined;
const responseStatus = !loading ? ping?.http?.response?.status_code : undefined;
@@ -29,10 +29,12 @@ export const SinglePingResult = ({ ping, loading }: { ping: Ping; loading: boole
{ip}
{DURATION_LABEL}
- {formatTestDuration(durationUs)}
+ {isNaN(durationUs) ? '' : formatTestDuration(durationUs)}
rtt
- {formatTestDuration(rtt)}
+
+ {isNaN(rtt) ? '' : formatTestDuration(rtt)}
+
URL
{url}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_ten_test_runs.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_ten_test_runs.tsx
index 85adcd7ff3c0c..fc01a5d9164ee 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_ten_test_runs.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_ten_test_runs.tsx
@@ -31,7 +31,6 @@ import {
import { useSyntheticsSettingsContext } from '../../../contexts/synthetics_settings_context';
import { sortPings } from '../../../utils/monitor_test_result/sort_pings';
-import { checkIsStalePing } from '../../../utils/monitor_test_result/check_pings';
import { selectPingsLoading, selectMonitorRecentPings, selectPingsError } from '../../../state';
import { parseBadgeStatus, StatusBadge } from '../../common/monitor_test_result/status_badge';
import { isStepEnd } from '../../common/monitor_test_result/browser_steps_list';
@@ -56,8 +55,6 @@ export const LastTenTestRuns = () => {
const { monitor } = useSelectedMonitor();
const isBrowserMonitor = monitor?.[ConfigKey.MONITOR_TYPE] === DataStream.BROWSER;
- const hasStalePings = checkIsStalePing(monitor, pings?.[0]);
- const loading = hasStalePings || pingsLoading;
const sorting: EuiTableSortingType = {
sort: {
@@ -146,12 +143,12 @@ export const LastTenTestRuns = () => {
{
latestPing?.monitor?.check_group
);
- const hasStalePings = checkIsStalePing(monitor, latestPing);
- const loading = hasStalePings || stepsLoading || pingsLoading;
+ const loading = stepsLoading || pingsLoading;
return (
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts
index b1fb95d5d5ee4..1c9df0c866ad2 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts
@@ -6,13 +6,16 @@
*/
import { createReducer } from '@reduxjs/toolkit';
+import { EncryptedSyntheticsSavedMonitor, Ping } from '../../../../../common/runtime_types';
+import { checkIsStalePing } from '../../utils/monitor_test_result/check_pings';
+
import { IHttpSerializedFetchError } from '../utils/http_error';
+
import {
getMonitorRecentPingsAction,
setMonitorDetailsLocationAction,
getMonitorAction,
} from './actions';
-import { EncryptedSyntheticsSavedMonitor, Ping } from '../../../../../common/runtime_types';
export interface MonitorDetailsState {
pings: Ping[];
@@ -38,8 +41,9 @@ export const monitorDetailsReducer = createReducer(initialState, (builder) => {
state.selectedLocationId = action.payload;
})
- .addCase(getMonitorRecentPingsAction.get, (state) => {
+ .addCase(getMonitorRecentPingsAction.get, (state, action) => {
state.loading = true;
+ state.pings = state.pings.filter((ping) => !checkIsStalePing(action.payload.monitorId, ping));
})
.addCase(getMonitorRecentPingsAction.success, (state, action) => {
state.pings = action.payload.pings;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/check_pings.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/check_pings.ts
index 043aefbac819b..233d80ea6b7da 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/check_pings.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/check_pings.ts
@@ -5,18 +5,15 @@
* 2.0.
*/
-import { EncryptedSyntheticsSavedMonitor, Ping } from '../../../../../common/runtime_types';
+import { Ping } from '../../../../../common/runtime_types';
/**
* Checks if the loaded/cached pings are of the current selected monitors
*/
-export function checkIsStalePing(
- monitor: EncryptedSyntheticsSavedMonitor | null,
- ping: Ping | undefined
-) {
- if (!monitor?.id || !ping?.monitor?.id) {
+export function checkIsStalePing(monitorOrConfigId: string | undefined, ping: Ping | undefined) {
+ if (!monitorOrConfigId || !ping?.monitor?.id) {
return true;
}
- return monitor.id !== ping.monitor.id && monitor.id !== ping.config_id;
+ return monitorOrConfigId !== ping.monitor.id && monitorOrConfigId !== ping.config_id;
}
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx
index 3f675597301f3..6dbef12159a18 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx
@@ -14,8 +14,7 @@ import {
} from './alert_monitor_status';
import { render } from '../../../../lib/helper/rtl_helpers';
-// FLAKY: https://github.com/elastic/kibana/issues/133226
-describe.skip('alert monitor status component', () => {
+describe('alert monitor status component', () => {
jest.setTimeout(10_000);
describe('hasFilters', () => {
diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts
index 93b47e9467be0..f447a21c3bee7 100644
--- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts
@@ -160,12 +160,11 @@ export class ServiceAPIClient {
stack_version: this.kibanaVersion,
is_edit: isEdit,
},
- headers:
- process.env.NODE_ENV !== 'production' && this.authorization
- ? {
- Authorization: this.authorization,
- }
- : undefined,
+ headers: this.authorization
+ ? {
+ Authorization: this.authorization,
+ }
+ : undefined,
httpsAgent: this.getHttpsAgent(url),
})
);
diff --git a/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
index c5d67894aa0ff..17c4d79e60dcc 100644
--- a/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
+++ b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
@@ -6,30 +6,31 @@
*/
import {
+ ADD_INTEGRATIONS_BUTTON,
+ BREADCRUMBS,
DEFAULT_LAYOUT_TITLE,
- FLYOUT_JSON,
- FLYOUT_TABLE,
- FLYOUT_TABS,
- FLYOUT_TITLE,
- INDICATORS_TABLE,
- TOGGLE_FLYOUT_BUTTON,
- FILTERS_GLOBAL_CONTAINER,
- TIME_RANGE_PICKER,
- QUERY_INPUT,
- TABLE_CONTROLS,
- INDICATOR_TYPE_CELL,
EMPTY_STATE,
- FIELD_SELECTOR,
- BREADCRUMBS,
- LEADING_BREADCRUMB,
ENDING_BREADCRUMB,
FIELD_BROWSER,
FIELD_BROWSER_MODAL,
- FIELD_SELECTOR_TOGGLE_BUTTON,
+ FIELD_SELECTOR,
FIELD_SELECTOR_INPUT,
FIELD_SELECTOR_LIST,
+ FIELD_SELECTOR_TOGGLE_BUTTON,
+ FILTERS_GLOBAL_CONTAINER,
+ FLYOUT_JSON,
+ FLYOUT_TABLE,
+ FLYOUT_TABS,
+ FLYOUT_TITLE,
+ INDICATOR_TYPE_CELL,
+ INDICATORS_TABLE,
INSPECTOR_BUTTON,
INSPECTOR_PANEL,
+ LEADING_BREADCRUMB,
+ QUERY_INPUT,
+ TABLE_CONTROLS,
+ TIME_RANGE_PICKER,
+ TOGGLE_FLYOUT_BUTTON,
} from '../screens/indicators';
import { login } from '../tasks/login';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
@@ -189,4 +190,20 @@ describe('Indicators', () => {
});
});
});
+
+ describe('Add integrations', () => {
+ before(() => {
+ cy.visit(THREAT_INTELLIGENCE);
+
+ selectRange();
+ });
+
+ describe('when the global header add integrations button is clicked', () => {
+ it('should navigate to the Integrations page with Threat Intelligence category selected', () => {
+ cy.get(ADD_INTEGRATIONS_BUTTON).click();
+
+ cy.url().should('include', 'threat_intel');
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
index 0464e57c6749b..9e32e8843c4c3 100644
--- a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
+++ b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
@@ -117,3 +117,5 @@ export const INDICATOR_FLYOUT_INVESTIGATE_IN_TIMELINE_BUTTON =
export const INSPECTOR_BUTTON = '[data-test-subj="tiIndicatorsGridInspect"]';
export const INSPECTOR_PANEL = '[data-test-subj="inspectorPanel"]';
+
+export const ADD_INTEGRATIONS_BUTTON = '[data-test-subj="add-data"]';
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 5076646708475..8a1fda344a013 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -9314,10 +9314,7 @@
"xpack.cases.casesStats.mttr": "Temps moyen avant fermeture",
"xpack.cases.casesStats.mttrDescription": "La durée moyenne (de la création à la clôture) de vos cas en cours",
"xpack.cases.caseTable.bulkActions": "Actions groupées",
- "xpack.cases.caseTable.bulkActions.closeSelectedTitle": "Fermer la sélection",
"xpack.cases.caseTable.bulkActions.deleteSelectedTitle": "Supprimer la sélection",
- "xpack.cases.caseTable.bulkActions.markInProgressTitle": "Marquer comme étant en cours",
- "xpack.cases.caseTable.bulkActions.openSelectedTitle": "Ouvrir la sélection",
"xpack.cases.caseTable.changeStatus": "Modifier le statut",
"xpack.cases.caseTable.closed": "Fermé",
"xpack.cases.caseTable.closedCases": "Cas fermés",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index be84b953d2d01..79f3d61efeae0 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9301,10 +9301,7 @@
"xpack.cases.casesStats.mttr": "クローズまでの平均時間",
"xpack.cases.casesStats.mttrDescription": "現在のアセットの平均期間(作成から終了まで)",
"xpack.cases.caseTable.bulkActions": "一斉アクション",
- "xpack.cases.caseTable.bulkActions.closeSelectedTitle": "選択した項目を閉じる",
"xpack.cases.caseTable.bulkActions.deleteSelectedTitle": "選択した項目を削除",
- "xpack.cases.caseTable.bulkActions.markInProgressTitle": "実行中に設定",
- "xpack.cases.caseTable.bulkActions.openSelectedTitle": "選択した項目を開く",
"xpack.cases.caseTable.changeStatus": "ステータスの変更",
"xpack.cases.caseTable.closed": "終了",
"xpack.cases.caseTable.closedCases": "終了したケース",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index bfcc8fdf4108f..d0852f4c84387 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9319,10 +9319,7 @@
"xpack.cases.casesStats.mttr": "平均关闭时间",
"xpack.cases.casesStats.mttrDescription": "当前案例的平均持续时间(从创建到关闭)",
"xpack.cases.caseTable.bulkActions": "批处理操作",
- "xpack.cases.caseTable.bulkActions.closeSelectedTitle": "关闭所选",
"xpack.cases.caseTable.bulkActions.deleteSelectedTitle": "删除所选",
- "xpack.cases.caseTable.bulkActions.markInProgressTitle": "标记为进行中",
- "xpack.cases.caseTable.bulkActions.openSelectedTitle": "打开所选",
"xpack.cases.caseTable.changeStatus": "更改状态",
"xpack.cases.caseTable.closed": "已关闭",
"xpack.cases.caseTable.closedCases": "已关闭案例",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx
index 76804970ebee5..0055349deb8ff 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx
@@ -266,7 +266,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab
/>
)}
- {alertsCount >= 0 && (
+ {alertsCount > 0 && (
{
+ describe('disable', () => {
const objectRemover = new ObjectRemover(supertestWithoutAuth);
const ruleUtils = new RuleUtils({ space: Spaces.space1, supertestWithoutAuth });
@@ -52,15 +51,17 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex
await ruleUtils.disable(createdRule.id);
// task doc should still exist but be disabled
- const taskRecord = await getScheduledTask(createdRule.scheduled_task_id);
- expect(taskRecord.type).to.eql('task');
- expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord.task.params)).to.eql({
- alertId: createdRule.id,
- spaceId: Spaces.space1.id,
- consumer: 'alertsFixture',
+ await retry.try(async () => {
+ const taskRecord = await getScheduledTask(createdRule.scheduled_task_id);
+ expect(taskRecord.type).to.eql('task');
+ expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord.task.params)).to.eql({
+ alertId: createdRule.id,
+ spaceId: Spaces.space1.id,
+ consumer: 'alertsFixture',
+ });
+ expect(taskRecord.task.enabled).to.eql(false);
});
- expect(taskRecord.task.enabled).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@@ -196,15 +197,17 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex
.expect(204);
// task doc should still exist but be disabled
- const taskRecord = await getScheduledTask(createdRule.scheduled_task_id);
- expect(taskRecord.type).to.eql('task');
- expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord.task.params)).to.eql({
- alertId: createdRule.id,
- spaceId: Spaces.space1.id,
- consumer: 'alertsFixture',
+ await retry.try(async () => {
+ const taskRecord = await getScheduledTask(createdRule.scheduled_task_id);
+ expect(taskRecord.type).to.eql('task');
+ expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord.task.params)).to.eql({
+ alertId: createdRule.id,
+ spaceId: Spaces.space1.id,
+ consumer: 'alertsFixture',
+ });
+ expect(taskRecord.task.enabled).to.eql(false);
});
- expect(taskRecord.task.enabled).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts
index f32665a5a1fac..bba958d47d241 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts
@@ -18,8 +18,7 @@ export default function createRunSoonTests({ getService }: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
- // Failing: See https://github.com/elastic/kibana/issues/142564
- describe.skip('runSoon', () => {
+ describe('runSoon', () => {
const objectRemover = new ObjectRemover(supertest);
before(async () => {
@@ -36,7 +35,7 @@ export default function createRunSoonTests({ getService }: FtrProviderContext) {
it('should successfully run rule where scheduled task id is different than rule id', async () => {
await retry.try(async () => {
- // Sometimes the rule may already be running. Try until it isn't
+ // Sometimes the rule may already be running, which returns a 200. Try until it isn't
const response = await supertest
.post(`${getUrlPrefix(``)}/internal/alerting/rule/${LOADED_RULE_ID}/_run_soon`)
.set('kbn-xsrf', 'foo');
@@ -53,10 +52,13 @@ export default function createRunSoonTests({ getService }: FtrProviderContext) {
expect(response.status).to.eql(200);
objectRemover.add('default', response.body.id, 'rule', 'alerting');
- const runSoonResponse = await supertest
- .post(`${getUrlPrefix(``)}/internal/alerting/rule/${response.body.id}/_run_soon`)
- .set('kbn-xsrf', 'foo');
- expect(runSoonResponse.status).to.eql(204);
+ await retry.try(async () => {
+ // Sometimes the rule may already be running, which returns a 200. Try until it isn't
+ const runSoonResponse = await supertest
+ .post(`${getUrlPrefix(``)}/internal/alerting/rule/${response.body.id}/_run_soon`)
+ .set('kbn-xsrf', 'foo');
+ expect(runSoonResponse.status).to.eql(204);
+ });
});
it('should return message when task does not exist for rule', async () => {
diff --git a/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts
index 30cad369259c3..494b9256cd6d7 100644
--- a/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts
+++ b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts
@@ -13,8 +13,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const ml = getService('ml');
const browser = getService('browser');
- // Failing: See https://github.com/elastic/kibana/issues/142248
- describe.skip('Notifications list', function () {
+ const configs = [
+ { jobId: 'fq_001', spaceId: undefined },
+ { jobId: 'fq_002', spaceId: 'space1' },
+ ];
+
+ describe('Notifications list', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
@@ -22,10 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// Prepare jobs to generate notifications
await Promise.all(
- [
- { jobId: 'fq_001', spaceId: undefined },
- { jobId: 'fq_002', spaceId: 'space1' },
- ].map(async (v) => {
+ configs.map(async (v) => {
const datafeedConfig = ml.commonConfig.getADFqDatafeedConfig(v.jobId);
await ml.api.createAnomalyDetectionJob(
@@ -45,7 +46,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
after(async () => {
- await ml.api.cleanMlIndices();
+ for (const { jobId } of configs) {
+ await ml.api.deleteAnomalyDetectionJobES(jobId);
+ }
await ml.testResources.cleanMLSavedObjects();
await ml.testResources.deleteIndexPatternByTitle('ft_farequote');
});
diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts
index a5f650198cf22..15e2e40b0ca71 100644
--- a/x-pack/test/functional/services/cases/list.ts
+++ b/x-pack/test/functional/services/cases/list.ts
@@ -23,6 +23,12 @@ export function CasesTableServiceProvider(
const retry = getService('retry');
const config = getService('config');
+ const assertCaseExists = (index: number, totalCases: number) => {
+ if (index > totalCases - 1) {
+ throw new Error('Cannot get case from table. Index is greater than the length of all rows');
+ }
+ };
+
return {
/**
* Goes to the first case listed on the table.
@@ -40,11 +46,10 @@ export function CasesTableServiceProvider(
});
},
- async deleteFirstListedCase() {
- await testSubjects.existOrFail('action-delete', {
- timeout: config.get('timeouts.waitFor'),
- });
- await testSubjects.click('action-delete');
+ async deleteCase(index: number = 0) {
+ this.openRowActions(index);
+ await testSubjects.existOrFail('cases-bulk-action-delete');
+ await testSubjects.click('cases-bulk-action-delete');
await testSubjects.existOrFail('confirmModalConfirmButton', {
timeout: config.get('timeouts.waitFor'),
});
@@ -55,10 +60,13 @@ export function CasesTableServiceProvider(
},
async bulkDeleteAllCases() {
- await testSubjects.setCheckbox('checkboxSelectAll', 'check');
- const button = await find.byCssSelector('[aria-label="Bulk actions"]');
- await button.click();
- await testSubjects.click('cases-bulk-delete-button');
+ await this.selectAllCasesAndOpenBulkActions();
+
+ await testSubjects.existOrFail('cases-bulk-action-delete');
+ await testSubjects.click('cases-bulk-action-delete');
+ await testSubjects.existOrFail('confirmModalConfirmButton', {
+ timeout: config.get('timeouts.waitFor'),
+ });
await testSubjects.click('confirmModalConfirmButton');
},
@@ -109,9 +117,7 @@ export function CasesTableServiceProvider(
async getCaseFromTable(index: number) {
const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100);
- if (index > rows.length) {
- throw new Error('Cannot get case from table. Index is greater than the length of all rows');
- }
+ assertCaseExists(index, rows.length);
return rows[index] ?? null;
},
@@ -155,5 +161,55 @@ export function CasesTableServiceProvider(
async refreshTable() {
await testSubjects.click('all-cases-refresh');
},
+
+ async openRowActions(index: number) {
+ const rows = await find.allByCssSelector(
+ '[data-test-subj*="case-action-popover-button-"',
+ 100
+ );
+
+ assertCaseExists(index, rows.length);
+
+ const row = rows[index];
+ await row.click();
+ await find.existsByCssSelector('[data-test-subj*="case-action-popover-"');
+ },
+
+ async selectAllCasesAndOpenBulkActions() {
+ await testSubjects.setCheckbox('checkboxSelectAll', 'check');
+ const button = await find.byCssSelector('[aria-label="Bulk actions"]');
+ await button.click();
+ },
+
+ async changeStatus(status: CaseStatuses, index: number) {
+ await this.openRowActions(index);
+
+ await testSubjects.existOrFail('cases-bulk-action-delete');
+
+ await find.existsByCssSelector('[data-test-subj*="case-action-status-panel-"');
+ const statusButton = await find.byCssSelector('[data-test-subj*="case-action-status-panel-"');
+
+ statusButton.click();
+
+ await testSubjects.existOrFail(`cases-bulk-action-status-${status}`);
+ await testSubjects.click(`cases-bulk-action-status-${status}`);
+ },
+
+ async bulkChangeStatusCases(status: CaseStatuses) {
+ await this.selectAllCasesAndOpenBulkActions();
+
+ await testSubjects.existOrFail('case-bulk-action-status');
+ await testSubjects.click('case-bulk-action-status');
+ await testSubjects.existOrFail(`cases-bulk-action-status-${status}`);
+ await testSubjects.click(`cases-bulk-action-status-${status}`);
+ },
+
+ async selectAndChangeStatusOfAllCases(status: CaseStatuses) {
+ await header.waitUntilLoadingHasFinished();
+ await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 });
+ await header.waitUntilLoadingHasFinished();
+ await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 });
+ await this.bulkChangeStatusCases(status);
+ },
};
}
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/deletion.ts b/x-pack/test/functional_with_es_ssl/apps/cases/deletion.ts
index b0e79c195b719..f0ea7c60bc7c9 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/deletion.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/deletion.ts
@@ -71,8 +71,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await cases.casesTable.validateCasesTableHasNthRows(0);
});
- it(`User ${user.username} can delete a case using the trash icon in the table row`, async () => {
- await cases.casesTable.deleteFirstListedCase();
+ it(`User ${user.username} can delete a case using the row actions`, async () => {
+ await cases.casesTable.deleteCase(0);
+ await cases.casesTable.waitForTableToFinishLoading();
+ await cases.casesTable.validateCasesTableHasNthRows(1);
});
});
});
@@ -103,10 +105,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
describe('all cases list page', () => {
+ it(`User ${user.username} cannot delete cases using individual row actions`, async () => {
+ await cases.casesTable.openRowActions(0);
+ await testSubjects.missingOrFail('cases-bulk-action-delete');
+ });
+
it(`User ${user.username} cannot delete cases using bulk actions or individual row trash icon`, async () => {
- await testSubjects.missingOrFail('case-table-bulk-actions');
- await testSubjects.missingOrFail('checkboxSelectAll');
- await testSubjects.missingOrFail('action-delete');
+ await cases.casesTable.selectAllCasesAndOpenBulkActions();
+ await testSubjects.missingOrFail('cases-bulk-action-delete');
});
});
});
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts
index ec8e05ceb9b9d..c09a5e67e1b86 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts
@@ -20,7 +20,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
const header = getPageObject('header');
const testSubjects = getService('testSubjects');
const cases = getService('cases');
- const retry = getService('retry');
const browser = getService('browser');
describe('cases list', () => {
@@ -56,33 +55,44 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
});
});
- describe('deleting', () => {
- before(async () => {
- await cases.api.createNthRandomCases(8);
- await cases.api.createCase({ title: 'delete me', tags: ['one'] });
- await header.waitUntilLoadingHasFinished();
- await cases.casesTable.waitForCasesToBeListed();
- });
+ describe('bulk actions', () => {
+ describe('delete', () => {
+ before(async () => {
+ await cases.api.createNthRandomCases(8);
+ await cases.api.createCase({ title: 'delete me', tags: ['one'] });
+ await header.waitUntilLoadingHasFinished();
+ await cases.casesTable.waitForCasesToBeListed();
+ });
- after(async () => {
- await cases.api.deleteAllCases();
- await cases.casesTable.waitForCasesToBeDeleted();
+ after(async () => {
+ await cases.api.deleteAllCases();
+ await cases.casesTable.waitForCasesToBeDeleted();
+ });
+
+ it('bulk delete cases from the list', async () => {
+ await cases.casesTable.selectAndDeleteAllCases();
+ await cases.casesTable.waitForTableToFinishLoading();
+ await cases.casesTable.validateCasesTableHasNthRows(0);
+ });
});
- it('deletes a case correctly from the list', async () => {
- await cases.casesTable.deleteFirstListedCase();
- await cases.casesTable.waitForTableToFinishLoading();
+ describe('status', () => {
+ before(async () => {
+ await cases.api.createNthRandomCases(2);
+ await header.waitUntilLoadingHasFinished();
+ await cases.casesTable.waitForCasesToBeListed();
+ });
- await retry.tryForTime(2000, async () => {
- const firstRow = await testSubjects.find('case-details-link');
- expect(await firstRow.getVisibleText()).not.to.be('delete me');
+ after(async () => {
+ await cases.api.deleteAllCases();
+ await cases.casesTable.waitForCasesToBeDeleted();
});
- });
- it('bulk delete cases from the list', async () => {
- await cases.casesTable.selectAndDeleteAllCases();
- await cases.casesTable.waitForTableToFinishLoading();
- await cases.casesTable.validateCasesTableHasNthRows(0);
+ it('change the status of cases to in-progress correctly', async () => {
+ await cases.casesTable.selectAndChangeStatusOfAllCases(CaseStatuses['in-progress']);
+ await cases.casesTable.waitForTableToFinishLoading();
+ await testSubjects.missingOrFail('status-badge-open');
+ });
});
});
@@ -193,7 +203,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
});
it('filters cases by status', async () => {
- await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']);
+ await cases.casesTable.changeStatus(CaseStatuses['in-progress'], 0);
+ await testSubjects.existOrFail(`status-badge-${CaseStatuses['in-progress']}`);
await cases.casesTable.filterByStatus(CaseStatuses['in-progress']);
await cases.casesTable.validateCasesTableHasNthRows(1);
});
@@ -277,28 +288,52 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
});
});
- describe('changes status from the list', () => {
- before(async () => {
- await cases.api.createNthRandomCases(1);
- await header.waitUntilLoadingHasFinished();
- await cases.casesTable.waitForCasesToBeListed();
- });
+ describe('row actions', () => {
+ describe('Status', () => {
+ before(async () => {
+ await cases.api.createNthRandomCases(1);
+ await header.waitUntilLoadingHasFinished();
+ await cases.casesTable.waitForCasesToBeListed();
+ });
- after(async () => {
- await cases.api.deleteAllCases();
- await cases.casesTable.waitForCasesToBeDeleted();
- });
+ after(async () => {
+ await cases.api.deleteAllCases();
+ await cases.casesTable.waitForCasesToBeDeleted();
+ });
- it('to in progress', async () => {
- await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']);
- });
+ it('to in progress', async () => {
+ await cases.casesTable.changeStatus(CaseStatuses['in-progress'], 0);
+ await testSubjects.existOrFail(`status-badge-${CaseStatuses['in-progress']}`);
+ });
+
+ it('to closed', async () => {
+ await cases.casesTable.changeStatus(CaseStatuses.closed, 0);
+ await testSubjects.existOrFail(`status-badge-${CaseStatuses.closed}`);
+ });
- it('to closed', async () => {
- await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.closed);
+ it('to open', async () => {
+ await cases.casesTable.changeStatus(CaseStatuses.open, 0);
+ await testSubjects.existOrFail(`status-badge-${CaseStatuses.open}`);
+ });
});
- it('to open', async () => {
- await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses.open);
+ describe('Delete', () => {
+ before(async () => {
+ await cases.api.createNthRandomCases(1);
+ await header.waitUntilLoadingHasFinished();
+ await cases.casesTable.waitForCasesToBeListed();
+ });
+
+ after(async () => {
+ await cases.api.deleteAllCases();
+ await cases.casesTable.waitForCasesToBeDeleted();
+ });
+
+ it('deletes a case correctly', async () => {
+ await cases.casesTable.deleteCase(0);
+ await cases.casesTable.waitForTableToFinishLoading();
+ await cases.casesTable.validateCasesTableHasNthRows(0);
+ });
});
});
});
diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
index fe2099df32bda..636f16c5b98ff 100644
--- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
@@ -142,8 +142,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/g
- describe.skip('tls alert', function () {
+ describe('tls alert', function () {
const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078';
const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078';
let alerts: any;
@@ -185,7 +184,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('has created a valid alert with expected parameters', async () => {
let alert: any;
- await retry.tryForTime(15000, async () => {
+ await retry.tryForTime(60 * 1000, async () => {
const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`);
const alertsFromThisTest = apiResponse.body.data.filter(
({ name }: { name: string }) => name === alertId
diff --git a/x-pack/test/reporting_functional/reporting_and_timeout/index.ts b/x-pack/test/reporting_functional/reporting_and_timeout/index.ts
index 59b2ef546bed1..17f13bf868019 100644
--- a/x-pack/test/reporting_functional/reporting_and_timeout/index.ts
+++ b/x-pack/test/reporting_functional/reporting_and_timeout/index.ts
@@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const config = getService('config');
const screenshotDir = config.get('screenshots.directory');
- describe('Reporting Functional Tests with forced timeout', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/135309
+ describe.skip('Reporting Functional Tests with forced timeout', function () {
const dashboardTitle = 'Ecom Dashboard Hidden Panel Titles';
const baselineAPng = path.resolve(__dirname, 'fixtures/baseline/warnings_capture_a.png');
const sessionPng = 'warnings_capture_session_a';
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
index 7779c604472e1..bb0bd27ce85d9 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
@@ -6,7 +6,6 @@
*/
import expect from '@kbn/expect';
-import { SuperTest } from 'supertest';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES, ALL_SPACES_ID } from '../lib/spaces';
@@ -17,6 +16,8 @@ import {
getRedactedNamespaces,
} from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types';
+import { FtrProviderContext } from '../ftr_provider_context';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
const {
DEFAULT: { spaceId: DEFAULT_SPACE_ID },
@@ -85,7 +86,10 @@ const createRequest = ({ type, id, initialNamespaces }: BulkCreateTestCase) => (
...(initialNamespaces && { initialNamespaces }),
});
-export function bulkCreateTestSuiteFactory(esArchiver: any, supertest: SuperTest) {
+export function bulkCreateTestSuiteFactory(context: FtrProviderContext) {
+ const testDataLoader = getTestDataLoader(context);
+ const supertest = context.getService('supertestWithoutAuth');
+
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_create');
const expectResponseBody =
(
@@ -193,16 +197,31 @@ export function bulkCreateTestSuiteFactory(esArchiver: any, supertest: SuperTest
const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition;
describeFn(description, () => {
- before(() =>
- esArchiver.load(
- 'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
- )
- );
- after(() =>
- esArchiver.unload(
- 'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
- )
- );
+ before(async () => {
+ await testDataLoader.createFtrSpaces();
+ await testDataLoader.createFtrSavedObjectsData([
+ {
+ spaceName: null,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+ ]);
+ });
+
+ after(async () => {
+ await testDataLoader.deleteFtrSpaces();
+ await testDataLoader.deleteFtrSavedObjectsData();
+ });
const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } };
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
index 2d8ca1c303bd4..4176b6707b124 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
@@ -113,12 +113,9 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
return { normalTypes, crossNamespace, hiddenType, allTypes };
};
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertestWithoutAuth');
- const esArchiver = getService('esArchiver');
-
+export default function (context: FtrProviderContext) {
const { addTests, createTestDefinitions, expectSavedObjectForbidden } =
- bulkCreateTestSuiteFactory(esArchiver, supertest);
+ bulkCreateTestSuiteFactory(context);
const createTests = (overwrite: boolean, spaceId: string, user: TestUser) => {
const { normalTypes, crossNamespace, hiddenType, allTypes } = createTestCases(
overwrite,
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
index 34e18b76088f1..bccc55403ba79 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
@@ -103,11 +103,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
];
};
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertest');
- const esArchiver = getService('esArchiver');
-
- const { addTests, createTestDefinitions } = bulkCreateTestSuiteFactory(esArchiver, supertest);
+export default function (context: FtrProviderContext) {
+ const { addTests, createTestDefinitions } = bulkCreateTestSuiteFactory(context);
const createTests = (overwrite: boolean, spaceId: string) => {
const testCases = createTestCases(overwrite, spaceId);
return createTestDefinitions(testCases, false, overwrite, {
diff --git a/yarn.lock b/yarn.lock
index 2babed56a8638..60a8900ecc859 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6523,11 +6523,6 @@
dependencies:
"@types/jquery" "*"
-"@types/fnv-plus@^1.3.0":
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/@types/fnv-plus/-/fnv-plus-1.3.0.tgz#0f43f0b7e7b4b24de3a1cab69bfa009508f4c084"
- integrity sha512-ijls8MsO6Q9JUSd5w1v4y2ijM6S4D/nmOyI/FwcepvrZfym0wZhLdYGFD5TJID7tga0O3I7SmtK69RzpSJ1Fcw==
-
"@types/fs-extra@^8.0.0":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068"
@@ -15499,11 +15494,6 @@ fn.name@1.x.x:
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
-fnv-plus@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/fnv-plus/-/fnv-plus-1.3.1.tgz#c34cb4572565434acb08ba257e4044ce2b006d67"
- integrity sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw==
-
focus-lock@^0.11.2:
version "0.11.2"
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.11.2.tgz#aeef3caf1cea757797ac8afdebaec8fd9ab243ed"