diff --git a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts index e7f79fc2086a2..d91342cdedbb0 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts @@ -18,6 +18,7 @@ import { ResponseBodyIndexPolicy, ScheduleUnit, ScreenshotOption, + SourceType, TCPAdvancedFields, TCPSimpleFields, TLSFields, @@ -41,6 +42,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = { [ConfigKey.NAME]: '', [ConfigKey.LOCATIONS]: [], [ConfigKey.NAMESPACE]: DEFAULT_NAMESPACE_STRING, + [ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.UI, }; export const DEFAULT_BROWSER_ADVANCED_FIELDS: BrowserAdvancedFields = { @@ -58,10 +60,9 @@ export const DEFAULT_BROWSER_ADVANCED_FIELDS: BrowserAdvancedFields = { export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = { ...DEFAULT_COMMON_FIELDS, - [ConfigKey.SCHEDULE]: { - unit: ScheduleUnit.MINUTES, - number: '10', - }, + [ConfigKey.JOURNEY_ID]: '', + [ConfigKey.PROJECT_ID]: '', + [ConfigKey.PLAYWRIGHT_OPTIONS]: '', [ConfigKey.METADATA]: { script_source: { is_generated_script: false, @@ -70,21 +71,26 @@ export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = { is_zip_url_tls_enabled: false, }, [ConfigKey.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKey.PARAMS]: '', + [ConfigKey.PORT]: undefined, + [ConfigKey.SCHEDULE]: { + unit: ScheduleUnit.MINUTES, + number: '10', + }, [ConfigKey.SOURCE_INLINE]: '', + [ConfigKey.SOURCE_PROJECT_CONTENT]: '', [ConfigKey.SOURCE_ZIP_URL]: '', [ConfigKey.SOURCE_ZIP_USERNAME]: '', [ConfigKey.SOURCE_ZIP_PASSWORD]: '', [ConfigKey.SOURCE_ZIP_FOLDER]: '', [ConfigKey.SOURCE_ZIP_PROXY_URL]: '', - [ConfigKey.PARAMS]: '', [ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: undefined, [ConfigKey.ZIP_URL_TLS_CERTIFICATE]: undefined, [ConfigKey.ZIP_URL_TLS_KEY]: undefined, [ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]: undefined, [ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: undefined, [ConfigKey.ZIP_URL_TLS_VERSION]: undefined, - [ConfigKey.URLS]: undefined, - [ConfigKey.PORT]: undefined, + [ConfigKey.URLS]: '', }; export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = { diff --git a/x-pack/plugins/synthetics/common/constants/monitor_management.ts b/x-pack/plugins/synthetics/common/constants/monitor_management.ts index c306d939c7128..db010391839d5 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_management.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_management.ts @@ -8,11 +8,14 @@ // values must match keys in the integration package export enum ConfigKey { APM_SERVICE_NAME = 'service.name', + CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id', ENABLED = 'enabled', HOSTS = 'hosts', IGNORE_HTTPS_ERRORS = 'ignore_https_errors', + MONITOR_SOURCE_TYPE = 'monitor.origin', JOURNEY_FILTERS_MATCH = 'filter_journeys.match', JOURNEY_FILTERS_TAGS = 'filter_journeys.tags', + JOURNEY_ID = 'journey_id', MAX_REDIRECTS = 'max_redirects', METADATA = '__ui', MONITOR_TYPE = 'type', @@ -21,6 +24,9 @@ export enum ConfigKey { LOCATIONS = 'locations', PARAMS = 'params', PASSWORD = 'password', + PLAYWRIGHT_OPTIONS = 'playwright_options', + ORIGINAL_SPACE = 'original_space', // the original space the montior was saved in. Used by push monitors to ensure uniqueness of monitor id sent to heartbeat and prevent data collisions + PORT = 'url.port', PROXY_URL = 'proxy_url', PROXY_USE_LOCAL_RESOLVER = 'proxy_use_local_resolver', RESPONSE_BODY_CHECK_NEGATIVE = 'check.response.body.negative', @@ -37,12 +43,14 @@ export enum ConfigKey { REVISION = 'revision', SCHEDULE = 'schedule', SCREENSHOTS = 'screenshots', + SOURCE_PROJECT_CONTENT = 'source.project.content', SOURCE_INLINE = 'source.inline.script', SOURCE_ZIP_URL = 'source.zip_url.url', SOURCE_ZIP_USERNAME = 'source.zip_url.username', SOURCE_ZIP_PASSWORD = 'source.zip_url.password', SOURCE_ZIP_FOLDER = 'source.zip_url.folder', SOURCE_ZIP_PROXY_URL = 'source.zip_url.proxy_url', + PROJECT_ID = 'project_id', SYNTHETICS_ARGS = 'synthetics_args', TLS_CERTIFICATE_AUTHORITIES = 'ssl.certificate_authorities', TLS_CERTIFICATE = 'ssl.certificate', @@ -58,7 +66,6 @@ export enum ConfigKey { UPLOAD_SPEED = 'throttling.upload_speed', LATENCY = 'throttling.latency', URLS = 'urls', - PORT = 'url.port', USERNAME = 'username', WAIT = 'wait', ZIP_URL_TLS_CERTIFICATE_AUTHORITIES = 'source.zip_url.ssl.certificate_authorities', @@ -80,6 +87,7 @@ export const secretKeys = [ ConfigKey.RESPONSE_HEADERS_CHECK, ConfigKey.RESPONSE_RECEIVE_CHECK, ConfigKey.SOURCE_INLINE, + ConfigKey.SOURCE_PROJECT_CONTENT, ConfigKey.SOURCE_ZIP_USERNAME, ConfigKey.SOURCE_ZIP_PASSWORD, ConfigKey.SYNTHETICS_ARGS, diff --git a/x-pack/plugins/synthetics/common/constants/rest_api.ts b/x-pack/plugins/synthetics/common/constants/rest_api.ts index fba2c2b750a4a..7400938e3a7cc 100644 --- a/x-pack/plugins/synthetics/common/constants/rest_api.ts +++ b/x-pack/plugins/synthetics/common/constants/rest_api.ts @@ -44,4 +44,7 @@ export enum API_URLS { RUN_ONCE_MONITOR = '/internal/uptime/service/monitors/run_once', TRIGGER_MONITOR = '/internal/uptime/service/monitors/trigger', SERVICE_ALLOWED = '/internal/uptime/service/allowed', + + // Project monitor public endpoint + SYNTHETICS_MONITORS_PROJECT = '/api/synthetics/service/project/monitors', } diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/index.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/index.ts index 94d153d56caa2..73355d3144eac 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/index.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/index.ts @@ -10,4 +10,5 @@ export * from './config_key'; export * from './monitor_configs'; export * from './monitor_meta_data'; export * from './monitor_types'; +export * from './monitor_types_project'; export * from './locations'; diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts index ac917dda0e142..4d1e22ffaea3b 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_configs.ts @@ -119,3 +119,10 @@ export enum ThrottlingSuffix { export const ThrottlingSuffixCodec = tEnum('ThrottlingSuffix', ThrottlingSuffix); export type ThrottlingSuffixType = t.TypeOf; + +export enum SourceType { + UI = 'ui', + PROJECT = 'project', +} + +export const SourceTypeCodec = tEnum('SourceType', SourceType); diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 0e96b8b9de6e2..2b343cfa68883 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -15,6 +15,7 @@ import { ModeCodec, ResponseBodyIndexPolicyCodec, ScheduleUnitCodec, + SourceTypeCodec, TLSVersionCodec, VerificationModeCodec, } from './monitor_configs'; @@ -76,6 +77,7 @@ export const CommonFieldsCodec = t.intersection([ t.partial({ [ConfigKey.TIMEOUT]: t.union([t.string, t.null]), [ConfigKey.REVISION]: t.number, + [ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec, }), ]); @@ -200,12 +202,21 @@ export const ThrottlingConfigKeyCodec = t.union([ export type ThrottlingConfigKey = t.TypeOf; export const EncryptedBrowserSimpleFieldsCodec = t.intersection([ - t.interface({ - [ConfigKey.METADATA]: MetadataCodec, - [ConfigKey.SOURCE_ZIP_URL]: t.string, - [ConfigKey.SOURCE_ZIP_FOLDER]: t.string, - [ConfigKey.SOURCE_ZIP_PROXY_URL]: t.string, - }), + t.intersection([ + t.interface({ + [ConfigKey.METADATA]: MetadataCodec, + [ConfigKey.SOURCE_ZIP_URL]: t.string, + [ConfigKey.SOURCE_ZIP_FOLDER]: t.string, + [ConfigKey.SOURCE_ZIP_PROXY_URL]: t.string, + }), + t.partial({ + [ConfigKey.PLAYWRIGHT_OPTIONS]: t.string, + [ConfigKey.JOURNEY_ID]: t.string, + [ConfigKey.PROJECT_ID]: t.string, + [ConfigKey.ORIGINAL_SPACE]: t.string, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string, + }), + ]), ZipUrlTLSFieldsCodec, ZipUrlTLSSensitiveFieldsCodec, CommonFieldsCodec, @@ -214,6 +225,7 @@ export const EncryptedBrowserSimpleFieldsCodec = t.intersection([ export const BrowserSensitiveSimpleFieldsCodec = t.intersection([ t.interface({ [ConfigKey.SOURCE_INLINE]: t.string, + [ConfigKey.SOURCE_PROJECT_CONTENT]: t.string, [ConfigKey.SOURCE_ZIP_USERNAME]: t.string, [ConfigKey.SOURCE_ZIP_PASSWORD]: t.string, [ConfigKey.PARAMS]: t.string, diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts new file mode 100644 index 0000000000000..324a17e08dd5e --- /dev/null +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { ScreenshotOptionCodec } from './monitor_configs'; + +export const ProjectMonitorThrottlingConfigCodec = t.interface({ + download: t.number, + upload: t.number, + latency: t.number, +}); + +export const ProjectBrowserMonitorCodec = t.intersection([ + t.interface({ + id: t.string, + name: t.string, + schedule: t.number, + content: t.string, + locations: t.array(t.string), + }), + t.partial({ + throttling: ProjectMonitorThrottlingConfigCodec, + screenshot: ScreenshotOptionCodec, + tags: t.array(t.string), + ignoreHTTPSErrors: t.boolean, + apmServiceName: t.string, + playwrightOptions: t.record(t.string, t.unknown), + filter: t.interface({ + match: t.string, + }), + params: t.record(t.string, t.unknown), + enabled: t.boolean, + }), +]); + +export const ProjectMonitorsRequestCodec = t.interface({ + project: t.string, + keep_stale: t.boolean, + monitors: t.array(ProjectBrowserMonitorCodec), +}); + +export type ProjectMonitorThrottlingConfig = t.TypeOf; + +export type ProjectBrowserMonitor = t.TypeOf; + +export type ProjectMonitorsRequest = t.TypeOf; diff --git a/x-pack/plugins/synthetics/kibana.json b/x-pack/plugins/synthetics/kibana.json index bb827019fc70a..9597d05a44229 100644 --- a/x-pack/plugins/synthetics/kibana.json +++ b/x-pack/plugins/synthetics/kibana.json @@ -20,7 +20,8 @@ "taskManager", "triggersActionsUi", "usageCollection", - "unifiedSearch" + "unifiedSearch", + "spaces" ], "server": true, "ui": true, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/formatters.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/formatters.ts index 374319d13d263..f69cf713b3907 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/formatters.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/formatters.ts @@ -45,6 +45,7 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.SOURCE_ZIP_PASSWORD]: null, [ConfigKey.SOURCE_ZIP_FOLDER]: null, [ConfigKey.SOURCE_ZIP_PROXY_URL]: null, + [ConfigKey.SOURCE_PROJECT_CONTENT]: null, [ConfigKey.SOURCE_INLINE]: (fields) => stringToJsonFormatter(fields[ConfigKey.SOURCE_INLINE]), [ConfigKey.PARAMS]: null, [ConfigKey.SCREENSHOTS]: null, @@ -71,5 +72,10 @@ export const browserFormatters: BrowserFormatMap = { arrayToJsonFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]), [ConfigKey.THROTTLING_CONFIG]: throttlingFormatter, [ConfigKey.IGNORE_HTTPS_ERRORS]: null, + [ConfigKey.JOURNEY_ID]: null, + [ConfigKey.PROJECT_ID]: null, + [ConfigKey.PLAYWRIGHT_OPTIONS]: null, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, + [ConfigKey.ORIGINAL_SPACE]: null, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts index b33ae9e27bc7f..6d1e51a66f49a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts @@ -73,6 +73,7 @@ export const browserNormalizers: BrowserNormalizerMap = { [ConfigKey.SOURCE_ZIP_USERNAME]: getBrowserNormalizer(ConfigKey.SOURCE_ZIP_USERNAME), [ConfigKey.SOURCE_ZIP_PASSWORD]: getBrowserNormalizer(ConfigKey.SOURCE_ZIP_PASSWORD), [ConfigKey.SOURCE_ZIP_FOLDER]: getBrowserNormalizer(ConfigKey.SOURCE_ZIP_FOLDER), + [ConfigKey.SOURCE_PROJECT_CONTENT]: getBrowserNormalizer(ConfigKey.SOURCE_PROJECT_CONTENT), [ConfigKey.SOURCE_INLINE]: getBrowserJsonToJavascriptNormalizer(ConfigKey.SOURCE_INLINE), [ConfigKey.SOURCE_ZIP_PROXY_URL]: getBrowserNormalizer(ConfigKey.SOURCE_ZIP_PROXY_URL), [ConfigKey.PARAMS]: getBrowserNormalizer(ConfigKey.PARAMS), @@ -106,5 +107,10 @@ export const browserNormalizers: BrowserNormalizerMap = { ConfigKey.JOURNEY_FILTERS_TAGS ), [ConfigKey.IGNORE_HTTPS_ERRORS]: getBrowserNormalizer(ConfigKey.IGNORE_HTTPS_ERRORS), + [ConfigKey.JOURNEY_ID]: getBrowserNormalizer(ConfigKey.JOURNEY_ID), + [ConfigKey.PROJECT_ID]: getBrowserNormalizer(ConfigKey.PROJECT_ID), + [ConfigKey.PLAYWRIGHT_OPTIONS]: getBrowserNormalizer(ConfigKey.PLAYWRIGHT_OPTIONS), + [ConfigKey.CUSTOM_HEARTBEAT_ID]: getBrowserNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID), + [ConfigKey.ORIGINAL_SPACE]: getBrowserNormalizer(ConfigKey.ORIGINAL_SPACE), ...commonNormalizers, }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/throttling_fields.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/throttling_fields.tsx index ba023b6a1632f..31b4e6ee1fad0 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/throttling_fields.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/throttling_fields.tsx @@ -22,9 +22,10 @@ import { useBrowserAdvancedFieldsContext, usePolicyConfigContext } from '../cont import { Validation, ConfigKey, BandwidthLimitKey } from '../types'; interface Props { - validate: Validation; + validate?: Validation; minColumnWidth?: string; onFieldBlur?: (field: ConfigKey) => void; + readOnly?: boolean; } type ThrottlingConfigs = @@ -89,193 +90,204 @@ export const ThrottlingExceededMessage = ({ ); }; -export const ThrottlingFields = memo(({ validate, minColumnWidth, onFieldBlur }) => { - const { fields, setFields } = useBrowserAdvancedFieldsContext(); - const { runsOnService, throttling } = usePolicyConfigContext(); +export const ThrottlingFields = memo( + ({ validate, minColumnWidth, onFieldBlur, readOnly = false }) => { + const { fields, setFields } = useBrowserAdvancedFieldsContext(); + const { runsOnService, throttling } = usePolicyConfigContext(); - const maxDownload = throttling[BandwidthLimitKey.DOWNLOAD]; - const maxUpload = throttling[BandwidthLimitKey.UPLOAD]; + const maxDownload = throttling[BandwidthLimitKey.DOWNLOAD]; + const maxUpload = throttling[BandwidthLimitKey.UPLOAD]; - const handleInputChange = useCallback( - ({ value, configKey }: { value: unknown; configKey: ThrottlingConfigs }) => { - setFields((prevFields) => ({ ...prevFields, [configKey]: value })); - }, - [setFields] - ); + const handleInputChange = useCallback( + ({ value, configKey }: { value: unknown; configKey: ThrottlingConfigs }) => { + setFields((prevFields) => ({ ...prevFields, [configKey]: value })); + }, + [setFields] + ); - const exceedsDownloadLimits = - runsOnService && parseFloat(fields[ConfigKey.DOWNLOAD_SPEED]) > maxDownload; - const exceedsUploadLimits = - runsOnService && parseFloat(fields[ConfigKey.UPLOAD_SPEED]) > maxUpload; - const isThrottlingEnabled = fields[ConfigKey.IS_THROTTLING_ENABLED]; + const exceedsDownloadLimits = + runsOnService && parseFloat(fields[ConfigKey.DOWNLOAD_SPEED]) > maxDownload; + const exceedsUploadLimits = + runsOnService && parseFloat(fields[ConfigKey.UPLOAD_SPEED]) > maxUpload; + const isThrottlingEnabled = fields[ConfigKey.IS_THROTTLING_ENABLED]; - const throttlingInputs = isThrottlingEnabled ? ( - <> - - + + + } + labelAppend={} + isInvalid={ + (validate ? !!validate[ConfigKey.DOWNLOAD_SPEED]?.(fields) : false) || + exceedsDownloadLimits + } + error={ + exceedsDownloadLimits ? ( + + ) : ( + + ) + } + > + { + handleInputChange({ + value: event.target.value, + configKey: ConfigKey.DOWNLOAD_SPEED, + }); + }} + onBlur={() => onFieldBlur?.(ConfigKey.DOWNLOAD_SPEED)} + data-test-subj="syntheticsBrowserDownloadSpeed" + append={ + + Mbps + + } + readOnly={readOnly} /> - } - labelAppend={} - isInvalid={!!validate[ConfigKey.DOWNLOAD_SPEED]?.(fields) || exceedsDownloadLimits} - error={ - exceedsDownloadLimits ? ( - - ) : ( + + - ) - } - > - { - handleInputChange({ - value: event.target.value, - configKey: ConfigKey.DOWNLOAD_SPEED, - }); - }} - onBlur={() => onFieldBlur?.(ConfigKey.DOWNLOAD_SPEED)} - data-test-subj="syntheticsBrowserDownloadSpeed" - append={ - - Mbps - } - /> - - } + isInvalid={ + (validate ? !!validate[ConfigKey.UPLOAD_SPEED]?.(fields) : false) || exceedsUploadLimits + } + error={ + exceedsUploadLimits ? ( + + ) : ( + + ) + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKey.UPLOAD_SPEED, + }) + } + onBlur={() => onFieldBlur?.(ConfigKey.UPLOAD_SPEED)} + data-test-subj="syntheticsBrowserUploadSpeed" + append={ + + Mbps + + } + readOnly={readOnly} /> - } - labelAppend={} - isInvalid={!!validate[ConfigKey.UPLOAD_SPEED]?.(fields) || exceedsUploadLimits} - error={ - exceedsUploadLimits ? ( - - ) : ( + + - ) - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKey.UPLOAD_SPEED, - }) } - onBlur={() => onFieldBlur?.(ConfigKey.UPLOAD_SPEED)} - data-test-subj="syntheticsBrowserUploadSpeed" - append={ - - Mbps - + labelAppend={} + isInvalid={validate ? !!validate[ConfigKey.LATENCY]?.(fields) : false} + error={ + } - /> - - + + handleInputChange({ + value: event.target.value, + configKey: ConfigKey.LATENCY, + }) + } + onBlur={() => onFieldBlur?.(ConfigKey.LATENCY)} + data-test-subj="syntheticsBrowserLatency" + append={ + + ms + + } + readOnly={readOnly} /> + + + ) : ( + <> + + + + ); + + return ( + + + } - labelAppend={} - isInvalid={!!validate[ConfigKey.LATENCY]?.(fields)} - error={ + description={ } > - + } onChange={(event) => handleInputChange({ - value: event.target.value, - configKey: ConfigKey.LATENCY, + value: event.target.checked, + configKey: ConfigKey.IS_THROTTLING_ENABLED, }) } - onBlur={() => onFieldBlur?.(ConfigKey.LATENCY)} - data-test-subj="syntheticsBrowserLatency" - append={ - - ms - - } - /> - - - ) : ( - <> - - - - ); - - return ( - - - - } - description={ - onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)} + disabled={readOnly} /> - } - > - - } - onChange={(event) => - handleInputChange({ - value: event.target.checked, - configKey: ConfigKey.IS_THROTTLING_ENABLED, - }) - } - onBlur={() => onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)} - /> - {isThrottlingEnabled && (exceedsDownloadLimits || exceedsUploadLimits) ? ( - <> - - - - ) : null} - {throttlingInputs} - - ); -}); + {isThrottlingEnabled && (exceedsDownloadLimits || exceedsUploadLimits) ? ( + <> + + + + ) : null} + {throttlingInputs} + + ); + } +); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/combo_box.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/combo_box.tsx index 16a31e8e5d623..553d94a9dc53a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/combo_box.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/combo_box.tsx @@ -12,9 +12,16 @@ export interface Props { onChange: (value: string[]) => void; onBlur?: () => void; selectedOptions: string[]; + readOnly?: boolean; } -export const ComboBox = ({ onChange, onBlur, selectedOptions, ...props }: Props) => { +export const ComboBox = ({ + onChange, + onBlur, + selectedOptions, + readOnly = false, + ...props +}: Props) => { const [formattedSelectedOptions, setSelectedOptions] = useState< Array> >(selectedOptions.map((option) => ({ label: option, key: option }))); @@ -68,6 +75,7 @@ export const ComboBox = ({ onChange, onBlur, selectedOptions, ...props }: Props) onBlur={() => onBlur?.()} onSearchChange={onSearchChange} isInvalid={isInvalid} + isDisabled={readOnly} {...props} /> ); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/enabled.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/enabled.tsx index 1d76fe8a72e86..22d8116ceacce 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/enabled.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/enabled.tsx @@ -14,9 +14,10 @@ interface Props { fields: CommonFields; onChange: ({ value, configKey }: { value: boolean; configKey: ConfigKey }) => void; onBlur?: () => void; + readOnly?: boolean; } -export function Enabled({ fields, onChange, onBlur }: Props) { +export function Enabled({ fields, onChange, onBlur, readOnly }: Props) { return ( <> onBlur?.()} + disabled={readOnly} /> diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/formatters.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/formatters.ts index a86894c3d7a48..dbe688039966b 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/formatters.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/formatters.ts @@ -25,6 +25,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT] || undefined), [ConfigKey.NAMESPACE]: null, [ConfigKey.REVISION]: null, + [ConfigKey.MONITOR_SOURCE_TYPE]: null, }; export const arrayToJsonFormatter = (value: string[] = []) => diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts index 0119b415faed8..1d329a598f623 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts @@ -87,4 +87,5 @@ export const commonNormalizers: CommonNormalizerMap = { [ConfigKey.NAMESPACE]: (fields) => fields?.[ConfigKey.NAMESPACE]?.value ?? DEFAULT_NAMESPACE_STRING, [ConfigKey.REVISION]: getCommonNormalizer(ConfigKey.REVISION), + [ConfigKey.MONITOR_SOURCE_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_SOURCE_TYPE), }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx index 99419e2ca9145..d089dacfaedb5 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx @@ -11,6 +11,7 @@ import { MONITOR_ADD_ROUTE } from '../../../../../common/constants'; import { DEFAULT_NAMESPACE_STRING } from '../../../../../common/constants/monitor_defaults'; import { ScheduleUnit, + SourceType, MonitorServiceLocations, ThrottlingOptions, DEFAULT_THROTTLING, @@ -41,6 +42,7 @@ interface IPolicyConfigContext { defaultNamespace?: string; namespace?: string; throttling: ThrottlingOptions; + sourceType?: SourceType; } export interface IPolicyConfigContextProvider { @@ -56,6 +58,7 @@ export interface IPolicyConfigContextProvider { isZipUrlSourceEnabled?: boolean; allowedScheduleUnits?: ScheduleUnit[]; throttling?: ThrottlingOptions; + sourceType?: SourceType; } export const initialMonitorTypeValue = DataStream.HTTP; @@ -93,6 +96,7 @@ export const defaultContext: IPolicyConfigContext = { allowedScheduleUnits: [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS], defaultNamespace: DEFAULT_NAMESPACE_STRING, throttling: DEFAULT_THROTTLING, + sourceType: SourceType.UI, }; export const PolicyConfigContext = createContext(defaultContext); @@ -110,6 +114,7 @@ export function PolicyConfigContextProvider({ runsOnService = false, isZipUrlSourceEnabled = true, allowedScheduleUnits = [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS], + sourceType, }: IPolicyConfigContextProvider) { const [monitorType, setMonitorType] = useState(defaultMonitorType); const [name, setName] = useState(defaultName); @@ -150,6 +155,7 @@ export function PolicyConfigContextProvider({ namespace, setNamespace, throttling, + sourceType, } as IPolicyConfigContext; }, [ monitorType, @@ -168,6 +174,7 @@ export function PolicyConfigContextProvider({ allowedScheduleUnits, namespace, throttling, + sourceType, ]); return ; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/schedule_field.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/schedule_field.tsx index e5d5d05a3cd77..70224b6df7047 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/schedule_field.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/schedule_field.tsx @@ -16,9 +16,10 @@ interface Props { onChange: (schedule: MonitorFields[ConfigKey.SCHEDULE]) => void; onBlur: () => void; unit: ScheduleUnit; + readOnly?: boolean; } -export const ScheduleField = ({ number, onChange, onBlur, unit }: Props) => { +export const ScheduleField = ({ number, onChange, onBlur, unit, readOnly = false }: Props) => { const { allowedScheduleUnits } = usePolicyConfigContext(); const options = !allowedScheduleUnits?.length ? allOptions @@ -55,6 +56,7 @@ export const ScheduleField = ({ number, onChange, onBlur, unit }: Props) => { onBlur(); }} + readOnly={readOnly} /> @@ -74,6 +76,7 @@ export const ScheduleField = ({ number, onChange, onBlur, unit }: Props) => { onChange({ number, unit: updatedUnit as ScheduleUnit }); }} onBlur={() => onBlur()} + disabled={readOnly} /> diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx index 0963500a168ba..5672c96314dc9 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx @@ -25,7 +25,7 @@ import { MONITOR_MANAGEMENT_ROUTE } from '../../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; import { setMonitor } from '../../../state/api'; -import { SyntheticsMonitor } from '../../../../../common/runtime_types'; +import { ConfigKey, SyntheticsMonitor, SourceType } from '../../../../../common/runtime_types'; import { TestRun } from '../test_now_mode/test_now_mode'; import { monitorManagementListSelector } from '../../../state/selectors'; @@ -58,6 +58,7 @@ export const ActionBar = ({ const [isSaving, setIsSaving] = useState(false); const [isSuccessful, setIsSuccessful] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(undefined); + const isReadOnly = monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT; const { data, status } = useFetcher(() => { if (!isSaving || !isValid) { @@ -111,72 +112,73 @@ export const ActionBar = ({ return isSuccessful ? ( ) : ( - - - {!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL} - + - - - - {DISCARD_LABEL} - - - - {onTestNow && ( + + {DISCARD_LABEL} + + + {!isReadOnly ? ( + + + + {!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL} + + {onTestNow && ( + + {/* Popover is used instead of EuiTooltip until the resolution of https://github.com/elastic/eui/issues/5604 */} + onTestNow()} + onMouseEnter={() => { + setIsPopoverOpen(true); + }} + onMouseLeave={() => { + setIsPopoverOpen(false); + }} + > + {testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL} + + } + isOpen={isPopoverOpen} + > + +

{TEST_NOW_DESCRIPTION}

+
+
+
+ )} + - {/* Popover is used instead of EuiTooltip until the resolution of https://github.com/elastic/eui/issues/5604 */} - onTestNow()} - onMouseEnter={() => { - setIsPopoverOpen(true); - }} - onMouseLeave={() => { - setIsPopoverOpen(false); - }} - > - {testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL} - - } - isOpen={isPopoverOpen} + - -

{TEST_NOW_DESCRIPTION}

-
-
+ {monitorId ? UPDATE_MONITOR_LABEL : SAVE_MONITOR_LABEL} +
- )} - - - - {monitorId ? UPDATE_MONITOR_LABEL : SAVE_MONITOR_LABEL} - - -
-
+
+ + ) : null}
); }; @@ -187,7 +189,7 @@ const WarningText = euiStyled(EuiText)` `; const DISCARD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.discardLabel', { - defaultMessage: 'Discard', + defaultMessage: 'Cancel', }); const SAVE_MONITOR_LABEL = i18n.translate('xpack.synthetics.monitorManagement.saveMonitorLabel', { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/edit_monitor_config.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/edit_monitor_config.tsx index 245f058c48d94..b04d548833ff9 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/edit_monitor_config.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/edit_monitor_config.tsx @@ -13,6 +13,7 @@ import { TLSFields, DataStream, ScheduleUnit, + SourceType, ThrottlingOptions, } from '../../../../common/runtime_types'; import { SyntheticsProviders } from '../fleet_package/contexts'; @@ -85,6 +86,7 @@ export const EditMonitorConfig = ({ monitor, throttling }: Props) => { isZipUrlSourceEnabled: false, allowedScheduleUnits: [ScheduleUnit.MINUTES], runsOnService: true, + sourceType: monitor[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, }} httpDefaultValues={fullDefaultConfig[DataStream.HTTP]} tcpDefaultValues={fullDefaultConfig[DataStream.TCP]} diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx index fe2461b98e83a..5cca80fefbce9 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx @@ -17,9 +17,16 @@ interface Props { setLocations: React.Dispatch>; isInvalid: boolean; onBlur?: () => void; + readOnly?: boolean; } -export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid, onBlur }: Props) => { +export const ServiceLocations = ({ + selectedLocations, + setLocations, + isInvalid, + onBlur, + readOnly = false, +}: Props) => { const [error, setError] = useState(null); const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>( {} @@ -76,6 +83,7 @@ export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid, o idToSelectedMap={checkboxIdToSelectedMap} onChange={(id) => onLocationChange(id)} onBlur={() => onBlur?.()} + disabled={readOnly} /> ); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.test.tsx index b4e97ba724393..5cd82233a5890 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.test.tsx @@ -9,7 +9,13 @@ import { fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { ConfigKey, DataStream, HTTPFields } from '../../../../../common/runtime_types'; +import { + ConfigKey, + DataStream, + HTTPFields, + BrowserFields, + SourceType, +} from '../../../../../common/runtime_types'; import { render } from '../../../lib/helper/rtl_helpers'; import { BrowserContextProvider, @@ -19,26 +25,51 @@ import { TCPContextProvider, TLSFieldsContextProvider, } from '../../fleet_package/contexts'; -import { defaultConfig } from '../../fleet_package/synthetics_policy_create_extension'; +import { DEFAULT_FIELDS } from '../../../../../common/constants/monitor_defaults'; import { MonitorFields } from './monitor_fields'; -const defaultHTTPConfig = defaultConfig[DataStream.HTTP] as HTTPFields; +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + +const defaultHTTPConfig = DEFAULT_FIELDS[DataStream.HTTP] as HTTPFields; +const defaultBrowserConfig = DEFAULT_FIELDS[DataStream.BROWSER]; describe('', () => { const WrappedComponent = ({ isEditable = true, isFormSubmitted = false, defaultSimpleHttpFields = defaultHTTPConfig, + defaultBrowserFields = defaultBrowserConfig, + readOnly = false, }: { isEditable?: boolean; isFormSubmitted?: boolean; defaultSimpleHttpFields?: HTTPFields; + defaultBrowserFields?: BrowserFields; + readOnly?: boolean; }) => { return ( - + - + @@ -82,13 +113,20 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeNull(); }); - it('does not show validation errors initially', async () => { - const httpInvalidValues = { ...defaultHTTPConfig, [ConfigKey.NAME]: '', [ConfigKey.URLS]: '' }; - const { queryByText } = render( - + it('is reradonly when source type is project', async () => { + const name = 'monitor name'; + const browserFields = { + ...defaultBrowserConfig, + [ConfigKey.NAME]: name, + }; + const { getByText } = render( + ); - expect(queryByText('Monitor name is required')).toBeNull(); - expect(queryByText('URL is required')).toBeNull(); + expect(getByText('Read only')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.tsx index eb109dd5d1063..2fe0afda08814 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_fields.tsx @@ -7,18 +7,19 @@ import React, { useMemo, useState } from 'react'; import { EuiForm } from '@elastic/eui'; -import { ConfigKey, DataStream } from '../../../../../common/runtime_types'; +import { ConfigKey, DataStream, SourceType } from '../../../../../common/runtime_types'; import { usePolicyConfigContext } from '../../fleet_package/contexts'; import { CustomFields } from '../../fleet_package/custom_fields'; import { validate } from '../validation'; import { MonitorNameAndLocation } from './monitor_name_location'; import { MonitorManagementAdvancedFields } from './monitor_advanced_fields'; +import { ProjectBrowserReadonlyFields } from './read_only_browser_fields'; const MIN_COLUMN_WRAP_WIDTH = '360px'; export const MonitorFields = ({ isFormSubmitted = false }: { isFormSubmitted?: boolean }) => { - const { monitorType } = usePolicyConfigContext(); + const { monitorType, sourceType } = usePolicyConfigContext(); const [touchedFieldsHash, setTouchedFieldsHash] = useState>({}); @@ -41,21 +42,25 @@ export const MonitorFields = ({ isFormSubmitted = false }: { isFormSubmitted?: b return ( - - } - onFieldBlur={handleFieldBlur} - > - - + {sourceType === SourceType.PROJECT ? ( + + ) : ( + + } + onFieldBlur={handleFieldBlur} + > + + + )} ); }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_name_location.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_name_location.tsx index 0f7206f65992f..f123d2e9800f8 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_name_location.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/monitor_name_location.tsx @@ -16,16 +16,19 @@ import { ServiceLocations } from './locations'; import { useMonitorName } from './use_monitor_name'; interface Props { - validate: Validation; + validate?: Validation; onFieldBlur?: (field: ConfigKey) => void; + readOnly?: boolean; } -export const MonitorNameAndLocation = ({ validate, onFieldBlur }: Props) => { +export const MonitorNameAndLocation = ({ validate, onFieldBlur, readOnly }: Props) => { const { name, setName, locations = [], setLocations } = usePolicyConfigContext(); - const isNameInvalid = !!validate[ConfigKey.NAME]?.({ [ConfigKey.NAME]: name }); - const isLocationsInvalid = !!validate[ConfigKey.LOCATIONS]?.({ - [ConfigKey.LOCATIONS]: locations, - }); + const isNameInvalid = validate ? !!validate[ConfigKey.NAME]?.({ [ConfigKey.NAME]: name }) : false; + const isLocationsInvalid = validate + ? !!validate[ConfigKey.LOCATIONS]?.({ + [ConfigKey.LOCATIONS]: locations, + }) + : false; const [localName, setLocalName] = useState(name); @@ -67,6 +70,7 @@ export const MonitorNameAndLocation = ({ validate, onFieldBlur }: Props) => { onChange={(event) => setLocalName(event.target.value)} onBlur={() => onFieldBlur?.(ConfigKey.NAME)} data-test-subj="monitorManagementMonitorName" + readOnly={readOnly} /> { selectedLocations={locations} isInvalid={isLocationsInvalid} onBlur={() => onFieldBlur?.(ConfigKey.LOCATIONS)} + readOnly={readOnly} /> ); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/read_only_browser_fields.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/read_only_browser_fields.tsx new file mode 100644 index 0000000000000..6614b4d1ce4aa --- /dev/null +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/read_only_browser_fields.tsx @@ -0,0 +1,168 @@ +/* + * 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 { EuiAccordion, EuiCallOut, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { ConfigKey } from '../../../../../common/runtime_types'; +import { + useBrowserSimpleFieldsContext, + useBrowserAdvancedFieldsContext, +} from '../../fleet_package/contexts'; +import { Enabled } from '../../fleet_package/common/enabled'; +import { ScheduleField } from '../../fleet_package/schedule_field'; +import { ComboBox } from '../../fleet_package/combo_box'; +import { MonitorNameAndLocation } from './monitor_name_location'; +import { ThrottlingFields } from '../../fleet_package/browser/throttling_fields'; +import { OptionalLabel } from '../../fleet_package/optional_label'; +import { DescribedFormGroupWithWrap } from '../../fleet_package/common/described_form_group_with_wrap'; + +const noop = () => {}; + +export const ProjectBrowserReadonlyFields = ({ minColumnWidth }: { minColumnWidth: string }) => { + const { fields } = useBrowserSimpleFieldsContext(); + const { fields: advancedFields } = useBrowserAdvancedFieldsContext(); + + return ( + <> + + } + iconType="document" + > +

