[Synthetics] Add project monitor api (#131270)

Add project monitor api for the Synthetics agent and Uptime Monitor Management
Co-authored-by: Shahzad <shahzad31comp@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co>
This commit is contained in:
Dominique Clarke 2022-05-24 15:18:29 -04:00 committed by GitHub
parent 577752ae7c
commit 16e12a20bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 2365 additions and 459 deletions

View file

@ -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 = {

View file

@ -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,

View file

@ -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',
}

View file

@ -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';

View file

@ -119,3 +119,10 @@ export enum ThrottlingSuffix {
export const ThrottlingSuffixCodec = tEnum<ThrottlingSuffix>('ThrottlingSuffix', ThrottlingSuffix);
export type ThrottlingSuffixType = t.TypeOf<typeof ThrottlingSuffixCodec>;
export enum SourceType {
UI = 'ui',
PROJECT = 'project',
}
export const SourceTypeCodec = tEnum<SourceType>('SourceType', SourceType);

View file

@ -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<typeof ThrottlingConfigKeyCodec>;
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,

View file

@ -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<typeof ProjectMonitorThrottlingConfigCodec>;
export type ProjectBrowserMonitor = t.TypeOf<typeof ProjectBrowserMonitorCodec>;
export type ProjectMonitorsRequest = t.TypeOf<typeof ProjectMonitorsRequestCodec>;

View file

@ -20,7 +20,8 @@
"taskManager",
"triggersActionsUi",
"usageCollection",
"unifiedSearch"
"unifiedSearch",
"spaces"
],
"server": true,
"ui": true,

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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<Props>(({ validate, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const { runsOnService, throttling } = usePolicyConfigContext();
export const ThrottlingFields = memo<Props>(
({ 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 ? (
<>
<EuiSpacer size="m" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.download.label"
defaultMessage="Download Speed"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.DOWNLOAD_SPEED]?.(fields) || exceedsDownloadLimits}
error={
exceedsDownloadLimits ? (
<ThrottlingExceededMessage throttlingField="download" limit={maxDownload} />
) : (
const throttlingInputs = isThrottlingEnabled ? (
<>
<EuiSpacer size="m" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.download.error"
defaultMessage="Download speed must be greater than zero."
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.download.label"
defaultMessage="Download Speed"
/>
)
}
>
<EuiFieldNumber
min={0}
step={0.001}
value={fields[ConfigKey.DOWNLOAD_SPEED]}
onChange={(event) => {
handleInputChange({
value: event.target.value,
configKey: ConfigKey.DOWNLOAD_SPEED,
});
}}
onBlur={() => onFieldBlur?.(ConfigKey.DOWNLOAD_SPEED)}
data-test-subj="syntheticsBrowserDownloadSpeed"
append={
<EuiText size="xs">
<strong>Mbps</strong>
</EuiText>
}
/>
</EuiFormRow>
<EuiFormRow
label={
labelAppend={<OptionalLabel />}
isInvalid={
(validate ? !!validate[ConfigKey.DOWNLOAD_SPEED]?.(fields) : false) ||
exceedsDownloadLimits
}
error={
exceedsDownloadLimits ? (
<ThrottlingExceededMessage throttlingField="download" limit={maxDownload} />
) : (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.download.error"
defaultMessage="Download speed must be greater than zero."
/>
)
}
>
<EuiFieldNumber
min={0}
step={0.001}
value={fields[ConfigKey.DOWNLOAD_SPEED]}
onChange={(event) => {
handleInputChange({
value: event.target.value,
configKey: ConfigKey.DOWNLOAD_SPEED,
});
}}
onBlur={() => onFieldBlur?.(ConfigKey.DOWNLOAD_SPEED)}
data-test-subj="syntheticsBrowserDownloadSpeed"
append={
<EuiText size="xs">
<strong>Mbps</strong>
</EuiText>
}
readOnly={readOnly}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.upload.label"
defaultMessage="Upload Speed"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={
(validate ? !!validate[ConfigKey.UPLOAD_SPEED]?.(fields) : false) || exceedsUploadLimits
}
error={
exceedsUploadLimits ? (
<ThrottlingExceededMessage throttlingField="upload" limit={maxUpload} />
) : (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.upload.error"
defaultMessage="Upload speed must be greater than zero."
/>
)
}
>
<EuiFieldNumber
min={0}
step={0.001}
value={fields[ConfigKey.UPLOAD_SPEED]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.UPLOAD_SPEED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.UPLOAD_SPEED)}
data-test-subj="syntheticsBrowserUploadSpeed"
append={
<EuiText size="xs">
<strong>Mbps</strong>
</EuiText>
}
readOnly={readOnly}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.latency.label"
defaultMessage="Latency"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={validate ? !!validate[ConfigKey.LATENCY]?.(fields) : false}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.latency.error"
defaultMessage="Latency must not be negative."
/>
}
>
<EuiFieldNumber
min={0}
value={fields[ConfigKey.LATENCY]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.LATENCY,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.LATENCY)}
data-test-subj="syntheticsBrowserLatency"
append={
<EuiText size="xs">
<strong>ms</strong>
</EuiText>
}
readOnly={readOnly}
/>
</EuiFormRow>
</>
) : (
<>
<EuiSpacer />
<ThrottlingDisabledCallout />
</>
);
return (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.title"
defaultMessage="Throttling options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.upload.label"
defaultMessage="Upload Speed"
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.description"
defaultMessage="Control the monitor's download and upload speeds, and its latency to simulate your application's behaviour on slower or laggier networks."
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.UPLOAD_SPEED]?.(fields) || exceedsUploadLimits}
error={
exceedsUploadLimits ? (
<ThrottlingExceededMessage throttlingField="upload" limit={maxUpload} />
) : (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.upload.error"
defaultMessage="Upload speed must be greater than zero."
/>
)
}
>
<EuiFieldNumber
min={0}
step={0.001}
value={fields[ConfigKey.UPLOAD_SPEED]}
<EuiSwitch
id={'uptimeFleetIsThrottlingEnabled'}
aria-label="enable throttling configuration"
data-test-subj="syntheticsBrowserIsThrottlingEnabled"
checked={fields[ConfigKey.IS_THROTTLING_ENABLED]}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.switch.description"
defaultMessage="Enable throttling"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.UPLOAD_SPEED,
value: event.target.checked,
configKey: ConfigKey.IS_THROTTLING_ENABLED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.UPLOAD_SPEED)}
data-test-subj="syntheticsBrowserUploadSpeed"
append={
<EuiText size="xs">
<strong>Mbps</strong>
</EuiText>
}
onBlur={() => onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)}
disabled={readOnly}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.latency.label"
defaultMessage="Latency"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.LATENCY]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.latency.error"
defaultMessage="Latency must not be negative."
/>
}
>
<EuiFieldNumber
min={0}
value={fields[ConfigKey.LATENCY]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.LATENCY,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.LATENCY)}
data-test-subj="syntheticsBrowserLatency"
append={
<EuiText size="xs">
<strong>ms</strong>
</EuiText>
}
/>
</EuiFormRow>
</>
) : (
<>
<EuiSpacer />
<ThrottlingDisabledCallout />
</>
);
return (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.title"
defaultMessage="Throttling options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.description"
defaultMessage="Control the monitor's download and upload speeds, and its latency to simulate your application's behaviour on slower or laggier networks."
/>
}
>
<EuiSwitch
id={'uptimeFleetIsThrottlingEnabled'}
aria-label="enable throttling configuration"
data-test-subj="syntheticsBrowserIsThrottlingEnabled"
checked={fields[ConfigKey.IS_THROTTLING_ENABLED]}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.switch.description"
defaultMessage="Enable throttling"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.IS_THROTTLING_ENABLED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)}
/>
{isThrottlingEnabled && (exceedsDownloadLimits || exceedsUploadLimits) ? (
<>
<EuiSpacer />
<ThrottlingExceededCallout />
</>
) : null}
{throttlingInputs}
</DescribedFormGroupWithWrap>
);
});
{isThrottlingEnabled && (exceedsDownloadLimits || exceedsUploadLimits) ? (
<>
<EuiSpacer />
<ThrottlingExceededCallout />
</>
) : null}
{throttlingInputs}
</DescribedFormGroupWithWrap>
);
}
);

