diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 33d693d3598b2..0c835458580af 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27232,7 +27232,6 @@ "xpack.uptime.alerts.monitorStatus.timerangeValueField.expression": "within", "xpack.uptime.alerts.monitorStatus.timerangeValueField.value": "最終{value}", "xpack.uptime.alerts.searchPlaceholder.kql": "KQL構文を使用してフィルタリング", - "xpack.uptime.alerts.settings.createConnector": "コネクターを作成", "xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel": "「日」の時間範囲選択項目", "xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel": "「時間」の時間範囲選択項目", "xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel": "「分」の時間範囲選択項目", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 416a2187fac95..8b8be507a8821 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27702,7 +27702,6 @@ "xpack.uptime.alerts.monitorStatus.timerangeValueField.expression": "之内", "xpack.uptime.alerts.monitorStatus.timerangeValueField.value": "上一 {value}", "xpack.uptime.alerts.searchPlaceholder.kql": "使用 kql 语法筛选", - "xpack.uptime.alerts.settings.createConnector": "创建连接器", "xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel": "“天”时间范围选择项", "xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel": "“小时”时间范围选择项", "xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel": "“分钟”时间范围选择项", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index 1187a736496ed..0f894e011d3ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -21,6 +21,9 @@ export const EmailParamsFields = ({ errors, messageVariables, defaultMessage, + isLoading, + isDisabled, + showEmailSubjectAndMessage = true, }: ActionParamsProps) => { const { to, cc, bcc, subject, message } = actionParams; const toOptions = to ? to.map((label: string) => ({ label })) : []; @@ -65,7 +68,7 @@ export const EmailParamsFields = ({ labelAppend={ <> - {!addCC ? ( + {!addCC && (!cc || cc?.length === 0) ? ( setAddCC(true)}> ) : null} - {!addBCC ? ( + {!addBCC && (!bcc || bcc?.length === 0) ? ( setAddBCC(true)}> - {addCC ? ( + {addCC || (cc && cc?.length > 0) ? ( ) : null} - {addBCC ? ( + {addBCC || (bcc && bcc?.length > 0) ? ( ) : null} - - + + + )} + {showEmailSubjectAndMessage && ( + - - + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index bfb1c9b6280c3..904aba2ca5984 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -106,6 +106,9 @@ export interface ActionParamsProps { messageVariables?: ActionVariable[]; defaultMessage?: string; actionConnector?: ActionConnector; + isLoading?: boolean; + isDisabled?: boolean; + showEmailSubjectAndMessage?: boolean; } export interface Pagination { diff --git a/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts index d7d20361bea96..d3f1f1d2886e9 100644 --- a/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -7,12 +7,27 @@ import * as t from 'io-ts'; -export const DynamicSettingsType = t.strict({ - heartbeatIndices: t.string, - certAgeThreshold: t.number, - certExpirationThreshold: t.number, - defaultConnectors: t.array(t.string), -}); +const DefaultEmailType = t.intersection([ + t.type({ + to: t.array(t.string), + }), + t.partial({ + cc: t.array(t.string), + bcc: t.array(t.string), + }), +]); + +export const DynamicSettingsType = t.intersection([ + t.strict({ + heartbeatIndices: t.string, + certAgeThreshold: t.number, + certExpirationThreshold: t.number, + defaultConnectors: t.array(t.string), + }), + t.partial({ + defaultEmail: DefaultEmailType, + }), +]); export const DynamicSettingsSaveType = t.intersection([ t.type({ @@ -24,4 +39,5 @@ export const DynamicSettingsSaveType = t.intersection([ ]); export type DynamicSettings = t.TypeOf; +export type DefaultEmail = t.TypeOf; export type DynamicSettingsSaveResponse = t.TypeOf; diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/default_email_settings.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/default_email_settings.ts new file mode 100644 index 0000000000000..e518b9be48ac9 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/default_email_settings.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, before } from '@elastic/synthetics'; +import { + assertNotText, + assertText, + byTestId, + loginToKibana, + waitForLoadingToFinish, +} from '../utils'; +import { settingsPageProvider } from '../../page_objects/settings'; + +journey('DefaultEmailSettings', async ({ page, params }) => { + const settings = settingsPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const queryParams = new URLSearchParams({ + dateRangeStart: '2021-11-21T22:06:06.502Z', + dateRangeEnd: '2021-11-21T22:10:08.203Z', + }).toString(); + + const baseUrl = `${params.kibanaUrl}/app/uptime/settings`; + + step('Go to uptime', async () => { + await page.goto(`${baseUrl}?${queryParams}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('clear existing settings', async () => { + await settings.dismissSyntheticsCallout(); + await page.waitForSelector(byTestId('"default-connectors-input-loaded"')); + await page.waitForTimeout(10 * 1000); + const toEmailInput = await page.$(byTestId('toEmailAddressInput')); + + if (toEmailInput !== null) { + await page.click(`${byTestId('toEmailAddressInput')} >> ${byTestId('comboBoxClearButton')}`); + await page.click( + `${byTestId('"default-connectors-input-loaded"')} >> ${byTestId('comboBoxClearButton')}` + ); + await settings.saveSettings(); + } + }); + + step('Add email connector', async () => { + await page.click(byTestId('createConnectorButton')); + await page.click(byTestId('".email-card"')); + await page.fill(byTestId('nameInput'), 'Test connector'); + await page.fill(byTestId('emailFromInput'), 'test@gmail.com'); + + await page.selectOption(byTestId('emailServiceSelectInput'), 'other'); + await page.fill(byTestId('emailHostInput'), 'test'); + await page.fill(byTestId('emailPortInput'), '1025'); + await page.click('text=Require authentication for this server'); + await page.click(byTestId('saveNewActionButton')); + }); + + step('Select email connector', async () => { + await assertNotText({ page, text: 'Bcc' }); + await page.click(byTestId('default-connectors-input-loaded')); + await page.click(byTestId('"Test connector"')); + + await assertText({ page, text: 'Bcc' }); + + await settings.assertText({ text: 'To email is required for email connector' }); + + await settings.assertApplyDisabled(); + + await settings.fillToEmail('test@gmail.com'); + + await settings.assertApplyEnabled(); + }); + + step('Checks for invalid email', async () => { + await settings.fillToEmail('test@gmail'); + + await settings.assertText({ text: 'test@gmail is not a valid email.' }); + + await settings.assertApplyDisabled(); + await settings.removeInvalidEmail('test@gmail'); + }); + + step('Save settings', async () => { + await settings.saveSettings(); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts index d8746d715581d..b810603f63397 100644 --- a/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts @@ -7,3 +7,4 @@ export * from './tls_alert_flyouts_in_alerting_app'; export * from './status_alert_flyouts_in_alerting_app'; +export * from './default_email_settings'; diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts index a1bc7eea29ffe..8dbc2699a438f 100644 --- a/x-pack/plugins/uptime/e2e/journeys/utils.ts +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -40,3 +40,7 @@ export const assertText = async ({ page, text }: { page: Page; text: string }) = await page.waitForSelector(`text=${text}`); expect(await page.$(`text=${text}`)).toBeTruthy(); }; + +export const assertNotText = async ({ page, text }: { page: Page; text: string }) => { + expect(await page.$(`text=${text}`)).toBeFalsy(); +}; diff --git a/x-pack/plugins/uptime/e2e/page_objects/login.tsx b/x-pack/plugins/uptime/e2e/page_objects/login.tsx index 79d9af39f1c4f..fa2abc525ee82 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/login.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/login.tsx @@ -8,12 +8,12 @@ import { Page } from '@elastic/synthetics'; export function loginPageProvider({ page, - isRemote, + isRemote = false, username = 'elastic', password = 'changeme', }: { page: Page; - isRemote: boolean; + isRemote?: boolean; username?: string; password?: string; }) { diff --git a/x-pack/plugins/uptime/e2e/page_objects/settings.tsx b/x-pack/plugins/uptime/e2e/page_objects/settings.tsx new file mode 100644 index 0000000000000..f4b67a846218d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/page_objects/settings.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect, Page } from '@elastic/synthetics'; +import { loginPageProvider } from './login'; +import { utilsPageProvider } from './utils'; +import { byTestId } from '../journeys/utils'; + +export function settingsPageProvider({ page }: { page: Page; kibanaUrl: string }) { + return { + ...loginPageProvider({ page }), + ...utilsPageProvider({ page }), + + async fillToEmail(text: string) { + await page.fill( + '[data-test-subj=toEmailAddressInput] >> [data-test-subj=comboBoxSearchInput]', + text + ); + + await page.click(byTestId('uptimeSettingsPage')); + }, + async saveSettings() { + await page.click(byTestId('apply-settings-button')); + await this.waitForLoadingToFinish(); + await this.assertText({ text: 'Settings saved!' }); + }, + async assertApplyEnabled() { + expect(await page.isEnabled(byTestId('apply-settings-button'))).toBeTruthy(); + }, + async assertApplyDisabled() { + expect(await page.isEnabled(byTestId('apply-settings-button'))).toBeFalsy(); + }, + async removeInvalidEmail(invalidEmail: string) { + await page.click(`[title="Remove ${invalidEmail} from selection in this group"]`); + }, + }; +} diff --git a/x-pack/plugins/uptime/e2e/page_objects/utils.tsx b/x-pack/plugins/uptime/e2e/page_objects/utils.tsx index 08ed473d3c8af..072d4497e856d 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/utils.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/utils.tsx @@ -19,6 +19,12 @@ export function utilsPageProvider({ page }: { page: Page }) { } }, + async dismissSyntheticsCallout() { + await page.click('[data-test-subj=uptimeDismissSyntheticsCallout]', { + timeout: 60 * 1000, + }); + }, + async assertText({ text }: { text: string }) { await page.waitForSelector(`text=${text}`); expect(await page.$(`text=${text}`)).toBeTruthy(); diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index 91af014e07ddf..6f6bcd73e3692 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -19,6 +19,7 @@ const listOfJourneys = [ 'StepsDuration', 'TlsFlyoutInAlertingApp', 'StatusFlyoutInAlertingApp', + 'DefaultEmailSettings', 'MonitorManagement-http', 'MonitorManagement-tcp', 'MonitorManagement-icmp', diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx index 9db6c3b4b0acb..3bc23592c4e64 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx @@ -68,6 +68,7 @@ export const EnableMonitorAlert = ({ monitorId, selectedMonitor }: Props) => { defaultActions, monitorId, selectedMonitor, + defaultEmail: settings?.defaultEmail, }) ); setIsLoading(true); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx index ca36cae058bb0..9a00b1f2b7289 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { UptimeSettingsContext } from '../../../../contexts'; import { Rule } from '../../../../../../triggers_actions_ui/public'; +import { getUrlForAlert } from '../../../../lib/alert_types/common'; interface Props { monitorAlerts: Rule[]; @@ -41,9 +42,9 @@ export const EnabledAlerts = ({ monitorAlerts, loading }: Props) => { (monitorAlerts ?? []).forEach((alert, ind) => { listItems.push({ - label: alert.name, - href: basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + alert.id, size: 's', + label: alert.name, + href: getUrlForAlert(alert.id, basePath), 'data-test-subj': 'uptimeMonitorListDrawerAlert' + ind, }); }); diff --git a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx index 99f9310129786..d69bcfee7efae 100644 --- a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx @@ -19,6 +19,7 @@ import { ActionTypeId } from './types'; interface Props { focusInput: () => void; + isDisabled: boolean; } interface KibanaDeps { @@ -34,16 +35,20 @@ export const ALLOWED_ACTION_TYPES: ActionTypeId[] = [ '.servicenow', '.jira', '.webhook', + '.email', ]; -export const AddConnectorFlyout = ({ focusInput }: Props) => { +export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => { const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); const { services: { + application, triggersActionsUi: { getAddConnectorFlyout }, }, } = useKibana(); + const canEdit: boolean = !!application?.capabilities.actions.save; + const dispatch = useDispatch(); const { data: actionTypes } = useFetcher(() => fetchActionTypes(), []); @@ -67,18 +72,18 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { return ( <> + {addFlyoutVisible ? ConnectorAddFlyout : null} setAddFlyoutVisibility(true)} + size="s" + isDisabled={isDisabled || !canEdit} > - {addFlyoutVisible ? ConnectorAddFlyout : null} ); }; diff --git a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx index 1a0cfdda55d51..5545f555bd81a 100644 --- a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx +++ b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx @@ -27,6 +27,7 @@ import { useInitApp } from '../../hooks/use_init_app'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public/'; import { ActionTypeId } from './types'; +import { DefaultEmail } from './default_email'; type ConnectorOption = EuiComboBoxOptionOption; @@ -146,7 +147,7 @@ export const AlertDefaultsForm: React.FC = ({ /> } > - = ({ defaultMessage="Default connectors" /> } + labelAppend={ + { + if (inputRef.current) { + inputRef.current.focus(); + } + }, [])} + /> + } > = ({ data-test-subj={`default-connectors-input-${loading ? 'loading' : 'loaded'}`} renderOption={renderOption} /> - - - { - if (inputRef.current) { - inputRef.current.focus(); - } - }, [])} - /> - + + + ); }; + +const RowWrapper = styled(EuiFormRow)` + &&& > .euiFormRow__labelWrapper { + align-items: baseline; + } +`; diff --git a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx index ba167290d308e..bfc45838a3f88 100644 --- a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx +++ b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx @@ -19,11 +19,15 @@ import { EuiFlexItem, } from '@elastic/eui'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; -import { DynamicSettings } from '../../../common/runtime_types'; +import { DefaultEmail, DynamicSettings } from '../../../common/runtime_types'; import { SettingsFormProps } from '../../pages/settings'; import { certificateFormTranslations } from './translations'; -export type OnFieldChangeType = (changedValues: Partial) => void; +export type PartialSettings = Partial> & { + defaultEmail?: Partial; +}; + +export type OnFieldChangeType = (changedValues: PartialSettings) => void; export const CertificateExpirationForm: React.FC = ({ loading, diff --git a/x-pack/plugins/uptime/public/components/settings/default_email.tsx b/x-pack/plugins/uptime/public/components/settings/default_email.tsx new file mode 100644 index 0000000000000..b965d986f5413 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/default_email.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useSelector } from 'react-redux'; +import { EuiDescribedFormGroup } from '@elastic/eui'; +import { OnFieldChangeType } from './certificate_form'; +import { connectorsSelector } from '../../state/alerts/alerts'; +import { DefaultEmail as DefaultEmailType } from '../../../common/runtime_types'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { UptimePluginServices } from '../../apps/plugin'; +import { SettingsPageFieldErrors } from '../../pages/settings'; + +export function DefaultEmail({ + errors, + value, + isLoading, + isDisabled, + onChange, + connectors, +}: { + errors: SettingsPageFieldErrors['invalidEmail']; + value?: DefaultEmailType; + isLoading: boolean; + isDisabled: boolean; + onChange: OnFieldChangeType; + connectors?: string[]; +}) { + const { actionTypeRegistry } = useKibana().services.triggersActionsUi; + + const { data = [] } = useSelector(connectorsSelector); + + if ( + !data?.find( + (connector) => connectors?.includes(connector.id) && connector.actionTypeId === '.email' + ) + ) { + return null; + } + + const emailActionType = actionTypeRegistry.get('.email'); + const ActionParams = emailActionType.actionParamsFields; + + const onEmailChange = (key: string, val: string[]) => { + onChange({ + defaultEmail: { + ...value, + [key]: val, + }, + }); + }; + + return ( + + + + } + description={ + + } + > + onEmailChange(key, val as string[])} + showEmailSubjectAndMessage={false} + index={1} + isLoading={isLoading} + isDisabled={isDisabled} + /> + + ); +} + +export const validateEmail = (email: string) => { + return String(email) + .toLowerCase() + .match( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ); +}; diff --git a/x-pack/plugins/uptime/public/components/settings/settings_actions.tsx b/x-pack/plugins/uptime/public/components/settings/settings_actions.tsx new file mode 100644 index 0000000000000..ae57233388427 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/settings_actions.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { SettingsPageFieldErrors } from '../../pages/settings'; + +export interface SettingsActionsProps { + isFormDisabled: boolean; + isFormDirty: boolean; + isFormValid: boolean; + onApply: (event: React.FormEvent) => void; + onCancel: () => void; + errors: SettingsPageFieldErrors | null; +} + +export const SettingsActions = ({ + isFormDisabled, + isFormDirty, + isFormValid, + onApply, + onCancel, + errors, +}: SettingsActionsProps) => { + const { heartbeatIndices, invalidEmail, expirationThresholdError, ageThresholdError } = + errors ?? {}; + + const { to, cc, bcc } = invalidEmail ?? {}; + + return ( + + + + {heartbeatIndices || to || cc || bcc || expirationThresholdError || ageThresholdError} + + + + { + onCancel(); + }} + > + + + + + + + + + + ); +}; + +const WarningText = euiStyled(EuiText)` + box-shadow: -4px 0 ${(props) => props.theme.eui.euiColorWarning}; + padding-left: 8px; +`; diff --git a/x-pack/plugins/uptime/public/components/settings/settings_bottom_bar.tsx b/x-pack/plugins/uptime/public/components/settings/settings_bottom_bar.tsx new file mode 100644 index 0000000000000..b080c3ea89712 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/settings_bottom_bar.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { OutPortal, createPortalNode, InPortal } from 'react-reverse-portal'; +import { SettingsActions, SettingsActionsProps } from './settings_actions'; + +export const SettingsBottomBar = () => { + return ( +
+ +
+ ); +}; + +export const SettingsActionBarPortal = (props: SettingsActionsProps) => { + return ( + + + + ); +}; +export const SettingsBarPortalNode = createPortalNode(); diff --git a/x-pack/plugins/uptime/public/components/settings/translations.ts b/x-pack/plugins/uptime/public/components/settings/translations.ts index b283c3ae3215a..1a0692f1f45bf 100644 --- a/x-pack/plugins/uptime/public/components/settings/translations.ts +++ b/x-pack/plugins/uptime/public/components/settings/translations.ts @@ -31,4 +31,10 @@ export const alertFormI18n = { defaultMessage: 'Please select one or more connectors', } ), + emailPlaceHolder: i18n.translate( + 'xpack.uptime.sourceConfiguration.alertDefaultForm.emailConnectorPlaceHolder', + { + defaultMessage: 'To: Email for email connector', + } + ), }; diff --git a/x-pack/plugins/uptime/public/components/settings/types.ts b/x-pack/plugins/uptime/public/components/settings/types.ts index 1456973acb1d2..41c4ae41e38c0 100644 --- a/x-pack/plugins/uptime/public/components/settings/types.ts +++ b/x-pack/plugins/uptime/public/components/settings/types.ts @@ -14,6 +14,7 @@ import { SlackActionTypeId, TeamsActionTypeId, WebhookActionTypeId, + EmailActionTypeId, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../actions/server/builtin_action_types'; @@ -25,4 +26,5 @@ export type ActionTypeId = | typeof TeamsActionTypeId | typeof ServiceNowActionTypeId | typeof JiraActionTypeId - | typeof WebhookActionTypeId; + | typeof WebhookActionTypeId + | typeof EmailActionTypeId; diff --git a/x-pack/plugins/uptime/public/components/settings/use_settings_errors.ts b/x-pack/plugins/uptime/public/components/settings/use_settings_errors.ts new file mode 100644 index 0000000000000..242a3d9a97799 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/use_settings_errors.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqual } from 'lodash'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { BLANK_STR, SPACE_STR } from '../../pages/translations'; +import { isValidCertVal, SettingsPageFieldErrors } from '../../pages/settings'; +import { validateEmail } from './default_email'; +import { selectDynamicSettings } from '../../state/selectors'; +import { PartialSettings } from './certificate_form'; +import { connectorsSelector } from '../../state/alerts/alerts'; + +const hasInvalidEmail = (defaultConnectors?: string[], value?: PartialSettings['defaultEmail']) => { + if (!defaultConnectors || defaultConnectors.length === 0) { + return; + } + if (!value || !value.to) { + return { to: REQUIRED_EMAIL }; + } + + const toError = value.to.length === 0 ? REQUIRED_EMAIL : getInvalidEmailError(value.to); + const ccError = getInvalidEmailError(value.cc); + const bccError = getInvalidEmailError(value.bcc); + + if (toError || ccError || bccError) { + return { + to: toError, + cc: ccError, + bcc: bccError, + }; + } +}; + +const isEmailChanged = ( + prev?: PartialSettings['defaultEmail'], + next?: PartialSettings['defaultEmail'] +) => { + if (!isEqual((prev?.to ?? []).sort(), (next?.to ?? []).sort())) { + return true; + } + if (!isEqual((prev?.cc ?? []).sort(), (next?.cc ?? []).sort())) { + return true; + } + if (!isEqual((prev?.bcc ?? []).sort(), (next?.bcc ?? []).sort())) { + return true; + } +}; + +const isDirtyForm = (formFields: PartialSettings | null, settings?: PartialSettings) => { + return ( + settings?.certAgeThreshold !== formFields?.certAgeThreshold || + settings?.certExpirationThreshold !== formFields?.certExpirationThreshold || + settings?.heartbeatIndices !== formFields?.heartbeatIndices || + isEmailChanged(settings?.defaultEmail, formFields?.defaultEmail) || + JSON.stringify(settings?.defaultConnectors) !== JSON.stringify(formFields?.defaultConnectors) + ); +}; + +export const useSettingsErrors = ( + formFields: PartialSettings | null +): { errors: SettingsPageFieldErrors | null; isFormDirty: boolean } => { + const dss = useSelector(selectDynamicSettings); + + const isFormDirty = isDirtyForm(formFields, dss.settings); + + const { data = [] } = useSelector(connectorsSelector); + + const hasEmailConnector = data?.find( + (connector) => + formFields?.defaultConnectors?.includes(connector.id) && connector.actionTypeId === '.email' + ); + + if (formFields) { + const { certAgeThreshold, certExpirationThreshold, heartbeatIndices } = formFields; + + const indErrorSpace = heartbeatIndices?.includes(' ') ? SPACE_STR : ''; + + const indError = indErrorSpace || (heartbeatIndices?.match(/^\S+$/) ? '' : BLANK_STR); + + const ageError = isValidCertVal(certAgeThreshold); + const expError = isValidCertVal(certExpirationThreshold); + + return { + isFormDirty, + errors: { + heartbeatIndices: indError, + expirationThresholdError: expError, + ageThresholdError: ageError, + invalidEmail: hasEmailConnector + ? hasInvalidEmail(formFields.defaultConnectors, formFields.defaultEmail) + : undefined, + }, + }; + } + + return { isFormDirty, errors: null }; +}; + +const REQUIRED_EMAIL = i18n.translate( + 'xpack.uptime.sourceConfiguration.alertDefaultForm.requiredEmail', + { + defaultMessage: 'To email is required for email connector', + } +); + +const getInvalidEmailError = (value?: string[]) => { + if (!value) { + return; + } + + const inValidEmail = value.find((val) => !validateEmail(val)); + + if (!inValidEmail) { + return; + } + + return i18n.translate('xpack.uptime.sourceConfiguration.alertDefaultForm.invalidEmail', { + defaultMessage: '{val} is not a valid email.', + values: { val: inValidEmail }, + }); +}; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx b/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx index 8e5a3076a154e..3d51051d28fe5 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx @@ -10,25 +10,44 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import type { CoreTheme } from 'kibana/public'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { RedirectAppLinks, toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { ActionConnector } from '../../state/alerts/alerts'; +import { Alert } from '../../../../alerting/common'; +import { kibanaService } from '../../state/kibana_service'; +import { getUrlForAlert } from './common'; export const simpleAlertEnabled = ( defaultActions: ActionConnector[], - theme$: Observable + theme$: Observable, + alert: Alert ) => { + const alertUrl = getUrlForAlert(alert.id, kibanaService.core.http.basePath.get()); + return { title: i18n.translate('xpack.uptime.overview.alerts.enabled.success', { defaultMessage: 'Rule successfully enabled ', }), text: toMountPoint( - {defaultActions.map(({ name }) => name).join(', ')}, - }} - />, + + + {defaultActions.map(({ name }) => name).join(', ')} + ), + }} + /> + + + + {i18n.translate('xpack.uptime.enableAlert.editAlert', { + defaultMessage: 'Edit alert', + })} + + , { theme$ } ), }; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/common.ts b/x-pack/plugins/uptime/public/lib/alert_types/common.ts index 09b02150957d0..6a45f73357597 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/common.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/common.ts @@ -38,3 +38,7 @@ export const getMonitorRouteFromMonitorId = ({ : {}), }, }); + +export const getUrlForAlert = (id: string, basePath: string) => { + return basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + id; +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index b0c8f61477d28..799c5350c6451 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -117,6 +117,9 @@ const mockCore: () => Partial = () => { save: true, show: true, }, + actions: { + save: true, + }, }, }, uiSettings: { diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index b9745b9a733b1..3e1c3c911efa1 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -5,17 +5,8 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiSpacer, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiForm, EuiSpacer } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; import { selectDynamicSettings } from '../state/selectors'; import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; @@ -26,6 +17,7 @@ import { IndicesForm } from '../components/settings/indices_form'; import { CertificateExpirationForm, OnFieldChangeType, + PartialSettings, } from '../components/settings/certificate_form'; import * as Translations from './translations'; import { @@ -33,12 +25,18 @@ import { VALUE_MUST_BE_AN_INTEGER, } from '../../common/translations'; import { AlertDefaultsForm } from '../components/settings/alert_defaults_form'; -import { BLANK_STR, SPACE_STR } from './translations'; +import { SettingsActionBarPortal } from '../components/settings/settings_bottom_bar'; +import { useSettingsErrors } from '../components/settings/use_settings_errors'; -interface SettingsPageFieldErrors { +export interface SettingsPageFieldErrors { heartbeatIndices: string | ''; expirationThresholdError?: string; ageThresholdError?: string; + invalidEmail?: { + to?: string; + cc?: string; + bcc?: string; + }; } export interface SettingsFormProps { @@ -61,35 +59,6 @@ export const isValidCertVal = (val?: number): string | undefined => { } }; -const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldErrors | null => { - if (formFields) { - const { certAgeThreshold, certExpirationThreshold, heartbeatIndices } = formFields; - - const indErrorSpace = heartbeatIndices.includes(' ') ? SPACE_STR : ''; - - const indError = indErrorSpace || (heartbeatIndices.match(/^\S+$/) ? '' : BLANK_STR); - - const expError = isValidCertVal(certExpirationThreshold); - const ageError = isValidCertVal(certAgeThreshold); - - return { - heartbeatIndices: indError, - expirationThresholdError: expError, - ageThresholdError: ageError, - }; - } - return null; -}; - -const isDirtyForm = (formFields: DynamicSettings | null, settings?: DynamicSettings) => { - return ( - settings?.certAgeThreshold !== formFields?.certAgeThreshold || - settings?.certExpirationThreshold !== formFields?.certExpirationThreshold || - settings?.heartbeatIndices !== formFields?.heartbeatIndices || - JSON.stringify(settings?.defaultConnectors) !== JSON.stringify(formFields?.defaultConnectors) - ); -}; - export const SettingsPage: React.FC = () => { const dss = useSelector(selectDynamicSettings); @@ -101,7 +70,7 @@ export const SettingsPage: React.FC = () => { dispatch(getDynamicSettings()); }, [dispatch]); - const [formFields, setFormFields] = useState( + const [formFields, setFormFields] = useState( dss.settings ? { ...dss.settings } : null ); @@ -109,30 +78,31 @@ export const SettingsPage: React.FC = () => { setFormFields(Object.assign({}, { ...dss.settings })); } - const fieldErrors = getFieldErrors(formFields); + const { errors: fieldErrors, isFormDirty } = useSettingsErrors(formFields); const isFormValid = !(fieldErrors && Object.values(fieldErrors).find((v) => !!v)); - const onChangeFormField: OnFieldChangeType = (changedField) => { - if (formFields) { - setFormFields({ - ...formFields, - ...changedField, - }); - } - }; + const onChangeFormField: OnFieldChangeType = useCallback( + (changedField) => { + if (formFields) { + setFormFields({ + ...formFields, + ...changedField, + }); + } + }, + [formFields] + ); const onApply = (event: React.FormEvent) => { event.preventDefault(); if (formFields) { - dispatch(setDynamicSettings(formFields)); + dispatch(setDynamicSettings(formFields as DynamicSettings)); } }; const resetForm = () => setFormFields(dss.settings ? { ...dss.settings } : null); - const isFormDirty = isDirtyForm(formFields, dss.settings); - const canEdit: boolean = !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; const isFormDisabled = dss.loading || !canEdit; @@ -158,13 +128,13 @@ export const SettingsPage: React.FC = () => { { - - - - - { - resetForm(); - }} - > - - - - - - - - - + ); }; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index aa7bf22593abe..7c2295e8ab919 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -57,6 +57,7 @@ import { apiService } from './state/api/utils'; import { useInspectorContext } from '../../observability/public'; import { UptimeConfig } from '../common/config'; import { AddMonitorBtn } from './components/monitor_management/add_monitor_btn'; +import { SettingsBottomBar } from './components/settings/settings_bottom_bar'; interface PageRouterProps { config: UptimeConfig; @@ -114,6 +115,8 @@ const getRoutes = (config: UptimeConfig): RouteProps[] => { ), }, + bottomBar: , + bottomBarProps: { paddingSize: 'm' as const }, }, { title: i18n.translate('xpack.uptime.certificatesRoute.title', { diff --git a/x-pack/plugins/uptime/public/state/alerts/alerts.ts b/x-pack/plugins/uptime/public/state/alerts/alerts.ts index a86de3ac02b83..c33bb7bde01b8 100644 --- a/x-pack/plugins/uptime/public/state/alerts/alerts.ts +++ b/x-pack/plugins/uptime/public/state/alerts/alerts.ts @@ -151,7 +151,7 @@ export function* fetchAlertsEffect() { yield put(createAlertAction.success(response)); kibanaService.core.notifications.toasts.addSuccess( - simpleAlertEnabled(action.payload.defaultActions, kibanaService.theme) + simpleAlertEnabled(action.payload.defaultActions, kibanaService.theme, response) ); yield put(getMonitorAlertsAction.get()); } catch (err) { diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts index af2dbec02ed89..0e29300f02a65 100644 --- a/x-pack/plugins/uptime/public/state/api/alert_actions.ts +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -17,10 +17,12 @@ import { ServiceNowActionParams, JiraActionParams, WebhookActionParams, + EmailActionParams, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../actions/server'; import { ActionTypeId } from '../../components/settings/types'; import { Ping } from '../../../common/runtime_types/ping'; +import { DefaultEmail } from '../../../common/runtime_types'; export const SLACK_ACTION_ID: ActionTypeId = '.slack'; export const PAGER_DUTY_ACTION_ID: ActionTypeId = '.pagerduty'; @@ -30,6 +32,7 @@ export const TEAMS_ACTION_ID: ActionTypeId = '.teams'; export const SERVICE_NOW_ACTION_ID: ActionTypeId = '.servicenow'; export const JIRA_ACTION_ID: ActionTypeId = '.jira'; export const WEBHOOK_ACTION_ID: ActionTypeId = '.webhook'; +export const EMAIL_ACTION_ID: ActionTypeId = '.email'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -45,7 +48,11 @@ const getRecoveryMessage = (selectedMonitor: Ping) => { }); }; -export function populateAlertActions({ defaultActions, selectedMonitor }: NewAlertParams) { +export function populateAlertActions({ + defaultActions, + selectedMonitor, + defaultEmail, +}: NewAlertParams) { const actions: RuleAction[] = []; defaultActions.forEach((aId) => { const action: RuleAction = { @@ -98,6 +105,11 @@ export function populateAlertActions({ defaultActions, selectedMonitor }: NewAle }; actions.push(recoveredAction); break; + case EMAIL_ACTION_ID: + if (defaultEmail) { + action.params = getEmailActionParams(defaultEmail, selectedMonitor); + } + break; default: action.params = { message: MonitorStatusTranslations.defaultActionMessage, @@ -214,3 +226,26 @@ function getJiraActionParams(): JiraActionParams { }, }; } + +function getEmailActionParams( + defaultEmail: DefaultEmail, + selectedMonitor: Ping +): EmailActionParams { + return { + to: defaultEmail.to, + subject: i18n.translate('xpack.uptime.monitor.simpleStatusAlert.email.subject', { + defaultMessage: 'Monitor {monitor} with url {url} is down', + values: { + monitor: selectedMonitor?.monitor?.name || selectedMonitor?.monitor?.id, + url: selectedMonitor?.url?.full, + }, + }), + message: MonitorStatusTranslations.defaultActionMessage, + cc: defaultEmail.cc ?? [], + bcc: defaultEmail.bcc ?? [], + kibanaFooterLink: { + path: '', + text: '', + }, + }; +} diff --git a/x-pack/plugins/uptime/public/state/api/alerts.ts b/x-pack/plugins/uptime/public/state/api/alerts.ts index e175f7ac61bd3..7ddfbb872fb95 100644 --- a/x-pack/plugins/uptime/public/state/api/alerts.ts +++ b/x-pack/plugins/uptime/public/state/api/alerts.ts @@ -17,6 +17,7 @@ import { AtomicStatusCheckParams } from '../../../common/runtime_types/alerts'; import { populateAlertActions, RuleAction } from './alert_actions'; import { Ping } from '../../../common/runtime_types/ping'; +import { DefaultEmail } from '../../../common/runtime_types'; const UPTIME_AUTO_ALERT = 'UPTIME_AUTO'; @@ -44,6 +45,7 @@ export const fetchConnectors = async (): Promise => { export interface NewAlertParams extends AlertTypeParams { selectedMonitor: Ping; defaultActions: ActionConnector[]; + defaultEmail?: DefaultEmail; } type NewMonitorStatusAlert = Omit< @@ -71,10 +73,12 @@ export const createAlert = async ({ defaultActions, monitorId, selectedMonitor, + defaultEmail, }: NewAlertParams): Promise => { const actions: RuleAction[] = populateAlertActions({ defaultActions, selectedMonitor, + defaultEmail, }); const data: NewMonitorStatusAlert = { diff --git a/x-pack/plugins/uptime/public/state/api/dynamic_settings.test.ts b/x-pack/plugins/uptime/public/state/api/dynamic_settings.test.ts deleted file mode 100644 index 46a10144edb08..0000000000000 --- a/x-pack/plugins/uptime/public/state/api/dynamic_settings.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash'; -import { apiService } from './utils'; -import { getDynamicSettings } from './dynamic_settings'; -import { HttpSetup } from 'src/core/public'; -import { DynamicSettings } from '../../../common/runtime_types/dynamic_settings'; - -describe('Dynamic Settings API', () => { - let fetchMock: jest.SpyInstance>; - const defaultResponse: DynamicSettings & { _inspect: never[] } = { - heartbeatIndices: 'heartbeat-8*', - certAgeThreshold: 1, - certExpirationThreshold: 1337, - defaultConnectors: [], - _inspect: [], - }; - - beforeEach(() => { - apiService.http = { - get: jest.fn(), - fetch: jest.fn(), - } as unknown as HttpSetup; - - apiService.addInspectorRequest = jest.fn(); - - fetchMock = jest.spyOn(apiService.http, 'fetch'); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('omits the _inspect prop on the response as decoding', async () => { - fetchMock.mockReturnValue(new Promise((r) => r(defaultResponse))); - - const resp = await getDynamicSettings(); - - expect(resp).toEqual(omit(defaultResponse, ['_inspect'])); - }); -}); diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index 79fe6f67908a6..e9bcccd8d8464 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -55,6 +55,13 @@ export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (_libs: UMS certAgeThreshold: schema.number(), certExpirationThreshold: schema.number(), defaultConnectors: schema.arrayOf(schema.string()), + defaultEmail: schema.maybe( + schema.object({ + to: schema.arrayOf(schema.string()), + cc: schema.maybe(schema.arrayOf(schema.string())), + bcc: schema.maybe(schema.arrayOf(schema.string())), + }) + ), }), }, writeAccess: true, diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index 4e40ce934a32e..2b96ce369294a 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -13,6 +13,7 @@ import { createUptimeESClient, inspectableEsQueriesMap } from '../lib/lib'; import { KibanaResponse } from '../../../../../src/core/server/http/router'; import { enableInspectEsQueries } from '../../../observability/common'; import { syntheticsServiceApiKey } from '../lib/saved_objects/service_api_key'; +import { API_URLS } from '../../common/constants'; export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => ({ ...uptimeRoute, @@ -62,7 +63,9 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => return response.ok({ body: { ...res, - ...(isInspectorEnabled ? { _inspect: inspectableEsQueriesMap.get(request) } : {}), + ...(isInspectorEnabled && uptimeRoute.path !== API_URLS.DYNAMIC_SETTINGS + ? { _inspect: inspectableEsQueriesMap.get(request) } + : {}), }, }); },