+ +

+
+ + + + + } + description={ + + } + data-test-subj="monitorSettingsSection" + minColumnWidth={minColumnWidth} + > + + + + } + error={ + + } + > + + + + } + helpText={ + + } + labelAppend={} + > + + + + + + + + + + } + description={ + + } + minColumnWidth={minColumnWidth} + > + + + } + helpText={ + + } + > + + + + + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx index 4b9374e991e6b..27beefa1c7693 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx @@ -11,6 +11,7 @@ import { EuiLink, EuiPanel, EuiSpacer, + EuiToolTip, } from '@elastic/eui'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; import { i18n } from '@kbn/i18n'; @@ -23,8 +24,10 @@ import { ICMPSimpleFields, Ping, ServiceLocations, + SourceType, EncryptedSyntheticsMonitorWithId, TCPSimpleFields, + BrowserFields, } from '../../../../../common/runtime_types'; import { UptimeSettingsContext } from '../../../contexts'; import { useBreakpoints } from '../../../../hooks/use_breakpoints'; @@ -119,8 +122,14 @@ export const MonitorManagementList = ({ defaultMessage: 'Monitor name', }), sortable: true, - render: (name: string, { id }: EncryptedSyntheticsMonitorWithId) => ( - {name} + render: (name: string, monitor: EncryptedSyntheticsMonitorWithId) => ( + + {name} + ), }, { @@ -174,12 +183,23 @@ export const MonitorManagementList = ({ defaultMessage: 'Enabled', }), render: (_enabled: boolean, monitor: EncryptedSyntheticsMonitorWithId) => ( - + + + ), }, { diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts index 1354681a2e938..34e8ff84535bf 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts @@ -27,6 +27,7 @@ import { MlPluginSetup as MlSetup } from '@kbn/ml-plugin/server'; import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; import { SecurityPluginStart } from '@kbn/security-plugin/server'; import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import { FleetStartContract } from '@kbn/fleet-plugin/server'; import { UptimeESClient } from '../../lib'; import type { TelemetryEventsSender } from '../../telemetry/sender'; @@ -50,6 +51,7 @@ export interface UptimeServerSetup { router: UptimeRouter; config: UptimeConfig; cloud?: CloudSetup; + spaces: SpacesPluginSetup; fleet: FleetStartContract; security: SecurityPluginStart; savedObjectsClient?: SavedObjectsClientContract; @@ -71,6 +73,7 @@ export interface UptimeCorePluginsSetup { usageCollection: UsageCollectionSetup; ml: MlSetup; cloud?: CloudSetup; + spaces: SpacesPluginSetup; ruleRegistry: RuleRegistryPluginSetupContract; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; taskManager: TaskManagerSetupContract; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor.ts index 5cd8eef0e4d38..9b92463f78641 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor.ts @@ -10,8 +10,10 @@ import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin import { SyntheticsMonitorWithSecrets, EncryptedSyntheticsMonitor, + SyntheticsMonitor, } from '../../../../common/runtime_types'; import { syntheticsMonitor, syntheticsMonitorType } from '../saved_objects/synthetics_monitor'; +import { normalizeSecrets } from '../../../synthetics_service/utils/secrets'; export const getSyntheticsMonitor = async ({ monitorId, @@ -21,20 +23,22 @@ export const getSyntheticsMonitor = async ({ monitorId: string; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; savedObjectsClient: SavedObjectsClientContract; -}): Promise> => { +}): Promise> => { try { const encryptedMonitor = await savedObjectsClient.get( syntheticsMonitorType, monitorId ); - return await encryptedSavedObjectsClient.getDecryptedAsInternalUser( - syntheticsMonitor.name, - monitorId, - { - namespace: encryptedMonitor.namespaces?.[0], - } - ); + const decryptedMonitor = + await encryptedSavedObjectsClient.getDecryptedAsInternalUser( + syntheticsMonitor.name, + monitorId, + { + namespace: encryptedMonitor.namespaces?.[0], + } + ); + return normalizeSecrets(decryptedMonitor); } catch (e) { throw e; } diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts index 19a975607a59a..e9a2bc1710860 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts @@ -44,6 +44,18 @@ export const syntheticsMonitor: SavedObjectsType = { }, }, }, + journey_id: { + type: 'keyword', + }, + project_id: { + type: 'keyword', + }, + 'monitor.origin': { + type: 'keyword', + }, + custom_heartbeat_id: { + type: 'keyword', + }, }, }, management: { diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts index 52aae103a2ff1..ac78560c696c7 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts @@ -27,7 +27,7 @@ export interface MonitorUpdateEvent { monitorInterval: number; locations: string[]; locationsCount: number; - scriptType?: 'inline' | 'recorder' | 'zip'; + scriptType?: 'inline' | 'recorder' | 'zip' | 'project'; revision?: number; errors?: ServiceLocationErrors; configId: string; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/monitors/monitor_status.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/monitors/monitor_status.ts index 713a7f8123153..2bf898bf309d4 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/monitors/monitor_status.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/monitors/monitor_status.ts @@ -10,6 +10,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { API_URLS } from '../../../../common/constants'; import { ConfigKey, MonitorFields } from '../../../../common/runtime_types'; +import { syntheticsMonitorType } from '../../../../common/types/saved_objects'; export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -23,7 +24,6 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib }, handler: async ({ uptimeEsClient, request, server, savedObjectsClient }): Promise => { const { monitorId, dateStart, dateEnd } = request.query; - const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient(); const latestMonitor = await libs.requests.getLatestMonitor({ uptimeEsClient, @@ -41,10 +41,13 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib } try { - const monitorSavedObject = await libs.requests.getSyntheticsMonitor({ - monitorId, - encryptedSavedObjectsClient, - savedObjectsClient, + const { + saved_objects: [monitorSavedObject], + } = await savedObjectsClient.find({ + type: syntheticsMonitorType, + perPage: 1, + page: 1, + filter: `${syntheticsMonitorType}.id: "${syntheticsMonitorType}:${monitorId}" OR ${syntheticsMonitorType}.attributes.${ConfigKey.CUSTOM_HEARTBEAT_ID}: "${monitorId}"`, }); if (!monitorSavedObject) { diff --git a/x-pack/plugins/synthetics/server/plugin.ts b/x-pack/plugins/synthetics/server/plugin.ts index a1b934b2ebbff..52b7120845940 100644 --- a/x-pack/plugins/synthetics/server/plugin.ts +++ b/x-pack/plugins/synthetics/server/plugin.ts @@ -84,6 +84,7 @@ export class Plugin implements PluginType { logger: this.logger, telemetry: this.telemetryEventsSender, isDev: this.initContext.env.mode.dev, + spaces: plugins.spaces, } as UptimeServerSetup; if (this.server.config.service) { diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index e4fa55b981a27..0f1e6f01682e6 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -22,9 +22,11 @@ import { testNowMonitorRoute } from './synthetics_service/test_now_monitor'; import { installIndexTemplatesRoute } from './synthetics_service/install_index_templates'; import { editSyntheticsMonitorRoute } from './monitor_cruds/edit_monitor'; import { addSyntheticsMonitorRoute } from './monitor_cruds/add_monitor'; +import { addSyntheticsProjectMonitorRoute } from './monitor_cruds/add_monitor_project'; import { UMRestApiRouteFactory } from '../legacy_uptime/routes'; export const syntheticsAppRestApiRoutes: UMRestApiRouteFactory[] = [ + addSyntheticsProjectMonitorRoute, addSyntheticsMonitorRoute, getSyntheticsEnablementRoute, deleteSyntheticsMonitorRoute, diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts index 2e3a76cdad964..6e9973a658cac 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts @@ -18,6 +18,7 @@ import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/syn import { validateMonitor } from './monitor_validation'; import { sendTelemetryEvents, formatTelemetryEvent } from '../telemetry/monitor_upgrade_sender'; import { formatSecrets } from '../../synthetics_service/utils/secrets'; +import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework'; export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ method: 'POST', @@ -58,27 +59,7 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ }); } - const { syntheticsService } = server; - - const errors = await syntheticsService.addConfig({ - ...monitor, - id: newMonitor.id, - fields: { - config_id: newMonitor.id, - }, - fields_under_root: true, - }); - - sendTelemetryEvents( - server.logger, - server.telemetry, - formatTelemetryEvent({ - monitor: newMonitor, - errors, - isInlineScript: Boolean((monitor as MonitorFields)[ConfigKey.SOURCE_INLINE]), - kibanaVersion: server.kibanaVersion, - }) - ); + const errors = await syncNewMonitor({ monitor, monitorSavedObject: newMonitor, server }); if (errors && errors.length > 0) { return response.ok({ @@ -93,3 +74,35 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ return response.ok({ body: newMonitor }); }, }); + +export const syncNewMonitor = async ({ + monitor, + monitorSavedObject, + server, +}: { + monitor: SyntheticsMonitor; + monitorSavedObject: SavedObject; + server: UptimeServerSetup; +}) => { + const errors = await server.syntheticsService.addConfig({ + ...monitor, + id: (monitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || monitorSavedObject.id, + fields: { + config_id: monitorSavedObject.id, + }, + fields_under_root: true, + }); + + sendTelemetryEvents( + server.logger, + server.telemetry, + formatTelemetryEvent({ + monitor: monitorSavedObject, + errors, + isInlineScript: Boolean((monitor as MonitorFields)[ConfigKey.SOURCE_INLINE]), + kibanaVersion: server.kibanaVersion, + }) + ); + + return errors; +}; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts new file mode 100644 index 0000000000000..003f1a34fce19 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../legacy_uptime/lib/lib'; +import { ProjectBrowserMonitor, Locations } from '../../../common/runtime_types'; + +import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types'; +import { API_URLS } from '../../../common/constants'; +import { getServiceLocations } from '../../synthetics_service/get_service_locations'; +import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor_formatter'; + +export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'PUT', + path: API_URLS.SYNTHETICS_MONITORS_PROJECT, + validate: { + body: schema.object({ + project: schema.string(), + keep_stale: schema.boolean(), + monitors: schema.arrayOf(schema.any()), + }), + }, + handler: async ({ request, response, savedObjectsClient, server }): Promise => { + const monitors = (request.body?.monitors as ProjectBrowserMonitor[]) || []; + const spaceId = server.spaces.spacesService.getSpaceId(request); + const { keep_stale: keepStale, project: projectId } = request.body || {}; + const locations: Locations = (await getServiceLocations(server)).locations; + const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient(); + + const pushMonitorFormatter = new ProjectMonitorFormatter({ + projectId, + spaceId, + keepStale, + locations, + encryptedSavedObjectsClient, + savedObjectsClient, + monitors, + server, + }); + + await pushMonitorFormatter.configureAllProjectMonitors(); + + return response.ok({ + body: { + createdMonitors: pushMonitorFormatter.createdMonitors, + updatedMonitors: pushMonitorFormatter.updatedMonitors, + staleMonitors: pushMonitorFormatter.staleMonitors, + deletedMonitors: pushMonitorFormatter.deletedMonitors, + failedMonitors: pushMonitorFormatter.failedMonitors, + failedStaleMonitors: pushMonitorFormatter.failedStaleMonitors, + }, + }); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts index 6504c0b0ac29b..43753101c56d4 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts @@ -5,7 +5,7 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server'; import { ConfigKey, MonitorFields, @@ -24,6 +24,7 @@ import { formatTelemetryDeleteEvent, } from '../telemetry/monitor_upgrade_sender'; import { normalizeSecrets } from '../../synthetics_service/utils/secrets'; +import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework'; export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ method: 'DELETE', @@ -33,49 +34,11 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ monitorId: schema.string({ minLength: 1, maxLength: 1024 }), }), }, - handler: async ({ - request, - response, - savedObjectsClient, - server: { encryptedSavedObjects, syntheticsService, logger, telemetry, kibanaVersion }, - }): Promise => { - const encryptedSavedObjectsClient = encryptedSavedObjects.getClient(); - + handler: async ({ request, response, savedObjectsClient, server }): Promise => { const { monitorId } = request.params; try { - const encryptedMonitor = await savedObjectsClient.get( - syntheticsMonitorType, - monitorId - ); - - const monitor = - await encryptedSavedObjectsClient.getDecryptedAsInternalUser( - syntheticsMonitor.name, - monitorId, - { - namespace: encryptedMonitor.namespaces?.[0], - } - ); - - const normalizedMonitor = normalizeSecrets(monitor); - - await savedObjectsClient.delete(syntheticsMonitorType, monitorId); - const errors = await syntheticsService.deleteConfigs([ - { ...normalizedMonitor.attributes, id: monitorId }, - ]); - - sendTelemetryEvents( - logger, - telemetry, - formatTelemetryDeleteEvent( - monitor, - kibanaVersion, - new Date().toISOString(), - Boolean((normalizedMonitor.attributes as MonitorFields)[ConfigKey.SOURCE_INLINE]), - errors - ) - ); + const errors = await deleteMonitor({ savedObjectsClient, server, monitorId }); if (errors && errors.length > 0) { return response.ok({ @@ -93,3 +56,59 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ } }, }); + +export const deleteMonitor = async ({ + savedObjectsClient, + server, + monitorId, +}: { + savedObjectsClient: SavedObjectsClientContract; + server: UptimeServerSetup; + monitorId: string; +}) => { + const { syntheticsService, logger, telemetry, kibanaVersion, encryptedSavedObjects } = server; + const encryptedSavedObjectsClient = encryptedSavedObjects.getClient(); + try { + const encryptedMonitor = await savedObjectsClient.get( + syntheticsMonitorType, + monitorId + ); + + const monitor = + await encryptedSavedObjectsClient.getDecryptedAsInternalUser( + syntheticsMonitor.name, + monitorId, + { + namespace: encryptedMonitor.namespaces?.[0], + } + ); + + const normalizedMonitor = normalizeSecrets(monitor); + + await savedObjectsClient.delete(syntheticsMonitorType, monitorId); + const errors = await syntheticsService.deleteConfigs([ + { + ...normalizedMonitor.attributes, + id: + (normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || + monitorId, + }, + ]); + + sendTelemetryEvents( + logger, + telemetry, + formatTelemetryDeleteEvent( + monitor, + kibanaVersion, + new Date().toISOString(), + Boolean((normalizedMonitor.attributes as MonitorFields)[ConfigKey.SOURCE_INLINE]), + errors + ) + ); + + return errors; + } catch (e) { + throw e; + } +}; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index 8bd29c460b8cf..391e310ea97f8 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -28,6 +28,7 @@ import { formatTelemetryUpdateEvent, } from '../telemetry/monitor_upgrade_sender'; import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets'; +import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework'; // Simplify return promise type and type it with runtime_types export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ @@ -39,12 +40,8 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ }), body: schema.any(), }, - handler: async ({ - request, - response, - savedObjectsClient, - server: { encryptedSavedObjects, syntheticsService, logger, telemetry, kibanaVersion }, - }): Promise => { + handler: async ({ request, response, savedObjectsClient, server }): Promise => { + const { encryptedSavedObjects, logger } = server; const encryptedSavedObjectsClient = encryptedSavedObjects.getClient(); const monitor = request.body as SyntheticsMonitor; const { monitorId } = request.params; @@ -85,38 +82,19 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ }; const formattedMonitor = formatSecrets(monitorWithRevision); - const updatedMonitor: SavedObjectsUpdateResponse = + const editedMonitorSavedObject: SavedObjectsUpdateResponse = await savedObjectsClient.update( syntheticsMonitorType, monitorId, monitor.type === 'browser' ? { ...formattedMonitor, urls: '' } : formattedMonitor ); - const errors = await syntheticsService.pushConfigs( - [ - { - ...editedMonitor, - id: updatedMonitor.id, - fields: { - config_id: updatedMonitor.id, - }, - fields_under_root: true, - }, - ], - true - ); - - sendTelemetryEvents( - logger, - telemetry, - formatTelemetryUpdateEvent( - updatedMonitor, - previousMonitor, - kibanaVersion, - Boolean((monitor as MonitorFields)[ConfigKey.SOURCE_INLINE]), - errors - ) - ); + const errors = await syncEditedMonitor({ + server, + editedMonitor, + editedMonitorSavedObject, + previousMonitor, + }); // Return service sync errors in OK response if (errors && errors.length > 0) { @@ -125,7 +103,7 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ }); } - return updatedMonitor; + return editedMonitorSavedObject; } catch (updateErr) { if (SavedObjectsErrorHelpers.isNotFoundError(updateErr)) { return getMonitorNotFoundResponse(response, monitorId); @@ -136,3 +114,42 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ } }, }); + +export const syncEditedMonitor = async ({ + editedMonitor, + editedMonitorSavedObject, + previousMonitor, + server, +}: { + editedMonitor: SyntheticsMonitor; + editedMonitorSavedObject: SavedObjectsUpdateResponse; + previousMonitor: SavedObject; + server: UptimeServerSetup; +}) => { + const errors = await server.syntheticsService.pushConfigs([ + { + ...editedMonitor, + id: + (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || + editedMonitorSavedObject.id, + fields: { + config_id: editedMonitorSavedObject.id, + }, + fields_under_root: true, + }, + ]); + + sendTelemetryEvents( + server.logger, + server.telemetry, + formatTelemetryUpdateEvent( + editedMonitorSavedObject, + previousMonitor, + server.kibanaVersion, + Boolean((editedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]), + errors + ) + ); + + return errors; +}; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts index ca708b794af7c..5af415c1062ac 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -12,7 +12,6 @@ import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor'; import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors'; -import { normalizeSecrets } from '../../synthetics_service/utils/secrets'; export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -31,12 +30,11 @@ export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerL const { monitorId } = request.params; const encryptedSavedObjectsClient = encryptedSavedObjects.getClient(); try { - const monitorWithSecrets = await libs.requests.getSyntheticsMonitor({ + return await libs.requests.getSyntheticsMonitor({ monitorId, encryptedSavedObjectsClient, savedObjectsClient, }); - return normalizeSecrets(monitorWithSecrets); } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { return getMonitorNotFoundResponse(response, monitorId); @@ -57,10 +55,11 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ sortField: schema.maybe(schema.string()), sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), search: schema.maybe(schema.string()), + query: schema.maybe(schema.string()), }), }, handler: async ({ request, savedObjectsClient, server }): Promise => { - const { perPage = 50, page, sortField, sortOrder, search } = request.query; + const { perPage = 50, page, sortField, sortOrder, search, query } = request.query; // TODO: add query/filtering params const { saved_objects: monitors, @@ -72,7 +71,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ page, sortField, sortOrder, - filter: search ? `${syntheticsMonitorType}.attributes.name: ${search}` : '', + filter: query || (search ? `${syntheticsMonitorType}.attributes.name: ${search}` : ''), }); return { ...rest, diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/monitor_validation.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts similarity index 98% rename from x-pack/plugins/synthetics/server/routes/synthetics_service/monitor_validation.test.ts rename to x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts index 3ede4a0ab464a..e862bec6e92cc 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/monitor_validation.test.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts @@ -21,6 +21,7 @@ import { MonitorFields, ResponseBodyIndexPolicy, ScheduleUnit, + SourceType, TCPAdvancedFields, TCPFields, TCPSimpleFields, @@ -29,7 +30,7 @@ import { VerificationMode, ZipUrlTLSFields, } from '../../../common/runtime_types'; -import { validateMonitor } from '../monitor_cruds/monitor_validation'; +import { validateMonitor } from './monitor_validation'; describe('validateMonitor', () => { let testSchedule; @@ -160,8 +161,12 @@ describe('validateMonitor', () => { testBrowserSimpleFields = { ...testZipUrlTLSFields, ...testCommonFields, + [ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT, + [ConfigKey.JOURNEY_ID]: '', + [ConfigKey.PROJECT_ID]: '', [ConfigKey.METADATA]: testMetaData, [ConfigKey.SOURCE_INLINE]: '', + [ConfigKey.SOURCE_PROJECT_CONTENT]: '', [ConfigKey.SOURCE_ZIP_URL]: '', [ConfigKey.SOURCE_ZIP_FOLDER]: '', [ConfigKey.SOURCE_ZIP_USERNAME]: 'test-username', diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts index 446eaa5847319..92a8f8cd81d82 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts @@ -10,6 +10,8 @@ import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { BrowserFieldsCodec, + ProjectBrowserMonitorCodec, + ProjectBrowserMonitor, ConfigKey, DataStream, DataStreamCodec, @@ -79,3 +81,42 @@ export function validateMonitor(monitorFields: MonitorFields): { return { valid: true, reason: '', details: '', payload: monitorFields }; } + +export function validateProjectMonitor( + monitorFields: ProjectBrowserMonitor, + projectId: string +): { + valid: boolean; + reason: string; + details: string; + payload: object; +} { + const locationsError = + monitorFields.locations && monitorFields.locations.length === 0 + ? 'Invalid value "[]" supplied to field "locations"' + : ''; + // Cast it to ICMPCodec to satisfy typing. During runtime, correct codec will be used to decode. + const decodedMonitor = ProjectBrowserMonitorCodec.decode(monitorFields); + + if (isLeft(decodedMonitor)) { + return { + valid: false, + reason: `Failed to save or update monitor. Configuration is not valid`, + details: [...formatErrors(decodedMonitor.left), locationsError] + .filter((error) => error !== '') + .join(' | '), + payload: monitorFields, + }; + } + + if (locationsError) { + return { + valid: false, + reason: `Failed to save or update monitor. Configuration is not valid`, + details: locationsError, + payload: monitorFields, + }; + } + + return { valid: true, reason: '', details: '', payload: monitorFields }; +} diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts index c2c897d153849..fe5c7d35fd3d9 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { v4 as uuidv4 } from 'uuid'; import { ConfigKey, + MonitorFields, SyntheticsMonitor, SyntheticsMonitorWithSecrets, } from '../../../common/runtime_types'; @@ -41,6 +42,7 @@ export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({ syntheticsMonitor.name, monitorId ); + const normalizedMonitor = normalizeSecrets(monitorWithSecrets); const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } = monitor.attributes; @@ -50,8 +52,10 @@ export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({ const errors = await syntheticsService.triggerConfigs(request, [ { - ...normalizeSecrets(monitorWithSecrets).attributes, - id: monitorId, + ...normalizedMonitor.attributes, + id: + (normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || + monitorId, fields_under_root: true, fields: { config_id: monitorId, test_run_id: testRunId }, }, diff --git a/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts b/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts index b9c9869ddf60d..692f55d87297f 100644 --- a/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts +++ b/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.test.ts @@ -13,7 +13,9 @@ import { ConfigKey, DataStream, ScheduleUnit, + SourceType, } from '../../../common/runtime_types/monitor_management'; +import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults'; import type { TelemetryEventsSender } from '../../legacy_uptime/lib/telemetry/sender'; import { createMockTelemetryEventsSender } from '../../legacy_uptime/lib/telemetry/__mocks__'; @@ -45,6 +47,7 @@ const testConfig: SavedObject = { updated_at: '2011-10-05T14:48:00.000Z', id, attributes: { + ...DEFAULT_FIELDS[DataStream.BROWSER], [ConfigKey.MONITOR_TYPE]: DataStream.HTTP, [ConfigKey.LOCATIONS]: [ { @@ -111,15 +114,16 @@ describe('monitor upgrade telemetry helpers', () => { }); it.each([ - [ConfigKey.SOURCE_INLINE, 'recorder', true, true], - [ConfigKey.SOURCE_INLINE, 'inline', false, true], - [ConfigKey.SOURCE_ZIP_URL, 'zip', false, false], + [ConfigKey.MONITOR_SOURCE_TYPE, SourceType.PROJECT, 'project', false, false], + [ConfigKey.SOURCE_INLINE, 'test', 'recorder', true, true], + [ConfigKey.SOURCE_INLINE, 'test', 'inline', false, true], + [ConfigKey.SOURCE_ZIP_URL, 'test', 'zip', false, false], ])( 'handles formatting scriptType for browser monitors', - (config, scriptType, isRecorder, isInlineScript) => { + (config, value, scriptType, isRecorder, isInlineScript) => { const actual = formatTelemetryEvent({ monitor: createTestConfig({ - [config]: 'test', + [config]: value, [ConfigKey.METADATA]: { script_source: { is_generated_script: isRecorder, diff --git a/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts b/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts index f71ec21ea732b..a38517d9d8e70 100644 --- a/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts +++ b/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts @@ -13,6 +13,7 @@ import { EncryptedSyntheticsMonitor, ConfigKey, ServiceLocationErrors, + SourceType, } from '../../../common/runtime_types'; import type { MonitorUpdateEvent } from '../../legacy_uptime/lib/telemetry/types'; @@ -166,14 +167,19 @@ export function formatTelemetrySyncEvent() {} function getScriptType( attributes: Partial, isInlineScript: boolean -): 'inline' | 'recorder' | 'zip' | undefined { - if (attributes[ConfigKey.SOURCE_ZIP_URL]) { - return 'zip'; - } else if (isInlineScript && attributes[ConfigKey.METADATA]?.script_source?.is_generated_script) { - return 'recorder'; - } else if (isInlineScript) { - return 'inline'; +): MonitorUpdateEvent['scriptType'] | undefined { + switch (true) { + case Boolean(attributes[ConfigKey.SOURCE_ZIP_URL]): + return 'zip'; + case Boolean( + isInlineScript && attributes[ConfigKey.METADATA]?.script_source?.is_generated_script + ): + return 'recorder'; + case Boolean(isInlineScript): + return 'inline'; + case attributes[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT: + return 'project'; + default: + return undefined; } - - return undefined; } diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts index 78135de89639a..4192cf41eed81 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts @@ -7,22 +7,27 @@ import { Formatter, commonFormatters, objectFormatter, arrayFormatter } from './common'; import { BrowserFields, ConfigKey } from '../../../common/runtime_types/monitor_management'; +import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../../../common/constants/monitor_defaults'; export type BrowserFormatMap = Record; const throttlingFormatter: Formatter = (fields) => { if (!fields[ConfigKey.IS_THROTTLING_ENABLED]) return false; - const getThrottlingValue = (v: string | undefined, suffix: 'd' | 'u' | 'l') => - v !== '' && v !== undefined ? `${v}${suffix}` : null; - - return [ - getThrottlingValue(fields[ConfigKey.DOWNLOAD_SPEED], 'd'), - getThrottlingValue(fields[ConfigKey.UPLOAD_SPEED], 'u'), - getThrottlingValue(fields[ConfigKey.LATENCY], 'l'), - ] - .filter((v) => v !== null) - .join('/'); + return { + download: parseInt( + fields[ConfigKey.DOWNLOAD_SPEED] || DEFAULT_BROWSER_ADVANCED_FIELDS[ConfigKey.DOWNLOAD_SPEED], + 10 + ), + upload: parseInt( + fields[ConfigKey.UPLOAD_SPEED] || DEFAULT_BROWSER_ADVANCED_FIELDS[ConfigKey.UPLOAD_SPEED], + 10 + ), + latency: parseInt( + fields[ConfigKey.LATENCY] || DEFAULT_BROWSER_ADVANCED_FIELDS[ConfigKey.LATENCY], + 10 + ), + }; }; export const browserFormatters: BrowserFormatMap = { @@ -36,6 +41,7 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.SOURCE_ZIP_PASSWORD]: null, [ConfigKey.SOURCE_ZIP_FOLDER]: null, [ConfigKey.SOURCE_ZIP_PROXY_URL]: null, + [ConfigKey.SOURCE_PROJECT_CONTENT]: null, [ConfigKey.SOURCE_INLINE]: null, [ConfigKey.PARAMS]: null, [ConfigKey.SCREENSHOTS]: null, @@ -54,5 +60,10 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.JOURNEY_FILTERS_TAGS]: (fields) => arrayFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]), [ConfigKey.IGNORE_HTTPS_ERRORS]: null, + [ConfigKey.JOURNEY_ID]: null, + [ConfigKey.PROJECT_ID]: null, + [ConfigKey.PLAYWRIGHT_OPTIONS]: null, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, + [ConfigKey.ORIGINAL_SPACE]: null, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts index 8756ea9c67c8e..63307f5cb80ab 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { CommonFields, ConfigKey, MonitorFields } from '../../../common/runtime_types'; +import { CommonFields, ConfigKey, MonitorFields, SourceType } from '../../../common/runtime_types'; -export type FormattedValue = boolean | string | string[] | Record | null; +export type FormattedValue = boolean | string | string[] | Record | null; export type Formatter = null | ((fields: Partial) => FormattedValue); @@ -26,6 +26,8 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT] || undefined), [ConfigKey.NAMESPACE]: null, [ConfigKey.REVISION]: null, + [ConfigKey.MONITOR_SOURCE_TYPE]: (fields) => + fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, }; export const arrayFormatter = (value: string[] = []) => (value.length ? value : null); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts index 48d052d35a1f8..1dc96a5e2eefa 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts @@ -116,7 +116,11 @@ describe('formatMonitorConfig', () => { screenshots: 'on', 'source.inline.script': "step('Go to https://www.google.com/', async () => {\n await page.goto('https://www.google.com/');\n});", - throttling: '5d/3u/20l', + throttling: { + download: 5, + latency: 20, + upload: 3, + }, timeout: '16s', type: 'browser', synthetics_args: ['--hasTouch true'], diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts index ea298992d2246..002cf91616745 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts @@ -10,12 +10,15 @@ import { ConfigKey, MonitorFields } from '../../../common/runtime_types'; import { formatters } from '.'; const UI_KEYS_TO_SKIP = [ + ConfigKey.JOURNEY_ID, + ConfigKey.PROJECT_ID, ConfigKey.METADATA, ConfigKey.UPLOAD_SPEED, ConfigKey.DOWNLOAD_SPEED, ConfigKey.LATENCY, ConfigKey.IS_THROTTLING_ENABLED, ConfigKey.REVISION, + ConfigKey.CUSTOM_HEARTBEAT_ID, 'secrets', ]; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.test.ts new file mode 100644 index 0000000000000..d5583384a6d5c --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { + DataStream, + ScreenshotOption, + Locations, + LocationStatus, + ProjectBrowserMonitor, +} from '../../../common/runtime_types'; +import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults'; +import { normalizeProjectMonitors } from './browser'; + +describe('browser normalizers', () => { + describe('normalize push monitors', () => { + const playwrightOptions = { + headless: true, + }; + const params = { + url: 'test-url', + }; + const projectId = 'test-project-id'; + const locations: Locations = [ + { + id: 'us_central', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + { + id: 'us_east', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + ]; + const monitors: ProjectBrowserMonitor[] = [ + { + id: 'test-id-1', + screenshot: ScreenshotOption.OFF, + name: 'test-name-1', + content: 'test content 1', + schedule: 3, + throttling: { + latency: 20, + upload: 10, + download: 5, + }, + locations: ['us_central'], + tags: ['tag1', 'tag2'], + ignoreHTTPSErrors: true, + apmServiceName: 'cart-service', + }, + { + id: 'test-id-2', + screenshot: ScreenshotOption.ON, + name: 'test-name-2', + content: 'test content 2', + schedule: 10, + throttling: { + latency: 18, + upload: 15, + download: 10, + }, + params: {}, + playwrightOptions: {}, + locations: ['us_central', 'us_east'], + tags: ['tag3', 'tag4'], + ignoreHTTPSErrors: false, + apmServiceName: 'bean-service', + }, + { + id: 'test-id-3', + screenshot: ScreenshotOption.ON, + name: 'test-name-3', + content: 'test content 3', + schedule: 10, + throttling: { + latency: 18, + upload: 15, + download: 10, + }, + params, + playwrightOptions, + locations: ['us_central', 'us_east'], + tags: ['tag3', 'tag4'], + ignoreHTTPSErrors: false, + apmServiceName: 'bean-service', + }, + ]; + + it('properly normalizes browser monitor', () => { + const actual = normalizeProjectMonitors({ + locations, + monitors, + projectId, + namespace: 'test-space', + }); + expect(actual).toEqual([ + { + ...DEFAULT_FIELDS[DataStream.BROWSER], + journey_id: 'test-id-1', + ignore_https_errors: true, + 'monitor.origin': 'project', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + url: 'test-url', + status: 'ga', + }, + ], + name: 'test-name-1', + schedule: { + number: '3', + unit: 'm', + }, + screenshots: 'off', + 'service.name': 'cart-service', + 'source.project.content': 'test content 1', + tags: ['tag1', 'tag2'], + 'throttling.config': '5d/10u/20l', + 'throttling.download_speed': '5', + 'throttling.is_enabled': true, + 'throttling.latency': '20', + 'throttling.upload_speed': '10', + params: '', + type: 'browser', + project_id: projectId, + namespace: 'test-space', + original_space: 'test-space', + custom_heartbeat_id: 'test-id-1-test-project-id-test-space', + timeout: null, + }, + { + ...DEFAULT_FIELDS[DataStream.BROWSER], + journey_id: 'test-id-2', + ignore_https_errors: false, + 'monitor.origin': 'project', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + url: 'test-url', + status: 'ga', + }, + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_east', + isServiceManaged: true, + label: 'Test Location', + url: 'test-url', + status: 'ga', + }, + ], + name: 'test-name-2', + params: '', + playwright_options: '', + schedule: { + number: '10', + unit: 'm', + }, + screenshots: 'on', + 'service.name': 'bean-service', + 'source.project.content': 'test content 2', + tags: ['tag3', 'tag4'], + 'throttling.config': '10d/15u/18l', + 'throttling.download_speed': '10', + 'throttling.is_enabled': true, + 'throttling.latency': '18', + 'throttling.upload_speed': '15', + type: 'browser', + project_id: projectId, + namespace: 'test-space', + original_space: 'test-space', + custom_heartbeat_id: 'test-id-2-test-project-id-test-space', + timeout: null, + }, + { + ...DEFAULT_FIELDS[DataStream.BROWSER], + journey_id: 'test-id-3', + ignore_https_errors: false, + 'monitor.origin': 'project', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + url: 'test-url', + status: 'ga', + }, + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_east', + isServiceManaged: true, + label: 'Test Location', + url: 'test-url', + status: 'ga', + }, + ], + name: 'test-name-3', + params: JSON.stringify(params), + playwright_options: JSON.stringify(playwrightOptions), + schedule: { + number: '10', + unit: 'm', + }, + screenshots: 'on', + 'service.name': 'bean-service', + 'source.project.content': 'test content 3', + tags: ['tag3', 'tag4'], + 'throttling.config': '10d/15u/18l', + 'throttling.download_speed': '10', + 'throttling.is_enabled': true, + 'throttling.latency': '18', + 'throttling.upload_speed': '15', + type: 'browser', + project_id: projectId, + namespace: 'test-space', + original_space: 'test-space', + custom_heartbeat_id: 'test-id-3-test-project-id-test-space', + timeout: null, + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts new file mode 100644 index 0000000000000..92d598192844b --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.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 { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults'; + +import { + BrowserFields, + ConfigKey, + DataStream, + Locations, + ProjectBrowserMonitor, + ScheduleUnit, + SourceType, +} from '../../../common/runtime_types/monitor_management'; + +/* Represents all of the push-monitor related fields that need to be + * normalized. Excludes fields that we do not support for push monitors + * This type ensures that contributors remember to add normalizers for push + * monitors where appropriate when new keys are added to browser montiors */ +type NormalizedPublicFields = Omit< + BrowserFields, + | ConfigKey.METADATA + | ConfigKey.SOURCE_INLINE + | ConfigKey.SOURCE_ZIP_URL + | ConfigKey.SOURCE_ZIP_USERNAME + | ConfigKey.SOURCE_ZIP_PASSWORD + | ConfigKey.SOURCE_ZIP_FOLDER + | ConfigKey.SOURCE_ZIP_PROXY_URL + | ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES + | ConfigKey.ZIP_URL_TLS_CERTIFICATE + | ConfigKey.ZIP_URL_TLS_KEY + | ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE + | ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE + | ConfigKey.ZIP_URL_TLS_VERSION + | ConfigKey.JOURNEY_FILTERS_TAGS + | ConfigKey.SYNTHETICS_ARGS + | ConfigKey.PORT + | ConfigKey.URLS +>; + +export const normalizeProjectMonitor = ({ + locations = [], + monitor, + projectId, + namespace, +}: { + locations: Locations; + monitor: ProjectBrowserMonitor; + projectId: string; + namespace: string; +}): BrowserFields => { + const defaultFields = DEFAULT_FIELDS[DataStream.BROWSER]; + const normalizedFields: NormalizedPublicFields = { + [ConfigKey.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT, + [ConfigKey.NAME]: monitor.name || '', + [ConfigKey.SCHEDULE]: { + number: `${monitor.schedule}`, + unit: ScheduleUnit.MINUTES, + }, + [ConfigKey.PROJECT_ID]: projectId || defaultFields[ConfigKey.PROJECT_ID], + [ConfigKey.JOURNEY_ID]: monitor.id || defaultFields[ConfigKey.JOURNEY_ID], + [ConfigKey.SOURCE_PROJECT_CONTENT]: + monitor.content || defaultFields[ConfigKey.SOURCE_PROJECT_CONTENT], + [ConfigKey.LOCATIONS]: monitor.locations + ?.map((key) => { + return locations.find((location) => location.id === key); + }) + .filter((location) => location !== undefined) as BrowserFields[ConfigKey.LOCATIONS], + [ConfigKey.THROTTLING_CONFIG]: monitor.throttling + ? `${monitor.throttling.download}d/${monitor.throttling.upload}u/${monitor.throttling.latency}l` + : defaultFields[ConfigKey.THROTTLING_CONFIG], + [ConfigKey.DOWNLOAD_SPEED]: `${ + monitor.throttling?.download || defaultFields[ConfigKey.DOWNLOAD_SPEED] + }`, + [ConfigKey.UPLOAD_SPEED]: `${ + monitor.throttling?.upload || defaultFields[ConfigKey.UPLOAD_SPEED] + }`, + [ConfigKey.IS_THROTTLING_ENABLED]: + Boolean(monitor.throttling) || defaultFields[ConfigKey.IS_THROTTLING_ENABLED], + [ConfigKey.LATENCY]: `${monitor.throttling?.latency || defaultFields[ConfigKey.LATENCY]}`, + [ConfigKey.APM_SERVICE_NAME]: + monitor.apmServiceName || defaultFields[ConfigKey.APM_SERVICE_NAME], + [ConfigKey.IGNORE_HTTPS_ERRORS]: + monitor.ignoreHTTPSErrors || defaultFields[ConfigKey.IGNORE_HTTPS_ERRORS], + [ConfigKey.SCREENSHOTS]: monitor.screenshot || defaultFields[ConfigKey.SCREENSHOTS], + [ConfigKey.TAGS]: monitor.tags || defaultFields[ConfigKey.TAGS], + [ConfigKey.PLAYWRIGHT_OPTIONS]: Object.keys(monitor.playwrightOptions || {}).length + ? JSON.stringify(monitor.playwrightOptions) + : defaultFields[ConfigKey.PLAYWRIGHT_OPTIONS], + [ConfigKey.PARAMS]: Object.keys(monitor.params || {}).length + ? JSON.stringify(monitor.params) + : defaultFields[ConfigKey.PARAMS], + [ConfigKey.JOURNEY_FILTERS_MATCH]: + monitor.filter?.match || defaultFields[ConfigKey.JOURNEY_FILTERS_MATCH], + [ConfigKey.NAMESPACE]: namespace || defaultFields[ConfigKey.NAMESPACE], + [ConfigKey.ORIGINAL_SPACE]: namespace || defaultFields[ConfigKey.ORIGINAL_SPACE], + [ConfigKey.CUSTOM_HEARTBEAT_ID]: `${monitor.id}-${projectId}-${namespace}`, + [ConfigKey.TIMEOUT]: null, + [ConfigKey.ENABLED]: monitor.enabled || defaultFields[ConfigKey.ENABLED], + }; + return { + ...DEFAULT_FIELDS[DataStream.BROWSER], + ...normalizedFields, + }; +}; + +export const normalizeProjectMonitors = ({ + locations = [], + monitors = [], + projectId, + namespace, +}: { + locations: Locations; + monitors: ProjectBrowserMonitor[]; + projectId: string; + namespace: string; +}) => { + return monitors.map((monitor) => { + return normalizeProjectMonitor({ monitor, locations, projectId, namespace }); + }); +}; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts new file mode 100644 index 0000000000000..3060075701ec3 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor_formatter.ts @@ -0,0 +1,302 @@ +/* + * 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 { + SavedObjectsUpdateResponse, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '@kbn/core/server'; +import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; +import { + BrowserFields, + ConfigKey, + MonitorFields, + SyntheticsMonitorWithSecrets, + EncryptedSyntheticsMonitor, + ServiceLocationErrors, + ProjectBrowserMonitor, + Locations, +} from '../../common/runtime_types'; +import { + syntheticsMonitorType, + syntheticsMonitor, +} from '../legacy_uptime/lib/saved_objects/synthetics_monitor'; +import { normalizeProjectMonitor } from './normalizers/browser'; +import { formatSecrets, normalizeSecrets } from './utils/secrets'; +import { syncNewMonitor } from '../routes/monitor_cruds/add_monitor'; +import { syncEditedMonitor } from '../routes/monitor_cruds/edit_monitor'; +import { deleteMonitor } from '../routes/monitor_cruds/delete_monitor'; +import { validateProjectMonitor } from '../routes/monitor_cruds/monitor_validation'; +import type { UptimeServerSetup } from '../legacy_uptime/lib/adapters/framework'; + +interface StaleMonitor { + stale: boolean; + journeyId: string; + savedObjectId: string; +} +type StaleMonitorMap = Record; +type FailedMonitors = Array<{ id: string; reason: string; details: string; payload?: object }>; + +export class ProjectMonitorFormatter { + private projectId: string; + private spaceId: string; + private keepStale: boolean; + private locations: Locations; + private savedObjectsClient: SavedObjectsClientContract; + private encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + private staleMonitorsMap: StaleMonitorMap = {}; + private monitors: ProjectBrowserMonitor[] = []; + public createdMonitors: string[] = []; + public deletedMonitors: string[] = []; + public updatedMonitors: string[] = []; + public staleMonitors: string[] = []; + public failedMonitors: FailedMonitors = []; + public failedStaleMonitors: FailedMonitors = []; + private server: UptimeServerSetup; + private projectFilter: string; + + constructor({ + locations, + keepStale, + savedObjectsClient, + encryptedSavedObjectsClient, + projectId, + spaceId, + monitors, + server, + }: { + locations: Locations; + keepStale: boolean; + savedObjectsClient: SavedObjectsClientContract; + encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + projectId: string; + spaceId: string; + monitors: ProjectBrowserMonitor[]; + server: UptimeServerSetup; + }) { + this.projectId = projectId; + this.spaceId = spaceId; + this.locations = locations; + this.keepStale = keepStale; + this.savedObjectsClient = savedObjectsClient; + this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; + this.monitors = monitors; + this.server = server; + this.projectFilter = `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${this.projectId}"`; + } + + public configureAllProjectMonitors = async () => { + this.staleMonitorsMap = await this.getAllProjectMonitorsForProject(); + await Promise.all( + this.monitors.map((monitor) => + this.configureProjectMonitor({ + monitor, + }) + ) + ); + + await this.handleStaleMonitors(); + }; + + private configureProjectMonitor = async ({ monitor }: { monitor: ProjectBrowserMonitor }) => { + try { + // check to see if monitor already exists + const normalizedMonitor = normalizeProjectMonitor({ + locations: this.locations, + monitor, + projectId: this.projectId, + namespace: this.spaceId, + }); + + const validationResult = validateProjectMonitor(monitor, this.projectId); + + if (!validationResult.valid) { + const { reason: message, details, payload } = validationResult; + this.failedMonitors.push({ + id: monitor.id, + reason: message, + details, + payload, + }); + if (this.staleMonitorsMap[monitor.id]) { + this.staleMonitorsMap[monitor.id].stale = false; + } + return null; + } + + const previousMonitor = await this.getExistingMonitor(monitor.id); + + if (previousMonitor) { + await this.updateMonitor(previousMonitor, normalizedMonitor); + this.updatedMonitors.push(monitor.id); + if (this.staleMonitorsMap[monitor.id]) { + this.staleMonitorsMap[monitor.id].stale = false; + } + } else { + const newMonitor = await this.savedObjectsClient.create( + syntheticsMonitorType, + formatSecrets({ + ...normalizedMonitor, + revision: 1, + }) + ); + await syncNewMonitor({ + server: this.server, + monitor: normalizedMonitor, + monitorSavedObject: newMonitor, + }); + this.createdMonitors.push(monitor.id); + } + } catch (e) { + this.server.logger.error(e); + this.failedMonitors.push({ + id: monitor.id, + reason: 'Failed to create or update monitor', + details: e.message, + payload: monitor, + }); + } + }; + + private getAllProjectMonitorsForProject = async (): Promise => { + const staleMonitors: StaleMonitorMap = {}; + let page = 1; + let totalMonitors = 0; + do { + const { total, saved_objects: savedObjects } = await this.getProjectMonitorsForProject(page); + savedObjects.forEach((savedObject) => { + const journeyId = (savedObject.attributes as BrowserFields)[ConfigKey.JOURNEY_ID]; + if (journeyId) { + staleMonitors[journeyId] = { + stale: true, + savedObjectId: savedObject.id, + journeyId, + }; + } + }); + + page++; + totalMonitors = total; + } while (Object.keys(staleMonitors).length < totalMonitors); + return staleMonitors; + }; + + private getProjectMonitorsForProject = async (page: number) => { + return await this.savedObjectsClient.find({ + type: syntheticsMonitorType, + page, + perPage: 500, + filter: this.projectFilter, + }); + }; + + private getExistingMonitor = async ( + journeyId: string + ): Promise> => { + const filter = `${this.projectFilter} AND ${syntheticsMonitorType}.attributes.${ConfigKey.JOURNEY_ID}: "${journeyId}"`; + const { saved_objects: savedObjects } = + await this.savedObjectsClient.find({ + type: syntheticsMonitorType, + perPage: 1, + filter, + }); + return savedObjects?.[0]; + }; + + private updateMonitor = async ( + previousMonitor: SavedObjectsFindResult, + normalizedMonitor: BrowserFields + ): Promise<{ + editedMonitor: SavedObjectsUpdateResponse; + errors: ServiceLocationErrors; + }> => { + const decryptedPreviousMonitor = + await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + syntheticsMonitor.name, + previousMonitor.id, + { + namespace: previousMonitor.namespaces?.[0], + } + ); + const { + attributes: { [ConfigKey.REVISION]: _, ...normalizedPreviousMonitorAttributes }, + } = normalizeSecrets(decryptedPreviousMonitor); + const hasMonitorBeenEdited = !isEqual(normalizedMonitor, normalizedPreviousMonitorAttributes); + const monitorWithRevision = formatSecrets({ + ...normalizedMonitor, + revision: hasMonitorBeenEdited + ? (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1 + : previousMonitor.attributes[ConfigKey.REVISION], + }); + const editedMonitor: SavedObjectsUpdateResponse = + await this.savedObjectsClient.update( + syntheticsMonitorType, + previousMonitor.id, + { + ...monitorWithRevision, + urls: '', + } + ); + + if (hasMonitorBeenEdited) { + syncEditedMonitor({ + editedMonitor: normalizedMonitor, + editedMonitorSavedObject: editedMonitor, + previousMonitor, + server: this.server, + }); + } + + return { editedMonitor, errors: [] }; + }; + + private handleStaleMonitors = async () => { + try { + const staleMonitorsData = Object.values(this.staleMonitorsMap).filter( + (monitor) => monitor.stale === true + ); + await Promise.all( + staleMonitorsData.map((monitor) => { + if (!this.keepStale) { + return this.deleteStaleMonitor({ + monitorId: monitor.savedObjectId, + journeyId: monitor.journeyId, + }); + } else { + this.staleMonitors.push(monitor.journeyId); + return null; + } + }) + ); + } catch (e) { + this.server.logger.error(e); + } + }; + + private deleteStaleMonitor = async ({ + monitorId, + journeyId, + }: { + monitorId: string; + journeyId: string; + }) => { + try { + await deleteMonitor({ + savedObjectsClient: this.savedObjectsClient, + server: this.server, + monitorId, + }); + this.deletedMonitors.push(journeyId); + } catch (e) { + this.failedStaleMonitors.push({ + id: monitorId, + reason: 'Failed to delete stale monitor', + details: e.message, + }); + } + }; +} diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index e200bdb7fcfa1..d79396da78ce9 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -408,12 +408,16 @@ export class SyntheticsService { }); } - return (monitors ?? []).map((monitor) => ({ - ...normalizeSecrets(monitor).attributes, - id: monitor.id, - fields_under_root: true, - fields: { config_id: monitor.id }, - })); + return (monitors ?? []).map((monitor) => { + const attributes = monitor.attributes as unknown as MonitorFields; + const id = attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] || monitor.id; + return { + ...normalizeSecrets(monitor).attributes, + id, // heartbeat id + fields_under_root: true, + fields: { config_id: monitor.id }, // monitor saved object id + }; + }); } formatConfigs(configs: SyntheticsMonitorWithId[]) { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/utils/index.ts b/x-pack/plugins/synthetics/server/synthetics_service/utils/index.ts new file mode 100644 index 0000000000000..7fb84ce480a91 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './secrets'; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts b/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts index e0dcfea006932..ed59f2043c9d7 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/utils/secrets.ts @@ -8,9 +8,11 @@ import { omit, pick } from 'lodash'; import { SavedObject } from '@kbn/core/server'; import { secretKeys } from '../../../common/constants/monitor_management'; import { + ConfigKey, SyntheticsMonitor, SyntheticsMonitorWithSecrets, } from '../../../common/runtime_types/monitor_management'; +import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults'; export function formatSecrets(monitor: SyntheticsMonitor): SyntheticsMonitorWithSecrets { const monitorWithoutSecrets = omit(monitor, secretKeys) as SyntheticsMonitorWithSecrets; @@ -25,11 +27,15 @@ export function formatSecrets(monitor: SyntheticsMonitor): SyntheticsMonitorWith export function normalizeSecrets( monitor: SavedObject ): SavedObject { - return { + const defaultFields = DEFAULT_FIELDS[monitor.attributes[ConfigKey.MONITOR_TYPE]]; + const normalizedMonitor = { ...monitor, attributes: { + ...defaultFields, ...monitor.attributes, ...JSON.parse(monitor.attributes.secrets || ''), }, }; + delete normalizedMonitor.attributes.secrets; + return normalizedMonitor; } diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts new file mode 100644 index 0000000000000..8daccb85b7e6d --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts @@ -0,0 +1,578 @@ +/* + * 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 uuid from 'uuid'; +import expect from '@kbn/expect'; +import { ConfigKey, ProjectMonitorsRequest } from '@kbn/synthetics-plugin/common/runtime_types'; +import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { syntheticsMonitorType } from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/synthetics_monitor'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getFixtureJson } from './helper/get_fixture_json'; + +export default function ({ getService }: FtrProviderContext) { + describe('[PUT] /api/uptime/service/monitors', () => { + const supertest = getService('supertest'); + const security = getService('security'); + const kibanaServer = getService('kibanaServer'); + + let projectMonitors: ProjectMonitorsRequest; + + const setUniqueIds = (request: ProjectMonitorsRequest) => { + return { + ...request, + monitors: request.monitors.map((monitor) => ({ ...monitor, id: uuid.v4() })), + }; + }; + + const deleteMonitor = async ( + journeyId: string, + projectId: string, + space: string = 'default', + username: string = '', + password: string = '' + ) => { + try { + const response = await supertest + .get(`/s/${space}${API_URLS.SYNTHETICS_MONITORS}`) + .auth(username, password) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorType}.attributes.project_id: "${projectId}"`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + const { monitors } = response.body; + if (monitors[0]?.id) { + await supertest + .delete(`/s/${space}${API_URLS.SYNTHETICS_MONITORS}/${monitors[0].id}`) + .set('kbn-xsrf', 'true') + .send(projectMonitors) + .expect(200); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }; + + beforeEach(() => { + projectMonitors = setUniqueIds(getFixtureJson('project_browser_monitor')); + }); + + it('project monitors - returns a list of successfully created monitors', async () => { + try { + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + expect(apiResponse.body.updatedMonitors).eql([]); + expect(apiResponse.body.failedMonitors).eql([]); + expect(apiResponse.body.createdMonitors).eql( + projectMonitors.monitors.map((monitor) => monitor.id) + ); + } finally { + await Promise.all([ + projectMonitors.monitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - returns a list of successfully updated monitors', async () => { + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + expect(apiResponse.body.createdMonitors).eql([]); + expect(apiResponse.body.failedMonitors).eql([]); + expect(apiResponse.body.updatedMonitors).eql( + projectMonitors.monitors.map((monitor) => monitor.id) + ); + } finally { + await Promise.all([ + projectMonitors.monitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - does not increment monitor revision unless a change has been made', async () => { + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + const updatedMonitorsResponse = await Promise.all( + projectMonitors.monitors.map((monitor) => { + return supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .query({ query: `${syntheticsMonitorType}.attributes.journey_id: ${monitor.id}` }) + .set('kbn-xsrf', 'true') + .expect(200); + }) + ); + + updatedMonitorsResponse.forEach((response) => { + expect(response.body.monitors[0].attributes.revision).eql(1); + }); + } finally { + await Promise.all([ + projectMonitors.monitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - increments monitor revision when a change has been made', async () => { + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + const editedMonitors = { + ...projectMonitors, + monitors: projectMonitors.monitors.map((monitor) => ({ + ...monitor, + content: 'changed content', + })), + }; + + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(editedMonitors); + + const updatedMonitorsResponse = await Promise.all( + projectMonitors.monitors.map((monitor) => { + return supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .query({ query: `${syntheticsMonitorType}.attributes.journey_id: ${monitor.id}` }) + .set('kbn-xsrf', 'true') + .expect(200); + }) + ); + + updatedMonitorsResponse.forEach((response) => { + expect(response.body.monitors[0].attributes.revision).eql(2); + }); + } finally { + await Promise.all([ + projectMonitors.monitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - does not delete monitors when keep stale is true', async () => { + const secondMonitor = { ...projectMonitors.monitors[0], id: 'test-id-2' }; + const testMonitors = [projectMonitors.monitors[0], secondMonitor]; + + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ + ...projectMonitors, + monitors: testMonitors, + }) + .expect(200); + + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors) + .expect(200); + + // does not delete the stale monitor + const getResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: ${secondMonitor.id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + const { monitors } = getResponse.body; + + expect(monitors.length).eql(1); + + expect(apiResponse.body.createdMonitors).eql([]); + expect(apiResponse.body.failedMonitors).eql([]); + expect(apiResponse.body.deletedMonitors).eql([]); + expect(apiResponse.body.updatedMonitors).eql([projectMonitors.monitors[0].id]); + expect(apiResponse.body.staleMonitors).eql([secondMonitor.id]); + } finally { + await Promise.all([ + testMonitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - deletes monitors when keep stale is false', async () => { + const secondMonitor = { ...projectMonitors.monitors[0], id: 'test-id-2' }; + const testMonitors = [projectMonitors.monitors[0], secondMonitor]; + + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ + ...projectMonitors, + keep_stale: false, + monitors: testMonitors, + }) + .expect(200); + + const projectResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ ...projectMonitors, keep_stale: false }) + .expect(200); + + // expect monitor to have been deleted + const getResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: ${secondMonitor.id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + const { monitors } = getResponse.body; + + expect(monitors[0]).eql(undefined); + + expect(projectResponse.body.createdMonitors).eql([]); + expect(projectResponse.body.failedMonitors).eql([]); + expect(projectResponse.body.updatedMonitors).eql([projectMonitors.monitors[0].id]); + expect(projectResponse.body.deletedMonitors).eql([secondMonitor.id]); + expect(projectResponse.body.staleMonitors).eql([]); + } finally { + await Promise.all([ + testMonitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - does not delete monitors from different suites when keep stale is false', async () => { + const secondMonitor = { ...projectMonitors.monitors[0], id: 'test-id-2' }; + const testMonitors = [projectMonitors.monitors[0], secondMonitor]; + const testprojectId = 'test-suite-2'; + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ + ...projectMonitors, + keep_stale: false, + monitors: testMonitors, + }) + .expect(200); + + const projectResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ ...projectMonitors, keep_stale: false, project: testprojectId }) + .expect(200); + + // expect monitor not to have been deleted + const getResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: ${secondMonitor.id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + const { monitors } = getResponse.body; + + expect(monitors.length).eql(1); + + expect(projectResponse.body.createdMonitors).eql([projectMonitors.monitors[0].id]); + expect(projectResponse.body.failedMonitors).eql([]); + expect(projectResponse.body.deletedMonitors).eql([]); + expect(projectResponse.body.updatedMonitors).eql([]); + expect(projectResponse.body.staleMonitors).eql([]); + } finally { + await Promise.all([ + testMonitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + + await Promise.all([ + testMonitors.map((monitor) => { + return deleteMonitor(monitor.id, testprojectId); + }), + ]); + } + }); + + it('project monitors - does not delete a monitor from the same suite in a different space', async () => { + const secondMonitor = { ...projectMonitors.monitors[0], id: 'test-id-2' }; + const testMonitors = [projectMonitors.monitors[0], secondMonitor]; + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + const SPACE_ID = `test-space-${uuid.v4()}`; + const SPACE_NAME = `test-space-name ${uuid.v4()}`; + await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ...projectMonitors, + keep_stale: false, + monitors: testMonitors, + }) + .expect(200); + const projectResponse = await supertest + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ ...projectMonitors, keep_stale: false }) + .expect(200); + // expect monitor not to have been deleted + const getResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .auth(username, password) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: ${secondMonitor.id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + const { monitors } = getResponse.body; + expect(monitors.length).eql(1); + expect(projectResponse.body.createdMonitors).eql([projectMonitors.monitors[0].id]); + expect(projectResponse.body.failedMonitors).eql([]); + expect(projectResponse.body.deletedMonitors).eql([]); + expect(projectResponse.body.updatedMonitors).eql([]); + expect(projectResponse.body.staleMonitors).eql([]); + } finally { + await Promise.all([ + testMonitors.map((monitor) => { + return deleteMonitor( + monitor.id, + projectMonitors.project, + 'default', + username, + password + ); + }), + ]); + await deleteMonitor( + projectMonitors.monitors[0].id, + projectMonitors.project, + SPACE_ID, + username, + password + ); + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it('project monitors - validates monitor type', async () => { + try { + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ + ...projectMonitors, + monitors: [{ ...projectMonitors.monitors[0], schedule: '3m', tags: '' }], + }); + + expect(apiResponse.body.updatedMonitors).eql([]); + expect(apiResponse.body.failedMonitors).eql([ + { + details: + 'Invalid value "3m" supplied to "schedule" | Invalid value "" supplied to "tags"', + id: projectMonitors.monitors[0].id, + payload: { + content: + 'UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA', + filter: { + match: 'check if title is present', + }, + id: projectMonitors.monitors[0].id, + locations: ['us-east4-a'], + name: 'check if title is present', + params: {}, + playwrightOptions: { + chromiumSandbox: false, + headless: true, + }, + schedule: '3m', + tags: '', + throttling: { + download: 5, + latency: 20, + upload: 3, + }, + }, + reason: 'Failed to save or update monitor. Configuration is not valid', + }, + ]); + expect(apiResponse.body.createdMonitors).eql([]); + } finally { + await Promise.all([ + projectMonitors.monitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); + + it('project monitors - saves space as data stream namespace', async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + const SPACE_ID = `test-space-${uuid.v4()}`; + const SPACE_NAME = `test-space-name ${uuid.v4()}`; + await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + await supertest + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send(projectMonitors) + .expect(200); + // expect monitor not to have been deleted + const getResponse = await supertest + .get(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS}`) + .auth(username, password) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + const { monitors } = getResponse.body; + expect(monitors.length).eql(1); + expect(monitors[0].attributes[ConfigKey.NAMESPACE]).eql(SPACE_ID); + } finally { + await deleteMonitor( + projectMonitors.monitors[0].id, + projectMonitors.project, + SPACE_ID, + username, + password + ); + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it('project monitors - formats custom id appropriately', async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + const SPACE_ID = `test-space-${uuid.v4()}`; + const SPACE_NAME = `test-space-name ${uuid.v4()}`; + await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + await supertest + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send(projectMonitors) + .expect(200); + // expect monitor not to have been deleted + const getResponse = await supertest + .get(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS}`) + .auth(username, password) + .query({ + query: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + const { monitors } = getResponse.body; + expect(monitors.length).eql(1); + expect(monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]).eql( + `${projectMonitors.monitors[0].id}-${projectMonitors.project}-${SPACE_ID}` + ); + } finally { + await deleteMonitor( + projectMonitors.monitors[0].id, + projectMonitors.project, + SPACE_ID, + username, + password + ); + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json index d3f46fac442ee..554f95ffd4587 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json @@ -1,6 +1,8 @@ { "type": "browser", "enabled": true, + "journey_id": "", + "project_id": "", "schedule": { "number": "3", "unit": "m" @@ -25,6 +27,8 @@ "source.zip_url.folder": "", "source.zip_url.proxy_url": "", "source.inline.script": "step(\"Visit /users api route\", async () => {\\n const response = await page.goto('https://nextjs-test-synthetics.vercel.app/api/users');\\n expect(response.status()).toEqual(200);\\n});", + "source.project.content": "", + "is_push_monitor": false, "params": "", "screenshots": "on", "synthetics_args": [], @@ -38,5 +42,6 @@ "throttling.config": "5d/3u/20l", "locations": [], "name": "Test HTTP Monitor 03", - "namespace": "testnamespace" + "namespace": "testnamespace", + "monitor.origin": "ui" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json index 11d4dc67ff585..a91630c9acee4 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json @@ -60,5 +60,6 @@ "isServiceManaged": true }], "namespace": "testnamespace", - "revision": 1 + "revision": 1, + "monitor.origin": "ui" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json index 60816bb4538e5..80286677c0697 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json @@ -32,5 +32,6 @@ "TLSv1.3" ], "name": "Test HTTP Monitor 04", - "namespace": "testnamespace" + "namespace": "testnamespace", + "monitor.origin": "ui" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json new file mode 100644 index 0000000000000..e0618e749c2f6 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json @@ -0,0 +1,28 @@ +{ + "keep_stale": true, + "project": "test-suite", + "monitors": [{ + "throttling": { + "download": 5, + "upload": 3, + "latency": 20 + }, + "schedule": 10, + "locations": [ + "us-east4-a" + ], + "params": {}, + "playwrightOptions": { + "headless": true, + "chromiumSandbox": false + }, + "name": "check if title is present", + "id": "check-if-title-is-present", + "tags": [], + "content": "UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA", + "filter": { + "match": "check if title is present" + } + }] +} + \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json index 79476fc442b18..d5ac3b4721d48 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json @@ -28,5 +28,6 @@ "TLSv1.3" ], "name": "Test HTTP Monitor 04", - "namespace": "testnamespace" + "namespace": "testnamespace", + "monitor.origin": "ui" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts b/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts index 5fa6b18634faa..36b7b253bd8f7 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/get_monitor.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { SimpleSavedObject } from '@kbn/core/public'; import { MonitorFields } from '@kbn/synthetics-plugin/common/runtime_types'; import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import { formatSecrets } from '@kbn/synthetics-plugin/server/synthetics_service/utils/secrets'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; @@ -97,7 +96,6 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body.attributes).eql({ ...monitors[0], revision: 1, - secrets: formatSecrets(monitors[0]).secrets, }); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index 014ddc5e928a9..891076c1730bc 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -75,6 +75,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('uptime CRUD routes', () => { loadTestFile(require.resolve('./get_monitor')); loadTestFile(require.resolve('./add_monitor')); + loadTestFile(require.resolve('./add_monitor_project')); loadTestFile(require.resolve('./edit_monitor')); loadTestFile(require.resolve('./delete_monitor')); loadTestFile(require.resolve('./synthetics_enablement'));