View file

@ -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<EuiComboBoxOptionOption<string>>
>(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}
/>
);

View file

@ -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 (
<>
<EuiFormRow
@ -43,6 +44,7 @@ export function Enabled({ fields, onChange, onBlur }: Props) {
})
}
onBlur={() => onBlur?.()}
disabled={readOnly}
/>
</EuiFormRow>
</>

View file

@ -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[] = []) =>

View file

@ -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),
};

View file

@ -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<ExtraFields = unknown>({
runsOnService = false,
isZipUrlSourceEnabled = true,
allowedScheduleUnits = [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS],
sourceType,
}: IPolicyConfigContextProvider) {
const [monitorType, setMonitorType] = useState<DataStream>(defaultMonitorType);
const [name, setName] = useState<string>(defaultName);
@ -150,6 +155,7 @@ export function PolicyConfigContextProvider<ExtraFields = unknown>({
namespace,
setNamespace,
throttling,
sourceType,
} as IPolicyConfigContext;
}, [
monitorType,
@ -168,6 +174,7 @@ export function PolicyConfigContextProvider<ExtraFields = unknown>({
allowedScheduleUnits,
namespace,
throttling,
sourceType,
]);
return <PolicyConfigContext.Provider value={value} children={children} />;

View file

@ -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}
/>
</EuiFlexItem>
<EuiFlexItem>
@ -74,6 +76,7 @@ export const ScheduleField = ({ number, onChange, onBlur, unit }: Props) => {
onChange({ number, unit: updatedUnit as ScheduleUnit });
}}
onBlur={() => onBlur()}
disabled={readOnly}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -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<boolean | undefined>(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 ? (
<Redirect to={MONITOR_MANAGEMENT_ROUTE + '/all'} />
) : (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<WarningText>{!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL}</WarningText>
</EuiFlexItem>
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="ghost"
size="s"
href={`${basePath}/app/uptime/${MONITOR_MANAGEMENT_ROUTE}/all`}
>
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
{onTestNow && (
<EuiFlexItem grow={false}>
{/* Popover is used instead of EuiTooltip until the resolution of https://github.com/elastic/eui/issues/5604 */}
<EuiPopover
repositionOnScroll={true}
initialFocus={false}
button={
<EuiButton
css={{ width: '100%' }}
fill
size="s"
color="success"
iconType="play"
disabled={!isValid || isTestRunInProgress}
data-test-subj={'monitorTestNowRunBtn'}
onClick={() => onTestNow()}
onMouseEnter={() => {
setIsPopoverOpen(true);
}}
onMouseLeave={() => {
setIsPopoverOpen(false);
}}
>
{testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL}
</EuiButton>
}
isOpen={isPopoverOpen}
>
<EuiText style={{ width: 260, outline: 'none' }}>
<p>{TEST_NOW_DESCRIPTION}</p>
</EuiText>
</EuiPopover>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
fill
size="s"
iconType="check"
onClick={handleOnSave}
isLoading={isSaving}
disabled={hasBeenSubmitted && !isValid}
>
{monitorId ? UPDATE_MONITOR_LABEL : SAVE_MONITOR_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiButtonEmpty
color="ghost"
size="s"
href={`${basePath}/app/uptime/${MONITOR_MANAGEMENT_ROUTE}/all`}
>
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
{!isReadOnly ? (
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexItem grow={false}>
<WarningText>{!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL}</WarningText>
</EuiFlexItem>
{onTestNow && (
<EuiFlexItem grow={false}>
{/* Popover is used instead of EuiTooltip until the resolution of https://github.com/elastic/eui/issues/5604 */}
<EuiPopover
repositionOnScroll={true}
initialFocus={false}
button={
<EuiButton
css={{ width: '100%' }}
fill
size="s"
color="success"
iconType="play"
disabled={!isValid || isTestRunInProgress}
data-test-subj={'monitorTestNowRunBtn'}
onClick={() => onTestNow()}
onMouseEnter={() => {
setIsPopoverOpen(true);
}}
onMouseLeave={() => {
setIsPopoverOpen(false);
}}
>
{testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL}
</EuiButton>
}
isOpen={isPopoverOpen}
>
<EuiText style={{ width: 260, outline: 'none' }}>
<p>{TEST_NOW_DESCRIPTION}</p>
</EuiText>
</EuiPopover>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
fill
size="s"
iconType="check"
onClick={handleOnSave}
isLoading={isSaving}
disabled={hasBeenSubmitted && !isValid}
>
{monitorId ? UPDATE_MONITOR_LABEL : SAVE_MONITOR_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
};
@ -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', {

View file

@ -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]}

View file

@ -17,9 +17,16 @@ interface Props {
setLocations: React.Dispatch<React.SetStateAction<MonitorServiceLocations>>;
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<string | null>(null);
const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState<Record<string, boolean>>(
{}
@ -76,6 +83,7 @@ export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid, o
idToSelectedMap={checkboxIdToSelectedMap}
onChange={(id) => onLocationChange(id)}
onBlur={() => onBlur?.()}
disabled={readOnly}
/>
</EuiFormRow>
);

View file

@ -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) => (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-currentvalue={props.value}
onChange={(e: any) => {
props.onChange(e.jsonContent);
}}
/>
),
};
});
const defaultHTTPConfig = DEFAULT_FIELDS[DataStream.HTTP] as HTTPFields;
const defaultBrowserConfig = DEFAULT_FIELDS[DataStream.BROWSER];
describe('<MonitorFields />', () => {
const WrappedComponent = ({
isEditable = true,
isFormSubmitted = false,
defaultSimpleHttpFields = defaultHTTPConfig,
defaultBrowserFields = defaultBrowserConfig,
readOnly = false,
}: {
isEditable?: boolean;
isFormSubmitted?: boolean;
defaultSimpleHttpFields?: HTTPFields;
defaultBrowserFields?: BrowserFields;
readOnly?: boolean;
}) => {
return (
<HTTPContextProvider defaultValues={defaultSimpleHttpFields}>
<PolicyConfigContextProvider isEditable={isEditable}>
<PolicyConfigContextProvider
isEditable={isEditable}
sourceType={readOnly ? SourceType.PROJECT : SourceType.UI}
>
<TCPContextProvider>
<BrowserContextProvider>
<BrowserContextProvider defaultValues={defaultBrowserFields}>
<ICMPSimpleFieldsContextProvider>
<TLSFieldsContextProvider>
<MonitorFields isFormSubmitted={isFormSubmitted} />
@ -82,13 +113,20 @@ describe('<MonitorFields />', () => {
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(
<WrappedComponent isFormSubmitted={false} defaultSimpleHttpFields={httpInvalidValues} />
it('is reradonly when source type is project', async () => {
const name = 'monitor name';
const browserFields = {
...defaultBrowserConfig,
[ConfigKey.NAME]: name,
};
const { getByText } = render(
<WrappedComponent
isFormSubmitted={false}
defaultBrowserFields={browserFields}
readOnly={true}
/>
);
expect(queryByText('Monitor name is required')).toBeNull();
expect(queryByText('URL is required')).toBeNull();
expect(getByText('Read only')).toBeInTheDocument();
});
});

View file

@ -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<Record<string, boolean>>({});
@ -41,21 +42,25 @@ export const MonitorFields = ({ isFormSubmitted = false }: { isFormSubmitted?: b
return (
<EuiForm id="syntheticsServiceCreateMonitorForm" component="form">
<CustomFields
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
validate={fieldValidation}
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER]}
appendAdvancedFields={
<MonitorManagementAdvancedFields
validate={fieldValidation}
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
onFieldBlur={handleFieldBlur}
/>
}
onFieldBlur={handleFieldBlur}
>
<MonitorNameAndLocation validate={fieldValidation} onFieldBlur={handleFieldBlur} />
</CustomFields>
{sourceType === SourceType.PROJECT ? (
<ProjectBrowserReadonlyFields minColumnWidth={MIN_COLUMN_WRAP_WIDTH} />
) : (
<CustomFields
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
validate={fieldValidation}
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER]}
appendAdvancedFields={
<MonitorManagementAdvancedFields
validate={fieldValidation}
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
onFieldBlur={handleFieldBlur}
/>
}
onFieldBlur={handleFieldBlur}
>
<MonitorNameAndLocation validate={fieldValidation} onFieldBlur={handleFieldBlur} />
</CustomFields>
)}
</EuiForm>
);
};

View file

@ -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}
/>
</EuiFormRow>
<ServiceLocations
@ -74,6 +78,7 @@ export const MonitorNameAndLocation = ({ validate, onFieldBlur }: Props) => {
selectedLocations={locations}
isInvalid={isLocationsInvalid}
onBlur={() => onFieldBlur?.(ConfigKey.LOCATIONS)}
readOnly={readOnly}
/>
</>
);

View file

@ -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 (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.browser.project.readOnly.callout.title"
defaultMessage="Read only"
/>
}
iconType="document"
>
<p>
<FormattedMessage
id="xpack.synthetics.browser.project.readOnly.callout.content"
defaultMessage="This monitor was added from an external project. Configuration is read only."
/>
</p>
</EuiCallOut>
<EuiSpacer />
<DescribedFormGroupWithWrap
title={
<h4>
<FormattedMessage
id="xpack.synthetics.browser.project.monitorIntegrationSettingsSectionTitle"
defaultMessage="Monitor settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.browser.project.monitorIntegrationSettingsSectionDescription"
defaultMessage="Configure your monitor with the following options."
/>
}
data-test-subj="monitorSettingsSection"
minColumnWidth={minColumnWidth}
>
<MonitorNameAndLocation readOnly={true} />
<Enabled fields={fields} onChange={noop} onBlur={noop} readOnly={true} />
<EuiFormRow
id="syntheticsFleetScheduleField--number syntheticsFleetScheduleField--unit"
label={
<FormattedMessage
id="xpack.synthetics.browser.project.monitorIntegrationSettingsSection.monitorInterval"
defaultMessage="Frequency"
/>
}
error={
<FormattedMessage
id="xpack.synthetics.browser.project.monitorIntegrationSettingsSection.monitorInterval.error"
defaultMessage="Monitor frequency is required"
/>
}
>
<ScheduleField
onBlur={noop}
onChange={noop}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
readOnly={true}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.browser.project.monitorIntegrationSettingsSection.tags.label"
defaultMessage="Tags"
/>
}
helpText={
<FormattedMessage
id="xpack.synthetics.browser.project.monitorIntegrationSettingsSection.tags.helpText"
defaultMessage="A list of tags that will be sent with the monitor event. Press enter to add a new tag. Displayed in Uptime and enables searching by tag."
/>
}
labelAppend={<OptionalLabel />}
>
<ComboBox
selectedOptions={fields[ConfigKey.TAGS]}
onChange={noop}
onBlur={noop}
data-test-subj="syntheticsTags"
readOnly={true}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<EuiSpacer />
<EuiAccordion
id="syntheticsIntegrationBrowserAdvancedOptions"
buttonContent="Advanced Browser options"
data-test-subj="syntheticsBrowserAdvancedFieldsAccordion"
>
<EuiSpacer size="m" />
<DescribedFormGroupWithWrap
title={
<h4>
<FormattedMessage
id="xpack.synthetics.browser.project.browserAdvancedSettings.title"
defaultMessage="Synthetics agent options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.browser.project.browserAdvancedSettings.description"
defaultMessage="Provide fine-tuned configuration for the synthetics agent."
/>
}
minColumnWidth={minColumnWidth}
>
<EuiSpacer size="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.browser.project.browserAdvancedSettings.screenshots.label"
defaultMessage="Screenshot options"
/>
}
helpText={
<FormattedMessage
id="xpack.synthetics.browser.project.browserAdvancedSettings.screenshots.helpText"
defaultMessage="Set this option to manage the screenshots captured by the synthetics agent."
/>
}
>
<EuiFieldText
value={advancedFields[ConfigKey.SCREENSHOTS]}
onChange={noop}
data-test-subj="syntheticsBrowserScreenshots"
readOnly={true}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<ThrottlingFields minColumnWidth={minColumnWidth} readOnly={true} />
</EuiAccordion>
</>
);
};

View file

@ -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) => (
<EuiLink href={`${basePath}/app/uptime/monitor/${btoa(id)}`}>{name}</EuiLink>
render: (name: string, monitor: EncryptedSyntheticsMonitorWithId) => (
<EuiLink
href={`${basePath}/app/uptime/monitor/${btoa(
(monitor as unknown as BrowserFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || monitor.id
)}`}
>
{name}
</EuiLink>
),
},
{
@ -174,12 +183,23 @@ export const MonitorManagementList = ({
defaultMessage: 'Enabled',
}),
render: (_enabled: boolean, monitor: EncryptedSyntheticsMonitorWithId) => (
<MonitorEnabled
id={monitor.id}
monitor={monitor}
isDisabled={!canEdit}
onUpdate={onUpdate}
/>
<EuiToolTip
content={
monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT
? i18n.translate('xpack.synthetics.monitorManagement.monitorList.enabled.tooltip', {
defaultMessage:
'This monitor was added from an external project. Configuration is read only.',
})
: ''
}
>
<MonitorEnabled
id={monitor.id}
monitor={monitor}
isDisabled={!canEdit || monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT}
onUpdate={onUpdate}
/>
</EuiToolTip>
),
},
{

View file

@ -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;

View file

@ -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<SavedObject<SyntheticsMonitorWithSecrets>> => {
}): Promise<SavedObject<SyntheticsMonitor>> => {
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitor>(
syntheticsMonitorType,
monitorId
);
return await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>(
syntheticsMonitor.name,
monitorId,
{
namespace: encryptedMonitor.namespaces?.[0],
}
);
const decryptedMonitor =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>(
syntheticsMonitor.name,
monitorId,
{
namespace: encryptedMonitor.namespaces?.[0],
}
);
return normalizeSecrets(decryptedMonitor);
} catch (e) {
throw e;
}

