From 3453cb9b16ac41d7835a9e906457b62a3f6924d1 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:32:25 +0200 Subject: [PATCH] [Cases] Case action UI implementation (#179409) ## Summary Implements https://github.com/elastic/kibana/issues/179433 This PR adds case connector with params fields in the UI for case action ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/cases/common/constants/index.ts | 8 + .../system_actions/cases/cases.test.tsx | 86 ++++++ .../components/system_actions/cases/cases.tsx | 57 ++++ .../cases/cases_params.test.tsx | 268 ++++++++++++++++++ .../system_actions/cases/cases_params.tsx | 225 +++++++++++++++ .../system_actions/cases/constants.ts | 15 + .../system_actions/cases/translations.ts | 67 +++++ .../components/system_actions/cases/types.ts | 17 ++ .../system_actions/cases/utils.test.ts | 60 ++++ .../components/system_actions/cases/utils.ts | 31 ++ .../system_actions/hooks/alert_fields.ts | 27 ++ .../system_actions/hooks/alert_index.ts | 25 ++ .../hooks/use_alert_data_view.test.tsx | 135 +++++++++ .../hooks/use_alert_data_view.ts | 161 +++++++++++ .../public/components/system_actions/index.ts | 13 + x-pack/plugins/cases/public/plugin.test.ts | 1 + x-pack/plugins/cases/public/plugin.ts | 3 + x-pack/plugins/cases/public/types.ts | 6 +- .../connectors/cases/cases_connector.test.ts | 2 +- .../connectors/cases/cases_connector.ts | 3 +- .../server/connectors/cases/constants.ts | 6 - .../cases/server/connectors/cases/index.ts | 14 +- .../cases/server/connectors/cases/schema.ts | 3 +- x-pack/plugins/cases/tsconfig.json | 1 + .../action_type_form.tsx | 1 + .../system_action_type_form.tsx | 1 + .../triggers_actions_ui/public/types.ts | 1 + 27 files changed, 1223 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/cases.test.tsx create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/constants.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/translations.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/types.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/utils.test.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/cases/utils.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/hooks/alert_fields.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/hooks/alert_index.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.test.tsx create mode 100644 x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.ts create mode 100644 x-pack/plugins/cases/public/components/system_actions/index.ts diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index 8dc9720fca781..a5a5027eab827 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -211,7 +211,15 @@ export const LOCAL_STORAGE_KEYS = { * Connectors */ +export enum CASES_CONNECTOR_SUB_ACTION { + RUN = 'run', +} + export const NONE_CONNECTOR_ID: string = 'none'; +export const CASES_CONNECTOR_ID = '.cases'; +export const CASES_CONNECTOR_TITLE = 'Cases'; + +export const CASES_CONNECTOR_TIME_WINDOW_REGEX = '^[1-9][0-9]*[d,w,M,y]$'; /** * This field is used for authorization of the entities within the cases plugin. Each entity within Cases will have the owner field diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases.test.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases.test.tsx new file mode 100644 index 0000000000000..e468807e3db92 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 { ActionTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { getConnectorType } from './cases'; +const CONNECTOR_TYPE_ID = '.cases'; +let connectorTypeModel: ActionTypeModel; + +beforeAll(() => { + connectorTypeModel = getConnectorType(); +}); + +describe('has correct connector id', () => { + test('connector type static data is as expected', () => { + expect(connectorTypeModel.id).toEqual(CONNECTOR_TYPE_ID); + }); +}); + +describe('action params validation', () => { + test('action params validation succeeds when action params is valid', async () => { + const actionParams = { + subActionParams: { + timeWindow: '7d', + reopenClosedCases: false, + groupingBy: [], + owner: 'cases', + }, + }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: [] }, + }); + }); + + test('params validation succeeds when valid timeWindow', async () => { + const actionParams = { subActionParams: { timeWindow: '17w' } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: [] }, + }); + }); + + test('params validation fails when timeWindow is empty', async () => { + const actionParams = { subActionParams: { timeWindow: '' } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: ['Invalid time window.'] }, + }); + }); + + test('params validation fails when timeWindow is undefined', async () => { + const actionParams = { subActionParams: { timeWindow: undefined } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: ['Invalid time window.'] }, + }); + }); + + test('params validation fails when timeWindow is null', async () => { + const actionParams = { subActionParams: { timeWindow: null } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: ['Invalid time window.'] }, + }); + }); + + test('params validation fails when timeWindow size is 0', async () => { + const actionParams = { subActionParams: { timeWindow: '0d' } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: ['Invalid time window.'] }, + }); + }); + + test('params validation fails when timeWindow size is negative', async () => { + const actionParams = { subActionParams: { timeWindow: '-5w' } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { timeWindow: ['Invalid time window.'] }, + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx new file mode 100644 index 0000000000000..ddbfbdb5d6bd2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx @@ -0,0 +1,57 @@ +/* + * 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 type { + GenericValidationResult, + ActionTypeModel as ConnectorTypeModel, +} from '@kbn/triggers-actions-ui-plugin/public'; + +import { + CASES_CONNECTOR_ID, + CASES_CONNECTOR_TITLE, + CASES_CONNECTOR_TIME_WINDOW_REGEX, +} from '../../../../common/constants'; +import type { CasesActionParams } from './types'; +import * as i18n from './translations'; + +interface ValidationErrors { + timeWindow: string[]; +} + +export function getConnectorType(): ConnectorTypeModel<{}, {}, CasesActionParams> { + return { + id: CASES_CONNECTOR_ID, + iconClass: 'casesApp', + selectMessage: i18n.CASE_ACTION_DESC, + actionTypeTitle: CASES_CONNECTOR_TITLE, + actionConnectorFields: null, + isExperimental: true, + validateParams: async ( + actionParams: CasesActionParams + ): Promise> => { + const errors: ValidationErrors = { + timeWindow: [], + }; + const validationResult = { + errors, + }; + const timeWindowRegex = new RegExp(CASES_CONNECTOR_TIME_WINDOW_REGEX, 'g'); + + if ( + actionParams.subActionParams && + (!actionParams.subActionParams.timeWindow || + !actionParams.subActionParams.timeWindow.length || + !timeWindowRegex.test(actionParams.subActionParams.timeWindow)) + ) { + errors.timeWindow.push(i18n.TIME_WINDOW_SIZE_ERROR); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./cases_params')), + }; +} diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx new file mode 100644 index 0000000000000..89b50bb2ef5a3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx @@ -0,0 +1,268 @@ +/* + * 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 type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useApplication } from '../../../common/lib/kibana/use_application'; +import { useAlertDataViews } from '../hooks/use_alert_data_view'; +import { CasesParamsFields } from './cases_params'; +import { showEuiComboBoxOptions } from '@elastic/eui/lib/test/rtl'; + +jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana'); +jest.mock('../../../common/lib/kibana/use_application'); +jest.mock('../hooks/use_alert_data_view'); + +const useAlertDataViewsMock = useAlertDataViews as jest.Mock; +const useApplicationMock = useApplication as jest.Mock; + +const actionParams = { + subAction: 'run', + subActionParams: { + owner: 'cases', + timeWindow: '6w', + reopenClosedCases: false, + groupingBy: [], + }, +}; + +const connector: ActionConnector = { + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true as const, +}; +const editAction = jest.fn(); +const defaultProps = { + actionConnector: connector, + actionParams, + editAction, + errors: { 'subActionParams.timeWindow.size': [] }, + index: 0, + producerId: 'test', +}; + +describe('CasesParamsFields renders', () => { + beforeEach(() => { + jest.clearAllMocks(); + useApplicationMock.mockReturnValueOnce({ appId: 'management' }); + useAlertDataViewsMock.mockReturnValue({ + loading: false, + dataViews: [ + { + title: '.alerts-test', + fields: [ + { + name: 'host.ip', + type: 'ip', + esTypes: ['keyword'], + }, + { + name: 'host.geo.location', + type: 'geo_point', + }, + ], + }, + ], + }); + }); + + it('all params fields are rendered', async () => { + render(); + + expect(await screen.findByTestId('group-by-alert-field-combobox')).toBeInTheDocument(); + expect(await screen.findByTestId('time-window-size-input')).toBeInTheDocument(); + expect(await screen.findByTestId('time-window-unit-select')).toBeInTheDocument(); + expect(await screen.findByTestId('reopen-case')).toBeInTheDocument(); + }); + + it('renders loading state of grouping by fields correctly', async () => { + useAlertDataViewsMock.mockReturnValue({ loading: true }); + render(); + + expect(await screen.findByRole('progressbar')).toBeInTheDocument(); + }); + + it('disables dropdown when loading grouping by fields', async () => { + useAlertDataViewsMock.mockReturnValue({ loading: true }); + render(); + + expect(await screen.findByRole('progressbar')).toBeInTheDocument(); + expect(await screen.findByTestId('comboBoxSearchInput')).toBeDisabled(); + }); + + it('when subAction undefined, sets to default', () => { + const newProps = { + ...defaultProps, + actionParams: {}, + }; + render(); + + expect(editAction.mock.calls[0][1]).toEqual('run'); + }); + + it('when subActionParams undefined, sets to default', () => { + const newProps = { + ...defaultProps, + actionParams: { + subAction: 'run', + }, + }; + render(); + expect(editAction.mock.calls[0][1]).toEqual({ + timeWindow: '7d', + reopenClosedCases: false, + groupingBy: [], + owner: 'cases', + }); + }); + + it('sets owner to default if appId not matched', async () => { + useApplicationMock.mockReturnValue({ appId: 'testAppId' }); + + const newProps = { + ...defaultProps, + actionParams: { + subAction: 'run', + }, + }; + render(); + + expect(editAction.mock.calls[0][1].owner).toEqual('cases'); + }); + + it('If timeWindow has errors, form row is invalid', async () => { + const newProps = { + ...defaultProps, + errors: { timeWindow: ['error'] }, + }; + + render(); + + expect(await screen.findByText('error')).toBeInTheDocument(); + }); + + it('updates owner correctly', async () => { + useApplicationMock.mockReturnValueOnce({ appId: 'securitySolutionUI' }); + + const newProps = { + ...defaultProps, + actionParams: { + subAction: 'run', + }, + }; + + const { rerender } = render(); + + expect(editAction.mock.calls[0][1].owner).toEqual('cases'); + + rerender(); + + expect(editAction.mock.calls[1][1].owner).toEqual('securitySolution'); + }); + + describe('UI updates', () => { + it('renders grouping by field options', async () => { + render(); + + userEvent.click(await screen.findByTestId('group-by-alert-field-combobox')); + + await showEuiComboBoxOptions(); + + expect(await screen.findByText('host.ip')).toBeInTheDocument(); + + expect(screen.queryByText('host.geo.location')).not.toBeInTheDocument(); + }); + + it('updates grouping by field', async () => { + render(); + + userEvent.click(await screen.findByTestId('group-by-alert-field-combobox')); + + await showEuiComboBoxOptions(); + + expect(await screen.findByText('host.ip')).toBeInTheDocument(); + + userEvent.click(await screen.findByText('host.ip')); + + expect(editAction.mock.calls[0][1].groupingBy).toEqual(['host.ip']); + }); + + it('updates grouping by field by search', async () => { + useAlertDataViewsMock.mockReturnValue({ + loading: false, + dataViews: [ + { + title: '.alerts-test', + fields: [ + { + name: 'host.ip', + type: 'ip', + esTypes: ['keyword'], + }, + { + name: 'host.geo.location', + type: 'geo_point', + }, + { + name: 'alert.name', + type: 'string', + esTypes: ['keyword'], + }, + ], + }, + ], + }); + + render(); + + userEvent.click(await screen.findByTestId('group-by-alert-field-combobox')); + + await showEuiComboBoxOptions(); + + userEvent.type(await screen.findByTestId('comboBoxSearchInput'), 'alert.name{enter}'); + + expect(editAction.mock.calls[0][1].groupingBy).toEqual(['alert.name']); + }); + + it('updates time window size', async () => { + render(); + + expect(await screen.findByTestId('time-window-size-input')).toBeInTheDocument(); + + userEvent.clear(await screen.findByTestId('time-window-size-input')); + userEvent.paste(await screen.findByTestId('time-window-size-input'), '5'); + + expect(editAction.mock.calls[0][1].timeWindow).toEqual('5w'); + }); + + it('updates time window unit', async () => { + render(); + + expect(await screen.findByTestId('time-window-unit-select')).toBeInTheDocument(); + + fireEvent.change(await screen.findByTestId('time-window-unit-select'), { + target: { value: 'M' }, + }); + + expect(editAction.mock.calls[0][1].timeWindow).toEqual('6M'); + }); + + it('updates reopenClosedCases', async () => { + render(); + + expect(await screen.findByTestId('reopen-case')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('reopen-case')); + + expect(editAction.mock.calls[0][1].reopenClosedCases).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx new file mode 100644 index 0000000000000..fdfbca7de63dd --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx @@ -0,0 +1,225 @@ +/* + * 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, { memo, useCallback, useEffect, useMemo } from 'react'; + +import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public/types'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EuiCheckbox, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiComboBox, +} from '@elastic/eui'; +import type { ValidFeatureId } from '@kbn/rule-data-utils'; +import { useApplication } from '../../../common/lib/kibana/use_application'; +import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; +import { CASES_CONNECTOR_SUB_ACTION, OWNER_INFO } from '../../../../common/constants'; +import * as i18n from './translations'; +import type { CasesActionParams } from './types'; +import { DEFAULT_TIME_WINDOW, TIME_UNITS } from './constants'; +import { getTimeUnitOptions } from './utils'; +import { useAlertDataViews } from '../hooks/use_alert_data_view'; + +export const CasesParamsFieldsComponent: React.FunctionComponent< + ActionParamsProps +> = ({ actionParams, editAction, errors, index, producerId }) => { + const { appId } = useApplication(); + const owner = getCaseOwnerByAppId(appId); + + const { dataViews, loading: loadingAlertDataViews } = useAlertDataViews( + producerId ? [producerId as ValidFeatureId] : [] + ); + + const { timeWindow, reopenClosedCases, groupingBy } = useMemo( + () => + actionParams.subActionParams ?? { + timeWindow: `${DEFAULT_TIME_WINDOW}`, + reopenClosedCases: false, + groupingBy: [], + }, + [actionParams.subActionParams] + ); + + const parsedTimeWindowSize = timeWindow.slice(0, timeWindow.length - 1); + const parsedTimeWindowUnit = timeWindow.slice(-1); + const timeWindowSize = isNaN(parseInt(parsedTimeWindowSize, 10)) + ? DEFAULT_TIME_WINDOW[0] + : parsedTimeWindowSize.toString(); + const timeWindowUnit = Object.values(TIME_UNITS).includes(parsedTimeWindowUnit as TIME_UNITS) + ? parsedTimeWindowUnit + : DEFAULT_TIME_WINDOW[1]; + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', CASES_CONNECTOR_SUB_ACTION.RUN, index); + } + + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + timeWindow: `${DEFAULT_TIME_WINDOW}`, + reopenClosedCases: false, + groupingBy: [], + owner: OWNER_INFO.cases.id, + }, + index + ); + } + + if (actionParams.subActionParams && actionParams.subActionParams?.owner !== owner) { + editAction( + 'subActionParams', + { + ...actionParams.subActionParams, + owner, + }, + index + ); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionParams, owner, appId]); + + const editSubActionProperty = useCallback( + (key: string, value: unknown) => { + return editAction( + 'subActionParams', + { ...actionParams.subActionParams, [key]: value }, + index + ); + }, + [editAction, index, actionParams.subActionParams] + ); + + const handleTimeWindowChange = useCallback( + (key: 'timeWindowSize' | 'timeWindowUnit', value: string) => { + if (!value) { + return; + } + + const newTimeWindow = + key === 'timeWindowSize' ? `${value}${timeWindowUnit}` : `${timeWindowSize}${value}`; + + editSubActionProperty('timeWindow', newTimeWindow); + }, + [editSubActionProperty, timeWindowUnit, timeWindowSize] + ); + + const onChangeComboBox = useCallback( + (optionsValue: Array>) => { + editSubActionProperty('groupingBy', optionsValue?.length ? [optionsValue[0].value] : []); + }, + [editSubActionProperty] + ); + + const options: Array> = useMemo(() => { + if (!dataViews?.length) { + return []; + } + + return dataViews + .map((dataView) => { + return dataView.fields + .filter((field) => field.esTypes?.includes('keyword')) + .map((field) => ({ + value: field.name, + label: field.name, + })); + }) + .flat(); + }, [dataViews]); + + const selectedOptions = groupingBy.map((field) => ({ value: field, label: field })); + + return ( + <> + + + + + + + + + 0 && + timeWindow !== undefined + } + > + + + { + handleTimeWindowChange('timeWindowSize', e.target.value); + }} + /> + + + { + handleTimeWindowChange('timeWindowUnit', e.target.value); + }} + options={getTimeUnitOptions(timeWindowSize)} + /> + + + + + + + { + editSubActionProperty('reopenClosedCases', e.target.checked); + }} + /> + + + + ); +}; + +CasesParamsFieldsComponent.displayName = 'CasesParamsFields'; + +export const CasesParamsFields = memo(CasesParamsFieldsComponent); + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { CasesParamsFields as default }; diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/constants.ts b/x-pack/plugins/cases/public/components/system_actions/cases/constants.ts new file mode 100644 index 0000000000000..f91f8d1a01611 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_TIME_WINDOW = '7d'; + +export enum TIME_UNITS { + DAYS = 'd', + WEEKS = 'w', + MONTHS = 'M', + YEARS = 'y', +} diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/translations.ts b/x-pack/plugins/cases/public/components/system_actions/cases/translations.ts new file mode 100644 index 0000000000000..012c5c6fe681c --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const CASE_ACTION_DESC = i18n.translate( + 'xpack.cases.systemActions.casesConnector.selectMessageText', + { + defaultMessage: 'Create a case in Kibana.', + } +); + +export const GROUP_BY_ALERT = i18n.translate( + 'xpack.cases.systemActions.casesConnector.groupByLabel', + { + defaultMessage: 'Group by alert field', + } +); + +export const TIME_WINDOW = i18n.translate( + 'xpack.cases.systemActions.casesConnector.timeWindowLabel', + { + defaultMessage: 'Time window', + } +); + +export const TIME_WINDOW_SIZE_ERROR = i18n.translate( + 'xpack.cases.systemActions.casesConnector.timeWindowSizeError', + { + defaultMessage: 'Invalid time window.', + } +); + +export const REOPEN_WHEN_CASE_IS_CLOSED = i18n.translate( + 'xpack.cases.systemActions.casesConnector.reopenWhenCaseIsClosed', + { + defaultMessage: 'Reopen when the case is closed', + } +); + +export const DAYS = (timeValue: string) => + i18n.translate('xpack.cases.systemActions.casesConnector.daysLabel', { + defaultMessage: '{timeValue, plural, one {day} other {days}}', + values: { timeValue }, + }); + +export const YEARS = (timeValue: string) => + i18n.translate('xpack.cases.systemActions.casesConnector.yearsLabel', { + defaultMessage: '{timeValue, plural, one {year} other {years}}', + values: { timeValue }, + }); + +export const MONTHS = (timeValue: string) => + i18n.translate('xpack.cases.systemActions.casesConnector.monthsLabel', { + defaultMessage: '{timeValue, plural, one {month} other {months}}', + values: { timeValue }, + }); + +export const WEEKS = (timeValue: string) => + i18n.translate('xpack.cases.systemActions.casesConnector.weeksLabel', { + defaultMessage: '{timeValue, plural, one {week} other {weeks}}', + values: { timeValue }, + }); diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/types.ts b/x-pack/plugins/cases/public/components/system_actions/cases/types.ts new file mode 100644 index 0000000000000..da83a8a10f439 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CasesSubActionParamsUI { + timeWindow: string; + reopenClosedCases: boolean; + groupingBy: string[]; + owner: string; +} +export interface CasesActionParams { + subAction: string; + subActionParams: CasesSubActionParamsUI; +} diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/utils.test.ts b/x-pack/plugins/cases/public/components/system_actions/cases/utils.test.ts new file mode 100644 index 0000000000000..6818afcc7a857 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/utils.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { getTimeUnitOptions } from './utils'; + +describe('getTimeUnitOptions', () => { + test('return single unit time options', () => { + const timeUnitValue = getTimeUnitOptions('1'); + expect(timeUnitValue).toMatchObject([ + { text: 'day', value: 'd' }, + { text: 'week', value: 'w' }, + { text: 'month', value: 'M' }, + { text: 'year', value: 'y' }, + ]); + }); + + test('return multiple unit time options', () => { + const timeUnitValue = getTimeUnitOptions('10'); + expect(timeUnitValue).toMatchObject([ + { text: 'days', value: 'd' }, + { text: 'weeks', value: 'w' }, + { text: 'months', value: 'M' }, + { text: 'years', value: 'y' }, + ]); + }); + + test('return correct unit time options for 0', () => { + const timeUnitValue = getTimeUnitOptions('0'); + expect(timeUnitValue).toMatchObject([ + { text: 'days', value: 'd' }, + { text: 'weeks', value: 'w' }, + { text: 'months', value: 'M' }, + { text: 'years', value: 'y' }, + ]); + }); + + test('return correct unit time options for negative size', () => { + const timeUnitValue = getTimeUnitOptions('-5'); + expect(timeUnitValue).toMatchObject([ + { text: 'days', value: 'd' }, + { text: 'weeks', value: 'w' }, + { text: 'months', value: 'M' }, + { text: 'years', value: 'y' }, + ]); + }); + + test('return correct unit time options for empty string', () => { + const timeUnitValue = getTimeUnitOptions(''); + expect(timeUnitValue).toMatchObject([ + { text: 'days', value: 'd' }, + { text: 'weeks', value: 'w' }, + { text: 'months', value: 'M' }, + { text: 'years', value: 'y' }, + ]); + }); +}); diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/utils.ts b/x-pack/plugins/cases/public/components/system_actions/cases/utils.ts new file mode 100644 index 0000000000000..d000ef56fa950 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/cases/utils.ts @@ -0,0 +1,31 @@ +/* + * 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 { TIME_UNITS } from './constants'; +import * as i18n from './translations'; + +export const getTimeUnitOptions = (unitSize: string) => { + return Object.entries(TIME_UNITS).map(([_key, value]) => { + return { + text: getTimeUnitLabels(value, unitSize === '' ? '0' : unitSize), + value, + }; + }); +}; + +export const getTimeUnitLabels = (timeUnit = TIME_UNITS.DAYS, timeValue = '0') => { + switch (timeUnit) { + case TIME_UNITS.DAYS: + return i18n.DAYS(timeValue); + case TIME_UNITS.WEEKS: + return i18n.WEEKS(timeValue); + case TIME_UNITS.MONTHS: + return i18n.MONTHS(timeValue); + case TIME_UNITS.YEARS: + return i18n.YEARS(timeValue); + } +}; diff --git a/x-pack/plugins/cases/public/components/system_actions/hooks/alert_fields.ts b/x-pack/plugins/cases/public/components/system_actions/hooks/alert_fields.ts new file mode 100644 index 0000000000000..8a73674a5e264 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/hooks/alert_fields.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ValidFeatureId } from '@kbn/rule-data-utils'; +import type { HttpSetup } from '@kbn/core/public'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; +import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; + +export async function fetchAlertFields({ + http, + featureIds, +}: { + http: HttpSetup; + featureIds: ValidFeatureId[]; +}): Promise { + const { fields: alertFields = [] } = await http.get<{ fields: FieldSpec[] }>( + `${BASE_RAC_ALERTS_API_PATH}/browser_fields`, + { + query: { featureIds }, + } + ); + return alertFields; +} diff --git a/x-pack/plugins/cases/public/components/system_actions/hooks/alert_index.ts b/x-pack/plugins/cases/public/components/system_actions/hooks/alert_index.ts new file mode 100644 index 0000000000000..4b5a496f226bb --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/hooks/alert_index.ts @@ -0,0 +1,25 @@ +/* + * 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 { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; +import type { HttpSetup } from '@kbn/core/public'; + +export async function fetchAlertIndexNames({ + http, + features, +}: { + http: HttpSetup; + features: string; +}): Promise { + const { index_name: indexNamesStr = [] } = await http.get<{ index_name: string[] }>( + `${BASE_RAC_ALERTS_API_PATH}/index`, + { + query: { features }, + } + ); + return indexNamesStr; +} diff --git a/x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.test.tsx b/x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.test.tsx new file mode 100644 index 0000000000000..8dbb501e4cf15 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks/dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import type { ValidFeatureId } from '@kbn/rule-data-utils'; +import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; +import { useAlertDataViews } from './use_alert_data_view'; + +const mockUseKibanaReturnValue = createStartServicesMock(); + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + __esModule: true, + useKibana: jest.fn(() => ({ + services: mockUseKibanaReturnValue, + })), +})); + +jest.mock('./alert_index', () => ({ + fetchAlertIndexNames: jest.fn(), +})); + +const { fetchAlertIndexNames } = jest.requireMock('./alert_index'); + +jest.mock('./alert_fields', () => ({ + fetchAlertFields: jest.fn(), +})); +const { fetchAlertFields } = jest.requireMock('./alert_fields'); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}); +const wrapper = ({ children }: { children: Node }) => ( + {children} +); + +describe('useAlertDataView', () => { + const observabilityAlertFeatureIds: ValidFeatureId[] = [ + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.UPTIME, + ]; + + beforeEach(() => { + fetchAlertIndexNames.mockResolvedValue([ + '.alerts-observability.uptime.alerts-*', + '.alerts-observability.metrics.alerts-*', + '.alerts-observability.logs.alerts-*', + '.alerts-observability.apm.alerts-*', + ]); + fetchAlertFields.mockResolvedValue([{ data: ' fields' }]); + }); + + afterEach(() => { + queryClient.clear(); + jest.clearAllMocks(); + }); + + it('initially is loading and does not have data', async () => { + const mockedAsyncDataView = { + loading: true, + dataview: undefined, + }; + + const { result } = renderHook(() => useAlertDataViews(observabilityAlertFeatureIds), { + wrapper, + }); + + await waitFor(() => expect(result.current).toEqual(mockedAsyncDataView)); + }); + + it('fetch index names + fields for the provided o11y featureIds', async () => { + renderHook(() => useAlertDataViews(observabilityAlertFeatureIds), { + wrapper, + }); + + await waitFor(() => expect(fetchAlertIndexNames).toHaveBeenCalledTimes(1)); + expect(fetchAlertFields).toHaveBeenCalledTimes(1); + }); + + it('only fetch index names for security featureId', async () => { + renderHook(() => useAlertDataViews([AlertConsumers.SIEM]), { + wrapper, + }); + + await waitFor(() => expect(fetchAlertIndexNames).toHaveBeenCalledTimes(1)); + expect(fetchAlertFields).toHaveBeenCalledTimes(0); + }); + + it('Do not fetch anything if security and o11y featureIds are mixed together', async () => { + const { result } = renderHook( + () => useAlertDataViews([AlertConsumers.SIEM, AlertConsumers.LOGS]), + { + wrapper, + } + ); + + await waitFor(() => + expect(result.current).toEqual({ + loading: false, + dataview: undefined, + }) + ); + expect(fetchAlertIndexNames).toHaveBeenCalledTimes(0); + expect(fetchAlertFields).toHaveBeenCalledTimes(0); + }); + + it('if fetch throws error return no data', async () => { + fetchAlertIndexNames.mockRejectedValue('error'); + + const { result } = renderHook(() => useAlertDataViews(observabilityAlertFeatureIds), { + wrapper, + }); + + await waitFor(() => + expect(result.current).toEqual({ + loading: false, + dataview: undefined, + }) + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.ts b/x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.ts new file mode 100644 index 0000000000000..863b07949af9d --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/hooks/use_alert_data_view.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { ValidFeatureId } from '@kbn/rule-data-utils'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { useEffect, useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import type { TriggersAndActionsUiServices } from '@kbn/triggers-actions-ui-plugin/public'; +import { fetchAlertIndexNames } from './alert_index'; +import { fetchAlertFields } from './alert_fields'; + +export interface UserAlertDataViews { + dataViews?: DataView[]; + loading: boolean; +} + +export function useAlertDataViews(featureIds: ValidFeatureId[]): UserAlertDataViews { + const { + http, + data: dataService, + notifications: { toasts }, + } = useKibana().services; + const [dataViews, setDataViews] = useState(undefined); + const features = featureIds.sort().join(','); + const isOnlySecurity = featureIds.length === 1 && featureIds.includes(AlertConsumers.SIEM); + + const hasSecurityAndO11yFeatureIds = + featureIds.length > 1 && featureIds.includes(AlertConsumers.SIEM); + + const hasNoSecuritySolution = + featureIds.length > 0 && !isOnlySecurity && !hasSecurityAndO11yFeatureIds; + + const queryIndexNameFn = () => { + return fetchAlertIndexNames({ http, features }); + }; + + const queryAlertFieldsFn = () => { + return fetchAlertFields({ http, featureIds }); + }; + + const onErrorFn = () => { + toasts.addDanger( + i18n.translate('xpack.cases.systemActions.useAlertDataView.useAlertDataMessage', { + defaultMessage: 'Unable to load alert data view', + }) + ); + }; + + const { + data: indexNames, + isSuccess: isIndexNameSuccess, + isInitialLoading: isIndexNameInitialLoading, + isLoading: isIndexNameLoading, + } = useQuery({ + queryKey: ['loadAlertIndexNames', features], + queryFn: queryIndexNameFn, + onError: onErrorFn, + refetchOnWindowFocus: false, + enabled: featureIds.length > 0 && !hasSecurityAndO11yFeatureIds, + }); + + const { + data: alertFields, + isSuccess: isAlertFieldsSuccess, + isInitialLoading: isAlertFieldsInitialLoading, + isLoading: isAlertFieldsLoading, + } = useQuery({ + queryKey: ['loadAlertFields', features], + queryFn: queryAlertFieldsFn, + onError: onErrorFn, + refetchOnWindowFocus: false, + enabled: hasNoSecuritySolution, + }); + + useEffect(() => { + return () => { + dataViews?.map((dv) => dataService.dataViews.clearInstanceCache(dv.id)); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataViews]); + + // FUTURE ENGINEER this useEffect is for security solution user since + // we are using the user privilege to access the security alert index + useEffect(() => { + async function createDataView() { + const localDataview = await dataService?.dataViews.create({ + title: (indexNames ?? []).join(','), + allowNoIndex: true, + }); + setDataViews([localDataview]); + } + + if (isOnlySecurity && isIndexNameSuccess) { + createDataView(); + } + }, [dataService?.dataViews, indexNames, isIndexNameSuccess, isOnlySecurity]); + + // FUTURE ENGINEER this useEffect is for o11y and stack solution user since + // we are using the kibana user privilege to access the alert index + useEffect(() => { + if ( + indexNames && + alertFields && + !isOnlySecurity && + isAlertFieldsSuccess && + isIndexNameSuccess + ) { + setDataViews([ + { + title: (indexNames ?? []).join(','), + fieldFormatMap: {}, + fields: (alertFields ?? [])?.map((field) => { + return { + ...field, + ...(field.esTypes && field.esTypes.includes('flattened') ? { type: 'string' } : {}), + }; + }), + }, + ] as unknown as DataView[]); + } + }, [ + alertFields, + dataService?.dataViews, + indexNames, + isIndexNameSuccess, + isOnlySecurity, + isAlertFieldsSuccess, + ]); + + return useMemo( + () => ({ + dataViews, + loading: + featureIds.length === 0 || hasSecurityAndO11yFeatureIds + ? false + : isOnlySecurity + ? isIndexNameInitialLoading || isIndexNameLoading + : isIndexNameInitialLoading || + isIndexNameLoading || + isAlertFieldsInitialLoading || + isAlertFieldsLoading, + }), + [ + dataViews, + featureIds.length, + hasSecurityAndO11yFeatureIds, + isOnlySecurity, + isIndexNameInitialLoading, + isIndexNameLoading, + isAlertFieldsInitialLoading, + isAlertFieldsLoading, + ] + ); +} diff --git a/x-pack/plugins/cases/public/components/system_actions/index.ts b/x-pack/plugins/cases/public/components/system_actions/index.ts new file mode 100644 index 0000000000000..bcb57c130e5b4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/system_actions/index.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 { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public'; + +import { getConnectorType } from './cases/cases'; + +export const registerSystemActions = (triggersActionsUi: TriggersAndActionsUIPublicPluginSetup) => + triggersActionsUi.actionTypeRegistry.register(getConnectorType()); diff --git a/x-pack/plugins/cases/public/plugin.test.ts b/x-pack/plugins/cases/public/plugin.test.ts index 03a17d5bf2cb0..bfe00078a04d6 100644 --- a/x-pack/plugins/cases/public/plugin.test.ts +++ b/x-pack/plugins/cases/public/plugin.test.ts @@ -52,6 +52,7 @@ describe('Cases Ui Plugin', () => { }, security: securityMock.createSetup(), management: managementPluginMock.createSetupContract(), + triggersActionsUi: triggersActionsUiMock.createStart(), }; pluginsStart = { diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index b9117746d87df..44393473767e6 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -36,6 +36,7 @@ import type { CasesPublicSetupDependencies, CasesPublicStartDependencies, } from './types'; +import { registerSystemActions } from './components/system_actions'; /** * @public @@ -113,6 +114,8 @@ export class CasesUiPlugin }); } + registerSystemActions(plugins.triggersActionsUi); + return { attachmentFramework: { registerExternalReference: (externalReferenceAttachmentType) => { diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 91ca41f89aae2..7e3d5293e3674 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -18,7 +18,10 @@ import type { FeaturesPluginStart } from '@kbn/features-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public'; +import type { + TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, + TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, +} from '@kbn/triggers-actions-ui-plugin/public'; import type { DistributiveOmit } from '@elastic/eui'; import type { ApmBase } from '@elastic/apm-rum'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; @@ -64,6 +67,7 @@ export interface CasesPublicSetupDependencies { serverless?: ServerlessPluginSetup; management: ManagementSetup; home?: HomePublicPluginSetup; + triggersActionsUi: TriggersActionsSetup; } export interface CasesPublicStartDependencies { diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_connector.test.ts b/x-pack/plugins/cases/server/connectors/cases/cases_connector.test.ts index b51b4694cfcee..f7f6c0d510f2f 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_connector.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_connector.test.ts @@ -11,7 +11,7 @@ import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.moc import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { CasesConnector } from './cases_connector'; import { CasesConnectorExecutor } from './cases_connector_executor'; -import { CASES_CONNECTOR_ID } from './constants'; +import { CASES_CONNECTOR_ID } from '../../../common/constants'; import { CasesOracleService } from './cases_oracle_service'; import { CasesService } from './cases_service'; import { CasesConnectorError } from './cases_connector_error'; diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts b/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts index 73cd78ffccd24..e0ff5d87235e0 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts @@ -11,7 +11,6 @@ import { SubActionConnector } from '@kbn/actions-plugin/server'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import { SAVED_OBJECT_TYPES } from '../../../common'; -import { CASES_CONNECTOR_SUB_ACTION } from './constants'; import type { CasesConnectorConfig, CasesConnectorRunParams, CasesConnectorSecrets } from './types'; import { CasesConnectorRunParamsSchema } from './schema'; import { CasesOracleService } from './cases_oracle_service'; @@ -25,7 +24,7 @@ import { import { CasesConnectorExecutor } from './cases_connector_executor'; import { CaseConnectorRetryService } from './retry_service'; import { fullJitterBackoffFactory } from './full_jitter_backoff'; -import { CASE_ORACLE_SAVED_OBJECT } from '../../../common/constants'; +import { CASE_ORACLE_SAVED_OBJECT, CASES_CONNECTOR_SUB_ACTION } from '../../../common/constants'; interface CasesConnectorParams { connectorParams: ServiceParams; diff --git a/x-pack/plugins/cases/server/connectors/cases/constants.ts b/x-pack/plugins/cases/server/connectors/cases/constants.ts index b18706a750886..123eb6e0ac660 100644 --- a/x-pack/plugins/cases/server/connectors/cases/constants.ts +++ b/x-pack/plugins/cases/server/connectors/cases/constants.ts @@ -7,16 +7,10 @@ import { CustomFieldTypes } from '../../../common/types/domain'; -export const CASES_CONNECTOR_ID = '.cases'; -export const CASES_CONNECTOR_TITLE = 'Cases'; export const MAX_CONCURRENT_ES_REQUEST = 5; export const MAX_OPEN_CASES = 10; export const INITIAL_ORACLE_RECORD_COUNTER = 1; -export enum CASES_CONNECTOR_SUB_ACTION { - RUN = 'run', -} - export const VALUES_FOR_CUSTOM_FIELDS_MISSING_DEFAULTS: Record = { [CustomFieldTypes.TEXT]: 'N/A', [CustomFieldTypes.TOGGLE]: false, diff --git a/x-pack/plugins/cases/server/connectors/cases/index.ts b/x-pack/plugins/cases/server/connectors/cases/index.ts index 51a4193607aff..43235e8a90379 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.ts @@ -5,12 +5,16 @@ * 2.0. */ -import { SecurityConnectorFeatureId, UptimeConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { + AlertingConnectorFeatureId, + SecurityConnectorFeatureId, + UptimeConnectorFeatureId, +} from '@kbn/actions-plugin/common'; import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import { CasesConnector } from './cases_connector'; -import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE } from './constants'; +import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE } from '../../../common/constants'; import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; import { CasesConnectorConfigSchema, CasesConnectorSecretsSchema } from './schema'; import type { CasesClient } from '../../client'; @@ -48,7 +52,11 @@ export const getCasesConnectorType = ({ * TODO: Limit only to rule types that support * alerts-as-data */ - supportedFeatureIds: [SecurityConnectorFeatureId, UptimeConnectorFeatureId], + supportedFeatureIds: [ + SecurityConnectorFeatureId, + UptimeConnectorFeatureId, + AlertingConnectorFeatureId, + ], /** * TODO: Verify license */ diff --git a/x-pack/plugins/cases/server/connectors/cases/schema.ts b/x-pack/plugins/cases/server/connectors/cases/schema.ts index ddea351e2ffe2..bf1fec00c806c 100644 --- a/x-pack/plugins/cases/server/connectors/cases/schema.ts +++ b/x-pack/plugins/cases/server/connectors/cases/schema.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import dateMath from '@kbn/datemath'; import { MAX_OPEN_CASES } from './constants'; +import { CASES_CONNECTOR_TIME_WINDOW_REGEX } from '../../../common/constants'; const AlertSchema = schema.recordOf(schema.string(), schema.any(), { validate: (value) => { @@ -53,7 +54,7 @@ export const CasesConnectorRunParamsSchema = schema.object({ * * Example: 20d, 2w, 1M, etc */ - const timeWindowRegex = new RegExp(/^[1-9][0-9]*[d,w,M,y]$/, 'g'); + const timeWindowRegex = new RegExp(CASES_CONNECTOR_TIME_WINDOW_REGEX, 'g'); if (!timeWindowRegex.test(value)) { return 'Not a valid time window'; diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index c0cb7daf20a3f..aea9a42c45508 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -77,6 +77,7 @@ "@kbn/rison", "@kbn/core-application-browser", "@kbn/react-kibana-context-render", + "@kbn/data-views-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 641c480603e95..cdcf0284025e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -571,6 +571,7 @@ export const ActionTypeForm = ({ actionConnector={actionConnector} executionMode={ActionConnectorMode.ActionForm} ruleTypeId={ruleTypeId} + producerId={producerId} /> {warning ? ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx index 62263a7f87ad4..06d0aa17c2b7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/system_action_type_form.tsx @@ -195,6 +195,7 @@ export const SystemActionTypeForm = ({ actionConnector={actionConnector} executionMode={ActionConnectorMode.ActionForm} ruleTypeId={ruleTypeId} + producerId={producerId} /> {warning ? ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 7533746c87236..3d04a2a4a3c6a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -255,6 +255,7 @@ export interface ActionParamsProps { showEmailSubjectAndMessage?: boolean; executionMode?: ActionConnectorMode; onBlur?: (field?: string) => void; + producerId?: string; } export interface Pagination {