From 34a3982f3ae361a2cbdd3eccab8129e32fadbcbf Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 6 Jan 2021 11:40:40 +0200 Subject: [PATCH 01/21] [Security Solution][Case] Fix comment content when pushing alerts to external services (#86812) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cases/components/case_view/index.tsx | 6 ++ .../use_push_to_service/index.test.tsx | 28 ++++++ .../components/use_push_to_service/index.tsx | 6 +- .../public/cases/containers/translations.ts | 2 + .../use_post_push_to_service.test.tsx | 98 +++++++++++++++++-- .../containers/use_post_push_to_service.tsx | 80 +++++++++++++-- .../public/cases/translations.ts | 11 +++ 7 files changed, 212 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 6007038b33ab7..2425b4c74d12a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -90,6 +90,8 @@ interface Signal { rule: { id: string; name: string; + to: string; + from: string; }; } @@ -97,6 +99,7 @@ interface SignalHit { _id: string; _index: string; _source: { + '@timestamp': string; signal: Signal; }; } @@ -104,6 +107,7 @@ interface SignalHit { export type Alert = { _id: string; _index: string; + '@timestamp': string; } & Signal; export const CaseComponent = React.memo( @@ -153,6 +157,7 @@ export const CaseComponent = React.memo( [_id]: { _id, _index, + '@timestamp': _source['@timestamp'], ..._source.signal, }, }), @@ -291,6 +296,7 @@ export const CaseComponent = React.memo( updateCase: handleUpdateCase, userCanCrud, isValidConnector, + alerts, }); const onSubmitConnector = useCallback( diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx index dc361d87bad0a..1709413c7bd7f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx @@ -42,6 +42,7 @@ describe('usePushToService', () => { isLoading: false, postPushToService, }; + const mockConnector = connectorsMock[0]; const actionLicense = actionLicenses[0]; const caseServices = { @@ -53,6 +54,7 @@ describe('usePushToService', () => { hasDataToPush: true, }, }; + const defaultArgs = { connector: { id: mockConnector.id, @@ -67,6 +69,19 @@ describe('usePushToService', () => { updateCase, userCanCrud: true, isValidConnector: true, + alerts: { + 'alert-id-1': { + _id: 'alert-id-1', + _index: 'alert-index-1', + '@timestamp': '2020-11-20T15:35:28.373Z', + rule: { + id: 'rule-id-1', + name: 'Awesome rule', + from: 'now-360s', + to: 'now', + }, + }, + }, }; beforeEach(() => { @@ -98,6 +113,19 @@ describe('usePushToService', () => { type: ConnectorTypes.servicenow, }, updateCase, + alerts: { + 'alert-id-1': { + _id: 'alert-id-1', + _index: 'alert-index-1', + '@timestamp': '2020-11-20T15:35:28.373Z', + rule: { + id: 'rule-id-1', + name: 'Awesome rule', + from: 'now-360s', + to: 'now', + }, + }, + }, }); expect(result.current.pushCallouts).toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx index 15a01406c5724..24b17dcbcc1e6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx @@ -21,6 +21,7 @@ import { CaseServices } from '../../containers/use_get_case_user_actions'; import { LinkAnchor } from '../../../common/components/links'; import { SecurityPageName } from '../../../app/types'; import { ErrorMessage } from '../callout/types'; +import { Alert } from '../case_view'; export interface UsePushToService { caseId: string; @@ -31,6 +32,7 @@ export interface UsePushToService { updateCase: (newCase: Case) => void; userCanCrud: boolean; isValidConnector: boolean; + alerts: Record; } export interface ReturnUsePushToService { @@ -47,6 +49,7 @@ export const usePushToService = ({ updateCase, userCanCrud, isValidConnector, + alerts, }: UsePushToService): ReturnUsePushToService => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); @@ -61,9 +64,10 @@ export const usePushToService = ({ caseServices, connector, updateCase, + alerts, }); } - }, [caseId, caseServices, connector, postPushToService, updateCase]); + }, [alerts, caseId, caseServices, connector, postPushToService, updateCase]); const goToConfigureCases = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index b0dafcec97cce..a45fd342a23ea 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +export * from '../translations'; + export const ERROR_TITLE = i18n.translate('xpack.securitySolution.containers.case.errorTitle', { defaultMessage: 'Error fetching data', }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx index 71711dae69319..c8d00a4c5cf0f 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx @@ -23,10 +23,20 @@ import { CaseServices } from './use_get_case_user_actions'; import { CaseConnector, ConnectorTypes, CommentType } from '../../../../case/common/api'; jest.mock('./api'); +jest.mock('../../common/components/link_to', () => { + const originalModule = jest.requireActual('../../common/components/link_to'); + return { + ...originalModule, + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), + }; +}); describe('usePostPushToService', () => { const abortCtrl = new AbortController(); const updateCase = jest.fn(); + const formatUrl = jest.fn(); + const samplePush = { caseId: pushedCase.id, caseServices: { @@ -45,7 +55,21 @@ describe('usePostPushToService', () => { fields: { issueType: 'Task', priority: 'Low', parent: null }, } as CaseConnector, updateCase, + alerts: { + 'alert-id-1': { + _id: 'alert-id-1', + _index: 'alert-index-1', + '@timestamp': '2020-11-20T15:35:28.373Z', + rule: { + id: 'rule-id-1', + name: 'Awesome rule', + from: 'now-360s', + to: 'now', + }, + }, + }, }; + const sampleServiceRequestData = { savedObjectId: pushedCase.id, createdAt: pushedCase.createdAt, @@ -142,11 +166,13 @@ describe('usePostPushToService', () => { expect(spyOnPushToService).toBeCalledWith( samplePush.connector.id, samplePush.connector.type, - formatServiceRequestData( - basicCase, - samplePush.connector, - sampleCaseServices as CaseServices - ), + formatServiceRequestData({ + myCase: basicCase, + connector: samplePush.connector, + caseServices: sampleCaseServices as CaseServices, + alerts: samplePush.alerts, + formatUrl, + }), abortCtrl.signal ); }); @@ -162,6 +188,7 @@ describe('usePostPushToService', () => { type: ConnectorTypes.none, fields: null, }, + alerts: samplePush.alerts, updateCase, }; const spyOnPushToService = jest.spyOn(api, 'pushToService'); @@ -176,7 +203,13 @@ describe('usePostPushToService', () => { expect(spyOnPushToService).toBeCalledWith( samplePush2.connector.id, samplePush2.connector.type, - formatServiceRequestData(basicCase, samplePush2.connector, {}), + formatServiceRequestData({ + myCase: basicCase, + connector: samplePush2.connector, + caseServices: {}, + alerts: samplePush.alerts, + formatUrl, + }), abortCtrl.signal ); }); @@ -213,7 +246,13 @@ describe('usePostPushToService', () => { it('formatServiceRequestData - current connector', () => { const caseServices = sampleCaseServices; - const result = formatServiceRequestData(pushedCase, samplePush.connector, caseServices); + const result = formatServiceRequestData({ + myCase: pushedCase, + connector: samplePush.connector, + caseServices, + alerts: samplePush.alerts, + formatUrl, + }); expect(result).toEqual(sampleServiceRequestData); }); @@ -225,7 +264,13 @@ describe('usePostPushToService', () => { type: ConnectorTypes.jira, fields: { issueType: 'Task', priority: 'High', parent: 'RJ-01' }, }; - const result = formatServiceRequestData(pushedCase, connector as CaseConnector, caseServices); + const result = formatServiceRequestData({ + myCase: pushedCase, + connector: connector as CaseConnector, + caseServices, + alerts: samplePush.alerts, + formatUrl, + }); expect(result).toEqual({ ...sampleServiceRequestData, ...connector.fields, @@ -237,13 +282,22 @@ describe('usePostPushToService', () => { const caseServices = { '123': sampleCaseServices['123'], }; + const connector = { id: '456', name: 'connector 2', type: ConnectorTypes.jira, fields: { issueType: 'Task', priority: 'High', parent: null }, }; - const result = formatServiceRequestData(pushedCase, connector as CaseConnector, caseServices); + + const result = formatServiceRequestData({ + myCase: pushedCase, + connector: connector as CaseConnector, + caseServices, + alerts: samplePush.alerts, + formatUrl, + }); + expect(result).toEqual({ ...sampleServiceRequestData, ...connector.fields, @@ -251,6 +305,32 @@ describe('usePostPushToService', () => { }); }); + it('formatServiceRequestData - Alert comment content', () => { + formatUrl.mockReturnValue('https://app.com/detections'); + const caseServices = sampleCaseServices; + const result = formatServiceRequestData({ + myCase: { + ...pushedCase, + comments: [ + { + ...pushedCase.comments[0], + type: CommentType.alert, + alertId: 'alert-id-1', + index: 'alert-index-1', + }, + ], + }, + connector: samplePush.connector, + caseServices, + alerts: samplePush.alerts, + formatUrl, + }); + + expect(result.comments![0].comment).toEqual( + '[Alert](https://app.com/detections?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:alert-id-1),type:phrase),query:(match:(_id:(query:alert-id-1,type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-11-20T15:29:28.373Z%27,kind:absolute,to:%272020-11-20T15:35:28.373Z%27)),timeline:(linkTo:!(global),timerange:(from:%272020-11-20T15:29:28.373Z%27,kind:absolute,to:%272020-11-20T15:35:28.373Z%27)))) added to case.' + ); + }); + it('unhappy path', async () => { const spyOnPushToService = jest.spyOn(api, 'pushToService'); spyOnPushToService.mockImplementation(() => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index 97fd0c99ffd96..b46840cae60e2 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -5,6 +5,8 @@ */ import { useReducer, useCallback } from 'react'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; import { ServiceConnectorCaseResponse, @@ -12,15 +14,18 @@ import { CaseConnector, CommentType, } from '../../../../case/common/api'; +import { SecurityPageName } from '../../app/types'; +import { useFormatUrl, FormatUrl, getRuleDetailsUrl } from '../../common/components/link_to'; import { errorToToaster, useStateToaster, displaySuccessToast, } from '../../common/components/toasters'; +import { Alert } from '../components/case_view'; import { getCase, pushToService, pushCase } from './api'; import * as i18n from './translations'; -import { Case } from './types'; +import { Case, Comment } from './types'; import { CaseServices } from './use_get_case_user_actions'; interface PushToServiceState { @@ -72,6 +77,7 @@ interface PushToServiceRequest { caseId: string; connector: CaseConnector; caseServices: CaseServices; + alerts: Record; updateCase: (newCase: Case) => void; } @@ -80,6 +86,7 @@ export interface UsePostPushToService extends PushToServiceState { caseId, caseServices, connector, + alerts, updateCase, }: PushToServiceRequest) => void; } @@ -92,9 +99,10 @@ export const usePostPushToService = (): UsePostPushToService => { isError: false, }); const [, dispatchToaster] = useStateToaster(); + const { formatUrl } = useFormatUrl(SecurityPageName.detections); const postPushToService = useCallback( - async ({ caseId, caseServices, connector, updateCase }: PushToServiceRequest) => { + async ({ caseId, caseServices, connector, alerts, updateCase }: PushToServiceRequest) => { let cancel = false; const abortCtrl = new AbortController(); try { @@ -103,7 +111,13 @@ export const usePostPushToService = (): UsePostPushToService => { const responseService = await pushToService( connector.id, connector.type, - formatServiceRequestData(casePushData, connector, caseServices), + formatServiceRequestData({ + myCase: casePushData, + connector, + caseServices, + alerts, + formatUrl, + }), abortCtrl.signal ); const responseCase = await pushCase( @@ -148,11 +162,59 @@ export const usePostPushToService = (): UsePostPushToService => { return { ...state, postPushToService }; }; -export const formatServiceRequestData = ( - myCase: Case, - connector: CaseConnector, - caseServices: CaseServices -): ServiceConnectorCaseParams => { +export const determineToAndFrom = (alert: Alert) => { + const ellapsedTimeRule = moment.duration( + moment().diff(dateMath.parse(alert.rule?.from != null ? alert.rule.from : 'now-0s')) + ); + + const from = moment(alert['@timestamp'] ?? new Date()) + .subtract(ellapsedTimeRule) + .toISOString(); + const to = moment(alert['@timestamp'] ?? new Date()).toISOString(); + + return { to, from }; +}; + +const getAlertFilterUrl = (alert: Alert): string => { + const { to, from } = determineToAndFrom(alert); + return `?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:${alert._id}),type:phrase),query:(match:(_id:(query:${alert._id},type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)),timeline:(linkTo:!(global),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`; +}; + +const getCommentContent = ( + comment: Comment, + alerts: Record, + formatUrl: FormatUrl +): string => { + if (comment.type === CommentType.user) { + return comment.comment; + } else if (comment.type === CommentType.alert) { + const alert = alerts[comment.alertId]; + const ruleDetailsLink = formatUrl(getRuleDetailsUrl(alert.rule.id), { + absolute: true, + skipSearch: true, + }); + + return `[${i18n.ALERT}](${ruleDetailsLink}${getAlertFilterUrl(alert)}) ${ + i18n.ALERT_ADDED_TO_CASE + }.`; + } + + return ''; +}; + +export const formatServiceRequestData = ({ + myCase, + connector, + caseServices, + alerts, + formatUrl, +}: { + myCase: Case; + connector: CaseConnector; + caseServices: CaseServices; + alerts: Record; + formatUrl: FormatUrl; +}): ServiceConnectorCaseParams => { const { id: caseId, createdAt, @@ -179,7 +241,7 @@ export const formatServiceRequestData = ( ) .map((c) => ({ commentId: c.id, - comment: c.type === CommentType.user ? c.comment : '', + comment: getCommentContent(c, alerts, formatUrl), createdAt: c.createdAt, createdBy: { fullName: c.createdBy.fullName ?? null, diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index fd217457f9e7d..6e3dcd91312de 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -278,3 +278,14 @@ export const SYNC_ALERTS_HELP = i18n.translate( 'Enabling this option will sync the status of alerts in this case with the case status.', } ); + +export const ALERT = i18n.translate('xpack.securitySolution.common.alertLabel', { + defaultMessage: 'Alert', +}); + +export const ALERT_ADDED_TO_CASE = i18n.translate( + 'xpack.securitySolution.common.alertAddedToCase', + { + defaultMessage: 'added to case', + } +); From 8ead390813d2d6d7c87596b563a096332061bd0a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 6 Jan 2021 11:43:18 +0200 Subject: [PATCH 02/21] [Security Solution][Case] Attach/Sync alert to case: Fix toast messages (#87206) --- .../cases/components/case_view/index.tsx | 12 +- .../timeline_actions/add_to_case_action.tsx | 7 +- .../timeline_actions/helpers.test.ts | 43 +++++ .../components/timeline_actions/helpers.ts | 25 +++ .../timeline_actions/translations.ts | 7 + .../public/cases/containers/api.ts | 5 +- .../public/cases/containers/translations.ts | 13 ++ .../public/cases/containers/types.ts | 16 ++ .../cases/containers/use_get_cases.test.tsx | 2 +- .../public/cases/containers/use_get_cases.tsx | 5 +- .../cases/containers/use_update_case.test.tsx | 5 +- .../cases/containers/use_update_case.tsx | 35 ++-- .../public/cases/containers/utils.test.ts | 169 ++++++++++++++++++ .../public/cases/containers/utils.ts | 52 +++++- 14 files changed, 353 insertions(+), 43 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.ts create mode 100644 x-pack/plugins/security_solution/public/cases/containers/utils.test.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 2425b4c74d12a..56bec02b6e6c6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -180,7 +180,7 @@ export const CaseComponent = React.memo( updateKey: 'title', updateValue: titleUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -194,7 +194,7 @@ export const CaseComponent = React.memo( updateKey: 'connector', updateValue: connector, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -208,7 +208,7 @@ export const CaseComponent = React.memo( updateKey: 'description', updateValue: descriptionUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -221,7 +221,7 @@ export const CaseComponent = React.memo( updateKey: 'tags', updateValue: tagsUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -234,7 +234,7 @@ export const CaseComponent = React.memo( updateKey: 'status', updateValue: statusUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); @@ -248,7 +248,7 @@ export const CaseComponent = React.memo( updateKey: 'settings', updateValue: settingsUpdate, updateCase: handleUpdateNewCase, - version: caseData.version, + caseData, onSuccess, onError, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index d2993fa63937d..3ebc0654fc019 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -17,12 +17,13 @@ import { import { CommentType } from '../../../../../case/common/api'; import { Ecs } from '../../../../common/ecs'; import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item'; -import * as i18n from './translations'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; -import { displaySuccessToast, useStateToaster } from '../../../common/components/toasters'; +import { useStateToaster } from '../../../common/components/toasters'; import { useCreateCaseModal } from '../use_create_case_modal'; import { useAllCasesModal } from '../use_all_cases_modal'; +import { createUpdateSuccessToaster } from './helpers'; +import * as i18n from './translations'; interface AddToCaseActionProps { ariaLabel?: string; @@ -53,7 +54,7 @@ const AddToCaseActionComponent: React.FC = ({ alertId: eventId, index: eventIndex ?? '', }, - () => displaySuccessToast(i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title), dispatchToaster) + () => dispatchToaster({ type: 'addToaster', toast: createUpdateSuccessToaster(theCase) }) ); }, [postComment, eventId, eventIndex, dispatchToaster] diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts new file mode 100644 index 0000000000000..58c9c4baf82eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createUpdateSuccessToaster } from './helpers'; +import { Case } from '../../containers/types'; + +const theCase = { + title: 'My case', + settings: { + syncAlerts: true, + }, +} as Case; + +describe('helpers', () => { + describe('createUpdateSuccessToaster', () => { + it('creates the correct toast when the sync alerts is on', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster(theCase); + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + text: 'Alerts in this case have their status synched with the case status', + title: 'An alert has been added to "My case"', + }); + }); + + it('creates the correct toast when the sync alerts is off', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster({ + ...theCase, + settings: { syncAlerts: false }, + }); + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'An alert has been added to "My case"', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.ts new file mode 100644 index 0000000000000..abafa55c28903 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/helpers.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { AppToast } from '../../../common/components/toasters'; +import { Case } from '../../containers/types'; +import * as i18n from './translations'; + +export const createUpdateSuccessToaster = (theCase: Case): AppToast => { + const toast: AppToast = { + id: uuid.v4(), + color: 'success', + iconType: 'check', + title: i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title), + }; + + if (theCase.settings.syncAlerts) { + return { ...toast, text: i18n.CASE_CREATED_SUCCESS_TOAST_TEXT }; + } + + return toast; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts index 0ec6a5c89e65a..479323ed1301c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts @@ -46,3 +46,10 @@ export const CASE_CREATED_SUCCESS_TOAST = (title: string) => values: { title }, defaultMessage: 'An alert has been added to "{title}"', }); + +export const CASE_CREATED_SUCCESS_TOAST_TEXT = i18n.translate( + 'xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToastText', + { + defaultMessage: 'Alerts in this case have their status synched with the case status', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 07f7391ca94d9..fd130aa01196a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -156,7 +156,10 @@ export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): P export const patchCase = async ( caseId: string, - updatedCase: Pick, + updatedCase: Pick< + CasePatchRequest, + 'description' | 'status' | 'tags' | 'title' | 'settings' | 'connector' + >, version: string, signal: AbortSignal ): Promise => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index a45fd342a23ea..39baa09a02bbf 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -74,3 +74,16 @@ export const ERROR_GET_FIELDS = i18n.translate( defaultMessage: 'Error getting fields from service', } ); + +export const SYNC_CASE = (caseTitle: string) => + i18n.translate('xpack.securitySolution.containers.case.syncCase', { + values: { caseTitle }, + defaultMessage: 'Alerts in "{caseTitle}" have been synced', + }); + +export const STATUS_CHANGED_TOASTER_TEXT = i18n.translate( + 'xpack.securitySolution.case.containers.statusChangeToasterText', + { + defaultMessage: 'Alerts in this case have been also had their status updated', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index f83f8c70e5d87..4e9baed62c644 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -12,6 +12,7 @@ import { CommentRequest, CaseStatuses, CaseAttributes, + CasePatchRequest, } from '../../../../case/common/api'; export { CaseConnector, ActionConnector } from '../../../../case/common/api'; @@ -137,3 +138,18 @@ export interface FieldMappings { id: string; title?: string; } + +export type UpdateKey = keyof Pick< + CasePatchRequest, + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' +>; + +export interface UpdateByKey { + updateKey: UpdateKey; + updateValue: CasePatchRequest[UpdateKey]; + fetchCaseUserActions?: (caseId: string) => void; + updateCase?: (newCase: Case) => void; + caseData: Case; + onSuccess?: () => void; + onError?: () => void; +} diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx index 9b4bf966a1434..f8e8d8d6c6969 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.test.tsx @@ -13,7 +13,7 @@ import { useGetCases, UseGetCases, } from './use_get_cases'; -import { UpdateKey } from './use_update_case'; +import { UpdateKey } from './types'; import { allCases, basicCase } from './mock'; import * as api from './api'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index e773a25237d0a..3dd0ad3d564a3 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -7,10 +7,9 @@ import { useCallback, useEffect, useReducer } from 'react'; import { CaseStatuses } from '../../../../case/common/api'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; -import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import * as i18n from './translations'; -import { UpdateByKey } from './use_update_case'; import { getCases, patchCase } from './api'; export interface UseGetCasesState { @@ -22,7 +21,7 @@ export interface UseGetCasesState { selectedCases: Case[]; } -export interface UpdateCase extends UpdateByKey { +export interface UpdateCase extends Omit { caseId: string; version: string; refetchCasesStatus: () => void; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx index 01e64fa780d52..7b0b7159f2601 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx @@ -5,9 +5,10 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { useUpdateCase, UseUpdateCase, UpdateKey } from './use_update_case'; +import { useUpdateCase, UseUpdateCase } from './use_update_case'; import { basicCase } from './mock'; import * as api from './api'; +import { UpdateKey } from './types'; jest.mock('./api'); @@ -24,7 +25,7 @@ describe('useUpdateCase', () => { updateKey, updateValue: 'updated description', updateCase, - version: basicCase.version, + caseData: basicCase, onSuccess, onError, }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index 08333416d3c46..ba589acaca507 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -6,21 +6,12 @@ import { useReducer, useCallback } from 'react'; -import { - displaySuccessToast, - errorToToaster, - useStateToaster, -} from '../../common/components/toasters'; -import { CasePatchRequest } from '../../../../case/common/api'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { patchCase } from './api'; +import { UpdateKey, UpdateByKey } from './types'; import * as i18n from './translations'; -import { Case } from './types'; - -export type UpdateKey = keyof Pick< - CasePatchRequest, - 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' ->; +import { createUpdateSuccessToaster } from './utils'; interface NewCaseState { isLoading: boolean; @@ -28,16 +19,6 @@ interface NewCaseState { updateKey: UpdateKey | null; } -export interface UpdateByKey { - updateKey: UpdateKey; - updateValue: CasePatchRequest[UpdateKey]; - fetchCaseUserActions?: (caseId: string) => void; - updateCase?: (newCase: Case) => void; - version: string; - onSuccess?: () => void; - onError?: () => void; -} - type Action = | { type: 'FETCH_INIT'; payload: UpdateKey } | { type: 'FETCH_SUCCESS' } @@ -89,7 +70,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => updateKey, updateValue, updateCase, - version, + caseData, onSuccess, onError, }: UpdateByKey) => { @@ -101,7 +82,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => const response = await patchCase( caseId, { [updateKey]: updateValue }, - version, + caseData.version, abortCtrl.signal ); if (!cancel) { @@ -112,7 +93,11 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => updateCase(response[0]); } dispatch({ type: 'FETCH_SUCCESS' }); - displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster); + dispatchToaster({ + type: 'addToaster', + toast: createUpdateSuccessToaster(caseData, response[0], updateKey, updateValue), + }); + if (onSuccess) { onSuccess(); } diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.test.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.test.ts new file mode 100644 index 0000000000000..9fec6049f61bc --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + valueToUpdateIsSettings, + valueToUpdateIsStatus, + createUpdateSuccessToaster, +} from './utils'; + +import { Case } from './types'; + +const caseBeforeUpdate = { + comments: [ + { + type: 'alert', + }, + ], + settings: { + syncAlerts: true, + }, +} as Case; + +const caseAfterUpdate = { title: 'My case' } as Case; + +describe('utils', () => { + describe('valueToUpdateIsSettings', () => { + it('returns true if key is settings', () => { + expect(valueToUpdateIsSettings('settings', 'value')).toBe(true); + }); + + it('returns false if key is NOT settings', () => { + expect(valueToUpdateIsSettings('tags', 'value')).toBe(false); + }); + }); + + describe('valueToUpdateIsStatus', () => { + it('returns true if key is status', () => { + expect(valueToUpdateIsStatus('status', 'value')).toBe(true); + }); + + it('returns false if key is NOT status', () => { + expect(valueToUpdateIsStatus('tags', 'value')).toBe(false); + }); + }); + + describe('createUpdateSuccessToaster', () => { + it('creates the correct toast when sync alerts is turned on and case has alerts', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'settings', + { + syncAlerts: true, + } + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Alerts in "My case" have been synced', + }); + }); + + it('creates the correct toast when sync alerts is turned on and case does NOT have alerts', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + { ...caseBeforeUpdate, comments: [] }, + caseAfterUpdate, + 'settings', + { + syncAlerts: true, + } + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast when sync alerts is turned off and case has alerts', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'settings', + { + syncAlerts: false, + } + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast when the status change, case has alerts, and sync alerts is on', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'status', + 'closed' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + text: 'Alerts in this case have been also had their status updated', + }); + }); + + it('creates the correct toast when the status change, case has alerts, and sync alerts is off', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + { ...caseBeforeUpdate, settings: { syncAlerts: false } }, + caseAfterUpdate, + 'status', + 'closed' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast when the status change, case does NOT have alerts, and sync alerts is on', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + { ...caseBeforeUpdate, comments: [] }, + caseAfterUpdate, + 'status', + 'closed' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast if not a status or a setting', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'title', + 'My new title' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 6d0d9fa0f030d..388fc58d91c23 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { set } from '@elastic/safer-lodash-set'; import { camelCase, isArray, isObject } from 'lodash'; import { fold } from 'fp-ts/lib/Either'; @@ -26,9 +27,12 @@ import { CaseUserActionsResponseRt, ServiceConnectorCaseResponseRt, ServiceConnectorCaseResponse, + CommentType, + CasePatchRequest, } from '../../../../case/common/api'; -import { ToasterError } from '../../common/components/toasters'; -import { AllCases, Case } from './types'; +import { AppToast, ToasterError } from '../../common/components/toasters'; +import { AllCases, Case, UpdateByKey } from './types'; +import * as i18n from './translations'; export const getTypedPayload = (a: unknown): T => a as T; @@ -107,3 +111,47 @@ export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnect ServiceConnectorCaseResponseRt.decode(respPushCase), fold(throwErrors(createToasterPlainError), identity) ); + +export const valueToUpdateIsSettings = ( + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): value is CasePatchRequest['settings'] => key === 'settings'; + +export const valueToUpdateIsStatus = ( + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): value is CasePatchRequest['status'] => key === 'status'; + +export const createUpdateSuccessToaster = ( + caseBeforeUpdate: Case, + caseAfterUpdate: Case, + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): AppToast => { + const caseHasAlerts = caseBeforeUpdate.comments.some( + (comment) => comment.type === CommentType.alert + ); + + const toast: AppToast = { + id: uuid.v4(), + color: 'success', + iconType: 'check', + title: i18n.UPDATED_CASE(caseAfterUpdate.title), + }; + + if (valueToUpdateIsSettings(key, value) && value?.syncAlerts && caseHasAlerts) { + return { + ...toast, + title: i18n.SYNC_CASE(caseAfterUpdate.title), + }; + } + + if (valueToUpdateIsStatus(key, value) && caseHasAlerts && caseBeforeUpdate.settings.syncAlerts) { + return { + ...toast, + text: i18n.STATUS_CHANGED_TOASTER_TEXT, + }; + } + + return toast; +}; From b99ca969e02dc0f67835cee4dfcabc505e7c7395 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 6 Jan 2021 10:38:33 +0000 Subject: [PATCH 03/21] [Alerting] revert the revert of `Enforces typing of Alert's ActionGroups` (#87382) The https://github.com/elastic/kibana/pull/86761 PR was reverted due to a small typing issue. This PR reverts that revert and adds a commit to address the issue: https://github.com/elastic/kibana/pull/87382/commits/9e4ab2002c640e7aefbd00ce9e79d3edd48796b2. --- .../alerting_example/common/constants.ts | 13 +- .../public/alert_types/always_firing.tsx | 6 +- .../server/alert_types/always_firing.ts | 4 +- .../server/alert_types/astros.ts | 5 +- x-pack/plugins/alerts/README.md | 41 +++- x-pack/plugins/alerts/common/alert_type.ts | 22 +- .../alerts/common/builtin_action_groups.ts | 22 +- .../alert_instance/alert_instance.test.ts | 205 +++++++++++++++--- .../server/alert_instance/alert_instance.ts | 26 ++- .../create_alert_instance_factory.ts | 9 +- .../alerts/server/alert_type_registry.test.ts | 39 +++- .../alerts/server/alert_type_registry.ts | 119 ++++++++-- x-pack/plugins/alerts/server/index.ts | 1 + .../alerts/server/lib/license_state.test.ts | 4 +- .../alerts/server/lib/license_state.ts | 26 ++- x-pack/plugins/alerts/server/plugin.test.ts | 2 +- x-pack/plugins/alerts/server/plugin.ts | 28 ++- .../create_execution_handler.test.ts | 19 +- .../task_runner/create_execution_handler.ts | 36 ++- .../server/task_runner/task_runner.test.ts | 36 ++- .../alerts/server/task_runner/task_runner.ts | 93 ++++++-- .../server/task_runner/task_runner_factory.ts | 40 +++- x-pack/plugins/alerts/server/types.ts | 40 +++- x-pack/plugins/apm/common/alert_types.ts | 26 ++- .../server/lib/alerts/alerting_es_client.ts | 13 +- .../alerts/register_error_count_alert_type.ts | 23 +- .../inventory_metric_threshold_executor.ts | 41 ++-- ...r_inventory_metric_threshold_alert_type.ts | 10 +- .../log_threshold/log_threshold_executor.ts | 21 +- .../register_log_threshold_alert_type.ts | 17 +- .../register_metric_threshold_alert_type.ts | 7 +- .../monitoring/server/alerts/base_alert.ts | 17 +- .../detection_engine/notifications/types.ts | 2 +- .../signals/bulk_create_ml_signals.ts | 8 +- .../signals/bulk_create_threshold_signals.ts | 8 +- .../signals/find_threshold_signals.ts | 8 +- .../detection_engine/signals/get_filter.ts | 8 +- .../signals/get_input_output_index.ts | 8 +- .../signals/siem_rule_action_groups.ts | 3 +- .../signals/single_bulk_create.ts | 10 +- .../signals/single_search_after.ts | 8 +- .../signals/threat_mapping/types.ts | 10 +- .../threshold_find_previous_signals.ts | 8 +- .../signals/threshold_get_bucket_filters.ts | 8 +- .../lib/detection_engine/signals/types.ts | 13 +- .../lib/detection_engine/signals/utils.ts | 14 +- .../alert_types/geo_containment/alert_type.ts | 7 +- .../geo_containment/geo_containment.ts | 3 +- .../alert_types/geo_containment/index.ts | 6 +- .../alert_types/geo_threshold/alert_type.ts | 3 +- .../alert_types/index_threshold/alert_type.ts | 6 +- .../action_connector_form/action_form.tsx | 2 +- .../components/alert_details.test.tsx | 3 +- .../components/alert_instances.tsx | 2 +- .../alert_form/alert_conditions.test.tsx | 12 +- .../sections/alert_form/alert_conditions.tsx | 21 +- .../alert_form/alert_conditions_group.tsx | 4 +- .../triggers_actions_ui/public/types.ts | 13 +- .../plugins/uptime/common/constants/alerts.ts | 13 +- .../lib/alerts/__tests__/status_check.test.ts | 8 +- .../server/lib/alerts/duration_anomaly.ts | 10 +- .../plugins/uptime/server/lib/alerts/index.ts | 19 +- .../uptime/server/lib/alerts/status_check.ts | 6 +- .../plugins/uptime/server/lib/alerts/tls.ts | 6 +- .../plugins/uptime/server/lib/alerts/types.ts | 5 +- .../server/lib/alerts/uptime_alert_wrapper.ts | 14 +- .../plugins/alerts/server/alert_types.ts | 32 +-- .../alerts_restricted/server/alert_types.ts | 10 +- .../fixtures/plugins/alerts/server/plugin.ts | 8 +- 69 files changed, 1011 insertions(+), 329 deletions(-) diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index 8e4ea4faf014c..721b8cb82f65f 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -10,15 +10,16 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; // always firing export const DEFAULT_INSTANCES_TO_GENERATE = 5; +export interface AlwaysFiringThresholds { + small?: number; + medium?: number; + large?: number; +} export interface AlwaysFiringParams extends AlertTypeParams { instances?: number; - thresholds?: { - small?: number; - medium?: number; - large?: number; - }; + thresholds?: AlwaysFiringThresholds; } -export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds']; +export type AlwaysFiringActionGroupIds = keyof AlwaysFiringThresholds; // Astros export enum Craft { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index cee7ee62e3210..42ce6df6d1a6f 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -133,8 +133,10 @@ export const AlwaysFiringExpression: React.FunctionComponent< }; interface TShirtSelectorProps { - actionGroup?: ActionGroupWithCondition; - setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: ( + actionGroup: ActionGroupWithCondition + ) => void; } const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index 4fde4183b414e..fc837fee08b6f 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -11,6 +11,7 @@ import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID, AlwaysFiringParams, + AlwaysFiringActionGroupIds, } from '../../common/constants'; type ActionGroups = 'small' | 'medium' | 'large'; @@ -39,7 +40,8 @@ export const alertType: AlertType< AlwaysFiringParams, { count?: number }, { triggerdOnCycle: number }, - never + never, + AlwaysFiringActionGroupIds > = { id: 'example.always-firing', name: 'Always firing', diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index 22c2f25c410cd..938baa8b317ba 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -41,7 +41,10 @@ function getCraftFilter(craft: string) { export const alertType: AlertType< { outerSpaceCapacity: number; craft: string; op: string }, { peopleInSpace: number }, - { craft: string } + { craft: string }, + never, + 'default', + 'hasLandedBackOnEarth' > = { id: 'example.people-in-space', name: 'People In Space Right Now', diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 39dc23c7bbb73..2191b23eec11e 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -142,8 +142,41 @@ This example receives server and threshold as parameters. It will read the CPU u ```typescript import { schema } from '@kbn/config-schema'; +import { + Alert, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext +} from 'x-pack/plugins/alerts/common'; ... -server.newPlatform.setup.plugins.alerts.registerType({ +interface MyAlertTypeParams extends AlertTypeParams { + server: string; + threshold: number; +} + +interface MyAlertTypeState extends AlertTypeState { + lastChecked: number; +} + +interface MyAlertTypeInstanceState extends AlertInstanceState { + cpuUsage: number; +} + +interface MyAlertTypeInstanceContext extends AlertInstanceContext { + server: string; + hasCpuUsageIncreased: boolean; +} + +type MyAlertTypeActionGroups = 'default' | 'warning'; + +const myAlertType: AlertType< + MyAlertTypeParams, + MyAlertTypeState, + MyAlertTypeInstanceState, + MyAlertTypeInstanceContext, + MyAlertTypeActionGroups +> = { id: 'my-alert-type', name: 'My alert type', validate: { @@ -180,7 +213,7 @@ server.newPlatform.setup.plugins.alerts.registerType({ services, params, state, - }: AlertExecutorOptions) { + }: AlertExecutorOptions) { // Let's assume params is { server: 'server_1', threshold: 0.8 } const { server, threshold } = params; @@ -219,7 +252,9 @@ server.newPlatform.setup.plugins.alerts.registerType({ }; }, producer: 'alerting', -}); +}; + +server.newPlatform.setup.plugins.alerts.registerType(myAlertType); ``` This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. diff --git a/x-pack/plugins/alerts/common/alert_type.ts b/x-pack/plugins/alerts/common/alert_type.ts index 4ab3ddc7ca810..d10d2467516cc 100644 --- a/x-pack/plugins/alerts/common/alert_type.ts +++ b/x-pack/plugins/alerts/common/alert_type.ts @@ -5,19 +5,29 @@ */ import { LicenseType } from '../../licensing/common/types'; +import { RecoveredActionGroupId, DefaultActionGroupId } from './builtin_action_groups'; -export interface AlertType { +export interface AlertType< + ActionGroupIds extends Exclude = DefaultActionGroupId, + RecoveryActionGroupId extends string = RecoveredActionGroupId +> { id: string; name: string; - actionGroups: ActionGroup[]; - recoveryActionGroup: ActionGroup; + actionGroups: Array>; + recoveryActionGroup: ActionGroup; actionVariables: string[]; - defaultActionGroupId: ActionGroup['id']; + defaultActionGroupId: ActionGroupIds; producer: string; minimumLicenseRequired: LicenseType; } -export interface ActionGroup { - id: string; +export interface ActionGroup { + id: ActionGroupIds; name: string; } + +export type ActionGroupIdsOf = T extends ActionGroup + ? groups + : T extends Readonly> + ? groups + : never; diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index e23bbcc54b24d..f2b7ec855b86e 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,27 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const RecoveredActionGroup: Readonly = { +export type DefaultActionGroupId = 'default'; + +export type RecoveredActionGroupId = typeof RecoveredActionGroup['id']; +export const RecoveredActionGroup: Readonly> = Object.freeze({ id: 'recovered', name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { defaultMessage: 'Recovered', }), -}; +}); + +export type ReservedActionGroups = + | RecoveryActionGroupId + | RecoveredActionGroupId; + +export type WithoutReservedActionGroups< + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = ActionGroupIds extends ReservedActionGroups ? never : ActionGroupIds; -export function getBuiltinActionGroups(customRecoveryGroup?: ActionGroup): ActionGroup[] { - return [customRecoveryGroup ?? Object.freeze(RecoveredActionGroup)]; +export function getBuiltinActionGroups( + customRecoveryGroup?: ActionGroup +): [ActionGroup>] { + return [customRecoveryGroup ?? RecoveredActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts index b428f6c1a9134..1bd08fc3ac32d 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts @@ -6,6 +6,7 @@ import sinon from 'sinon'; import { AlertInstance } from './alert_instance'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; let clock: sinon.SinonFakeTimers; @@ -17,12 +18,20 @@ afterAll(() => clock.restore()); describe('hasScheduledActions()', () => { test('defaults to false', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.hasScheduledActions()).toEqual(false); }); test('returns true when scheduleActions is called', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.hasScheduledActions()).toEqual(true); }); @@ -30,7 +39,11 @@ describe('hasScheduledActions()', () => { describe('isThrottled', () => { test(`should throttle when group didn't change and throttle period is still active`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -44,7 +57,11 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -58,7 +75,7 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group changes`, () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -74,12 +91,20 @@ describe('isThrottled', () => { describe('scheduledActionGroupOrSubgroupHasChanged()', () => { test('should be false if no last scheduled and nothing scheduled', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); }); test('should be false if group does not change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -92,7 +117,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group and subgroup does not change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -106,7 +135,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -119,7 +152,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -133,13 +170,17 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if no last scheduled and has scheduled action', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); }); test('should be true if group does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -152,7 +193,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does change and subgroup does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance({ meta: { lastScheduledActions: { date: new Date(), @@ -166,7 +207,11 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does not change and subgroup does change', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: { lastScheduledActions: { date: new Date(), @@ -182,14 +227,22 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { describe('getScheduledActionOptions()', () => { test('defaults to undefined', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); }); }); describe('unscheduleActions()', () => { test('makes hasScheduledActions() return false', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.hasScheduledActions()).toEqual(true); alertInstance.unscheduleActions(); @@ -197,7 +250,11 @@ describe('unscheduleActions()', () => { }); test('makes getScheduledActionOptions() return undefined', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default'); expect(alertInstance.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -212,14 +269,22 @@ describe('unscheduleActions()', () => { describe('getState()', () => { test('returns state passed to constructor', () => { const state = { foo: true }; - const alertInstance = new AlertInstance({ state }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state }); expect(alertInstance.getState()).toEqual(state); }); }); describe('scheduleActions()', () => { test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -233,7 +298,11 @@ describe('scheduleActions()', () => { }); test('makes isThrottled() return true when throttled', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -247,7 +316,11 @@ describe('scheduleActions()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -262,7 +335,11 @@ describe('scheduleActions()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: {} }); alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); expect(alertInstance.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -272,7 +349,11 @@ describe('scheduleActions()', () => { }); test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default', { field: true }); expect(() => alertInstance.scheduleActions('default', { field: false }) @@ -284,7 +365,11 @@ describe('scheduleActions()', () => { describe('scheduleActionsWithSubGroup()', () => { test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -300,7 +385,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -317,7 +406,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -333,7 +426,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -350,7 +447,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -367,7 +468,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance({ state: { foo: true }, meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: {} }); alertInstance .replaceState({ otherField: true }) .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); @@ -380,7 +485,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -390,7 +499,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice with different subgroups', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -400,7 +513,11 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice whether there are subgroups', () => { - const alertInstance = new AlertInstance(); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(); alertInstance.scheduleActions('default', { field: true }); expect(() => alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -412,7 +529,11 @@ describe('scheduleActionsWithSubGroup()', () => { describe('replaceState()', () => { test('replaces previous state', () => { - const alertInstance = new AlertInstance({ state: { foo: true } }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true } }); alertInstance.replaceState({ bar: true }); expect(alertInstance.getState()).toEqual({ bar: true }); alertInstance.replaceState({ baz: true }); @@ -422,7 +543,11 @@ describe('replaceState()', () => { describe('updateLastScheduledActions()', () => { test('replaces previous lastScheduledActions', () => { - const alertInstance = new AlertInstance({ meta: {} }); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ meta: {} }); alertInstance.updateLastScheduledActions('default'); expect(alertInstance.toJSON()).toEqual({ state: {}, @@ -438,7 +563,11 @@ describe('updateLastScheduledActions()', () => { describe('toJSON', () => { test('only serializes state and meta', () => { - const alertInstance = new AlertInstance({ + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >({ state: { foo: true }, meta: { lastScheduledActions: { @@ -464,7 +593,11 @@ describe('toRaw', () => { }, }, }; - const alertInstance = new AlertInstance(raw); + const alertInstance = new AlertInstance< + AlertInstanceState, + AlertInstanceContext, + DefaultActionGroupId + >(raw); expect(alertInstance.toRaw()).toEqual(raw); }); }); diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts index 8841f3115d547..c49b38e157a07 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts @@ -9,15 +9,17 @@ import { RawAlertInstance, rawAlertInstance, AlertInstanceContext, + DefaultActionGroupId, } from '../../common'; import { parseDuration } from '../lib'; interface ScheduledExecutionOptions< State extends AlertInstanceState, - Context extends AlertInstanceContext + Context extends AlertInstanceContext, + ActionGroupIds extends string = DefaultActionGroupId > { - actionGroup: string; + actionGroup: ActionGroupIds; subgroup?: string; context: Context; state: State; @@ -25,17 +27,19 @@ interface ScheduledExecutionOptions< export type PublicAlertInstance< State extends AlertInstanceState = AlertInstanceState, - Context extends AlertInstanceContext = AlertInstanceContext + Context extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = DefaultActionGroupId > = Pick< - AlertInstance, + AlertInstance, 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' >; export class AlertInstance< State extends AlertInstanceState = AlertInstanceState, - Context extends AlertInstanceContext = AlertInstanceContext + Context extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never > { - private scheduledExecutionOptions?: ScheduledExecutionOptions; + private scheduledExecutionOptions?: ScheduledExecutionOptions; private meta: AlertInstanceMeta; private state: State; @@ -97,14 +101,14 @@ export class AlertInstance< private scheduledActionGroupIsUnchanged( lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions + scheduledExecutionOptions: ScheduledExecutionOptions ) { return lastScheduledActions.group === scheduledExecutionOptions.actionGroup; } private scheduledActionSubgroupIsUnchanged( lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions + scheduledExecutionOptions: ScheduledExecutionOptions ) { return lastScheduledActions.subgroup && scheduledExecutionOptions.subgroup ? lastScheduledActions.subgroup === scheduledExecutionOptions.subgroup @@ -128,7 +132,7 @@ export class AlertInstance< return this.state; } - scheduleActions(actionGroup: string, context: Context = {} as Context) { + scheduleActions(actionGroup: ActionGroupIds, context: Context = {} as Context) { this.ensureHasNoScheduledActions(); this.scheduledExecutionOptions = { actionGroup, @@ -139,7 +143,7 @@ export class AlertInstance< } scheduleActionsWithSubGroup( - actionGroup: string, + actionGroup: ActionGroupIds, subgroup: string, context: Context = {} as Context ) { @@ -164,7 +168,7 @@ export class AlertInstance< return this; } - updateLastScheduledActions(group: string, subgroup?: string) { + updateLastScheduledActions(group: ActionGroupIds, subgroup?: string) { this.meta.lastScheduledActions = { group, subgroup, date: new Date() }; } diff --git a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts index 47f013a5d0e55..6ba4a8b57d9de 100644 --- a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts +++ b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts @@ -9,11 +9,12 @@ import { AlertInstance } from './alert_instance'; export function createAlertInstanceFactory< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext ->(alertInstances: Record>) { - return (id: string): AlertInstance => { + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +>(alertInstances: Record>) { + return (id: string): AlertInstance => { if (!alertInstances[id]) { - alertInstances[id] = new AlertInstance(); + alertInstances[id] = new AlertInstance(); } return alertInstances[id]; diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 58b2cb74f2353..1fdd64d56d466 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -6,7 +6,7 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertTypeRegistry, ConstructorOptions } from './alert_type_registry'; -import { AlertType } from './types'; +import { ActionGroup, AlertType } from './types'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { ILicenseState } from './lib/license_state'; import { licenseStateMock } from './lib/license_state.mock'; @@ -55,7 +55,7 @@ describe('has()', () => { describe('register()', () => { test('throws if AlertType Id contains invalid characters', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -87,7 +87,7 @@ describe('register()', () => { }); test('throws if AlertType Id isnt a string', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: (123 as unknown) as string, name: 'Test', actionGroups: [ @@ -109,7 +109,7 @@ describe('register()', () => { }); test('throws if AlertType action groups contains reserved group id', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -117,10 +117,14 @@ describe('register()', () => { id: 'default', name: 'Default', }, - { + /** + * The type system will ensure you can't use the `recovered` action group + * but we also want to ensure this at runtime + */ + ({ id: 'recovered', name: 'Recovered', - }, + } as unknown) as ActionGroup<'NotReserved'>, ], defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -137,7 +141,7 @@ describe('register()', () => { }); test('allows an AlertType to specify a custom recovery group', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -172,7 +176,14 @@ describe('register()', () => { }); test('throws if the custom recovery group is contained in the AlertType action groups', () => { - const alertType: AlertType = { + const alertType: AlertType< + never, + never, + never, + never, + 'default' | 'backToAwesome', + 'backToAwesome' + > = { id: 'test', name: 'Test', actionGroups: [ @@ -204,7 +215,7 @@ describe('register()', () => { }); test('registers the executor with the task manager', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -234,7 +245,7 @@ describe('register()', () => { }); test('shallow clones the given alert type', () => { - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -475,8 +486,12 @@ describe('ensureAlertTypeEnabled', () => { }); }); -function alertTypeWithVariables(id: string, context: string, state: string): AlertType { - const baseAlert: AlertType = { +function alertTypeWithVariables( + id: ActionGroupIds, + context: string, + state: string +): AlertType { + const baseAlert: AlertType = { id, name: `${id}-name`, actionGroups: [], diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index 5e4188c1f3bc1..c26088b6bce3c 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -19,7 +19,12 @@ import { AlertInstanceState, AlertInstanceContext, } from './types'; -import { RecoveredActionGroup, getBuiltinActionGroups } from '../common'; +import { + RecoveredActionGroup, + getBuiltinActionGroups, + RecoveredActionGroupId, + ActionGroup, +} from '../common'; import { ILicenseState } from './lib/license_state'; import { getAlertTypeFeatureUsageName } from './lib/get_alert_type_feature_usage_name'; @@ -69,15 +74,36 @@ export type NormalizedAlertType< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext -> = Omit, 'recoveryActionGroup'> & - Pick>, 'recoveryActionGroup'>; + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +> = { + actionGroups: Array>; +} & Omit< + AlertType, + 'recoveryActionGroup' | 'actionGroups' +> & + Pick< + Required< + AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + >, + 'recoveryActionGroup' + >; export type UntypedNormalizedAlertType = NormalizedAlertType< AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string, + string >; export class AlertTypeRegistry { @@ -106,8 +132,19 @@ export class AlertTypeRegistry { Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext - >(alertType: AlertType) { + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { if (this.has(alertType.id)) { throw new Error( i18n.translate('xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError', { @@ -124,18 +161,28 @@ export class AlertTypeRegistry { Params, State, InstanceState, - InstanceContext + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId >(alertType); this.alertTypes.set( alertIdSchema.validate(alertType.id), - normalizedAlertType as UntypedNormalizedAlertType + /** stripping the typing is required in order to store the AlertTypes in a Map */ + (normalizedAlertType as unknown) as UntypedNormalizedAlertType ); this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { title: alertType.name, createTaskRunner: (context: RunContext) => - this.taskRunnerFactory.create(normalizedAlertType as UntypedNormalizedAlertType, context), + this.taskRunnerFactory.create< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId | RecoveredActionGroupId + >(normalizedAlertType, context), }, }); // No need to notify usage on basic alert types @@ -151,8 +198,19 @@ export class AlertTypeRegistry { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(id: string): NormalizedAlertType { + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = string, + RecoveryActionGroupId extends string = string + >( + id: string + ): NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > { if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', { @@ -163,11 +221,18 @@ export class AlertTypeRegistry { }) ); } - return this.alertTypes.get(id)! as NormalizedAlertType< + /** + * When we store the AlertTypes in the Map we strip the typing. + * This means that returning a typed AlertType in `get` is an inherently + * unsafe operation. Down casting to `unknown` is the only way to achieve this. + */ + return (this.alertTypes.get(id)! as unknown) as NormalizedAlertType< Params, State, InstanceState, - InstanceContext + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId >; } @@ -217,15 +282,31 @@ function augmentActionGroupsWithReserved< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string >( - alertType: AlertType -): NormalizedAlertType { + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > +): NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveredActionGroupId | RecoveryActionGroupId +> { const reservedActionGroups = getBuiltinActionGroups(alertType.recoveryActionGroup); const { id, actionGroups, recoveryActionGroup } = alertType; - const activeActionGroups = new Set(actionGroups.map((item) => item.id)); - const intersectingReservedActionGroups = intersection( + const activeActionGroups = new Set(actionGroups.map((item) => item.id)); + const intersectingReservedActionGroups = intersection( [...activeActionGroups.values()], reservedActionGroups.map((item) => item.id) ); diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts index 7bb54cd87bc33..da56da671f9b0 100644 --- a/x-pack/plugins/alerts/server/index.ts +++ b/x-pack/plugins/alerts/server/index.ts @@ -16,6 +16,7 @@ export { ActionVariable, AlertType, ActionGroup, + ActionGroupIdsOf, AlertingPlugin, AlertExecutorOptions, AlertActionParams, diff --git a/x-pack/plugins/alerts/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts index 94db4c946ab00..2bba0a910b65e 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -56,7 +56,7 @@ describe('getLicenseCheckForAlertType', () => { let license: Subject; let licenseState: ILicenseState; const mockNotifyUsage = jest.fn(); - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -190,7 +190,7 @@ describe('ensureLicenseForAlertType()', () => { let license: Subject; let licenseState: ILicenseState; const mockNotifyUsage = jest.fn(); - const alertType: AlertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ diff --git a/x-pack/plugins/alerts/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts index dea5b3338a5be..e20ccea7c834f 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -13,7 +13,13 @@ import { LicensingPluginStart } from '../../../licensing/server'; import { ILicense, LicenseType } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { getAlertTypeFeatureUsageName } from './get_alert_type_feature_usage_name'; -import { AlertType } from '../types'; +import { + AlertType, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { AlertTypeDisabledError } from './errors/alert_type_disabled'; export type ILicenseState = PublicMethodsOf; @@ -130,7 +136,23 @@ export class LicenseState { } } - public ensureLicenseForAlertType(alertType: AlertType) { + public ensureLicenseForAlertType< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { this.notifyUsage(alertType.name, alertType.minimumLicenseRequired); const check = this.getLicenseCheckForAlertType( diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 6288d27c6ebe0..ece6fa2328d68 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -58,7 +58,7 @@ describe('Alerting Plugin', () => { describe('registerType()', () => { let setup: PluginSetupContract; - const sampleAlertType: AlertType = { + const sampleAlertType: AlertType = { id: 'test', name: 'test', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 63861f5050f25..d15ae0ca55ef9 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -102,9 +102,18 @@ export interface PluginSetupContract { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never >( - alertType: AlertType + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > ): void; } @@ -273,8 +282,19 @@ export class AlertingPlugin { Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext - >(alertType: AlertType) { + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never + >( + alertType: AlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > + ) { if (!(alertType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${alertType.minimumLicenseRequired}" is not a valid license type`); } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 5603b13a3b1f5..5ab44a6ccdb51 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -15,7 +15,7 @@ import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { KibanaRequest } from 'kibana/server'; import { asSavedObjectExecutionSource } from '../../../actions/server'; import { InjectActionParamsOpts } from './inject_action_params'; -import { UntypedNormalizedAlertType } from '../alert_type_registry'; +import { NormalizedAlertType } from '../alert_type_registry'; import { AlertTypeParams, AlertTypeState, @@ -27,7 +27,14 @@ jest.mock('./inject_action_params', () => ({ injectActionParams: jest.fn(), })); -const alertType: UntypedNormalizedAlertType = { +const alertType: NormalizedAlertType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' | 'other-group', + 'recovered' +> = { id: 'test', name: 'Test', actionGroups: [ @@ -53,7 +60,9 @@ const createExecutionHandlerParams: jest.Mocked< AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + 'default' | 'other-group', + 'recovered' > > = { actionsPlugin: mockActionsPlugin, @@ -348,7 +357,9 @@ test('state attribute gets parameterized', async () => { test(`logs an error when action group isn't part of actionGroups available for the alertType`, async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); const result = await executionHandler({ - actionGroup: 'invalid-group', + // we have to trick the compiler as this is an invalid type and this test checks whether we + // enforce this at runtime as well as compile time + actionGroup: 'invalid-group' as 'default' | 'other-group', context: {}, state: {}, alertInstanceId: '2', diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 8b4412aeb23e5..c3d90c7bcf08b 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -27,7 +27,9 @@ export interface CreateExecutionHandlerOptions< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string > { alertId: string; alertName: string; @@ -36,26 +38,39 @@ export interface CreateExecutionHandlerOptions< actions: AlertAction[]; spaceId: string; apiKey: RawAlert['apiKey']; - alertType: NormalizedAlertType; + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >; logger: Logger; eventLogger: IEventLogger; request: KibanaRequest; alertParams: AlertTypeParams; } -interface ExecutionHandlerOptions { - actionGroup: string; +interface ExecutionHandlerOptions { + actionGroup: ActionGroupIds; actionSubgroup?: string; alertInstanceId: string; context: AlertInstanceContext; state: AlertInstanceState; } +export type ExecutionHandler = ( + options: ExecutionHandlerOptions +) => Promise; + export function createExecutionHandler< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string >({ logger, alertId, @@ -69,7 +84,14 @@ export function createExecutionHandler< eventLogger, request, alertParams, -}: CreateExecutionHandlerOptions) { +}: CreateExecutionHandlerOptions< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId +>): ExecutionHandler { const alertTypeActionGroups = new Map( alertType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) ); @@ -79,7 +101,7 @@ export function createExecutionHandler< context, state, alertInstanceId, - }: ExecutionHandlerOptions) => { + }: ExecutionHandlerOptions) => { if (!alertTypeActionGroups.has(actionGroup)) { logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`); return; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 967c5263b9730..75be9d371aee4 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -266,7 +266,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices .alertInstanceFactory('1') @@ -426,7 +427,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -542,7 +544,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); executorServices.alertInstanceFactory('2').scheduleActions('default'); @@ -595,7 +598,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -696,7 +700,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -743,7 +748,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices .alertInstanceFactory('1') @@ -798,7 +804,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -973,7 +980,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -1080,7 +1088,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -1178,7 +1187,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } @@ -1447,7 +1457,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { throw new Error('OMG'); } @@ -1822,7 +1833,8 @@ describe('Task Runner', () => { AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + string >) => { throw new Error('OMG'); } diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index c4187145e5a16..12f7c33ae5052 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -10,7 +10,7 @@ import { addSpaceIdToPath } from '../../../spaces/server'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; -import { createExecutionHandler } from './create_execution_handler'; +import { createExecutionHandler, ExecutionHandler } from './create_execution_handler'; import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; import { validateAlertTypeParams, @@ -44,6 +44,7 @@ import { AlertTypeState, AlertInstanceState, AlertInstanceContext, + WithoutReservedActionGroups, } from '../../common'; import { NormalizedAlertType } from '../alert_type_registry'; @@ -64,16 +65,32 @@ export class TaskRunner< Params extends AlertTypeParams, State extends AlertTypeState, InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string > { private context: TaskRunnerContext; private logger: Logger; private taskInstance: AlertTaskInstance; - private alertType: NormalizedAlertType; + private alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >; private readonly alertTypeRegistry: AlertTypeRegistry; constructor( - alertType: NormalizedAlertType, + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >, taskInstance: ConcreteTaskInstance, context: TaskRunnerContext ) { @@ -144,7 +161,14 @@ export class TaskRunner< actions: Alert['actions'], alertParams: Params ) { - return createExecutionHandler({ + return createExecutionHandler< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >({ alertId, alertName, tags, @@ -163,7 +187,7 @@ export class TaskRunner< async executeAlertInstance( alertInstanceId: string, alertInstance: AlertInstance, - executionHandler: ReturnType + executionHandler: ExecutionHandler ) { const { actionGroup, @@ -180,7 +204,7 @@ export class TaskRunner< services: Services, alert: SanitizedAlert, params: Params, - executionHandler: ReturnType, + executionHandler: ExecutionHandler, spaceId: string, event: Event ): Promise { @@ -218,9 +242,11 @@ export class TaskRunner< alertId, services: { ...services, - alertInstanceFactory: createAlertInstanceFactory( - alertInstances - ), + alertInstanceFactory: createAlertInstanceFactory< + InstanceState, + InstanceContext, + WithoutReservedActionGroups + >(alertInstances), }, params, state: alertTypeState as State, @@ -278,7 +304,7 @@ export class TaskRunner< if (!muteAll) { const mutedInstanceIdsSet = new Set(mutedInstanceIds); - scheduleActionsForRecoveredInstances({ + scheduleActionsForRecoveredInstances({ recoveryActionGroup: this.alertType.recoveryActionGroup, recoveredAlertInstances, executionHandler, @@ -615,20 +641,30 @@ function generateNewAndRecoveredInstanceEvents< interface ScheduleActionsForRecoveredInstancesParams< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string > { logger: Logger; - recoveryActionGroup: ActionGroup; - recoveredAlertInstances: Dictionary>; - executionHandler: ReturnType; + recoveryActionGroup: ActionGroup; + recoveredAlertInstances: Dictionary< + AlertInstance + >; + executionHandler: ExecutionHandler; mutedInstanceIdsSet: Set; alertLabel: string; } function scheduleActionsForRecoveredInstances< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext ->(params: ScheduleActionsForRecoveredInstancesParams) { + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string +>( + params: ScheduleActionsForRecoveredInstancesParams< + InstanceState, + InstanceContext, + RecoveryActionGroupId + > +) { const { logger, recoveryActionGroup, @@ -660,18 +696,31 @@ function scheduleActionsForRecoveredInstances< interface LogActiveAndRecoveredInstancesParams< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string > { logger: Logger; - activeAlertInstances: Dictionary>; - recoveredAlertInstances: Dictionary>; + activeAlertInstances: Dictionary>; + recoveredAlertInstances: Dictionary< + AlertInstance + >; alertLabel: string; } function logActiveAndRecoveredInstances< InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext ->(params: LogActiveAndRecoveredInstancesParams) { + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string +>( + params: LogActiveAndRecoveredInstancesParams< + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + > +) { const { logger, activeAlertInstances, recoveredAlertInstances, alertLabel } = params; const activeInstanceIds = Object.keys(activeAlertInstances); const recoveredInstanceIds = Object.keys(recoveredAlertInstances); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index e266608d80880..2d57467075987 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -13,11 +13,19 @@ import { import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; -import { AlertTypeRegistry, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types'; +import { + AlertTypeParams, + AlertTypeRegistry, + GetServicesFunction, + SpaceIdToNamespaceFunction, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; import { TaskRunner } from './task_runner'; import { IEventLogger } from '../../../event_log/server'; import { AlertsClient } from '../alerts_client'; -import { UntypedNormalizedAlertType } from '../alert_type_registry'; +import { NormalizedAlertType } from '../alert_type_registry'; export interface TaskRunnerContext { logger: Logger; @@ -44,11 +52,35 @@ export class TaskRunnerFactory { this.taskRunnerContext = taskRunnerContext; } - public create(alertType: UntypedNormalizedAlertType, { taskInstance }: RunContext) { + public create< + Params extends AlertTypeParams, + State extends AlertTypeState, + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string, + RecoveryActionGroupId extends string + >( + alertType: NormalizedAlertType< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >, + { taskInstance }: RunContext + ) { if (!this.isInitialized) { throw new Error('TaskRunnerFactory not initialized'); } - return new TaskRunner(alertType, taskInstance, this.taskRunnerContext!); + return new TaskRunner< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + RecoveryActionGroupId + >(alertType, taskInstance, this.taskRunnerContext!); } } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index bb2d429a7c8b5..39c52d9653aaa 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -29,6 +29,7 @@ import { AlertExecutionStatusErrorReasons, AlertsHealth, AlertNotifyWhenType, + WithoutReservedActionGroups, } from '../common'; import { LicenseType } from '../../licensing/server'; @@ -58,21 +59,25 @@ export interface Services { export interface AlertServices< InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = never > extends Services { - alertInstanceFactory: (id: string) => PublicAlertInstance; + alertInstanceFactory: ( + id: string + ) => PublicAlertInstance; } export interface AlertExecutorOptions< Params extends AlertTypeParams = never, State extends AlertTypeState = never, InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never > { alertId: string; startedAt: Date; previousStartedAt: Date | null; - services: AlertServices; + services: AlertServices; params: Params; state: State; spaceId: string; @@ -92,9 +97,10 @@ export type ExecutorType< Params extends AlertTypeParams = never, State extends AlertTypeState = never, InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never > = ( - options: AlertExecutorOptions + options: AlertExecutorOptions ) => Promise; export interface AlertTypeParamsValidator { @@ -104,17 +110,29 @@ export interface AlertType< Params extends AlertTypeParams = never, State extends AlertTypeState = never, InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never, + RecoveryActionGroupId extends string = never > { id: string; name: string; validate?: { params?: AlertTypeParamsValidator; }; - actionGroups: ActionGroup[]; - defaultActionGroupId: ActionGroup['id']; - recoveryActionGroup?: ActionGroup; - executor: ExecutorType; + actionGroups: Array>; + defaultActionGroupId: ActionGroup['id']; + recoveryActionGroup?: ActionGroup; + executor: ExecutorType< + Params, + State, + InstanceState, + InstanceContext, + /** + * Ensure that the reserved ActionGroups (such as `Recovered`) are not + * available for scheduling in the Executor + */ + WithoutReservedActionGroups + >; producer: string; actionVariables?: { context?: ActionVariable[]; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 7cc36253ef581..bb42c8acd167a 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ValuesType } from 'utility-types'; +import { ActionGroup } from '../../alerts/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common'; export enum AlertType { @@ -15,20 +16,31 @@ export enum AlertType { TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } -const THRESHOLD_MET_GROUP = { - id: 'threshold_met', +export const THRESHOLD_MET_GROUP_ID = 'threshold_met'; +export type ThresholdMetActionGroupId = typeof THRESHOLD_MET_GROUP_ID; +const THRESHOLD_MET_GROUP: ActionGroup = { + id: THRESHOLD_MET_GROUP_ID, name: i18n.translate('xpack.apm.a.thresholdMet', { defaultMessage: 'Threshold met', }), }; -export const ALERT_TYPES_CONFIG = { +export const ALERT_TYPES_CONFIG: Record< + AlertType, + { + name: string; + actionGroups: Array>; + defaultActionGroupId: ThresholdMetActionGroupId; + minimumLicenseRequired: string; + producer: string; + } +> = { [AlertType.ErrorCount]: { name: i18n.translate('xpack.apm.errorCountAlert.name', { defaultMessage: 'Error count threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -37,7 +49,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction duration threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -46,7 +58,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction duration anomaly', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, @@ -55,7 +67,7 @@ export const ALERT_TYPES_CONFIG = { defaultMessage: 'Transaction error rate threshold', }), actionGroups: [THRESHOLD_MET_GROUP], - defaultActionGroupId: 'threshold_met', + defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', producer: 'apm', }, diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index 161d5d03fcb40..4d1f53c9d4f94 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -4,14 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ThresholdMetActionGroupId } from '../../../common/alert_types'; import { ESSearchRequest, ESSearchResponse, } from '../../../../../typings/elasticsearch'; -import { AlertServices } from '../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../alerts/server'; export function alertingEsClient( - services: AlertServices, + services: AlertServices< + AlertInstanceState, + AlertInstanceContext, + ThresholdMetActionGroupId + >, params: TParams ): Promise> { return services.callCluster('search', { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 36fdf45d805f1..764e706834a70 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -4,13 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; -import { AlertingPlugin } from '../../../../alerts/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertingPlugin, + AlertInstanceContext, + AlertInstanceState, + AlertTypeState, +} from '../../../../alerts/server'; +import { + AlertType, + ALERT_TYPES_CONFIG, + ThresholdMetActionGroupId, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -41,7 +50,13 @@ export function registerErrorCountAlertType({ alerts, config$, }: RegisterAlertParams) { - alerts.registerType({ + alerts.registerType< + TypeOf, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + ThresholdMetActionGroupId + >({ id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 54cf8658a3f0d..6d851eeaab542 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -10,6 +10,7 @@ import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_m import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; import { + ActionGroup, AlertInstanceContext, AlertInstanceState, RecoveredActionGroup, @@ -27,6 +28,7 @@ import { stateToAlertMessage, } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; +import { InventoryMetricThresholdAllowedActionGroups } from './register_inventory_metric_threshold_alert_type'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -46,7 +48,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups >) => { const { criteria, @@ -115,18 +118,25 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; - alertInstance.scheduleActions(actionGroupId, { - group: item, - alertState: stateToAlertMessage[nextState], - reason, - timestamp: moment().toISOString(), - value: mapToConditionsLookup(results, (result) => - formatMetric(result[item].metric, result[item].currentValue) - ), - threshold: mapToConditionsLookup(criteria, (c) => c.threshold), - metric: mapToConditionsLookup(criteria, (c) => c.metric), - }); + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS_ID; + alertInstance.scheduleActions( + /** + * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on + * the RecoveredActionGroup isn't allowed + */ + (actionGroupId as unknown) as InventoryMetricThresholdAllowedActionGroups, + { + group: item, + alertState: stateToAlertMessage[nextState], + reason, + timestamp: moment().toISOString(), + value: mapToConditionsLookup(results, (result) => + formatMetric(result[item].metric, result[item].currentValue) + ), + threshold: mapToConditionsLookup(criteria, (c) => c.threshold), + metric: mapToConditionsLookup(criteria, (c) => c.metric), + } + ); } alertInstance.replaceState({ @@ -160,8 +170,9 @@ const mapToConditionsLookup = ( {} ); -export const FIRED_ACTIONS = { - id: 'metrics.invenotry_threshold.fired', +export const FIRED_ACTIONS_ID = 'metrics.invenotry_threshold.fired'; +export const FIRED_ACTIONS: ActionGroup = { + id: FIRED_ACTIONS_ID, name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { defaultMessage: 'Fired', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index a2e8eff34ef98..48efe8fd45a3c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -9,6 +9,7 @@ import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../.. import { createInventoryMetricThresholdExecutor, FIRED_ACTIONS, + FIRED_ACTIONS_ID, } from './inventory_metric_threshold_executor'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; @@ -22,6 +23,7 @@ import { metricActionVariableDescription, thresholdActionVariableDescription, } from '../common/messages'; +import { RecoveredActionGroupId } from '../../../../../alerts/common'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), @@ -40,6 +42,8 @@ const condition = schema.object({ ), }); +export type InventoryMetricThresholdAllowedActionGroups = typeof FIRED_ACTIONS_ID; + export const registerMetricInventoryThresholdAlertType = ( libs: InfraBackendLibs ): AlertType< @@ -49,7 +53,9 @@ export const registerMetricInventoryThresholdAlertType = ( Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + InventoryMetricThresholdAllowedActionGroups, + RecoveredActionGroupId > => ({ id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.inventory.alertName', { @@ -69,7 +75,7 @@ export const registerMetricInventoryThresholdAlertType = ( { unknowns: 'allow' } ), }, - defaultActionGroupId: FIRED_ACTIONS.id, + defaultActionGroupId: FIRED_ACTIONS_ID, actionGroups: [FIRED_ACTIONS], producer: 'infrastructure', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 09d7e482772c2..f4a9e8fdef3ff 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -13,6 +13,8 @@ import { AlertTypeState, AlertInstanceContext, AlertInstanceState, + ActionGroup, + ActionGroupIdsOf, } from '../../../../../alerts/server'; import { AlertStates, @@ -37,12 +39,18 @@ import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; -type LogThresholdAlertServices = AlertServices; +type LogThresholdActionGroups = ActionGroupIdsOf; +type LogThresholdAlertServices = AlertServices< + AlertInstanceState, + AlertInstanceContext, + LogThresholdActionGroups +>; type LogThresholdAlertExecutorOptions = AlertExecutorOptions< AlertTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + LogThresholdActionGroups >; const COMPOSITE_GROUP_SIZE = 40; @@ -344,9 +352,9 @@ export const processGroupByRatioResults = ( }; type AlertInstanceUpdater = ( - alertInstance: AlertInstance, + alertInstance: AlertInstance, state: AlertStates, - actions?: Array<{ actionGroup: string; context: AlertInstanceContext }> + actions?: Array<{ actionGroup: LogThresholdActionGroups; context: AlertInstanceContext }> ) => void; export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, actions) => { @@ -653,8 +661,9 @@ const createConditionsMessageForCriteria = (criteria: CountCriteria) => { // When the Alerting plugin implements support for multiple action groups, add additional // action groups here to send different messages, e.g. a recovery notification -export const FIRED_ACTIONS = { - id: 'logs.threshold.fired', +export const LogsThresholdFiredActionGroupId = 'logs.threshold.fired'; +export const FIRED_ACTIONS: ActionGroup<'logs.threshold.fired'> = { + id: LogsThresholdFiredActionGroupId, name: i18n.translate('xpack.infra.logs.alerting.threshold.fired', { defaultMessage: 'Fired', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index e248d3b3ddcfa..236ab9b97fdc3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { PluginSetupContract } from '../../../../../alerts/server'; +import { + PluginSetupContract, + AlertTypeParams, + AlertTypeState, + AlertInstanceContext, + AlertInstanceState, + ActionGroupIdsOf, +} from '../../../../../alerts/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, @@ -79,7 +86,13 @@ export async function registerLogThresholdAlertType( ); } - alertingPlugin.registerType({ + alertingPlugin.registerType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + ActionGroupIdsOf + >({ id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.logs.alertName', { defaultMessage: 'Log threshold', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 000c89f5899ef..77126e7d9454c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -10,6 +10,7 @@ import { AlertInstanceState, AlertInstanceContext, AlertExecutorOptions, + ActionGroupIdsOf, } from '../../../../../alerts/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; @@ -33,7 +34,8 @@ export type MetricThresholdAlertType = AlertType< Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIdsOf >; export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< /** @@ -42,7 +44,8 @@ export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< Record, Record, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIdsOf >; export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 46adfebfd17bf..405518d5de378 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -93,7 +93,7 @@ export class BaseAlert { this.scopedLogger = Globals.app.getLogger(alertOptions.id!); } - public getAlertType(): AlertType { + public getAlertType(): AlertType { const { id, name, actionVariables } = this.alertOptions; return { id, @@ -108,8 +108,11 @@ export class BaseAlert { ], defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', - executor: (options: AlertExecutorOptions & { state: ExecutedState }): Promise => - this.execute(options), + executor: ( + options: AlertExecutorOptions & { + state: ExecutedState; + } + ): Promise => this.execute(options), producer: 'monitoring', actionVariables: { context: actionVariables, @@ -238,7 +241,9 @@ export class BaseAlert { services, params, state, - }: AlertExecutorOptions & { state: ExecutedState }): Promise { + }: AlertExecutorOptions & { + state: ExecutedState; + }): Promise { this.scopedLogger.debug( `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); @@ -333,7 +338,7 @@ export class BaseAlert { protected async processData( data: AlertData[], clusters: AlertCluster[], - services: AlertServices, + services: AlertServices, state: ExecutedState ) { const currentUTC = +new Date(); @@ -387,7 +392,7 @@ export class BaseAlert { protected async processLegacyData( data: AlertData[], clusters: AlertCluster[], - services: AlertServices, + services: AlertServices, state: ExecutedState ) { const currentUTC = +new Date(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts index e4e9df552101b..b450be6c0ac08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts @@ -105,7 +105,7 @@ export const isNotificationAlertExecutor = ( }; export type NotificationAlertTypeDefinition = Omit< - AlertType, + AlertType, 'executor' > & { executor: ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index d530fe10c6498..d46bfc8cda069 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -8,7 +8,11 @@ import { flow, omit } from 'lodash/fp'; import set from 'set-value'; import { Logger } from '../../../../../../../src/core/server'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; @@ -20,7 +24,7 @@ interface BulkCreateMlSignalsParams { actions: RuleAlertAction[]; someResult: AnomalyResults; ruleParams: RuleTypeParams; - services: AlertServices; + services: AlertServices; logger: Logger; id: string; signalsIndex: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts index 3cad33b278749..438f08656a90f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts @@ -12,7 +12,11 @@ import { TimestampOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { Logger } from '../../../../../../../src/core/server'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; @@ -24,7 +28,7 @@ interface BulkCreateThresholdSignalsParams { actions: RuleAlertAction[]; someResult: SignalSearchResponse; ruleParams: RuleTypeParams; - services: AlertServices; + services: AlertServices; inputIndexPattern: string[]; logger: Logger; id: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index 239edcd1f1845..d52c2f5253711 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -12,7 +12,11 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; import { singleSearchAfter } from './single_search_after'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { BuildRuleMessage } from './rule_messages'; @@ -21,7 +25,7 @@ interface FindThresholdSignalsParams { from: string; to: string; inputIndexPattern: string[]; - services: AlertServices; + services: AlertServices; logger: Logger; filter: unknown; threshold: Threshold; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 522f4bfa5ef98..d4e59bb97bb72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -15,7 +15,11 @@ import { Language, } from '../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { PartialFilter } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; import { QueryFilter } from './types'; @@ -26,7 +30,7 @@ interface GetFilterArgs { language: LanguageOrUndefined; query: QueryOrUndefined; savedId: SavedIdOrUndefined; - services: AlertServices; + services: AlertServices; index: IndexOrUndefined; lists: ExceptionListItemSchema[]; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts index 03a63fe315a33..b6730ff55cfd2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts @@ -5,10 +5,14 @@ */ import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; export const getInputIndex = async ( - services: AlertServices, + services: AlertServices, version: string, inputIndex: string[] | null | undefined ): Promise => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/siem_rule_action_groups.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/siem_rule_action_groups.ts index 46fdb739bf143..6b5480accaf62 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/siem_rule_action_groups.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/siem_rule_action_groups.ts @@ -5,8 +5,9 @@ */ import { i18n } from '@kbn/i18n'; +import { ActionGroup } from '../../../../../alerts/common'; -export const siemRuleActionGroups = [ +export const siemRuleActionGroups: Array> = [ { id: 'default', name: i18n.translate( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index 943b70794a9b1..02abb05785642 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -6,7 +6,11 @@ import { countBy, isEmpty, get } from 'lodash'; import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { SignalSearchResponse, BulkResponse, SignalHit, WrappedSignalHit } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; @@ -19,7 +23,7 @@ import { isEventTypeSignal } from './build_event_type_signal'; interface SingleBulkCreateParams { filteredEvents: SignalSearchResponse; ruleParams: RuleTypeParams; - services: AlertServices; + services: AlertServices; logger: Logger; id: string; signalsIndex: string; @@ -222,7 +226,7 @@ export const singleBulkCreate = async ({ export const bulkInsertSignals = async ( signals: WrappedSignalHit[], logger: Logger, - services: AlertServices, + services: AlertServices, refresh: RefreshTypes ): Promise => { // index documents after creating an ID based on the diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 79e1f9896d63f..3a4538e8a9245 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -5,7 +5,11 @@ */ import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { BuildRuleMessage } from './rule_messages'; @@ -22,7 +26,7 @@ interface SingleSearchAfterParams { index: string[]; from: string; to: string; - services: AlertServices; + services: AlertServices; logger: Logger; pageSize: number; sortOrder?: SortOrderOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index faad51e4751e8..c328bf5bbb4ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -20,7 +20,11 @@ import { ItemsPerSearch, } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; import { PartialFilter, RuleTypeParams } from '../../types'; -import { AlertServices } from '../../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../../alerts/server'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; @@ -38,7 +42,7 @@ export interface CreateThreatSignalsOptions { filters: PartialFilter[]; language: LanguageOrUndefined; savedId: string | undefined; - services: AlertServices; + services: AlertServices; exceptionItems: ExceptionListItemSchema[]; gap: Duration | null; previousStartedAt: Date | null; @@ -77,7 +81,7 @@ export interface CreateThreatSignalOptions { filters: PartialFilter[]; language: LanguageOrUndefined; savedId: string | undefined; - services: AlertServices; + services: AlertServices; exceptionItems: ExceptionListItemSchema[]; gap: Duration | null; previousStartedAt: Date | null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts index 6e7f63deb06f7..b91ad86637208 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts @@ -7,7 +7,11 @@ import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { singleSearchAfter } from './single_search_after'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { BuildRuleMessage } from './rule_messages'; @@ -16,7 +20,7 @@ interface FindPreviousThresholdSignalsParams { from: string; to: string; indexPattern: string[]; - services: AlertServices; + services: AlertServices; logger: Logger; ruleId: string; bucketByField: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts index bf060da1e76b8..33eb13be6313f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts @@ -10,7 +10,11 @@ import { Filter } from 'src/plugins/data/common'; import { ESFilter } from '../../../../../../typings/elasticsearch'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; -import { AlertServices } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { ThresholdQueryBucket } from './types'; import { BuildRuleMessage } from './rule_messages'; @@ -20,7 +24,7 @@ interface GetThresholdBucketFiltersParams { from: string; to: string; indexPattern: string[]; - services: AlertServices; + services: AlertServices; logger: Logger; ruleId: string; bucketByField: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 62339f50d939c..5ae411678aa03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -141,7 +141,13 @@ export type RuleExecutorOptions = AlertExecutorOptions< // since we are only increasing the strictness of params. export const isAlertExecutor = ( obj: SignalRuleAlertTypeDefinition -): obj is AlertType => { +): obj is AlertType< + RuleTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' +> => { return true; }; @@ -149,7 +155,8 @@ export type SignalRuleAlertTypeDefinition = AlertType< RuleTypeParams, AlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + 'default' >; export interface Ancestor { @@ -224,7 +231,7 @@ export interface SearchAfterAndBulkCreateParams { gap: moment.Duration | null; previousStartedAt: Date | null | undefined; ruleParams: RuleTypeParams; - services: AlertServices; + services: AlertServices; listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; logger: Logger; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index ab14643f30e41..153696e85c2a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -10,7 +10,12 @@ import dateMath from '@elastic/datemath'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { Logger, SavedObjectsClientContract } from '../../../../../../../src/core/server'; -import { AlertServices, parseDuration } from '../../../../../alerts/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, + parseDuration, +} from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArray } from '../../../../common/detection_engine/schemas/types/lists'; @@ -52,7 +57,10 @@ export const shorthandMap = { }, }; -export const checkPrivileges = async (services: AlertServices, indices: string[]) => +export const checkPrivileges = async ( + services: AlertServices, + indices: string[] +) => services.callCluster('transport.request', { path: '/_security/user/_has_privileges', method: 'POST', @@ -154,7 +162,7 @@ export const getListsClient = ({ lists: ListPluginSetup | undefined; spaceId: string; updatedByUser: string | null; - services: AlertServices; + services: AlertServices; savedObjectClient: SavedObjectsClientContract; }): { listClient: ListClient; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts index 0d8628d00df85..85e02b21cc78c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts @@ -20,6 +20,7 @@ import { Query } from '../../../../../../src/plugins/data/common/query'; export const GEO_CONTAINMENT_ID = '.geo-containment'; export const ActionGroupId = 'Tracked entity contained'; +export const RecoveryActionGroupId = 'notGeoContained'; const actionVariableContextEntityIdLabel = i18n.translate( 'xpack.stackAlerts.geoContainment.actionVariableContextEntityIdLabel', @@ -141,7 +142,9 @@ export type GeoContainmentAlertType = AlertType< GeoContainmentParams, GeoContainmentState, GeoContainmentInstanceState, - GeoContainmentInstanceContext + GeoContainmentInstanceContext, + typeof ActionGroupId, + typeof RecoveryActionGroupId >; export function getAlertType(logger: Logger): GeoContainmentAlertType { @@ -161,7 +164,7 @@ export function getAlertType(logger: Logger): GeoContainmentAlertType { name: alertTypeName, actionGroups: [{ id: ActionGroupId, name: actionGroupName }], recoveryActionGroup: { - id: 'notGeoContained', + id: RecoveryActionGroupId, name: i18n.translate('xpack.stackAlerts.geoContainment.notGeoContained', { defaultMessage: 'No longer contained', }), diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 612eff3014985..24232e47225f0 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -100,7 +100,8 @@ export function getActiveEntriesAndGenerateAlerts( currLocationMap: Map, alertInstanceFactory: AlertServices< GeoContainmentInstanceState, - GeoContainmentInstanceContext + GeoContainmentInstanceContext, + typeof ActionGroupId >['alertInstanceFactory'], shapesIdsNamesMap: Record, currIntervalEndTime: Date diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts index 86e9e4fa3d672..21dddea5364df 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts @@ -12,6 +12,8 @@ import { GeoContainmentInstanceState, GeoContainmentInstanceContext, getAlertType, + ActionGroupId, + RecoveryActionGroupId, } from './alert_type'; interface RegisterParams { @@ -25,6 +27,8 @@ export function register(params: RegisterParams) { GeoContainmentParams, GeoContainmentState, GeoContainmentInstanceState, - GeoContainmentInstanceContext + GeoContainmentInstanceContext, + typeof ActionGroupId, + typeof RecoveryActionGroupId >(getAlertType(logger)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts index 2eccf2ff96fb0..27478049d4880 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts @@ -209,7 +209,8 @@ export type GeoThresholdAlertType = AlertType< GeoThresholdParams, GeoThresholdState, GeoThresholdInstanceState, - GeoThresholdInstanceContext + GeoThresholdInstanceContext, + typeof ActionGroupId >; export function getAlertType(logger: Logger): GeoThresholdAlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.geoThreshold.alertTypeTitle', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index 9600395c78218..3a7e795bd5dbf 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -42,7 +42,7 @@ export const ComparatorFnNames = new Set(ComparatorFns.keys()); export function getAlertType( logger: Logger, data: Promise -): AlertType { +): AlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', { defaultMessage: 'Index threshold', }); @@ -148,7 +148,9 @@ export function getAlertType( producer: STACK_ALERTS_FEATURE_ID, }; - async function executor(options: AlertExecutorOptions) { + async function executor( + options: AlertExecutorOptions + ) { const { alertId, name, services, params } = options; const compareFn = ComparatorFns.get(params.thresholdComparator); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 1cb1a68986192..6da3a16308af3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -39,7 +39,7 @@ import { ActionGroup, AlertActionParam } from '../../../../../alerts/common'; import { useKibana } from '../../../common/lib/kibana'; import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params'; -export interface ActionGroupWithMessageVariables extends ActionGroup { +export interface ActionGroupWithMessageVariables extends ActionGroup { omitOptionalMessageVariables?: boolean; defaultActionMessage?: string; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 30ca2c620f1d7..13f1aea91c7ac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -11,6 +11,7 @@ import { Alert, ActionType, AlertTypeModel, AlertType } from '../../../../types' import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; import { ViewInApp } from './view_in_app'; import { + ActionGroup, AlertExecutionStatusErrorReasons, ALERTS_FEATURE_ID, } from '../../../../../../alerts/common'; @@ -47,7 +48,7 @@ const mockAlertApis = { const authorizedConsumers = { [ALERTS_FEATURE_ID]: { read: true, all: true }, }; -const recoveryActionGroup = { id: 'recovered', name: 'Recovered' }; +const recoveryActionGroup: ActionGroup<'recovered'> = { id: 'recovered', name: 'Recovered' }; describe('alert_details', () => { // mock Api handlers diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 893f085cd664a..8252412cd261c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -231,7 +231,7 @@ const INACTIVE_LABEL = i18n.translate( function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { actionGroupId = actionGroupId || alertType.defaultActionGroupId; const actionGroup = alertType?.actionGroups?.find( - (group: ActionGroup) => group.id === actionGroupId + (group: ActionGroup) => group.id === actionGroupId ); return actionGroup?.name; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx index 8029b43a2cf53..c4d0e5d3c8e49 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx @@ -61,7 +61,7 @@ describe('alert_conditions', () => { const ConditionForm = ({ actionGroup, }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; }) => { return ( @@ -113,7 +113,7 @@ describe('alert_conditions', () => { const ConditionForm = ({ actionGroup, }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; }) => { return ( @@ -165,7 +165,7 @@ describe('alert_conditions', () => { const ConditionForm = ({ actionGroup, }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; }) => { return ( @@ -218,8 +218,10 @@ describe('alert_conditions', () => { actionGroup, someCallbackProp, }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }>; - someCallbackProp: (actionGroup: ActionGroupWithCondition<{ someProp: string }>) => void; + actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; + someCallbackProp: ( + actionGroup: ActionGroupWithCondition<{ someProp: string }, string> + ) => void; }) => { if (!actionGroup) { return
; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx index 1eb086dd6a2c5..63b654ea1a225 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx @@ -11,7 +11,10 @@ import { ActionGroup, getBuiltinActionGroups } from '../../../../../alerts/commo const BUILT_IN_ACTION_GROUPS: Set = new Set(getBuiltinActionGroups().map(({ id }) => id)); -export type ActionGroupWithCondition = ActionGroup & +export type ActionGroupWithCondition< + T, + ActionGroupIds extends string +> = ActionGroup & ( | // allow isRequired=false with or without conditions { @@ -25,22 +28,26 @@ export type ActionGroupWithCondition = ActionGroup & } ); -export interface AlertConditionsProps { +export interface AlertConditionsProps { headline?: string; - actionGroups: Array>; - onInitializeConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; - onResetConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; + actionGroups: Array>; + onInitializeConditionsFor?: ( + actionGroup: ActionGroupWithCondition + ) => void; + onResetConditionsFor?: ( + actionGroup: ActionGroupWithCondition + ) => void; includeBuiltInActionGroups?: boolean; } -export const AlertConditions = ({ +export const AlertConditions = ({ headline, actionGroups, onInitializeConditionsFor, onResetConditionsFor, includeBuiltInActionGroups = false, children, -}: PropsWithChildren>) => { +}: PropsWithChildren>) => { const [withConditions, withoutConditions] = partition( includeBuiltInActionGroups ? actionGroups diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx index 879f276317503..f92891750c468 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx @@ -9,8 +9,8 @@ import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui'; import { AlertConditionsProps, ActionGroupWithCondition } from './alert_conditions'; export type AlertConditionsGroupProps = { - actionGroup?: ActionGroupWithCondition; -} & Pick, 'onResetConditionsFor'>; + actionGroup?: ActionGroupWithCondition; +} & Pick, 'onResetConditionsFor'>; export const AlertConditionsGroup = ({ actionGroup, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 3ba50ae908631..0cf859ad64173 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -155,9 +155,11 @@ export const OPTIONAL_ACTION_VARIABLES = ['context'] as const; export type ActionVariables = AsActionVariables & Partial>; -export interface AlertType - extends Pick< - CommonAlertType, +export interface AlertType< + ActionGroupIds extends string = string, + RecoveryActionGroupId extends string = string +> extends Pick< + CommonAlertType, | 'id' | 'name' | 'actionGroups' @@ -184,7 +186,8 @@ export interface AlertTableItem extends Alert { export interface AlertTypeParamsExpressionProps< Params extends AlertTypeParams = AlertTypeParams, - MetaData = Record + MetaData = Record, + ActionGroupIds extends string = string > { alertParams: Params; alertInterval: string; @@ -196,7 +199,7 @@ export interface AlertTypeParamsExpressionProps< ) => void; errors: IErrorObject; defaultActionGroupId: string; - actionGroups: ActionGroup[]; + actionGroups: Array>; metadata?: MetaData; charts: ChartsPluginSetup; data: DataPublicPluginStart; diff --git a/x-pack/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts index 61a7a02bf8b30..afb3c82bcf496 100644 --- a/x-pack/plugins/uptime/common/constants/alerts.ts +++ b/x-pack/plugins/uptime/common/constants/alerts.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -interface ActionGroupDefinition { - id: string; - name: string; -} +import { ActionGroup } from '../../../alerts/common'; -type ActionGroupDefinitions = Record; - -export const ACTION_GROUP_DEFINITIONS: ActionGroupDefinitions = { +export const ACTION_GROUP_DEFINITIONS: { + MONITOR_STATUS: ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; + TLS: ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; + DURATION_ANOMALY: ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; +} = { MONITOR_STATUS: { id: 'xpack.uptime.alerts.actionGroups.monitorStatus', name: 'Uptime Down Monitor', diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index d5fd92476dd16..d88f3de902218 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -874,7 +874,13 @@ describe('status check alert', () => { }); describe('alert factory', () => { - let alert: AlertType; + let alert: AlertType< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'xpack.uptime.alerts.actionGroups.monitorStatus' + >; beforeEach(() => { const { server, libs, plugins } = bootstrapDependencies(); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index f5e79ad43336b..b8acf347a2a6d 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -7,6 +7,7 @@ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; +import { ActionGroupIdsOf } from '../../../../alerts/common'; import { updateState } from './common'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; @@ -20,6 +21,7 @@ import { getLatestMonitor } from '../requests/get_latest_monitor'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS; +export type ActionGroupIds = ActionGroupIdsOf; export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Ping) => { return { @@ -61,8 +63,12 @@ const getAnomalies = async ( ); }; -export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _libs, plugins) => - uptimeAlertWrapper({ +export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = ( + _server, + _libs, + plugins +) => + uptimeAlertWrapper({ id: 'xpack.uptime.alerts.durationAnomaly', name: durationAnomalyTranslations.alertFactoryName, validate: { diff --git a/x-pack/plugins/uptime/server/lib/alerts/index.ts b/x-pack/plugins/uptime/server/lib/alerts/index.ts index c8d3037f98aeb..0b4ff0b522396 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/index.ts @@ -5,12 +5,15 @@ */ import { UptimeAlertTypeFactory } from './types'; -import { statusCheckAlertFactory } from './status_check'; -import { tlsAlertFactory } from './tls'; -import { durationAnomalyAlertFactory } from './duration_anomaly'; - -export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [ - statusCheckAlertFactory, - tlsAlertFactory, +import { statusCheckAlertFactory, ActionGroupIds as statusCheckActionGroup } from './status_check'; +import { tlsAlertFactory, ActionGroupIds as tlsActionGroup } from './tls'; +import { durationAnomalyAlertFactory, -]; + ActionGroupIds as durationAnomalyActionGroup, +} from './duration_anomaly'; + +export const uptimeAlertTypeFactories: [ + UptimeAlertTypeFactory, + UptimeAlertTypeFactory, + UptimeAlertTypeFactory +] = [statusCheckAlertFactory, tlsAlertFactory, durationAnomalyAlertFactory]; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 56ca7a85784c5..1bcad155bd0dc 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; +import { ActionGroupIdsOf } from '../../../../alerts/common'; import { UptimeAlertTypeFactory } from './types'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; @@ -28,6 +29,7 @@ import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/g import { UMServerLibs, UptimeESClient } from '../lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; +export type ActionGroupIds = ActionGroupIdsOf; const getMonIdByLoc = (monitorId: string, location: string) => { return monitorId + '-' + location; @@ -178,8 +180,8 @@ const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { return `${urlText}_${monIdByLoc}`; }; -export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => - uptimeAlertWrapper({ +export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => + uptimeAlertWrapper({ id: 'xpack.uptime.alerts.monitorStatus', name: i18n.translate('xpack.uptime.alerts.monitorStatus', { defaultMessage: 'Uptime monitor status', diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index b6501f7d92059..f138e2799aa3c 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -14,8 +14,10 @@ import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; +import { ActionGroupIdsOf } from '../../../../alerts/common'; const { TLS } = ACTION_GROUP_DEFINITIONS; +export type ActionGroupIds = ActionGroupIdsOf; const DEFAULT_SIZE = 20; @@ -82,8 +84,8 @@ export const getCertSummary = ( }; }; -export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => - uptimeAlertWrapper({ +export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => + uptimeAlertWrapper({ id: 'xpack.uptime.alerts.tls', name: tlsTranslations.alertFactoryName, validate: { diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index d143e33fb8e96..36f5bc1973d33 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -10,7 +10,7 @@ import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../.. export type UptimeAlertTypeParam = Record; export type UptimeAlertTypeState = Record; -export type UptimeAlertTypeFactory = ( +export type UptimeAlertTypeFactory = ( server: UptimeCoreSetup, libs: UMServerLibs, plugins: UptimeCorePlugins @@ -18,5 +18,6 @@ export type UptimeAlertTypeFactory = ( UptimeAlertTypeParam, UptimeAlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIds >; diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts index a4a2f2c64db1b..85770144e7379 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts @@ -15,8 +15,8 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { createUptimeESClient, UptimeESClient } from '../lib'; import { UptimeAlertTypeFactory, UptimeAlertTypeParam, UptimeAlertTypeState } from './types'; -export interface UptimeAlertType - extends Omit, 'executor' | 'producer'> { +export interface UptimeAlertType + extends Omit>, 'executor' | 'producer'> { executor: ({ options, uptimeEsClient, @@ -26,7 +26,8 @@ export interface UptimeAlertType UptimeAlertTypeParam, UptimeAlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIds >; uptimeEsClient: UptimeESClient; dynamicSettings: DynamicSettings; @@ -34,7 +35,9 @@ export interface UptimeAlertType }) => Promise; } -export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({ +export const uptimeAlertWrapper = ( + uptimeAlert: UptimeAlertType +) => ({ ...uptimeAlert, producer: 'uptime', executor: async ( @@ -42,7 +45,8 @@ export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({ UptimeAlertTypeParam, UptimeAlertTypeState, AlertInstanceState, - AlertInstanceContext + AlertInstanceContext, + ActionGroupIds > ) => { const { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 30c19f735b75d..d5b952e991b30 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -61,7 +61,13 @@ function getAlwaysFiringAlertType() { interface InstanceContext extends AlertInstanceContext { instanceContextValue: boolean; } - const result: AlertType = { + const result: AlertType< + ParamsType & AlertTypeParams, + State, + InstanceState, + InstanceContext, + 'default' | 'other' + > = { id: 'test.always-firing', name: 'Test: Always Firing', actionGroups: [ @@ -149,7 +155,7 @@ function getCumulativeFiringAlertType() { interface InstanceState extends AlertInstanceState { instanceStateValue: boolean; } - const result: AlertType<{}, State, InstanceState, {}> = { + const result: AlertType<{}, State, InstanceState, {}, 'default' | 'other'> = { id: 'test.cumulative-firing', name: 'Test: Cumulative Firing', actionGroups: [ @@ -189,7 +195,7 @@ function getNeverFiringAlertType() { interface State extends AlertTypeState { globalStateValue: boolean; } - const result: AlertType = { + const result: AlertType = { id: 'test.never-firing', name: 'Test: Never firing', actionGroups: [ @@ -229,7 +235,7 @@ function getFailingAlertType() { reference: schema.string(), }); type ParamsType = TypeOf; - const result: AlertType = { + const result: AlertType = { id: 'test.failing', name: 'Test: Failing', validate: { @@ -271,7 +277,7 @@ function getAuthorizationAlertType(core: CoreSetup) { reference: schema.string(), }); type ParamsType = TypeOf; - const result: AlertType = { + const result: AlertType = { id: 'test.authorization', name: 'Test: Authorization', actionGroups: [ @@ -358,7 +364,7 @@ function getValidationAlertType() { param1: schema.string(), }); type ParamsType = TypeOf; - const result: AlertType = { + const result: AlertType = { id: 'test.validation', name: 'Test: Validation', actionGroups: [ @@ -390,7 +396,7 @@ function getPatternFiringAlertType() { interface State extends AlertTypeState { patternIndex?: number; } - const result: AlertType = { + const result: AlertType = { id: 'test.patternFiring', name: 'Test: Firing on a Pattern', actionGroups: [{ id: 'default', name: 'Default' }], @@ -454,7 +460,7 @@ export function defineAlertTypes( core: CoreSetup, { alerts }: Pick ) { - const noopAlertType: AlertType = { + const noopAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], @@ -463,7 +469,7 @@ export function defineAlertTypes( minimumLicenseRequired: 'basic', async executor() {}, }; - const goldNoopAlertType: AlertType = { + const goldNoopAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.gold.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], @@ -472,7 +478,7 @@ export function defineAlertTypes( minimumLicenseRequired: 'gold', async executor() {}, }; - const onlyContextVariablesAlertType: AlertType = { + const onlyContextVariablesAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.onlyContextVariables', name: 'Test: Only Context Variables', actionGroups: [{ id: 'default', name: 'Default' }], @@ -484,7 +490,7 @@ export function defineAlertTypes( }, async executor() {}, }; - const onlyStateVariablesAlertType: AlertType = { + const onlyStateVariablesAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.onlyStateVariables', name: 'Test: Only State Variables', actionGroups: [{ id: 'default', name: 'Default' }], @@ -496,7 +502,7 @@ export function defineAlertTypes( minimumLicenseRequired: 'basic', async executor() {}, }; - const throwAlertType: AlertType = { + const throwAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.throw', name: 'Test: Throw', actionGroups: [ @@ -512,7 +518,7 @@ export function defineAlertTypes( throw new Error('this alert is intended to fail'); }, }; - const longRunningAlertType: AlertType = { + const longRunningAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.longRunning', name: 'Test: Long Running', actionGroups: [ diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts index 3a81d41a2ca9c..f1821699642a7 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts @@ -6,13 +6,13 @@ import { CoreSetup } from 'src/core/server'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; -import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerts/server'; +import { AlertType } from '../../../../../../../plugins/alerts/server'; export function defineAlertTypes( core: CoreSetup, { alerts }: Pick ) { - const noopRestrictedAlertType: AlertType = { + const noopRestrictedAlertType: AlertType<{}, {}, {}, {}, 'default', 'restrictedRecovered'> = { id: 'test.restricted-noop', name: 'Test: Restricted Noop', actionGroups: [{ id: 'default', name: 'Default' }], @@ -20,16 +20,16 @@ export function defineAlertTypes( defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'restrictedRecovered', name: 'Restricted Recovery' }, - async executor({ services, params, state }: AlertExecutorOptions) {}, + async executor() {}, }; - const noopUnrestrictedAlertType: AlertType = { + const noopUnrestrictedAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.unrestricted-noop', name: 'Test: Unrestricted Noop', actionGroups: [{ id: 'default', name: 'Default' }], producer: 'alertsRestrictedFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', - async executor({ services, params, state }: AlertExecutorOptions) {}, + async executor() {}, }; alerts.registerType(noopRestrictedAlertType); alerts.registerType(noopUnrestrictedAlertType); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index cf09286fe1ba6..1e334051e4fee 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -17,7 +17,7 @@ export interface AlertingExampleDeps { features: FeaturesPluginSetup; } -export const noopAlertType: AlertType = { +export const noopAlertType: AlertType<{}, {}, {}, {}, 'default'> = { id: 'test.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], @@ -33,7 +33,9 @@ export const alwaysFiringAlertType: AlertType< globalStateValue: boolean; groupInSeriesIndex: number; }, - { instanceStateValue: boolean; globalStateValue: boolean; groupInSeriesIndex: number } + { instanceStateValue: boolean; globalStateValue: boolean; groupInSeriesIndex: number }, + never, + 'default' | 'other' > = { id: 'test.always-firing', name: 'Always Firing', @@ -61,7 +63,7 @@ export const alwaysFiringAlertType: AlertType< }, }; -export const failingAlertType: AlertType = { +export const failingAlertType: AlertType = { id: 'test.failing', name: 'Test: Failing', actionGroups: [ From 65acd6dfc17ce378733b81c1b53d5b47cebe9640 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 6 Jan 2021 12:24:19 +0100 Subject: [PATCH 04/21] Use `management.getTitle` for import summary (#87285) * fix import summary title * remove TODO --- .../import/import_saved_objects.test.ts | 71 +++++++++++++++--- .../import/import_saved_objects.ts | 31 ++++---- .../import/resolve_import_errors.test.ts | 74 ++++++++++++++++--- .../import/resolve_import_errors.ts | 9 ++- 4 files changed, 146 insertions(+), 39 deletions(-) diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index 77f49e336a7b9..294f716036f12 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -79,17 +79,18 @@ describe('#importSavedObjectsFromStream', () => { let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const setupOptions = (createNewCopies: boolean = false): SavedObjectsImportOptions => { + const setupOptions = ( + createNewCopies: boolean = false, + getTypeImpl: (name: string) => any = (type: string) => + ({ + // other attributes aren't needed for the purposes of injecting metadata + management: { icon: `${type}-icon` }, + } as any) + ): SavedObjectsImportOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - typeRegistry.getType.mockImplementation( - (type: string) => - ({ - // other attributes aren't needed for the purposes of injecting metadata - management: { icon: `${type}-icon` }, - } as any) - ); + typeRegistry.getType.mockImplementation(getTypeImpl); return { readStream, objectLimit, @@ -100,14 +101,17 @@ describe('#importSavedObjectsFromStream', () => { createNewCopies, }; }; - const createObject = (): SavedObject<{ + const createObject = ({ + type = 'foo-type', + title = 'some-title', + }: { type?: string; title?: string } = {}): SavedObject<{ title: string; }> => { return { - type: 'foo-type', + type, id: uuidv4(), references: [], - attributes: { title: 'some-title' }, + attributes: { title }, }; }; const createError = (): SavedObjectsImportError => { @@ -419,6 +423,51 @@ describe('#importSavedObjectsFromStream', () => { }); }); + test('uses `type.management.getTitle` to resolve the titles', async () => { + const obj1 = createObject({ type: 'foo' }); + const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + + const options = setupOptions(false, (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } + return { + management: { icon: `${type}-icon` }, + }; + }); + + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + pendingOverwrites: new Set(), + }); + getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [obj1, obj2] }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { + type: obj1.type, + id: obj1.id, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + }, + { + type: obj2.type, + id: obj2.id, + meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, + }, + ]; + + expect(result).toEqual({ + success: true, + successCount: 2, + successResults, + }); + }); + test('accumulates multiple errors', async () => { const options = setupOptions(); const errors = [createError(), createError(), createError(), createError(), createError()]; diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index 4530c7ff427da..fd169e92cc89a 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -111,20 +111,23 @@ export async function importSavedObjectsFromStream({ const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; - const successResults = createSavedObjectsResult.createdObjects.map( - ({ type, id, attributes: { title }, destinationId, originId }) => { - const meta = { title, icon: typeRegistry.getType(type)?.management?.icon }; - const attemptedOverwrite = pendingOverwrites.has(`${type}:${id}`); - return { - type, - id, - meta, - ...(attemptedOverwrite && { overwrite: true }), - ...(destinationId && { destinationId }), - ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), - }; - } - ); + const successResults = createSavedObjectsResult.createdObjects.map((createdObject) => { + const { type, id, destinationId, originId } = createdObject; + const getTitle = typeRegistry.getType(type)?.management?.getTitle; + const meta = { + title: getTitle ? getTitle(createdObject) : createdObject.attributes.title, + icon: typeRegistry.getType(type)?.management?.icon, + }; + const attemptedOverwrite = pendingOverwrites.has(`${type}:${id}`); + return { + type, + id, + meta, + ...(attemptedOverwrite && { overwrite: true }), + ...(destinationId && { destinationId }), + ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), + }; + }); const errorResults = errorAccumulator.map((error) => { const icon = typeRegistry.getType(error.type)?.management?.icon; const attemptedOverwrite = pendingOverwrites.has(`${error.type}:${error.id}`); diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index 51a48dc511e2a..a9dd00eb4ce92 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -89,18 +89,18 @@ describe('#importSavedObjectsFromStream', () => { const setupOptions = ( retries: SavedObjectsImportRetry[] = [], - createNewCopies: boolean = false + createNewCopies: boolean = false, + getTypeImpl: (name: string) => any = (type: string) => + ({ + // other attributes aren't needed for the purposes of injecting metadata + management: { icon: `${type}-icon` }, + } as any) ): SavedObjectsResolveImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); - typeRegistry.getType.mockImplementation( - (type: string) => - ({ - // other attributes aren't needed for the purposes of injecting metadata - management: { icon: `${type}-icon` }, - } as any) - ); + typeRegistry.getType.mockImplementation(getTypeImpl); + return { readStream, objectLimit, @@ -122,15 +122,16 @@ describe('#importSavedObjectsFromStream', () => { return { type: 'foo-type', id, overwrite, replaceReferences }; }; const createObject = ( - references?: SavedObjectReference[] + references?: SavedObjectReference[], + { type = 'foo-type', title = 'some-title' }: { type?: string; title?: string } = {} ): SavedObject<{ title: string; }> => { return { - type: 'foo-type', + type, id: uuidv4(), references: references || [], - attributes: { title: 'some-title' }, + attributes: { title }, }; }; const createError = (): SavedObjectsImportError => { @@ -267,7 +268,7 @@ describe('#importSavedObjectsFromStream', () => { expect(getImportIdMapForRetries).toHaveBeenCalledWith(getImportIdMapForRetriesParams); }); - test('splits objects to ovewrite from those not to overwrite', async () => { + test('splits objects to overwrite from those not to overwrite', async () => { const retries = [createRetry()]; const options = setupOptions(retries); const collectedObjects = [createObject()]; @@ -491,6 +492,55 @@ describe('#importSavedObjectsFromStream', () => { expect(result).toEqual({ success: false, successCount: 3, successResults, errors }); }); + test('uses `type.management.getTitle` to resolve the titles', async () => { + const obj1 = createObject([], { type: 'foo' }); + const obj2 = createObject([], { type: 'bar', title: 'bar-title' }); + + const options = setupOptions([], false, (type) => { + if (type === 'foo') { + return { + management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, + }; + } + return { + management: { icon: `${type}-icon` }, + }; + }); + + getMockFn(checkConflicts).mockResolvedValue({ + errors: [], + filteredObjects: [], + importIdMap: new Map(), + pendingOverwrites: new Set(), + }); + getMockFn(createSavedObjects) + .mockResolvedValueOnce({ errors: [], createdObjects: [obj1, obj2] }) + .mockResolvedValueOnce({ errors: [], createdObjects: [] }); + + const result = await resolveSavedObjectsImportErrors(options); + // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + const successResults = [ + { + type: obj1.type, + id: obj1.id, + overwrite: true, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + }, + { + type: obj2.type, + id: obj2.id, + overwrite: true, + meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, + }, + ]; + + expect(result).toEqual({ + success: true, + successCount: 2, + successResults, + }); + }); + test('accumulates multiple errors', async () => { const options = setupOptions(); const errors = [createError(), createError(), createError(), createError()]; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 2182d9252cd51..e1d7075b9371b 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -153,8 +153,13 @@ export async function resolveSavedObjectsImportErrors({ successCount += createdObjects.length; successResults = [ ...successResults, - ...createdObjects.map(({ type, id, attributes: { title }, destinationId, originId }) => { - const meta = { title, icon: typeRegistry.getType(type)?.management?.icon }; + ...createdObjects.map((createdObject) => { + const { type, id, destinationId, originId } = createdObject; + const getTitle = typeRegistry.getType(type)?.management?.getTitle; + const meta = { + title: getTitle ? getTitle(createdObject) : createdObject.attributes.title, + icon: typeRegistry.getType(type)?.management?.icon, + }; return { type, id, From b0a66da8f03db0b955a6e7a476706f29379a4d9d Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 6 Jan 2021 13:12:01 +0100 Subject: [PATCH 05/21] Remove support for setting server.host to '0' (breaking) (#87114) --- docs/setup/docker.asciidoc | 2 +- .../__snapshots__/http_config.test.ts.snap | 9 ++++++++ src/core/server/http/http_config.test.ts | 19 ++++++++-------- src/core/server/http/http_config.ts | 13 +++++------ .../templates/kibana_yml.template.ts | 2 +- .../server/config/create_config.test.ts | 22 ------------------- .../reporting/server/config/create_config.ts | 20 +++-------------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 29 insertions(+), 60 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 0dee112d15e86..5d79a81e0aa91 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -129,7 +129,7 @@ images: [horizontal] `server.name`:: `kibana` -`server.host`:: `"0"` +`server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 9b667f888771e..4545396c27b5e 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -24,6 +24,12 @@ Object { } `; +exports[`accepts valid hostnames 5`] = ` +Object { + "host": "0.0.0.0", +} +`; + exports[`basePath throws if appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; exports[`basePath throws if is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; @@ -105,6 +111,8 @@ Object { exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; +exports[`throws if invalid hostname 2`] = `"[host]: value 0 is not a valid hostname (use \\"0.0.0.0\\" to bind to all interfaces)"`; + exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; exports[`with compression accepts valid referrer whitelist 1`] = ` @@ -113,6 +121,7 @@ Array [ "8.8.8.8", "::1", "localhost", + "0.0.0.0", ] `; diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index b71763e8a2e14..b1b2ba5b295a7 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -22,8 +22,8 @@ import { config, HttpConfig } from './http_config'; import { CspConfig } from '../csp'; import { ExternalUrlConfig } from '../external_url'; -const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; -const invalidHostname = 'asdf$%^'; +const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost', '0.0.0.0']; +const invalidHostnames = ['asdf$%^', '0']; jest.mock('os', () => { const original = jest.requireActual('os'); @@ -48,11 +48,10 @@ test('accepts valid hostnames', () => { }); test('throws if invalid hostname', () => { - const httpSchema = config.schema; - const obj = { - host: invalidHostname, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + for (const host of invalidHostnames) { + const httpSchema = config.schema; + expect(() => httpSchema.validate({ host })).toThrowErrorMatchingSnapshot(); + } }); describe('requestId', () => { @@ -304,9 +303,9 @@ describe('with compression', () => { test('throws if invalid referrer whitelist', () => { const httpSchema = config.schema; - const invalidHostnames = { + const nonEmptyArray = { compression: { - referrerWhitelist: [invalidHostname], + referrerWhitelist: invalidHostnames, }, }; const emptyArray = { @@ -314,7 +313,7 @@ describe('with compression', () => { referrerWhitelist: [], }, }; - expect(() => httpSchema.validate(invalidHostnames)).toThrowErrorMatchingSnapshot(); + expect(() => httpSchema.validate(nonEmptyArray)).toThrowErrorMatchingSnapshot(); expect(() => httpSchema.validate(emptyArray)).toThrowErrorMatchingSnapshot(); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 61a9b5f04b23f..aa4db6f88d338 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -73,6 +73,11 @@ export const config = { host: schema.string({ defaultValue: 'localhost', hostname: true, + validate(value) { + if (value === '0') { + return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; + } + }, }), maxPayload: schema.byteSize({ defaultValue: '1048576b', @@ -195,13 +200,7 @@ export class HttpConfig { rawExternalUrlConfig: ExternalUrlConfig ) { this.autoListen = rawHttpConfig.autoListen; - // TODO: Consider dropping support for '0' in v8.0.0. This value is passed - // to hapi, which validates it. Prior to hapi v20, '0' was considered a - // valid host, however the validation logic internally in hapi was - // re-written for v20 and hapi no longer considers '0' a valid host. For - // details, see: - // https://github.com/elastic/kibana/issues/86716#issuecomment-749623781 - this.host = rawHttpConfig.host === '0' ? '0.0.0.0' : rawHttpConfig.host; + this.host = rawHttpConfig.host; this.port = rawHttpConfig.port; this.cors = rawHttpConfig.cors; this.customResponseHeaders = Object.entries(rawHttpConfig.customResponseHeaders ?? {}).reduce( diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 240ec6f4e9326..a849c6bf4992d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -29,7 +29,7 @@ function generator({ imageFlavor }: TemplateContext) { # Default Kibana configuration for docker target server.name: kibana - server.host: "0" + server.host: "0.0.0.0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} `); diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index 154a05742d747..9e533d9758521 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -117,28 +117,6 @@ describe('Reporting server createConfig$', () => { expect((mockLogger.warn as any).mock.calls.length).toBe(0); }); - it('show warning when kibanaServer.hostName === "0"', async () => { - mockInitContext = makeMockInitContext({ - encryptionKey: 'aaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaa', - kibanaServer: { hostname: '0' }, - }); - const mockConfig$: any = mockInitContext.config.create(); - const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); - - expect(result.kibanaServer).toMatchInlineSnapshot(` - Object { - "hostname": "0.0.0.0", - "port": 5601, - "protocol": "http", - } - `); - expect((mockLogger.warn as any).mock.calls.length).toBe(1); - expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ - `Found 'server.host: \"0\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically ` + - `to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.`, - ]); - }); - it('uses user-provided disableSandbox: false', async () => { mockInitContext = makeMockInitContext({ encryptionKey: '888888888888888888888888888888888', diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 2e07478c1663c..1f3d00540e81c 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -42,26 +42,12 @@ export function createConfig$( } const { kibanaServer: reportingServer } = config; const serverInfo = core.http.getServerInfo(); - // kibanaServer.hostname, default to server.host, don't allow "0" - let kibanaServerHostname = reportingServer.hostname + // kibanaServer.hostname, default to server.host + const kibanaServerHostname = reportingServer.hostname ? reportingServer.hostname : serverInfo.hostname; - if (kibanaServerHostname === '0') { - logger.warn( - i18n.translate('xpack.reporting.serverConfig.invalidServerHostname', { - defaultMessage: - `Found 'server.host: "0"' in Kibana configuration. This is incompatible with Reporting. ` + - `To enable Reporting to work, '{configKey}: 0.0.0.0' is being automatically to the configuration. ` + - `You can change the setting to 'server.host: 0.0.0.0' or add '{configKey}: 0.0.0.0' in kibana.yml to prevent this message.`, - values: { configKey: 'xpack.reporting.kibanaServer.hostname' }, - }) - ); - kibanaServerHostname = '0.0.0.0'; - } // kibanaServer.port, default to server.port - const kibanaServerPort = reportingServer.port - ? reportingServer.port - : serverInfo.port; // prettier-ignore + const kibanaServerPort = reportingServer.port ? reportingServer.port : serverInfo.port; // kibanaServer.protocol, default to server.protocol const kibanaServerProtocol = reportingServer.protocol ? reportingServer.protocol diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0411c2448bbfc..c340914b1d39c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15270,7 +15270,6 @@ "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化", "xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromiumサンドボックスは保護が強化されていますが、{osName} OSではサポートされていません。自動的に'{configKey}: true'を設定しています。", "xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromiumサンドボックスは保護が強化され、{osName} OSでサポートされています。自動的にChromiumサンドボックスを有効にしています。", - "xpack.reporting.serverConfig.invalidServerHostname": "Kibana構成で「server.host:\"0\"」が見つかりました。これはReportingと互換性がありません。レポートが動作するように、「{configKey}:0.0.0.0」が自動的に構成になります。設定を「server.host:0.0.0.0」に変更するか、kibana.ymlに「{configKey}:0.0.0.0'」を追加して、このメッセージが表示されないようにすることができます。", "xpack.reporting.serverConfig.osDetected": "OSは'{osName}'で実行しています", "xpack.reporting.serverConfig.randomEncryptionKey": "xpack.reporting.encryptionKeyのランダムキーを生成しています。再起動時にセッションが無効にならないようにするには、kibana.ymlでxpack.reporting.encryptionKeyを設定してください。", "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b045fadfea680..5739440c20435 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15287,7 +15287,6 @@ "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化", "xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromium 沙盒提供附加保护层,但不受 {osName} OS 支持。自动设置“{configKey}: true”。", "xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromium 沙盒提供附加保护层,受 {osName} OS 支持。自动启用 Chromium 沙盒。", - "xpack.reporting.serverConfig.invalidServerHostname": "在 Kibana 配置中找到“server.host:\"0\"”。其不与 Reporting 兼容。要使 Reporting 运行,“{configKey}:0.0.0.0”将自动添加到配置中。可以将该设置更改为“server.host:0.0.0.0”或在 kibana.yml 中添加“{configKey}:0.0.0.0”,以阻止此消息。", "xpack.reporting.serverConfig.osDetected": "正在以下 OS 上运行:“{osName}”", "xpack.reporting.serverConfig.randomEncryptionKey": "正在为 xpack.reporting.encryptionKey 生成随机密钥。要防止会话在重新启动时失效,请在 kibana.yml 中设置 xpack.reporting.encryptionKey", "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告", From 74731ef7dde042bf27374525e89222d1a6d67fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 6 Jan 2021 14:13:35 +0100 Subject: [PATCH 06/21] [Security Solution] Refactor Save Timeline modal to use useForm hook (#87103) --- .../cypress/screens/timeline.ts | 4 +- .../cypress/tasks/timeline.ts | 2 +- .../public/common/lib/telemetry/middleware.ts | 2 +- .../timeline/header/save_timeline_button.tsx | 1 - .../components/timeline/header/schema.ts | 22 ++ .../header/title_and_description.test.tsx | 91 ++++-- .../timeline/header/title_and_description.tsx | 273 ++++++++++-------- .../timeline/header/translations.ts | 28 ++ .../timeline/properties/helpers.test.tsx | 90 +----- .../timeline/properties/helpers.tsx | 148 +--------- .../components/timeline/properties/styles.tsx | 34 --- .../timeline/properties/translations.ts | 71 ----- .../timelines/store/timeline/actions.ts | 14 +- .../public/timelines/store/timeline/epic.ts | 9 +- .../timelines/store/timeline/helpers.ts | 31 +- .../timelines/store/timeline/reducer.test.ts | 54 +--- .../timelines/store/timeline/reducer.ts | 20 +- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- 19 files changed, 316 insertions(+), 606 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/schema.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index ea3c42e2650eb..c0299f5ab0c1c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -101,7 +101,7 @@ export const TIMELINE_DATA_PROVIDERS_EMPTY = export const TIMELINE_DESCRIPTION = '[data-test-subj="timeline-description"]'; -export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="timeline-description-textarea"]'; +export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-description"]'; export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]'; @@ -136,7 +136,7 @@ export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle" export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; -export const TIMELINE_TITLE_INPUT = '[data-test-subj="timeline-title-input"]'; +export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]'; export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 0361bf4b72b52..3196181f2a776 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -58,7 +58,7 @@ export const hostExistsQuery = 'host.name: *'; export const addDescriptionToTimeline = (description: string) => { cy.get(TIMELINE_EDIT_MODAL_OPEN_BUTTON).first().click(); - cy.get(TIMELINE_DESCRIPTION_INPUT).type(`${description}{enter}`); + cy.get(TIMELINE_DESCRIPTION_INPUT).type(description); cy.get(TIMELINE_DESCRIPTION_INPUT).invoke('val').should('equal', description); cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click(); cy.get(TIMELINE_TITLE_INPUT).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/middleware.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/middleware.ts index 87acdddf87ed7..13cb4d4808bff 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/middleware.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/middleware.ts @@ -12,7 +12,7 @@ import * as timelineActions from '../../../timelines/store/timeline/actions'; export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { if (timelineActions.endTimelineSaving.match(action)) { track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.TIMELINE_SAVED); - } else if (timelineActions.updateTitle.match(action)) { + } else if (timelineActions.updateTitleAndDescription.match(action)) { track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.TIMELINE_NAMED); } else if (timelineActions.showTimeline.match(action)) { if (action.payload.show) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx index 46898a8daaf89..cbf5920275a61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx @@ -63,7 +63,6 @@ export const SaveTimelineButton = React.memo( data-test-subj="save-timeline-modal-comp" closeSaveTimeline={closeSaveTimeline} initialFocus={initialFocus} - openSaveTimeline={openSaveTimeline} timelineId={timelineId} showWarning={initialFocus === 'title' && show} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/schema.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/schema.ts new file mode 100644 index 0000000000000..b26e3495416cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FIELD_TYPES, FormSchema, fieldValidators } from '../../../../shared_imports'; + +export const formSchema: FormSchema = { + title: { + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: fieldValidators.emptyField(''), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + validations: [], + }, +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx index 2b8ec62199478..8dfea797bbebc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; -import { TimelineTitleAndDescription } from './title_and_description'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../../../common/mock'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; +import { TimelineTitleAndDescription } from './title_and_description'; import * as i18n from './translations'; jest.mock('../../../../common/hooks/use_selector', () => ({ @@ -32,7 +34,6 @@ describe('TimelineTitleAndDescription', () => { const props = { initialFocus: 'title' as const, closeSaveTimeline: jest.fn(), - openSaveTimeline: jest.fn(), timelineId: 'timeline-1', onSaveTimeline: jest.fn(), updateTitle: jest.fn(), @@ -57,13 +58,17 @@ describe('TimelineTitleAndDescription', () => { }); test('show process bar while saving', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); }); test('Show correct header for save timeline modal', () => { - const component = shallow(); - expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual( i18n.SAVE_TIMELINE ); }); @@ -76,29 +81,39 @@ describe('TimelineTitleAndDescription', () => { title: 'my timeline', timelineType: TimelineType.template, }); - const component = shallow(); - expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual( i18n.SAVE_TIMELINE_TEMPLATE ); }); test('Show name field', () => { - const component = shallow(); - expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true); + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="save-timeline-title"]').exists()).toEqual(true); }); test('Show description field', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true); }); test('Show close button', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="close-button"]').exists()).toEqual(true); }); test('Show saveButton', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); }); }); @@ -133,13 +148,17 @@ describe('TimelineTitleAndDescription', () => { }); test('show process bar while saving', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); }); test('Show correct header for save timeline modal', () => { - const component = shallow(); - expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual( i18n.NAME_TIMELINE ); }); @@ -152,24 +171,32 @@ describe('TimelineTitleAndDescription', () => { title: 'my timeline', timelineType: TimelineType.template, }); - const component = shallow(); - expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual( i18n.NAME_TIMELINE_TEMPLATE ); }); test('Show name field', () => { - const component = shallow(); - expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true); + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="save-timeline-title"]').exists()).toEqual(true); }); test('Show description field', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true); }); test('Show saveButton', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); }); }); @@ -206,13 +233,17 @@ describe('TimelineTitleAndDescription', () => { }); test('Show EuiCallOut', () => { - const component = shallow(); + const component = mount(, { + wrappingComponent: TestProviders, + }); expect(component.find('[data-test-subj="save-timeline-callout"]').exists()).toEqual(true); }); test('Show discardTimelineButton', () => { - const component = shallow(); - expect(component.find('[data-test-subj="close-button"]').dive().text()).toEqual( + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="close-button"]').at(2).text()).toEqual( 'Discard Timeline' ); }); @@ -225,15 +256,17 @@ describe('TimelineTitleAndDescription', () => { title: 'my timeline', timelineType: TimelineType.template, }); - const component = shallow(); - expect(component.find('[data-test-subj="close-button"]').dive().text()).toEqual( + const component = mount(, { + wrappingComponent: TestProviders, + }); + expect(component.find('[data-test-subj="close-button"]').at(2).text()).toEqual( 'Discard Timeline Template' ); }); test('Show saveButton', () => { - const component = shallow(); - expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + const component = mount(); + expect(component.find('[data-test-subj="save-button"]').at(1).exists()).toEqual(true); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx index 87d4fcdb7075f..3b6404a383048 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from 'lodash/fp'; import { EuiButton, EuiFlexGroup, - EuiFormRow, EuiFlexItem, EuiOverlayMask, EuiModal, @@ -17,81 +17,87 @@ import { EuiProgress, EuiCallOut, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import styled from 'styled-components'; +import usePrevious from 'react-use/lib/usePrevious'; +import { getUseField, Field, Form, useForm } from '../../../../shared_imports'; import { TimelineId, TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; -import { TimelineInput } from '../../../store/timeline/actions'; -import { Description, Name } from '../properties/helpers'; import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; -import { TIMELINE_TITLE, DESCRIPTION, OPTIONAL } from '../properties/translations'; import { useCreateTimeline } from '../properties/use_create_timeline'; +import * as commonI18n from '../properties/translations'; import * as i18n from './translations'; +import { formSchema } from './schema'; +const CommonUseField = getUseField({ component: Field }); interface TimelineTitleAndDescriptionProps { closeSaveTimeline: () => void; initialFocus: 'title' | 'description'; - openSaveTimeline: () => void; timelineId: string; showWarning?: boolean; } -const Wrapper = styled(EuiModalBody)` - .euiFormRow { - max-width: none; - } - - .euiFormControlLayout { - max-width: none; - } - - .euiFieldText { - max-width: none; - } -`; - -Wrapper.displayName = 'Wrapper'; - -const usePrevious = (value: unknown) => { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; -}; - // when showWarning equals to true, // the modal is used as a reminder for users to save / discard // the unsaved timeline / template export const TimelineTitleAndDescription = React.memo( - ({ closeSaveTimeline, initialFocus, openSaveTimeline, timelineId, showWarning }) => { - // TODO: Refactor to use useForm() instead - const [isFormSubmitted, setFormSubmitted] = useState(false); + ({ closeSaveTimeline, initialFocus, timelineId, showWarning }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const timeline = useDeepEqualSelector((state) => getTimeline(state, timelineId)); - const { isSaving, status, title, timelineType } = timeline; + const { + isSaving, + description = '', + status, + title = '', + timelineType, + } = useDeepEqualSelector((state) => + pick( + ['isSaving', 'description', 'status', 'title', 'timelineType'], + getTimeline(state, timelineId) + ) + ); const prevIsSaving = usePrevious(isSaving); const dispatch = useDispatch(); const handleCreateNewTimeline = useCreateTimeline({ timelineId: TimelineId.active, timelineType: TimelineType.default, }); - const onSaveTimeline = useCallback( - (args: TimelineInput) => dispatch(timelineActions.saveTimeline(args)), - [dispatch] + + const handleSubmit = useCallback( + (titleAndDescription, isValid) => { + if (isValid) { + dispatch( + timelineActions.updateTitleAndDescription({ + id: timelineId, + ...titleAndDescription, + }) + ); + } + + return Promise.resolve(); + }, + [dispatch, timelineId] + ); + + const initialState = useMemo( + () => ({ + title, + description, + }), + [title, description] ); - const handleClick = useCallback(() => { - // TODO: Refactor action to take only title and description as params not the whole timeline - onSaveTimeline({ - ...timeline, - id: timelineId, - }); - setFormSubmitted(true); - }, [onSaveTimeline, timeline, timelineId]); + const { form } = useForm({ + id: 'timelineTitleAndDescriptionForm', + schema: formSchema, + onSubmit: handleSubmit, + options: { + stripEmptyFields: false, + }, + defaultValue: initialState, + }); + const { isSubmitted, isSubmitting, submit } = form; const handleCancel = useCallback(() => { if (showWarning) { @@ -109,34 +115,65 @@ export const TimelineTitleAndDescription = React.memo { - if (isFormSubmitted && !isSaving && prevIsSaving) { - closeSaveTimeline(); - } - }, [isFormSubmitted, isSaving, prevIsSaving, closeSaveTimeline]); - - const modalHeader = - status === TimelineStatus.draft - ? timelineType === TimelineType.template - ? i18n.SAVE_TIMELINE_TEMPLATE - : i18n.SAVE_TIMELINE - : timelineType === TimelineType.template - ? i18n.NAME_TIMELINE_TEMPLATE - : i18n.NAME_TIMELINE; - - const saveButtonTitle = - status === TimelineStatus.draft && showWarning - ? timelineType === TimelineType.template - ? i18n.SAVE_TIMELINE_TEMPLATE - : i18n.SAVE_TIMELINE - : i18n.SAVE; + const modalHeader = useMemo( + () => + status === TimelineStatus.draft + ? timelineType === TimelineType.template + ? i18n.SAVE_TIMELINE_TEMPLATE + : i18n.SAVE_TIMELINE + : timelineType === TimelineType.template + ? i18n.NAME_TIMELINE_TEMPLATE + : i18n.NAME_TIMELINE, + [status, timelineType] + ); + + const saveButtonTitle = useMemo( + () => + status === TimelineStatus.draft && showWarning + ? timelineType === TimelineType.template + ? i18n.SAVE_TIMELINE_TEMPLATE + : i18n.SAVE_TIMELINE + : i18n.SAVE, + [showWarning, status, timelineType] + ); const calloutMessage = useMemo(() => i18n.UNSAVED_TIMELINE_WARNING(timelineType), [ timelineType, ]); - const descriptionLabel = - status === TimelineStatus.draft ? `${DESCRIPTION} (${OPTIONAL})` : DESCRIPTION; + const descriptionLabel = useMemo(() => `${i18n.TIMELINE_DESCRIPTION} (${i18n.OPTIONAL})`, []); + + const titleFieldProps = useMemo( + () => ({ + 'aria-label': i18n.TIMELINE_TITLE, + autoFocus: initialFocus === 'title', + 'data-test-subj': 'save-timeline-title', + disabled: isSaving, + spellCheck: true, + placeholder: + timelineType === TimelineType.template + ? commonI18n.UNTITLED_TEMPLATE + : commonI18n.UNTITLED_TIMELINE, + }), + [initialFocus, isSaving, timelineType] + ); + + const descriptionFieldProps = useMemo( + () => ({ + 'aria-label': i18n.TIMELINE_DESCRIPTION, + autoFocus: initialFocus === 'description', + 'data-test-subj': 'save-timeline-description', + disabled: isSaving, + placeholder: commonI18n.DESCRIPTION, + }), + [initialFocus, isSaving] + ); + + useEffect(() => { + if (isSubmitted && !isSaving && prevIsSaving) { + closeSaveTimeline(); + } + }, [isSubmitted, isSaving, prevIsSaving, closeSaveTimeline]); return ( @@ -155,7 +192,7 @@ export const TimelineTitleAndDescription = React.memo{modalHeader} - + {showWarning && ( )} - - - + + - - - - - - + + + - - - - - - - - {closeModalText} - - - - - {saveButtonTitle} - - - - - + + + + + + + {closeModalText} + + + + + {saveButtonTitle} + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts index 1f01f1007231e..727c4e07b7a40 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts @@ -85,3 +85,31 @@ export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) => values: { timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline' }, defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?', }); + +export const TITLE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.titleTitle', + { + defaultMessage: 'Title', + } +); + +export const TIMELINE_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel', + { + defaultMessage: 'Title', + } +); + +export const TIMELINE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel', + { + defaultMessage: 'Description', + } +); + +export const OPTIONAL = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel', + { + defaultMessage: 'Optional', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index 3a75922ab72bd..b0b6b299af602 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -6,12 +6,8 @@ import React from 'react'; import { mount } from 'enzyme'; -import { Description, Name, NewTimeline, NewTimelineProps } from './helpers'; +import { NewTimeline, NewTimelineProps } from './helpers'; import { useCreateTimelineButton } from './use_create_timeline'; -import * as i18n from './translations'; -import { mockTimelineModel, TestProviders } from '../../../../common/mock'; -import { TimelineType } from '../../../../../common/types/timeline'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; jest.mock('../../../../common/hooks/use_selector'); @@ -86,87 +82,3 @@ describe('NewTimeline', () => { }); }); }); - -describe('Description', () => { - const props = { - description: 'xxx', - timelineId: 'timeline-1', - updateDescription: jest.fn(), - }; - - test('should render tooltip', () => { - const component = mount( - - - - ); - expect( - component.find('[data-test-subj="timeline-description-tool-tip"]').first().prop('content') - ).toEqual(i18n.DESCRIPTION_TOOL_TIP); - }); - - test('should render textarea if isTextArea is true', () => { - const testProps = { - ...props, - isTextArea: true, - }; - const component = mount( - - - - ); - expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( - true - ); - }); -}); - -describe('Name', () => { - const props = { - timelineId: 'timeline-1', - timelineType: TimelineType.default, - title: 'xxx', - updateTitle: jest.fn(), - }; - - beforeAll(() => { - (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); - }); - - test('should render tooltip', () => { - const component = mount( - - - - ); - expect( - component.find('[data-test-subj="timeline-title-tool-tip"]').first().prop('content') - ).toEqual(i18n.TITLE); - }); - - test('should render placeholder by timelineType - timeline', () => { - const component = mount( - - - - ); - expect( - component.find('[data-test-subj="timeline-title-input"]').first().prop('placeholder') - ).toEqual(i18n.UNTITLED_TIMELINE); - }); - - test('should render placeholder by timelineType - timeline template', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ - ...mockTimelineModel, - timelineType: TimelineType.template, - }); - const component = mount( - - - - ); - expect( - component.find('[data-test-subj="timeline-title-input"]').first().prop('placeholder') - ).toEqual(i18n.UNTITLED_TEMPLATE); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 299cf30fca394..4907ebad32fce 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -4,30 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiButton, EuiButtonIcon, EuiToolTip, EuiTextArea } from '@elastic/eui'; -import { pick } from 'lodash/fp'; +import { EuiBadge, EuiButton, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; -import { - useDeepEqualSelector, - useShallowEqualSelector, -} from '../../../../common/hooks/use_selector'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; -import { DescriptionContainer, NameField, NameWrapper } from './styles'; import * as i18n from './translations'; import { TimelineInput } from '../../../store/timeline/actions'; import { useCreateTimelineButton } from './use_create_timeline'; import { timelineDefaults } from '../../../store/timeline/defaults'; -export const historyToolTip = 'The chronological history of actions related to this timeline'; -export const streamLiveToolTip = 'Update the Timeline as new data arrives'; -export const newTimelineToolTip = 'Create a new timeline'; -export const TIMELINE_TITLE_CLASSNAME = 'timeline-title'; - const NotesCountBadge = (styled(EuiBadge)` margin-left: 5px; ` as unknown) as typeof EuiBadge; @@ -69,140 +59,6 @@ AddToFavoritesButtonComponent.displayName = 'AddToFavoritesButtonComponent'; export const AddToFavoritesButton = React.memo(AddToFavoritesButtonComponent); -interface DescriptionProps { - autoFocus?: boolean; - timelineId: string; - disableAutoSave?: boolean; - disableTooltip?: boolean; - disabled?: boolean; -} - -export const Description = React.memo( - ({ - autoFocus = false, - timelineId, - disableAutoSave = false, - disableTooltip = false, - disabled = false, - }) => { - const dispatch = useDispatch(); - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - - const description = useShallowEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).description - ); - - const onDescriptionChanged = useCallback( - (e) => { - dispatch( - timelineActions.updateDescription({ - id: timelineId, - description: e.target.value, - disableAutoSave, - }) - ); - }, - [dispatch, disableAutoSave, timelineId] - ); - - const inputField = useMemo( - () => ( - - ), - [autoFocus, description, onDescriptionChanged, disabled] - ); - - return ( - - {disableTooltip ? ( - inputField - ) : ( - - {inputField} - - )} - - ); - } -); -Description.displayName = 'Description'; - -interface NameProps { - autoFocus?: boolean; - disableAutoSave?: boolean; - disableTooltip?: boolean; - disabled?: boolean; - timelineId: string; -} - -export const Name = React.memo( - ({ - autoFocus = false, - disableAutoSave = false, - disableTooltip = false, - disabled = false, - timelineId, - }) => { - const dispatch = useDispatch(); - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - - const { title, timelineType } = useDeepEqualSelector((state) => - pick(['title', 'timelineType'], getTimeline(state, timelineId) ?? timelineDefaults) - ); - - const handleChange = useCallback( - (e) => - dispatch( - timelineActions.updateTitle({ id: timelineId, title: e.target.value, disableAutoSave }) - ), - [dispatch, timelineId, disableAutoSave] - ); - - const nameField = useMemo( - () => ( - - ), - [autoFocus, handleChange, timelineType, title, disabled] - ); - - return ( - - {disableTooltip ? ( - nameField - ) : ( - - {nameField} - - )} - - ); - } -); -Name.displayName = 'Name'; - export interface NewTimelineProps { closeGearMenu?: () => void; outline?: boolean; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx deleted file mode 100644 index c1f9b18f05c60..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx +++ /dev/null @@ -1,34 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFieldText } from '@elastic/eui'; -import styled from 'styled-components'; - -export const NameField = styled(EuiFieldText)` - .euiToolTipAnchor { - display: block; - } -`; -NameField.displayName = 'NameField'; - -export const NameWrapper = styled.div` - .euiToolTipAnchor { - display: block; - } -`; -NameWrapper.displayName = 'NameWrapper'; - -export const DescriptionContainer = styled.div` - .euiToolTipAnchor { - display: block; - } -`; -DescriptionContainer.displayName = 'DescriptionContainer'; - -export const LabelText = styled.div` - margin-left: 10px; -`; -LabelText.displayName = 'LabelText'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts index ad3aa4a4932e7..e62913c84726b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts @@ -13,10 +13,6 @@ export const TIMELINE_DESCRIPTION = i18n.translate( } ); -export const TITLE = i18n.translate('xpack.securitySolution.timeline.properties.titleTitle', { - defaultMessage: 'Title', -}); - export const ADD_TO_FAVORITES = i18n.translate( 'xpack.securitySolution.timeline.properties.addToFavoriteButtonLabel', { @@ -31,20 +27,6 @@ export const REMOVE_FROM_FAVORITES = i18n.translate( } ); -export const TIMELINE_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.properties.timelineTitleAriaLabel', - { - defaultMessage: 'Title', - } -); - -export const INSPECT_TIMELINE_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.properties.inspectTimelineTitle', - { - defaultMessage: 'Timeline', - } -); - export const UNTITLED_TIMELINE = i18n.translate( 'xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder', { @@ -66,49 +48,10 @@ export const DESCRIPTION = i18n.translate( } ); -export const DESCRIPTION_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.descriptionTooltip', - { - defaultMessage: 'A summary of the events and notes in this Timeline', - } -); - -export const HISTORY = i18n.translate('xpack.securitySolution.timeline.properties.historyLabel', { - defaultMessage: 'History', -}); - -export const IS_VIEWING = i18n.translate( - 'xpack.securitySolution.timeline.properties.isViewingTooltip', - { - defaultMessage: 'is viewing this Timeline', - } -); - export const NOTES = i18n.translate('xpack.securitySolution.timeline.properties.notesButtonLabel', { defaultMessage: 'Notes', }); -export const NOTES_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.notesToolTip', - { - defaultMessage: 'Add and review notes about this Timeline. Notes may also be added to events.', - } -); - -export const HISTORY_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.historyToolTip', - { - defaultMessage: 'The chronological history of actions related to this timeline', - } -); - -export const STREAM_LIVE_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.streamLiveToolTip', - { - defaultMessage: 'Update the Timeline as new data arrives', - } -); - export const NEW_TIMELINE = i18n.translate( 'xpack.securitySolution.timeline.properties.newTimelineButtonLabel', { @@ -171,17 +114,3 @@ export const ATTACH_TIMELINE_TO_CASE_TOOLTIP = i18n.translate( defaultMessage: 'Please provide a title for your timeline in order to attach it to a case', } ); - -export const STREAM_LIVE = i18n.translate( - 'xpack.securitySolution.timeline.properties.streamLiveButtonLabel', - { - defaultMessage: 'Stream Live', - } -); - -export const OPTIONAL = i18n.translate( - 'xpack.securitySolution.timeline.properties.timelineDescriptionOptional', - { - defaultMessage: 'Optional', - } -); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index aefeda04dd962..77eadc5a55ead 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -175,12 +175,6 @@ export const updateDataProviderType = actionCreator<{ providerId: string; }>('UPDATE_PROVIDER_TYPE'); -export const updateDescription = actionCreator<{ - id: string; - description: string; - disableAutoSave?: boolean; -}>('UPDATE_DESCRIPTION'); - export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); export const applyKqlFilterQuery = actionCreator<{ @@ -203,9 +197,11 @@ export const updateItemsPerPageOptions = actionCreator<{ itemsPerPageOptions: number[]; }>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); -export const updateTitle = actionCreator<{ id: string; title: string; disableAutoSave?: boolean }>( - 'UPDATE_TITLE' -); +export const updateTitleAndDescription = actionCreator<{ + description: string; + id: string; + title: string; +}>('UPDATE_TITLE_AND_DESCRIPTION'); export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( 'UPDATE_PAGE_INDEX' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 5b16a0d021a0c..58c2f5847bc81 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -60,7 +60,6 @@ import { updateDataProviderExcluded, updateDataProviderKqlQuery, updateDataProviderType, - updateDescription, updateKqlMode, updateProviders, updateRange, @@ -68,7 +67,7 @@ import { upsertColumn, updateIndexNames, updateTimeline, - updateTitle, + updateTitleAndDescription, updateAutoSaveMsg, setExcludedRowRendererIds, setFilters, @@ -105,13 +104,12 @@ const timelineActionsType = [ updateDataProviderExcluded.type, updateDataProviderKqlQuery.type, updateDataProviderType.type, - updateDescription.type, updateEventType.type, updateKqlMode.type, updateIndexNames.type, updateProviders.type, updateSort.type, - updateTitle.type, + updateTitleAndDescription.type, updateRange.type, upsertColumn.type, ]; @@ -181,8 +179,7 @@ export const createTimelineEpic = (): Epic< } else if ( timelineActionsType.includes(action.type) && !timelineObj.isLoading && - isItAtimelineAction(timelineId) && - !get('payload.disableAutoSave', action) + isItAtimelineAction(timelineId) ) { return true; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index f9f4622c9d63c..b8d1f0fe8bfa7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -602,46 +602,27 @@ export const updateTimelineColumns = ({ }; }; -interface UpdateTimelineDescriptionParams { - id: string; +interface UpdateTimelineTitleAndDescriptionParams { description: string; - timelineById: TimelineById; -} - -export const updateTimelineDescription = ({ - id, - description, - timelineById, -}: UpdateTimelineDescriptionParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), - }, - }; -}; - -interface UpdateTimelineTitleParams { id: string; title: string; timelineById: TimelineById; } -export const updateTimelineTitle = ({ +export const updateTimelineTitleAndDescription = ({ + description, id, title, timelineById, -}: UpdateTimelineTitleParams): TimelineById => { +}: UpdateTimelineTitleAndDescriptionParams): TimelineById => { const timeline = timelineById[id]; return { ...timelineById, [id]: { ...timeline, - title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), + description: description.trim(), + title: title.trim(), }, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 4ae271ed7a491..0733bf471a12c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -27,7 +27,6 @@ import { removeTimelineColumn, removeTimelineProvider, updateTimelineColumns, - updateTimelineDescription, updateTimelineItemsPerPage, updateTimelinePerPageOptions, updateTimelineProviderEnabled, @@ -37,7 +36,7 @@ import { updateTimelineRange, updateTimelineShowTimeline, updateTimelineSort, - updateTimelineTitle, + updateTimelineTitleAndDescription, upsertTimelineColumn, } from './helpers'; import { ColumnHeaderOptions, TimelineModel } from './model'; @@ -835,65 +834,40 @@ describe('Timeline', () => { }); }); - describe('#updateTimelineDescription', () => { - const newDescription = 'a new description'; - - test('should return a new reference and not the same reference', () => { - const update = updateTimelineDescription({ - id: 'foo', - description: newDescription, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the timeline description', () => { - const update = updateTimelineDescription({ - id: 'foo', - description: newDescription, - timelineById: timelineByIdMock, - }); - expect(update.foo.description).toEqual(newDescription); - }); - - test('should always trim all leading whitespace and allow only one trailing space', () => { - const update = updateTimelineDescription({ - id: 'foo', - description: ' breathing room ', - timelineById: timelineByIdMock, - }); - expect(update.foo.description).toEqual('breathing room '); - }); - }); - - describe('#updateTimelineTitle', () => { + describe('#updateTimelineTitleAndDescription', () => { const newTitle = 'a new title'; + const newDescription = 'breathing room'; test('should return a new reference and not the same reference', () => { - const update = updateTimelineTitle({ + const update = updateTimelineTitleAndDescription({ id: 'foo', + description: '', title: newTitle, timelineById: timelineByIdMock, }); expect(update).not.toBe(timelineByIdMock); }); - test('should update the timeline title', () => { - const update = updateTimelineTitle({ + test('should update the timeline title and description', () => { + const update = updateTimelineTitleAndDescription({ id: 'foo', + description: newDescription, title: newTitle, timelineById: timelineByIdMock, }); expect(update.foo.title).toEqual(newTitle); + expect(update.foo.description).toEqual(newDescription); }); - test('should always trim all leading whitespace and allow only one trailing space', () => { - const update = updateTimelineTitle({ + test('should always trim all leading whitespace', () => { + const update = updateTimelineTitleAndDescription({ id: 'foo', + description: ' breathing room ', title: ' room at the back ', timelineById: timelineByIdMock, }); - expect(update.foo.title).toEqual('room at the back '); + expect(update.foo.title).toEqual('room at the back'); + expect(update.foo.description).toEqual('breathing room'); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 2603c1c677956..cacd4db30b6a1 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { @@ -41,7 +42,6 @@ import { updateDataProviderExcluded, updateDataProviderKqlQuery, updateDataProviderType, - updateDescription, updateEventType, updateIndexNames, updateIsFavorite, @@ -56,7 +56,7 @@ import { updateSort, updateTimeline, updateTimelineGraphEventId, - updateTitle, + updateTitleAndDescription, upsertColumn, toggleModalSaveTimeline, } from './actions'; @@ -78,7 +78,6 @@ import { unPinTimelineEvent, updateExcludedRowRenderersIds, updateTimelineColumns, - updateTimelineDescription, updateTimelineIsFavorite, updateTimelineIsLive, updateTimelineItemsPerPage, @@ -94,7 +93,7 @@ import { updateTimelineRange, updateTimelineShowTimeline, updateTimelineSort, - updateTimelineTitle, + updateTimelineTitleAndDescription, upsertTimelineColumn, updateSavedQuery, updateGraphEventId, @@ -360,10 +359,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(updateDescription, (state, { id, description }) => ({ - ...state, - timelineById: updateTimelineDescription({ id, description, timelineById: state.timelineById }), - })) .case(updateEventType, (state, { id, eventType }) => ({ ...state, timelineById: updateTimelineEventType({ id, eventType, timelineById: state.timelineById }), @@ -380,9 +375,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: updateTimelineKqlMode({ id, kqlMode, timelineById: state.timelineById }), })) - .case(updateTitle, (state, { id, title, disableAutoSave }) => ({ + .case(updateTitleAndDescription, (state, { id, title, description }) => ({ ...state, - timelineById: updateTimelineTitle({ id, title, timelineById: state.timelineById }), + timelineById: updateTimelineTitleAndDescription({ + id, + title, + description, + timelineById: state.timelineById, + }), })) .case(updateProviders, (state, { id, providers }) => ({ ...state, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c340914b1d39c..4d15f444ad1a8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17878,24 +17878,13 @@ "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "既存のケースに添付...", "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "新しいケースに添付", "xpack.securitySolution.timeline.properties.descriptionPlaceholder": "説明", - "xpack.securitySolution.timeline.properties.descriptionTooltip": "このタイムラインのイベントのサマリーとメモ", "xpack.securitySolution.timeline.properties.existingCaseButtonLabel": "タイムラインを既存のケースに添付...", - "xpack.securitySolution.timeline.properties.historyLabel": "履歴", - "xpack.securitySolution.timeline.properties.historyToolTip": "このタイムラインに関連したアクションの履歴", - "xpack.securitySolution.timeline.properties.inspectTimelineTitle": "Timeline", - "xpack.securitySolution.timeline.properties.isViewingTooltip": "がこのタイムラインを表示しています", "xpack.securitySolution.timeline.properties.lockDatePickerDescription": "日付ピッカーをグローバル日付ピッカーにロック", "xpack.securitySolution.timeline.properties.lockDatePickerTooltip": "現在表示中のページとタイムラインの間の日付/時刻範囲の同期を無効にします", "xpack.securitySolution.timeline.properties.newCaseButtonLabel": "タイムラインを新しいケースに接続する", "xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel": "新規タイムラインテンプレートを作成", "xpack.securitySolution.timeline.properties.newTimelineButtonLabel": "新規タイムラインを作成", "xpack.securitySolution.timeline.properties.notesButtonLabel": "メモ", - "xpack.securitySolution.timeline.properties.notesToolTip": "このタイムラインに関するメモを追加して確認します。メモはイベントにも追加できます。", - "xpack.securitySolution.timeline.properties.streamLiveButtonLabel": "ライブストリーム", - "xpack.securitySolution.timeline.properties.streamLiveToolTip": "新しいデータが利用可能になるにつれタイムラインを更新します", - "xpack.securitySolution.timeline.properties.timelineDescription": "タイムラインの説明", - "xpack.securitySolution.timeline.properties.timelineTitleAriaLabel": "タイムラインのタイトル", - "xpack.securitySolution.timeline.properties.titleTitle": "タイトル", "xpack.securitySolution.timeline.properties.unlockDatePickerDescription": "日付ピッカーのグローバル日付ピッカーへのロックを解除", "xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "現在表示中のページとタイムラインの間の日付/時刻範囲の同期を有効にします", "xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "無題のテンプレート", @@ -17905,6 +17894,9 @@ "xpack.securitySolution.timeline.rangePicker.oneMonth": "1 か月", "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 週間", "xpack.securitySolution.timeline.rangePicker.oneYear": "1 年", + "xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel": "タイムラインの説明", + "xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel": "タイムラインのタイトル", + "xpack.securitySolution.timeline.saveTimeline.modal.titleTitle": "タイトル", "xpack.securitySolution.timeline.searchBoxPlaceholder": "例:{timeline}名、または説明", "xpack.securitySolution.timeline.searchOrFilter.customeIndexNames": "カスタム", "xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent": "すべてのデータソース", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5739440c20435..75dfeaaeeafac 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17896,24 +17896,13 @@ "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "附加到现有案例......", "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "附加到新案例", "xpack.securitySolution.timeline.properties.descriptionPlaceholder": "描述", - "xpack.securitySolution.timeline.properties.descriptionTooltip": "此时间线中的事件和备注摘要", "xpack.securitySolution.timeline.properties.existingCaseButtonLabel": "将时间线附加到现有案例......", - "xpack.securitySolution.timeline.properties.historyLabel": "历史记录", - "xpack.securitySolution.timeline.properties.historyToolTip": "按时间顺序排列的与此时间线相关的操作历史记录", - "xpack.securitySolution.timeline.properties.inspectTimelineTitle": "时间线", - "xpack.securitySolution.timeline.properties.isViewingTooltip": "正在查看此时间线", "xpack.securitySolution.timeline.properties.lockDatePickerDescription": "将日期选取器锁定到全局日期选取器", "xpack.securitySolution.timeline.properties.lockDatePickerTooltip": "禁用当前查看的页面与您的时间线之间的日期/时间范围同步", "xpack.securitySolution.timeline.properties.newCaseButtonLabel": "将时间线附加到新案例", "xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel": "创建新时间线模板", "xpack.securitySolution.timeline.properties.newTimelineButtonLabel": "创建新时间线", "xpack.securitySolution.timeline.properties.notesButtonLabel": "备注", - "xpack.securitySolution.timeline.properties.notesToolTip": "添加并审核此时间线的备注。也可以向事件添加备注。", - "xpack.securitySolution.timeline.properties.streamLiveButtonLabel": "实时流式传输", - "xpack.securitySolution.timeline.properties.streamLiveToolTip": "新数据到达时更新时间线", - "xpack.securitySolution.timeline.properties.timelineDescription": "时间线描述", - "xpack.securitySolution.timeline.properties.timelineTitleAriaLabel": "时间线标题", - "xpack.securitySolution.timeline.properties.titleTitle": "标题", "xpack.securitySolution.timeline.properties.unlockDatePickerDescription": "解除日期选取器与全局日期选取器的锁定", "xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "启用当前查看的页面与您的时间线之间的日期/时间范围同步", "xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "未命名模板", @@ -17923,6 +17912,9 @@ "xpack.securitySolution.timeline.rangePicker.oneMonth": "1 个月", "xpack.securitySolution.timeline.rangePicker.oneWeek": "1 周", "xpack.securitySolution.timeline.rangePicker.oneYear": "1 年", + "xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel": "时间线描述", + "xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel": "时间线标题", + "xpack.securitySolution.timeline.saveTimeline.modal.titleTitle": "标题", "xpack.securitySolution.timeline.searchBoxPlaceholder": "例如 {timeline} 名称或描述", "xpack.securitySolution.timeline.searchOrFilter.customeIndexNames": "定制", "xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent": "所有数据源", From 9b3e6455424356c9070492afb6ed57a84b022200 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Wed, 6 Jan 2021 08:18:51 -0500 Subject: [PATCH 07/21] Split up plugin dev docs from team docs. Add some welcome landing pages. (#87353) * Split up plugin dev docs from team docs. Add some welcome landing pages * update titles * sp * Update dev_docs/dev_welcome.mdx Co-authored-by: Brandon Kobel * Update dev_docs/api_welcome.mdx Co-authored-by: Brandon Kobel Co-authored-by: Brandon Kobel --- dev_docs/api_welcome.mdx | 14 ++++++++++++++ dev_docs/dev_welcome.mdx | 17 +++++++++++++++++ src/plugins/data/README.mdx | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 dev_docs/api_welcome.mdx create mode 100644 dev_docs/dev_welcome.mdx diff --git a/dev_docs/api_welcome.mdx b/dev_docs/api_welcome.mdx new file mode 100644 index 0000000000000..692a1bb2e582e --- /dev/null +++ b/dev_docs/api_welcome.mdx @@ -0,0 +1,14 @@ +--- +id: kibDevDocsApiWelcome +slug: /kibana-dev-docs/api-welcome +title: Welcome +summary: The home of automatically generated plugin API documentation using extracted TSDocs +date: 2021-01-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + +Welcome to Kibana's plugin API documentation. As a plugin developer, this is where you can +learn the details of every service you can take advantage of to help you build awe-inspiring creative solutions and applications! + +If you have any questions or issues, please reach out to the Kibana platform team or create an issue [here](https://github.com/elastic/kibana/issues). + diff --git a/dev_docs/dev_welcome.mdx b/dev_docs/dev_welcome.mdx new file mode 100644 index 0000000000000..cc185e689fa43 --- /dev/null +++ b/dev_docs/dev_welcome.mdx @@ -0,0 +1,17 @@ +--- +id: kibDevDocsWelcome +slug: /kibana-dev-docs/welcome +title: Welcome +summary: Build custom solutions and applications on top of Kibana. +date: 2021-01-02 +tags: ['kibana','dev', 'contributor'] +--- + +Welcome to Kibana's plugin developer documentation! + +Did you know that the vast majority of functionality built inside of Kibana is a plugin? A handful of core services hold the system together, +but it's our vast system of plugin developers that provide the amazing, out of the box, functionality you can use when building your own set of +custom utilities and applications. + +Browse the `Services` section to view all the plugins that offer functionality you can take advantage of, or check out the +`API documentation` to dig into the nitty gritty details of every public plugin API. diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx index 13bb8443ffef6..2448d5f22ced2 100644 --- a/src/plugins/data/README.mdx +++ b/src/plugins/data/README.mdx @@ -1,6 +1,6 @@ --- id: kibDataPlugin -slug: /kibana-dev-guide/services/data-plugin +slug: /kibana-dev-docs/services/data-plugin title: Data services image: https://source.unsplash.com/400x175/?Search summary: The data plugin contains services for searching, querying and filtering. From c012977eecfb427da618796724548d373bc00765 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 6 Jan 2021 08:22:20 -0500 Subject: [PATCH 08/21] [ILM] Update doc links (#87216) --- .../edit_policy/components/phases/cold_phase/cold_phase.tsx | 2 +- .../edit_policy/components/phases/hot_phase/hot_phase.tsx | 4 ++-- .../components/phases/shared_fields/forcemerge_field.tsx | 2 +- .../phases/shared_fields/set_priority_input_field.tsx | 2 +- .../components/phases/shared_fields/shrink_field.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 5eeb336ad1108..4e1ec76c52a77 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -178,7 +178,7 @@ export const ColdPhase: FunctionComponent = () => { id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText" defaultMessage="Make the index read-only and minimize its memory footprint." />{' '} - + } fullWidth diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index ae8fecd1a1958..d9976605393c7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -114,7 +114,7 @@ export const HotPhase: FunctionComponent = () => { defaultMessage="Learn more" /> } - docPath="indices-rollover-index.html" + docPath="ilm-rollover.html" />

@@ -165,7 +165,7 @@ export const HotPhase: FunctionComponent = () => { content={ } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index 69121cc2d1252..8776dbbbc7553 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -43,7 +43,7 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { id="xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableExplanationText" defaultMessage="Reduce the number of segments in your shard by merging smaller files and clearing deleted ones." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx index e5ec1d116ec6f..328587a379b76 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx @@ -36,7 +36,7 @@ export const SetPriorityInputField: FunctionComponent = ({ phase }) => { defaultMessage="Set the priority for recovering your indices after a node restart. Indices with higher priorities are recovered before indices with lower priorities." />{' '} - + } titleSize="xs" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index f1cfbeb3692f7..c5fc31d9839bd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -37,7 +37,7 @@ export const ShrinkField: FunctionComponent = ({ phase }) => { id="xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText" defaultMessage="Shrink the index into a new index with fewer primary shards." />{' '} - + } titleSize="xs" From 57afacfef221e020d1babe232d10d65c60e99902 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 6 Jan 2021 08:24:41 -0500 Subject: [PATCH 09/21] [ILM] Delete index_codec option if disabled in UI (#87267) --- .../form/deserializer_and_serializer.test.ts | 14 ++++++++++++++ .../edit_policy/form/serializer/serializer.ts | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index b494e87b0bf6f..d72dbb38f6c95 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -172,6 +172,20 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.actions.forcemerge).toBeUndefined(); }); + it('removes the index_codec option in the forcemerge action if it is disabled in the form', () => { + formInternal.phases.warm!.actions.forcemerge = { + max_num_segments: 22, + index_codec: 'best_compression', + }; + formInternal._meta.hot.bestCompression = false; + formInternal._meta.warm.bestCompression = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.forcemerge!.index_codec).toBeUndefined(); + expect(result.phases.warm!.actions.forcemerge!.index_codec).toBeUndefined(); + }); + it('removes the readonly action if it is disabled in hot', () => { formInternal._meta.hot.readonlyEnabled = false; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 2a7689b42554e..2a7c109fec950 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -65,6 +65,8 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete hotPhaseActions.forcemerge; } else if (_meta.hot.bestCompression) { hotPhaseActions.forcemerge!.index_codec = 'best_compression'; + } else { + delete hotPhaseActions.forcemerge!.index_codec; } if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { @@ -124,6 +126,8 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete warmPhase.actions.forcemerge; } else if (_meta.warm.bestCompression) { warmPhase.actions.forcemerge!.index_codec = 'best_compression'; + } else { + delete warmPhase.actions.forcemerge!.index_codec; } if (_meta.warm.readonlyEnabled) { From e68d9f3822cb72d35802f197958691777560ab96 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 6 Jan 2021 09:00:43 -0500 Subject: [PATCH 10/21] Fixes 500 error when using PKI authentication with an incomplete certificate chain (#86700) --- .../authentication/providers/pki.test.ts | 50 ++++++++++++-- .../server/authentication/providers/pki.ts | 65 ++++++++++++++++--- 2 files changed, 102 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index 763231f7fd0df..5ccf2ead0a8c8 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -42,6 +42,13 @@ function getMockPeerCertificate(chain: string[] | string) { // Imitate self-signed certificate that is issuer for itself. certificate.issuerCertificate = index === fingerprintChain.length - 1 ? certificate : {}; + // Imitate other fields for logging assertions + certificate.subject = 'mock subject'; + certificate.issuer = 'mock issuer'; + certificate.subjectaltname = 'mock subjectaltname'; + certificate.valid_from = 'mock valid_from'; + certificate.valid_to = 'mock valid_to'; + return certificate.issuerCertificate; }, mockPeerCertificate as Record @@ -59,6 +66,9 @@ function getMockSocket({ } = {}) { const socket = new TLSSocket(new Socket()); socket.authorized = authorized; + if (!authorized) { + socket.authorizationError = new Error('mock authorization error'); + } socket.getPeerCertificate = jest.fn().mockReturnValue(peerCertificate); return socket; } @@ -88,26 +98,58 @@ describe('PKIAuthenticationProvider', () => { function defineCommonLoginAndAuthenticateTests( operation: (request: KibanaRequest) => Promise ) { - it('does not handle requests without certificate.', async () => { + it('does not handle unauthorized requests.', async () => { const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ authorized: true }), + socket: getMockSocket({ + authorized: false, + peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'), + }), }); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Peer certificate chain: [{"subject":"mock subject","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]' + ); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Authentication is not possible since peer certificate was not authorized: Error: mock authorization error.' + ); }); - it('does not handle unauthorized requests.', async () => { + it('does not handle requests with a missing certificate chain.', async () => { const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }), + socket: getMockSocket({ authorized: true, peerCertificate: null }), }); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.logger.debug).toHaveBeenCalledWith('Peer certificate chain: []'); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Authentication is not possible due to missing peer certificate chain.' + ); + }); + + it('does not handle requests with an incomplete certificate chain.', async () => { + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + (peerCertificate as any).issuerCertificate = undefined; // This behavior has been observed, even though it's not valid according to the type definition + const request = httpServerMock.createKibanaRequest({ + socket: getMockSocket({ authorized: true, peerCertificate }), + }); + + await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Peer certificate chain: [{"subject":"mock subject","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]' + ); + expect(mockOptions.logger.debug).toHaveBeenCalledWith( + 'Authentication is not possible due to incomplete peer certificate chain.' + ); }); it('gets an access token in exchange to peer certificate chain and stores it in the state.', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 4bb0ddaa4ee65..5642a6feac2b5 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -40,6 +40,39 @@ function canStartNewSession(request: KibanaRequest) { return canRedirectRequest(request) && request.route.options.authRequired === true; } +/** + * Returns a stringified version of a certificate, including metadata + * @param peerCertificate DetailedPeerCertificate instance. + */ +function stringifyCertificate(peerCertificate: DetailedPeerCertificate) { + const { + subject, + issuer, + issuerCertificate, + subjectaltname, + valid_from: validFrom, + valid_to: validTo, + } = peerCertificate; + + // The issuerCertificate field can be three different values: + // * Object: In this case, the issuer certificate is an object + // * null: In this case, the issuer certificate is a null value; this should not happen according to the type definition but historically there was code in place to account for this + // * undefined: The issuer certificate chain is broken; this should not happen according to the type definition but we have observed this edge case behavior with certain client/server configurations + // This distinction can be useful for troubleshooting mutual TLS connection problems, so we include it in the stringified certificate that is printed to the debug logs. + // There are situations where a partial client certificate chain is accepted by Node, but we cannot verify the chain in Kibana because an intermediate issuerCertificate is undefined. + // If this happens, Kibana will reject the authentication attempt, and the client and/or server need to ensure that the entire CA chain is installed. + let issuerCertType: string; + if (issuerCertificate === undefined) { + issuerCertType = 'undefined'; + } else if (issuerCertificate === null) { + issuerCertType = 'null'; + } else { + issuerCertType = 'object'; + } + + return JSON.stringify({ subject, issuer, issuerCertType, subjectaltname, validFrom, validTo }); +} + /** * Provider that supports PKI request authentication. */ @@ -204,6 +237,10 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { private async authenticateViaPeerCertificate(request: KibanaRequest) { this.logger.debug('Trying to authenticate request via peer certificate chain.'); + // We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings. + const peerCertificate = request.socket.getPeerCertificate(true); + const { certificateChain, isChainIncomplete } = this.getCertificateChain(peerCertificate); + if (!request.socket.authorized) { this.logger.debug( `Authentication is not possible since peer certificate was not authorized: ${request.socket.authorizationError}.` @@ -211,14 +248,16 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { return AuthenticationResult.notHandled(); } - const peerCertificate = request.socket.getPeerCertificate(true); if (peerCertificate === null) { this.logger.debug('Authentication is not possible due to missing peer certificate chain.'); return AuthenticationResult.notHandled(); } - // We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings. - const certificateChain = this.getCertificateChain(peerCertificate); + if (isChainIncomplete) { + this.logger.debug('Authentication is not possible due to incomplete peer certificate chain.'); + return AuthenticationResult.notHandled(); + } + let result: { access_token: string; authentication: AuthenticationInfo }; try { result = await this.options.client.callAsInternalUser('shield.delegatePKI', { @@ -255,23 +294,31 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { */ private getCertificateChain(peerCertificate: DetailedPeerCertificate | null) { const certificateChain = []; + const certificateStrings = []; + let isChainIncomplete = false; let certificate: DetailedPeerCertificate | null = peerCertificate; - while (certificate !== null && Object.keys(certificate).length > 0) { + + while (certificate && Object.keys(certificate).length > 0) { certificateChain.push(certificate.raw.toString('base64')); + certificateStrings.push(stringifyCertificate(certificate)); // For self-signed certificates, `issuerCertificate` may be a circular reference. if (certificate === certificate.issuerCertificate) { this.logger.debug('Self-signed certificate is detected in certificate chain'); - certificate = null; + break; + } else if (certificate.issuerCertificate === undefined) { + // The chain is only considered to be incomplete if one or more issuerCertificate values is undefined; + // this is not an expected return value from Node, but it can happen in some edge cases + isChainIncomplete = true; + break; } else { + // Repeat the loop certificate = certificate.issuerCertificate; } } - this.logger.debug( - `Peer certificate chain consists of ${certificateChain.length} certificates.` - ); + this.logger.debug(`Peer certificate chain: [${certificateStrings.join(', ')}]`); - return certificateChain; + return { certificateChain, isChainIncomplete }; } } From 450d297ddf6a2b779019b369636b2c067a75f2ea Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 6 Jan 2021 09:13:57 -0500 Subject: [PATCH 11/21] Tweak timeout for failing cloud test (#86671) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test/functional/apps/monitoring/enable_monitoring/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js index 70f7e0559d034..659de2db31e71 100644 --- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js @@ -48,7 +48,7 @@ export default function ({ getService, getPageObjects }) { // Here we are checking that once Monitoring is enabled, // it moves on to the cluster overview page. - await retry.tryForTime(10000, async () => { + await retry.tryForTime(20000, async () => { // Click the refresh button await testSubjects.click('querySubmitButton'); expect(await clusterOverview.isOnClusterOverview()).to.be(true); From a9820a53f158f669fc898264cb570f4e9d4acf09 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 6 Jan 2021 06:40:38 -0800 Subject: [PATCH 12/21] Closes #85549 by always rendering the datepicker regardless of data (#87393) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/app/ServiceMap/index.test.tsx | 9 +++- .../components/app/ServiceMap/index.tsx | 43 +++++++++++-------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index 2f05842b6bdec..e7ce4bb24b38f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -5,6 +5,7 @@ */ import { render } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import { CoreStart } from 'kibana/public'; import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; @@ -15,6 +16,10 @@ import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_ap import { LicenseContext } from '../../../context/license/license_context'; import * as useFetcherModule from '../../../hooks/use_fetcher'; import { ServiceMap } from './'; +import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context'; +import { Router } from 'react-router-dom'; + +const history = createMemoryHistory(); const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -49,7 +54,9 @@ function createWrapper(license: License | null) { - {children} + + {children} + diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 48a7f8f77ab84..da4a8596970ec 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -39,21 +39,34 @@ const ServiceMapDatePickerFlexGroup = styled(EuiFlexGroup)` margin: 0; `; +function DatePickerSection() { + return ( + + + + + + ); +} + function PromptContainer({ children }: { children: ReactNode }) { return ( - - + + - {children} - - + + {children} + + + ); } @@ -137,11 +150,7 @@ export function ServiceMap({ return ( <> - - - - - +
Date: Wed, 6 Jan 2021 09:57:18 -0500 Subject: [PATCH 13/21] [DOCS] improving production documentation for task manager and alerting (#87484) * improving production documentation for task manager and alerting * calling it Kibana Task Manager to be more explicit * addressing PR feedback --- docs/settings/settings-xkb.asciidoc | 1 + docs/settings/task-manager-settings.asciidoc | 32 +++++++++++++++++++ .../alerting-getting-started.asciidoc | 2 +- ...erting-production-considerations.asciidoc} | 14 ++++---- docs/user/alerting/defining-alerts.asciidoc | 2 +- docs/user/alerting/index.asciidoc | 2 +- 6 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 docs/settings/task-manager-settings.asciidoc rename docs/user/alerting/{alerting-scale-performance.asciidoc => alerting-production-considerations.asciidoc} (65%) diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 9d9cc92401896..4a211976be8cf 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -19,5 +19,6 @@ include::logs-ui-settings.asciidoc[] include::ml-settings.asciidoc[] include::reporting-settings.asciidoc[] include::spaces-settings.asciidoc[] +include::task-manager-settings.asciidoc[] include::i18n-settings.asciidoc[] include::fleet-settings.asciidoc[] diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc new file mode 100644 index 0000000000000..507e54349276b --- /dev/null +++ b/docs/settings/task-manager-settings.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[task-manager-settings-kb]] +=== Task Manager settings in {kib} +++++ +Task Manager settings +++++ + +Task Manager runs background tasks by polling for work on an interval. You can configure its behavior to tune for performance and throughput. + +[float] +[[task-manager-settings]] +==== Task Manager settings + +[cols="2*<"] +|=== +| `xpack.task_manager.max_attempts` + | The maximum number of times a task will be attempted before being abandoned as failed. Defaults to 3. + +| `xpack.task_manager.poll_interval` + | How often, in milliseconds, the task manager will look for more work. Defaults to 3000 and cannot be lower than 100. + +| `xpack.task_manager.request_capacity` + | How many requests can Task Manager buffer before it rejects new requests. Defaults to 1000. + +| `xpack.task_manager.index` + | The name of the index used to store task information. Defaults to `.kibana_task_manager`. + + | `xpack.task_manager.max_workers` + | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. + + +|=== diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index cb2b9b19a0726..06370c64aedf8 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -57,7 +57,7 @@ Alert schedules are defined as an interval between subsequent checks, and can ra [IMPORTANT] ============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. ============================================== [float] diff --git a/docs/user/alerting/alerting-scale-performance.asciidoc b/docs/user/alerting/alerting-production-considerations.asciidoc similarity index 65% rename from docs/user/alerting/alerting-scale-performance.asciidoc rename to docs/user/alerting/alerting-production-considerations.asciidoc index 644a7143f8278..3a68e81879e24 100644 --- a/docs/user/alerting/alerting-scale-performance.asciidoc +++ b/docs/user/alerting/alerting-production-considerations.asciidoc @@ -1,10 +1,10 @@ [role="xpack"] -[[alerting-scale-performance]] -== Scale and performance +[[alerting-production-considerations]] +== Production considerations -{kib} alerting run both alert checks and actions as persistent background tasks. This has two major benefits: +{kib} alerting run both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: -* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. +* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by `xpack.task_manager.index` (defaults to `.kibana_task_manager`). It is important to have at least 1 replica of this index for production deployments, since if you lose this index all scheduled alerts and actions are also lost. * *Scaling*: multiple {kib} instances can read from and update the same task queue in {es}, allowing the alerting and action load to be distributed across instances. In cases where a {kib} instance no longer has capacity to run alert checks or actions, capacity can be increased by adding additional {kib} instances. [float] @@ -12,17 +12,19 @@ {kib} background tasks are managed by: -* Polling an {es} task index for overdue tasks at 3 second intervals. +* Polling an {es} task index for overdue tasks at 3 second intervals. This interval can be changed using the `xpack.task_manager.poll_interval` setting. * Tasks are then claiming them by updating them in the {es} index, using optimistic concurrency control to prevent conflicts. Each {kib} instance can run a maximum of 10 concurrent tasks, so a maximum of 10 tasks are claimed each interval. * Tasks are run on the {kib} server. * In the case of alerts which are recurring background checks, upon completion the task is scheduled again according to the <>. [IMPORTANT] ============================================== -Because tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: +Because by default tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: * Alerts use a small *check interval*. The lowest interval possible is 3 seconds, though intervals of 30 seconds or higher are recommended. * Many alerts or actions must be *run at once*. In this case pending tasks will queue in {es}, and be pulled 10 at a time from the queue at 3 second intervals. * *Long running tasks* occupy slots for an extended time, leaving fewer slots for other tasks. +For details on the settings that can influence the performance and throughput of Task Manager, see {task-manager-settings}. + ============================================== \ No newline at end of file diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index ffd72cc824336..94cca7f91494e 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -26,7 +26,7 @@ image::images/alert-flyout-general-details.png[alt='All alerts have name, tags, Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. -Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. +Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. Notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. [float] diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 56404d9a33b80..caef0c6e7332d 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -2,4 +2,4 @@ include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] -include::alerting-scale-performance.asciidoc[] +include::alerting-production-considerations.asciidoc[] From 086bc58a6512098b558995fb49362c3ce8899cf1 Mon Sep 17 00:00:00 2001 From: Kate Farrar Date: Wed, 6 Jan 2021 08:12:17 -0700 Subject: [PATCH 14/21] [Metrics UI] Responsive fixes for Legend Options / Bottom Drawer (#86009) Responsive fixes for Legend Options / Bottom Drawer --- .../components/timeline/timeline.tsx | 9 ++++++--- .../components/waffle/legend_controls.tsx | 17 +++++++++++------ .../components/waffle/palette_preview.tsx | 2 +- .../components/waffle/swatch_label.tsx | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index d66fd44feba56..f1e796ef8ba18 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -222,9 +222,9 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + - + @@ -240,7 +240,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + props.theme.eui.paddingSizes.s} ${(props) => props.theme.eui.paddingSizes.m}; + @media only screen and (max-width: 767px) { + margin-top: 30px; + } `; const TimelineChartContainer = euiStyled.div` diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 1c1baad30f473..f4da68d9dead7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -186,9 +186,8 @@ export const LegendControls = ({ button={buttonComponent} > Legend Options - + @@ -243,6 +240,10 @@ export const LegendControls = ({ checked={draftLegend.reverseColors} onChange={handleReverseColors} compressed + style={{ + position: 'relative', + top: '8px', + }} /> - + { }; const Swatch = euiStyled.div` - width: 16px; + width: 15px; height: 12px; flex: 0 0 auto; &:first-child { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx index ae64188f8a469..f4cec07b53b3b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/swatch_label.tsx @@ -14,7 +14,7 @@ export interface Props { export const SwatchLabel = ({ label, color }: Props) => { return ( - + From 6c87222e678734e57e69084319afeb264db4872a Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Wed, 6 Jan 2021 09:13:07 -0600 Subject: [PATCH 15/21] Updated favicons (#87271) * Replace favicons * update build process to swap favion version * Replace favicons with built, branded version at build time * update snapshot * Addressing feedback --- .../favicons/android-chrome-192x192.png | Bin 16433 -> 0 bytes .../favicons/android-chrome-256x256.png | Bin 22386 -> 0 bytes .../assets/favicons/apple-touch-icon.png | Bin 9914 -> 0 bytes .../assets/favicons/browserconfig.xml | 9 ----- .../assets/favicons/favicon-16x16.png | Bin 1280 -> 0 bytes .../assets/favicons/favicon-32x32.png | Bin 2231 -> 0 bytes .../assets/favicons/favicon.distribution.png | Bin 0 -> 5234 bytes .../assets/favicons/favicon.distribution.svg | 4 ++ .../core_app/assets/favicons/favicon.ico | Bin 15086 -> 0 bytes .../core_app/assets/favicons/favicon.png | Bin 0 -> 4335 bytes .../core_app/assets/favicons/favicon.svg | 4 ++ .../core_app/assets/favicons/manifest.json | 19 --------- .../assets/favicons/mstile-150x150.png | Bin 11497 -> 0 bytes .../assets/favicons/safari-pinned-tab.svg | 34 ----------------- .../integration_tests/static_assets.test.ts | 4 +- src/core/server/rendering/views/template.tsx | 30 ++------------- src/dev/build/build_distributables.ts | 1 + src/dev/build/tasks/copy_source_task.ts | 2 + src/dev/build/tasks/index.ts | 1 + src/dev/build/tasks/replace_favicon.ts | 36 ++++++++++++++++++ .../reset_session_page.test.tsx.snap | 2 +- .../authorization/reset_session_page.tsx | 32 +++------------- 22 files changed, 60 insertions(+), 118 deletions(-) delete mode 100644 src/core/server/core_app/assets/favicons/android-chrome-192x192.png delete mode 100644 src/core/server/core_app/assets/favicons/android-chrome-256x256.png delete mode 100644 src/core/server/core_app/assets/favicons/apple-touch-icon.png delete mode 100644 src/core/server/core_app/assets/favicons/browserconfig.xml delete mode 100644 src/core/server/core_app/assets/favicons/favicon-16x16.png delete mode 100644 src/core/server/core_app/assets/favicons/favicon-32x32.png create mode 100644 src/core/server/core_app/assets/favicons/favicon.distribution.png create mode 100644 src/core/server/core_app/assets/favicons/favicon.distribution.svg delete mode 100644 src/core/server/core_app/assets/favicons/favicon.ico create mode 100644 src/core/server/core_app/assets/favicons/favicon.png create mode 100644 src/core/server/core_app/assets/favicons/favicon.svg delete mode 100644 src/core/server/core_app/assets/favicons/manifest.json delete mode 100644 src/core/server/core_app/assets/favicons/mstile-150x150.png delete mode 100644 src/core/server/core_app/assets/favicons/safari-pinned-tab.svg create mode 100644 src/dev/build/tasks/replace_favicon.ts diff --git a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png deleted file mode 100644 index 18a86e5b95c46824357176c71e09c36d06e49946..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16433 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 Vz^;};CfEgFaNSs^R?9K^e*m@&8883< diff --git a/src/core/server/core_app/assets/favicons/apple-touch-icon.png b/src/core/server/core_app/assets/favicons/apple-touch-icon.png deleted file mode 100644 index 1ffeb0852a170fba8a13da8bc1b613164c86825c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9914 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 diff --git a/src/core/server/core_app/assets/favicons/browserconfig.xml b/src/core/server/core_app/assets/favicons/browserconfig.xml deleted file mode 100644 index b3930d0f04718..0000000000000 --- a/src/core/server/core_app/assets/favicons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #da532c - - - diff --git a/src/core/server/core_app/assets/favicons/favicon-16x16.png b/src/core/server/core_app/assets/favicons/favicon-16x16.png deleted file mode 100644 index 631f5b7c7d74b47897e3d4102962f511247ba9e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1280 zcmZ`%dsq@y6u)V&FSW<4G|fhlW+{ebFWlTLMa{R-v}uN+kS1YCD{c8v)|%6$v(?i1 zN@vOvj4C_A?*J8hNx zEh4Wpk$1X+#a8vXDLawM7nre&Wn-s`=lKs+Jt$?T9l1eBZyqXdm3)vgRbBR)z5}?j zzqjS_w&(L4i@F~4_8A7lR8YuADm%Am1^%5cYmWR{fhy!X7VvlE^KfOoB?1X&lx+SIoxWEZ$Hr|mSpSHI$h!> z@l#V**?^^2|2TdzeqtCXe(&?Fm%(!&Py!2=C z&h(|Q>!K6BULK1KMko;k%V<9Sp3B}W-e_U9a=1necY2YDkB2+nvX@8zMmW0wr_-Av z-BNHk#P#5>gS^8$TwEL@{349(Ia{~e+H6KzZ&+uu4!NzH&*d(yZh91_C4P1s_GgNfz8ZcveY58B9ch%?%=!`e)OGl)>nzI)n?ZD@pPw{L=ih_ zy8KyqM8uyB_2Xz~boIpx$&|sSmciEk?&C(1V_kGk(eP^PmAIfF01ndd^mDGb1o%_7 zG|w_#Xj<8XZeY`hUUw z#^6>c2u;R^(L$omBNEP1NwM)U2wGA?3?epxMg-tOo>XDF{LDA@_8oydzR%kbV2m_z zBAS>Z&3$jKH8w|h&|O-hjgc%of+u*{#^rN2a+mSw-bII;fZSpYO>toNEjR-Bcm{Y> I9w1!#2c?LkdjJ3c diff --git a/src/core/server/core_app/assets/favicons/favicon-32x32.png b/src/core/server/core_app/assets/favicons/favicon-32x32.png deleted file mode 100644 index bf94dfa995f37bd34ec2c67def99989dd84456f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2231 zcmV;o2uSydP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rf1RNI{4&bui`Tzh2SV=@dR9M5EmwS+wbrr`y z=l4ACb=h5(eHUJKmrJlLun3|8l}eGsG!T}WWjR_+$monkPEMnZP7M<|Nj5r_l2b$B zREn1z4aX5mGVk2lHFrT+Tv(Q6ukT*p`}_P(f4qzB71_>T&oj?6=lebPbA(BXs-Axg z1FM1SfK@;#pujocP2hRZgFsfqOsG%(Pw-{HJ$QW^Y@&XuChE~_Cy0v#Dly?@D!~Pv zn84G(FM(44B4goXVjy)uqCf;l04+cgSchhRWBXsP_BwWA+zfhhbT~#4uz<_~{bTxQ>ze*OwPcz)k$_8hRF`Bd8t{n1$~Xk01rhe+PQ!=$tU4zy;WnKH9pb;}jO`W6~S{c~)K6aqb zu{|$?7r(gl5eH45A=kD8?BG{GR*n8;bb3ZL=WXcUQ%knB7jX<2K?K}r2_~FI(`Sa< zevu*uJMcC(UONrg2s%8}V+fSm!BiyE)Hxzq6!6a~MNAi(?YcPYhez3qV+RfbtN;_r zYaE|ILL8Hjupd=LJquP55qz^FX!i8QyMJUdpy^Yfxfwvg@GtIA$pI7r>tsqWRCdQT zWE#$rI?+zLsgqo90{^TMH2HVX9EdYX0)R~e)){TII|N<{^3aF9$s1m1?Dl1WOP0-} z>Z(P|P)n#ki7z^ZT>D9eyKgcg{wRnqo5vJX^PY9{mdk^O+Xim>>D$d5?@5A!&k>3P zE)|6VY9a;P8YyCSU>5{n~+p)O8H<=lXVbAM1vajfgKwzUzHFFbHns2E2-G{Qm7;ZhGuF|Kt0Q zd_JDe{0YpLKrp~J_c!+i>$?*;W^9{uL~umd*A(Max)NXMmq7xc-UY&;Abxz&+ek$2 zVf)@j9(?IdJ~-FS&-c8=j#m$t=REHLV2QD6m(_L*;#s|*HPI|H)=iwNGXDvJbRmYLjZHs~1jHd+SIXhutS%s@B zi`jPd9RA(c!j?k^d8TJBvFJ7eRlmho_I1Qp25Lcbh;Ih|IX|I^>-fP_ukp-*dQ^vc zEcsy+iznHC^t1q9aD7HPXBPOQI>zRF2sX?s;jR_syxQ?0J3e@i{%nSKyV}@YSj_s0 zd8~{6kjk=KpyzLRgLSwwZ>O$j5%)d2kHe?VjS1^{-Ob&7ShZ0XWdBtaMc;V-L^o+K z?+FDQfg7vK*t~o?&o&=r*U6(~tcNQGRpoeJ4=4JM@Z9O+terWB4HcVM6!mkUsh_)_ zdYQ)do^ip?GYI-!j7Zi1J3duj*mCXsX{dr##cza7OQURF9%1LnI(9ZTkg*mS7APb6 zx0d(-yBgo;&Ug2-w>?I0BE{LB0gRcrTe^J8X8LawuI%N6DQx73s|v%t@m zD=N5gaWRh^JIvFkPT;9dm?K66BSL2?$rFtY)XXj8vkPaF&3RbWF?Eu&mZh_%b4|_M zB=8r>k6~PpucmVL-i}0ONlwzvLk)*`qrDX)7vJ{<;ohYysL~=Hef2QM+q!A%8zi32 zVuvQ+f*I4;e$zEvSvBiP&^rW1Y(fh?wJ&b1d#C!Z&5gK|?0Px1Y^lEgOwA7;xOKiqI5vl2cEC0@p`nI80akR9?9_ z7;&&_iD!E7G*`%|eP9>?0T>XZ_KMQ!_|4>IzN!)lgedU~-N`d!=taVF2{_?ypl<>K z=j~w6CnM4LqT*?IdhuScC=~^T_(;&3JU2wjVl~IqU^$o0+=z(t3eYutqxy)z|8_uq zEfNgBvZ-nb#XkQ><@0gw4>~SuW>iq*FVY2()s%+kFf~+8b>uT_So9;xi>S$f~hsFl})7wSL^FB&WLGT&Fg2JiXI)5?i z%I7o&jPqUK6`%x|PyT>y1x^FmN#2G){s_IVFOynd*V9#48|&m;GLBWHEEr;GNrcO% zO&=@^hW7$LV}kqdLNei>!?xuDkhQ?qfK^s)1P~*p8#o9&1MC9^CJPSmU)^&qV5d>O z0{{R3C3HntbYx+4WjbSWWnpw>05UK#FfA}NEiyP%GBY|dHaamhD=;!TFfiHS>3aYG z03~!qSaf7zbY(hiZ)9m^c>ppnGB7PLG%YeXR5CLfF7004NL({R$ zBOFHM__QdBa~!AIQ8G?yR}{q>e_s|waj*XSq1vtu?@Jb$(4GMTPl}?r#BnTi1}l(! z`A}__(OwY8$b|L)kaz|nb1biGC`-Oj$3wMU^!ZwvkO?h>Q8_*VffbR@B04oi?s-uZ zHxJeJK7#XcicDz7fHj_i$WrcvE8KzD$+#q$(2f9$d{Y#~haR53^0%*2Aa*8nM}Ww) zo>*iP6tSm$c11FwJ783fC$P#c1HBdgG+9eqiO&Yta7BJP{#4>f<+f4aT7EheA@CsROdhGgA0-g%|Hhi;}*VXG^yxOIaZ{; zYT=To)#Wnww0b7A0U-0za@oFx3>K|g(304S)U$_bI~P2)PRN8NFe=CAATxKF-r$D! zCXgDH;~9u;Sdw{zfT{O&5zy7=7S=jCQb$i{T;xehZFQrSP_nAbV6E<%H}&g7wVhd>Q`CUb zSXjgiQIR@ILgS)dfyl1X$rY^6yvdqc1c1>8M_tgOF6C`3aYIyu)`%=!;PVwe?+ce? zRE`%6Ke89{l?zzt?$KP6qtGW%0~hayr)srN%B?_z{|J1)iOkhd$gF@2;YY?x)harN zWesWp)D@ZA*>+H{nY*}AmN>8Rc9{BDhE&I8cd&BU9ZznD_IaiwIEOjI9SLS-Vh2{fwITMY9;)rB@1NpLw!@Rnd17~Xx<%@5;Xa(> zfuYVJXtW+PkeD)MNK5$KUg1-R=IrvQbzae>3J(hPuq=lt&)EQ3QpzYO%zSIpUI>kP z43qDwbl!81YQj13Z-ZAfn3!QTYr-+Zk~@Vj3vCLlb^($WR8W&u4F=$PnC@*_godhN z3c$RQ8G#{%Y@wMzdb}cDeGCqGfe#C93Jg96NLo%0o2 z^Jf3qArK%^R{9JzJK~VG(59s(RKEvGAI<)AJ49inr3p=yW8MW!DN~9|w$LpAD@`dL zUXoN|goe98AxKJ@vGip^w+FR@=a|8X&XBaw6mhD6*4Y#gCf5YqBTPohq%L)aB%!I3 z4>=Yg2ZTa^G1w5Fo~(t=giaTf+fkyhNeq&NZmin~Twk06LRpyZt*F}}LTJNNNG(aC z$N`}|kR+otX;D2|%0g3w4w;1`m%F^p${8pt-Ah7K2&p1a7js4YTZSDdLfh;Zvc}K| zP5E!xI=cgTEhTHDr3g*A6(O6EP!>At&yK)w-Nf7u5keb&ke4EreIQV!cFbf8?G{w< z-#ZH(Bgh9Sw<>TBYL)Jng$9~}EYYBV0Cf?ANr?#~$ciUP=*A!9t2TBIIUw|lAo8pv zp{XTYyh!B$C=EIj+An(PLsNwI+VcW35w&wB6WTAtR_NcQK3MI`8Oh1^p{1MxE)pYXm7$W;^El{QSdI91*h>m6d6zf(eYm{MA!Cin+@ zUbRQ?ww5%PjK+ny35`A%Hv(PwOC0a;Jv)$l4T5vE<4ov*pcA26OiSvDV~|@NUq|A(2(2GFRu(dvSNs>d5ns{qMGe;8WSYamMi=`bFzCI+=;kh6w4C1!X7NZDi93*lL z{QswaLpoe1xer$+`_FP{j9VmEMb=0POET{;N}|)wZV+52xr>Qb`v^LMELC8j>7NZh zPjucRm(Xh_nMLNJ6wmd{I>BA|;n!#fo8bsHLpm{c-RUZ6B(#Ok>~&%kR*g%IG|w&k@Q-LTn9=Rp+lmGIP^)sq2a^+ZFqmwQ&zP=lX@tFj2}w{EyMx}J6P-1EB!%2<0RLHK&&?-gBjK%Tj>@+ zT(c0!E3}Tqn(CoB1xC{zHD)mdemO9-1Xe$;Sg^6&=B7#;)@^n+y&^O|#N^n2?vvaF zzl>@mQ!)k#WdJA-UNU2pTBcLHdTY_pM;X+m?R`09p-uivsJSRzk*%}?K$iQ)B=fDV zIV<7dq1s-8*c^+kzT>;i6s;FNG-DRG@|#clTK_OA$4i5}_cpgPXoh7C&+S9CC5dSH zd8{~R-$EO$SkH@jq8bv}Cm^u4A)nEzBB}L;X|4a__|#7pnjzCAA#aV!@x3A7{UeX1 zcm@=&1v7EAgO+Ti7~wnSj4gXRjL&YoKe~lBStqYMwFQxVX(lv{nM>x&Hu9%h^&hJ3 z1U@$f9GlY09NdKrYu*v$VtFWF_HR=GCGfhh73bR zt*Ke9>`N1XIA9~oox93OFiHKXNzSV+G-aK4frtD^z%(RsD%rz0WeGvXV?R;DS*P>oK&?dqwP2T@dxVM&)>6 zQTqAOSEd-{6?eo-Uah1iLT?h^70vl=C6-}D?5mWBt;upG{kOns_9j48;*7D9KHU=& zp_APejV0D*cf~O>W&f!6_FdYutd)qSke z1DTi1jmI0*?}RjN#(1ax%h8b9WWirtJ8WI{4%J{wX!+3tfi72LUP4E@cB-l?Z{@8eyUQ>h+-&HAo_USO@vOu<r~um&p+t`_wpOZk_p3@4@b!_YML`ry0nWJF@!}{8NNJ zIWk0!K?cpYg~h%%CrhmIH?>{NrW)QRlsM;_*sU{=wmY&_Yk?k?Hc?qWbao4cO z{%u_Qi`bs8$n-}CV%MqlA8k_WXq3oxv%bbA%UOVY0FMmWz~~8Wnf-H4W*utj22uW{ z0u)2KmO`CcXEj=}tV>ZbL!WECNkS`?V}fO>3DNF?wv?qr8*Lu~&TrCjc4GlQeZKGS zw~*zmcVx#1Od&o9MQC+V{vpoWYu{%s5Xmo?L@_o!@o>IVNPb;X?-d@71p;<%qr~3G zavYF^R(&}P9r{-Ye4;_y>|YFk3vtzPP*Uy6aFvWcHP$3yO0Lwh9py(~#8mrhxg%M8 ztB@_U{%&0ZNE=x$rMh;Ki#4H5epM8tu-uXCZL3flXaVFO9RtWF+{RG)=^J`0Fe;@O zyh)=)_OF-0kCs}WI+*wsm7C=fh6az7o+*rWTfc?I+PWQ8XlSYR(~hb2=-e#FAa_{l zgf=x_d9`KS2P`z!WFmPjwQkcOT;aN>y}NBd?ty(GG_(1N*RgGu*}py&*s!1lkZ+g) zWYSXzU{EQUa=qX1`~M^XLlr?~^<;kOrgqN`)+H@8nI!vpfhLNu@j+-=b+?<=^Hm8%AMtXgJCy^jva`z1i^z>=fTam zVUU^RN@qfAC)9Q~b558z0<-+L39CJl`|MxBeGrYSxHCf60I_Ebi=9m4lbB#Z4ih#t zxz(Q%iW45d1=9$s5rf7~Ud{w?`L!CA(sg==cHBk6aEU#g+$q}%pX&M?DQgUw^OmFoC1D#wN9hu0*3 zA|W2Msb!3H3%8W*&x;Y-uuUuL%yJ0XG4?_y?5G@XK<-J4K=t6v8xtfg^dLtrM*!-& zzl92NiQ`;oZexwJ7N5UErnpsnCGB!#cmx7*$`Y=%sjQFc5>`^3;9Z(XcEueUOSWFY zB?(Q{MrR8x1IjvAhlqmzx6pTeAaK)3?jjRRVFO>ev+5O!(AkHU0YzxGjtpWwkqc6w zPVlb1_i9uaapx=-kzJ0oKLRxHet<@H6@GsGiDREaD@@sgN9%Y;q`F=l3)YIa=7ey@;S zj&wkgB)UYF)MPbDLQ@wpnlW)Hz|=lB;2uKn91tV4As;Wchpf9z28@6( zS@-0|K?IngRDW85P-EG!q`Kam1%?;1(L0M?80U=zCKMgMvzsKGP zjU-D&yyRUjSmJ}xCCQs)r%gDcuuF>20DHu3h7pvD=514~MrKoYC95u?ZQ+&B@Si9v zeTEM78D<3cpW$XSwJXrE@-?X;#1CXPvewu46h}?uw%--Z5CtQ2!CyXin1VD| zQadKIB!bVL4BD&_$(FB%m#O7-IdJ7;&LFdzpJKX}lBmb81$7aFTId?oJVOnGv(&K6 zbH-HOr-@Nm_2wRXpFCy&+f1yIKrDpj&Zr&?sV&@$gY&N;*$!~(g zwuMAJ$bKm?7sVx_B6MLtt9s)*6D0|zHea}Ky|4l3w7x9@dDC(1kszckttS zTd8N^ViZ}%q!A^d3wu%*s1KR-=|&^)yJA}90xQxqlernu61uPp6m>)wc^?9{D_vRq zHY=ZbsEC@-h5d4@BRXQkB2QY#X`N?a4+YT^y0A+y9KtQ8mIe!MpfJo(7i?QtbRA35 zq7Uc=p=)57Hf_XgLWM2#b00paCX-EDYQPn&a<5c|(C8JRi*7E6j;hvGp{;gyOSDqU z%Z`<5MRqaTqj!X^fn6Im#pWigkbc*XCY)$31TIl>=_19Pt#U{7me9rXvVPT~_&he$ zTl{abRqlpf6S@Xc=2tzP3UKRdi~kUi1fgqy*wY?Z?AjvFQ%T#$f}{vt1FpyvM@%yj zHz4vNTVxp|NoXCYUaJpR0i+GQ%7sHm@`Tm_$!`K>LdxIXX_DX~yA*ygC_?W81V4xD scCN{B97|s9NqmpHs55~Bq9}_00e?uvBRsGQ9smFU07*qoM6N<$f<%wLlK=n! literal 0 HcmV?d00001 diff --git a/src/core/server/core_app/assets/favicons/favicon.distribution.svg b/src/core/server/core_app/assets/favicons/favicon.distribution.svg new file mode 100644 index 0000000000000..2d02461a0b8f9 --- /dev/null +++ b/src/core/server/core_app/assets/favicons/favicon.distribution.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/favicons/favicon.ico b/src/core/server/core_app/assets/favicons/favicon.ico deleted file mode 100644 index db30798a6cf32b57fb7449e5618fd75383c42ca8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 diff --git a/src/core/server/core_app/assets/favicons/favicon.png b/src/core/server/core_app/assets/favicons/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..cba7a268c6c59ece07a6535c8aa47dc66c506bed GIT binary patch literal 4335 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 + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/favicons/manifest.json b/src/core/server/core_app/assets/favicons/manifest.json deleted file mode 100644 index de65106f489b7..0000000000000 --- a/src/core/server/core_app/assets/favicons/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/src/core/server/core_app/assets/favicons/mstile-150x150.png b/src/core/server/core_app/assets/favicons/mstile-150x150.png deleted file mode 100644 index 82769c1ef242b5a34fe8976bc8202e7f908f9c01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11497 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 - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index ca03c4228221f..45e7b79b5d5e6 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -34,11 +34,11 @@ describe('Platform assets', function () { }); it('exposes static assets', async () => { - await kbnTestServer.request.get(root, '/ui/favicons/favicon.ico').expect(200); + await kbnTestServer.request.get(root, '/ui/favicons/favicon.svg').expect(200); }); it('returns 404 if not found', async function () { - await kbnTestServer.request.get(root, '/ui/favicons/not-a-favicon.ico').expect(404); + await kbnTestServer.request.get(root, '/ui/favicons/not-a-favicon.svg').expect(404); }); it('does not expose folder content', async function () { diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index 76af229ac02ba..e4787ee26e12c 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -76,33 +76,11 @@ export const Template: FunctionComponent = ({ Elastic - {/* Favicons (generated from http://realfavicongenerator.net/) */} - - - - - - - + {/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */} + + + {/* Inject stylesheets into the before scripts so that KP plugins with bundled styles will override them */} diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 1081d5d0d6dbd..4613303808f8e 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -61,6 +61,7 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions */ await run(Tasks.CopySource); await run(Tasks.CopyBinScripts); + await run(Tasks.ReplaceFavicon); await run(Tasks.CreateEmptyDirsAndFiles); await run(Tasks.CreateReadme); await run(Tasks.BuildPackages); diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 710e504e58868..038ccba5ed17e 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -31,6 +31,8 @@ export const CopySource: Task = { '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', + '!src/core/server/core_app/assets/favicons/favicon.distribution.png', + '!src/core/server/core_app/assets/favicons/favicon.distribution.svg', '!src/test_utils/**', '!src/fixtures/**', '!src/cli/repl/**', diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index ec0de7ca84aad..ca10fcca80498 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -38,6 +38,7 @@ export * from './transpile_babel_task'; export * from './uuid_verification_task'; export * from './verify_env_task'; export * from './write_sha_sums_task'; +export * from './replace_favicon'; // @ts-expect-error this module can't be TS because it ends up pulling x-pack into Kibana export { InstallChromium } from './install_chromium'; diff --git a/src/dev/build/tasks/replace_favicon.ts b/src/dev/build/tasks/replace_favicon.ts new file mode 100644 index 0000000000000..bdf5764b0f4e7 --- /dev/null +++ b/src/dev/build/tasks/replace_favicon.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { copy, Task } from '../lib'; + +export const ReplaceFavicon: Task = { + description: 'Replacing favicons with built version', + + async run(config, log, build) { + await copy( + config.resolveFromRepo('src/core/server/core_app/assets/favicons/favicon.distribution.png'), + build.resolvePath('src/core/server/core_app/assets/favicons/favicon.png') + ); + + await copy( + config.resolveFromRepo('src/core/server/core_app/assets/favicons/favicon.distribution.svg'), + build.resolvePath('src/core/server/core_app/assets/favicons/favicon.svg') + ); + }, +}; diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index 75886b4573edd..6c6782f800ca6 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts"`; +exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.tsx index 5ab6fe941ae19..196191df4b655 100644 --- a/x-pack/plugins/security/server/authorization/reset_session_page.tsx +++ b/x-pack/plugins/security/server/authorization/reset_session_page.tsx @@ -46,37 +46,15 @@ export function ResetSessionPage({ ))} - - - - - - -