View file

@ -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: {

View file

@ -28,7 +28,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;

View file

@ -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<any> => {
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) {

View file

@ -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) {

View file

@ -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,

View file

@ -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<EncryptedSyntheticsMonitor>;
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;
};

View file

@ -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<any> => {
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,
},
});
},
});

View file

@ -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<any> => {
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
const { monitorId } = request.params;
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitor>(
syntheticsMonitorType,
monitorId
);
const monitor =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>(
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<EncryptedSyntheticsMonitor>(
syntheticsMonitorType,
monitorId
);
const monitor =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>(
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;
}
};

View file

@ -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<any> => {
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
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<EncryptedSyntheticsMonitor> =
const editedMonitorSavedObject: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor> =
await savedObjectsClient.update<MonitorFields>(
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<EncryptedSyntheticsMonitor>;
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
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;
};

View file

@ -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<any> => {
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,

View file

@ -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',

View file

@ -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 };
}

View file

@ -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 },
},

View file

@ -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<SyntheticsMonitor> = {
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,

View file

@ -13,6 +13,7 @@ import {
EncryptedSyntheticsMonitor,
ConfigKey,
ServiceLocationErrors,
SourceType,
} from '../../../common/runtime_types';
import type { MonitorUpdateEvent } from '../../legacy_uptime/lib/telemetry/types';
@ -184,14 +185,19 @@ export function formatTelemetrySyncEvent() {}
function getScriptType(
attributes: Partial<MonitorFields>,
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;
}

View file

@ -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<keyof BrowserFields, Formatter>;
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,
};

View file

@ -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<string, string> | null;
export type FormattedValue = boolean | string | string[] | Record<string, unknown> | null;
export type Formatter = null | ((fields: Partial<MonitorFields>) => 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);

View file

@ -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'],

View file

@ -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',
];

View file

@ -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,
},
]);
});
});
});

View file

@ -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 });
});
};

View file

@ -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<string, StaleMonitor>;
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<EncryptedSyntheticsMonitor>(
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<StaleMonitorMap> => {
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<EncryptedSyntheticsMonitor>({
type: syntheticsMonitorType,
page,
perPage: 500,
filter: this.projectFilter,
});
};
private getExistingMonitor = async (
journeyId: string
): Promise<SavedObjectsFindResult<EncryptedSyntheticsMonitor>> => {
const filter = `${this.projectFilter} AND ${syntheticsMonitorType}.attributes.${ConfigKey.JOURNEY_ID}: "${journeyId}"`;
const { saved_objects: savedObjects } =
await this.savedObjectsClient.find<EncryptedSyntheticsMonitor>({
type: syntheticsMonitorType,
perPage: 1,
filter,
});
return savedObjects?.[0];
};
private updateMonitor = async (
previousMonitor: SavedObjectsFindResult<EncryptedSyntheticsMonitor>,
normalizedMonitor: BrowserFields
): Promise<{
editedMonitor: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
errors: ServiceLocationErrors;
}> => {
const decryptedPreviousMonitor =
await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecrets>(
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<EncryptedSyntheticsMonitor> =
await this.savedObjectsClient.update<MonitorFields>(
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,
});
}
};
}

View file

@ -427,12 +427,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[]) {

View file

@ -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';

View file

@ -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<SyntheticsMonitorWithSecrets>
): SavedObject<SyntheticsMonitor> {
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;
}

View file

@ -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);
}
});
});
}

View file

@ -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"
}

View file

@ -60,5 +60,6 @@
"isServiceManaged": true
}],
"namespace": "testnamespace",
"revision": 1
"revision": 1,
"monitor.origin": "ui"
}

View file

@ -32,5 +32,6 @@
"TLSv1.3"
],
"name": "Test HTTP Monitor 04",
"namespace": "testnamespace"
"namespace": "testnamespace",
"monitor.origin": "ui"
}

View file

@ -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"
}
}]
}

View file

@ -28,5 +28,6 @@
"TLSv1.3"
],
"name": "Test HTTP Monitor 04",
"namespace": "testnamespace"
"namespace": "testnamespace",
"monitor.origin": "ui"
}

View file

@ -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,
});
});

View file

@ -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'));