mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics UI] Add or edit a monitor (#132204)
* add basic form setup * add validation * add edit flow * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Delete context.tsx * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * add locations * update schedule field * adjust script recorder fields * adjust disclaimer to only show when using a service location * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * adjust edit flow * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * add Kibana space * adjust Kibana space logic * add throttling * update content * adjust hrefs * adjust content * adjust types * add editing * adjust monitor validation tests * fix integration tests * adjust tests * adjust content * add source field tests * add tests * add legacy validation * rename file * adjust legacy components * add tests * update tests * update content * adjust types * adjust routes * remove console logs * adjust types * update tests * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * add Playwright link * add placeholder * update getting started page link * add monitor details link * adjust content * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/uploader.tsx * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Apply suggestions from code review * Update x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx * Update x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx * fix jest * adjust tests * renaming * move exported type * move hook * adjust file structure * adjust test * move hook * adjust test * add script recorder button * adjust tests * adjust tests * adjust validation * add ui keys to skip * i18n misses * move text assertion box down * adjust content * adjust validation * adjust helpers * align connection profile field baseline * do not clear out url for single step monitors * adjust defaults * adjust types * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/uploader.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * Update x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com> * merge edited monitor * add Loading spinners * update content * adjust errors * adjust types * adjust tests * adjust types * merge edited monitor settings * adjust tests * adjust types * subscribe to dirty fields Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co> Co-authored-by: Colleen McGinnis <colleen.j.mcginnis@gmail.com>
This commit is contained in:
parent
6ad1d1da86
commit
7a107392b4
89 changed files with 6228 additions and 125 deletions
|
@ -9,6 +9,7 @@ import {
|
|||
BrowserSimpleFields,
|
||||
CommonFields,
|
||||
DataStream,
|
||||
FormMonitorType,
|
||||
HTTPAdvancedFields,
|
||||
HTTPMethod,
|
||||
HTTPSimpleFields,
|
||||
|
@ -31,6 +32,7 @@ export const DEFAULT_NAMESPACE_STRING = 'default';
|
|||
|
||||
export const DEFAULT_COMMON_FIELDS: CommonFields = {
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
[ConfigKey.ENABLED]: true,
|
||||
[ConfigKey.SCHEDULE]: {
|
||||
number: '3',
|
||||
|
@ -85,6 +87,7 @@ export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = {
|
|||
[ConfigKey.SOURCE_ZIP_PASSWORD]: '',
|
||||
[ConfigKey.SOURCE_ZIP_FOLDER]: '',
|
||||
[ConfigKey.SOURCE_ZIP_PROXY_URL]: '',
|
||||
[ConfigKey.TEXT_ASSERTION]: '',
|
||||
[ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: undefined,
|
||||
[ConfigKey.ZIP_URL_TLS_CERTIFICATE]: undefined,
|
||||
[ConfigKey.ZIP_URL_TLS_KEY]: undefined,
|
||||
|
@ -92,6 +95,7 @@ export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = {
|
|||
[ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: undefined,
|
||||
[ConfigKey.ZIP_URL_TLS_VERSION]: undefined,
|
||||
[ConfigKey.URLS]: '',
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
};
|
||||
|
||||
export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = {
|
||||
|
@ -102,6 +106,7 @@ export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = {
|
|||
[ConfigKey.URLS]: '',
|
||||
[ConfigKey.MAX_REDIRECTS]: '0',
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
|
||||
};
|
||||
|
||||
export const DEFAULT_HTTP_ADVANCED_FIELDS: HTTPAdvancedFields = {
|
||||
|
@ -127,6 +132,7 @@ export const DEFAULT_ICMP_SIMPLE_FIELDS: ICMPSimpleFields = {
|
|||
[ConfigKey.HOSTS]: '',
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
|
||||
[ConfigKey.WAIT]: '1',
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP,
|
||||
};
|
||||
|
||||
export const DEFAULT_TCP_SIMPLE_FIELDS: TCPSimpleFields = {
|
||||
|
@ -136,6 +142,7 @@ export const DEFAULT_TCP_SIMPLE_FIELDS: TCPSimpleFields = {
|
|||
},
|
||||
[ConfigKey.HOSTS]: '',
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.TCP,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
|
||||
};
|
||||
|
||||
export const DEFAULT_TCP_ADVANCED_FIELDS: TCPAdvancedFields = {
|
||||
|
@ -165,7 +172,9 @@ export const DEFAULT_FIELDS: MonitorDefaults = {
|
|||
...DEFAULT_TCP_ADVANCED_FIELDS,
|
||||
...DEFAULT_TLS_FIELDS,
|
||||
},
|
||||
[DataStream.ICMP]: DEFAULT_ICMP_SIMPLE_FIELDS,
|
||||
[DataStream.ICMP]: {
|
||||
...DEFAULT_ICMP_SIMPLE_FIELDS,
|
||||
},
|
||||
[DataStream.BROWSER]: {
|
||||
...DEFAULT_BROWSER_SIMPLE_FIELDS,
|
||||
...DEFAULT_BROWSER_ADVANCED_FIELDS,
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum ConfigKey {
|
|||
CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id',
|
||||
CONFIG_ID = 'config_id',
|
||||
ENABLED = 'enabled',
|
||||
FORM_MONITOR_TYPE = 'form_monitor_type',
|
||||
HOSTS = 'hosts',
|
||||
IGNORE_HTTPS_ERRORS = 'ignore_https_errors',
|
||||
MONITOR_SOURCE_TYPE = 'origin',
|
||||
|
@ -53,6 +54,7 @@ export enum ConfigKey {
|
|||
SOURCE_ZIP_PROXY_URL = 'source.zip_url.proxy_url',
|
||||
PROJECT_ID = 'project_id',
|
||||
SYNTHETICS_ARGS = 'synthetics_args',
|
||||
TEXT_ASSERTION = 'playwright_text_assertion',
|
||||
TLS_CERTIFICATE_AUTHORITIES = 'ssl.certificate_authorities',
|
||||
TLS_CERTIFICATE = 'ssl.certificate',
|
||||
TLS_KEY = 'ssl.key',
|
||||
|
|
|
@ -77,5 +77,6 @@ export const browserFormatters: BrowserFormatMap = {
|
|||
[ConfigKey.PLAYWRIGHT_OPTIONS]: null,
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: null,
|
||||
[ConfigKey.ORIGINAL_SPACE]: null,
|
||||
[ConfigKey.TEXT_ASSERTION]: null,
|
||||
...commonFormatters,
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ export const commonFormatters: CommonFormatMap = {
|
|||
[ConfigKey.NAMESPACE]: null,
|
||||
[ConfigKey.REVISION]: null,
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: null,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: null,
|
||||
};
|
||||
|
||||
export const arrayToJsonFormatter = (value: string[] = []) =>
|
||||
|
|
|
@ -126,3 +126,13 @@ export enum SourceType {
|
|||
}
|
||||
|
||||
export const SourceTypeCodec = tEnum<SourceType>('SourceType', SourceType);
|
||||
|
||||
export enum FormMonitorType {
|
||||
SINGLE = 'single',
|
||||
MULTISTEP = 'multistep',
|
||||
HTTP = 'http',
|
||||
TCP = 'tcp',
|
||||
ICMP = 'icmp',
|
||||
}
|
||||
|
||||
export const FormMonitorTypeCodec = tEnum<FormMonitorType>('FormMonitorType', FormMonitorType);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import {
|
||||
DataStream,
|
||||
DataStreamCodec,
|
||||
FormMonitorTypeCodec,
|
||||
ModeCodec,
|
||||
ResponseBodyIndexPolicyCodec,
|
||||
ScheduleUnitCodec,
|
||||
|
@ -79,6 +80,7 @@ export const CommonFieldsCodec = t.intersection([
|
|||
[ConfigKey.LOCATIONS]: MonitorServiceLocationsCodec,
|
||||
}),
|
||||
t.partial({
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorTypeCodec,
|
||||
[ConfigKey.TIMEOUT]: t.union([t.string, t.null]),
|
||||
[ConfigKey.REVISION]: t.number,
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec,
|
||||
|
@ -220,6 +222,7 @@ export const EncryptedBrowserSimpleFieldsCodec = t.intersection([
|
|||
[ConfigKey.PROJECT_ID]: t.string,
|
||||
[ConfigKey.ORIGINAL_SPACE]: t.string,
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string,
|
||||
[ConfigKey.TEXT_ASSERTION]: t.string,
|
||||
}),
|
||||
]),
|
||||
ZipUrlTLSFieldsCodec,
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export * from './alerts';
|
||||
|
||||
export * from './synthetics';
|
||||
export * from './alerts';
|
||||
export * from './data_view_permissions';
|
||||
export * from './uptime.journey';
|
||||
export * from './step_duration.journey';
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { journey, step, expect, Page } from '@elastic/synthetics';
|
||||
import { FormMonitorType } from '../../../common/runtime_types/monitor_management';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app';
|
||||
|
||||
const customLocation = process.env.SYNTHETICS_TEST_LOCATION;
|
||||
|
||||
const basicMonitorDetails = {
|
||||
location: customLocation || 'US Central',
|
||||
schedule: '3',
|
||||
};
|
||||
const httpName = `http monitor ${uuid.v4()}`;
|
||||
const icmpName = `icmp monitor ${uuid.v4()}`;
|
||||
const tcpName = `tcp monitor ${uuid.v4()}`;
|
||||
const browserName = `browser monitor ${uuid.v4()}`;
|
||||
const browserRecorderName = `browser monitor recorder ${uuid.v4()}`;
|
||||
const apmServiceName = 'apmServiceName';
|
||||
|
||||
const configuration = {
|
||||
[FormMonitorType.HTTP]: {
|
||||
monitorType: FormMonitorType.HTTP,
|
||||
monitorConfig: {
|
||||
...basicMonitorDetails,
|
||||
name: httpName,
|
||||
url: 'https://elastic.co',
|
||||
locations: [basicMonitorDetails.location],
|
||||
apmServiceName,
|
||||
},
|
||||
monitorListDetails: {
|
||||
...basicMonitorDetails,
|
||||
name: httpName,
|
||||
},
|
||||
monitorEditDetails: [
|
||||
['[data-test-subj=syntheticsMonitorConfigSchedule]', '3'],
|
||||
['[data-test-subj=syntheticsMonitorConfigName]', httpName],
|
||||
['[data-test-subj=syntheticsMonitorConfigURL]', 'https://elastic.co'],
|
||||
['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName],
|
||||
],
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
monitorType: FormMonitorType.TCP,
|
||||
monitorConfig: {
|
||||
...basicMonitorDetails,
|
||||
name: tcpName,
|
||||
host: 'smtp.gmail.com:587',
|
||||
locations: [basicMonitorDetails.location],
|
||||
apmServiceName,
|
||||
},
|
||||
monitorListDetails: {
|
||||
...basicMonitorDetails,
|
||||
name: tcpName,
|
||||
},
|
||||
monitorEditDetails: [
|
||||
['[data-test-subj=syntheticsMonitorConfigSchedule]', '3'],
|
||||
['[data-test-subj=syntheticsMonitorConfigName]', tcpName],
|
||||
['[data-test-subj=syntheticsMonitorConfigHost]', 'smtp.gmail.com:587'],
|
||||
['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName],
|
||||
],
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
monitorType: FormMonitorType.ICMP,
|
||||
monitorConfig: {
|
||||
...basicMonitorDetails,
|
||||
name: icmpName,
|
||||
host: '1.1.1.1',
|
||||
locations: [basicMonitorDetails.location],
|
||||
apmServiceName,
|
||||
},
|
||||
monitorListDetails: {
|
||||
...basicMonitorDetails,
|
||||
name: icmpName,
|
||||
},
|
||||
monitorEditDetails: [
|
||||
['[data-test-subj=syntheticsMonitorConfigSchedule]', '3'],
|
||||
['[data-test-subj=syntheticsMonitorConfigName]', icmpName],
|
||||
['[data-test-subj=syntheticsMonitorConfigHost]', '1.1.1.1'],
|
||||
['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName],
|
||||
// name: httpName,
|
||||
],
|
||||
},
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
monitorType: FormMonitorType.MULTISTEP,
|
||||
monitorConfig: {
|
||||
...basicMonitorDetails,
|
||||
schedule: '10',
|
||||
name: browserName,
|
||||
inlineScript: 'step("test step", () => {})',
|
||||
locations: [basicMonitorDetails.location],
|
||||
apmServiceName,
|
||||
},
|
||||
monitorListDetails: {
|
||||
...basicMonitorDetails,
|
||||
schedule: '10',
|
||||
name: browserName,
|
||||
},
|
||||
monitorEditDetails: [
|
||||
['[data-test-subj=syntheticsMonitorConfigSchedule]', '10'],
|
||||
['[data-test-subj=syntheticsMonitorConfigName]', browserName],
|
||||
['[data-test-subj=codeEditorContainer] textarea', 'step("test step", () => {})'],
|
||||
['[data-test-subj=syntheticsMonitorConfigAPMServiceName]', apmServiceName],
|
||||
],
|
||||
},
|
||||
[`${FormMonitorType.MULTISTEP}__recorder`]: {
|
||||
monitorType: FormMonitorType.MULTISTEP,
|
||||
monitorConfig: {
|
||||
...basicMonitorDetails,
|
||||
schedule: '10',
|
||||
name: browserRecorderName,
|
||||
recorderScript: 'step("test step", () => {})',
|
||||
locations: [basicMonitorDetails.location],
|
||||
apmServiceName: 'Sample APM Service',
|
||||
},
|
||||
monitorListDetails: {
|
||||
...basicMonitorDetails,
|
||||
schedule: '10',
|
||||
name: browserRecorderName,
|
||||
},
|
||||
monitorEditDetails: [
|
||||
['[data-test-subj=syntheticsMonitorConfigSchedule]', '10'],
|
||||
['[data-test-subj=syntheticsMonitorConfigName]', browserRecorderName],
|
||||
['[data-test-subj=codeEditorContainer] textarea', 'step("test step", () => {})'],
|
||||
// name: httpName,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const createMonitorJourney = ({
|
||||
monitorName,
|
||||
monitorType,
|
||||
monitorConfig,
|
||||
monitorListDetails,
|
||||
monitorEditDetails,
|
||||
}: {
|
||||
monitorName: string;
|
||||
monitorType: FormMonitorType;
|
||||
monitorConfig: Record<string, string | string[]>;
|
||||
monitorListDetails: Record<string, string>;
|
||||
monitorEditDetails: Array<[string, string]>;
|
||||
}) => {
|
||||
journey(
|
||||
`Synthetics - add monitor - ${monitorName}`,
|
||||
async ({ page, params }: { page: Page; params: any }) => {
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
step('Go to monitor management', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
});
|
||||
|
||||
step('login to Kibana', async () => {
|
||||
await syntheticsApp.loginToKibana();
|
||||
const invalid = await page.locator(
|
||||
`text=Username or password is incorrect. Please try again.`
|
||||
);
|
||||
expect(await invalid.isVisible()).toBeFalsy();
|
||||
});
|
||||
|
||||
step('Ensure all montiors are deleted', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
await syntheticsApp.waitForLoadingToFinish();
|
||||
const isSuccessful = await syntheticsApp.deleteMonitors();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
});
|
||||
|
||||
step(`create ${monitorName}`, async () => {
|
||||
await syntheticsApp.navigateToAddMonitor();
|
||||
await syntheticsApp.createMonitor({ monitorConfig, monitorType });
|
||||
const isSuccessful = await syntheticsApp.confirmAndSave();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
});
|
||||
|
||||
step(`view ${monitorName} details in Monitor Management UI`, async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
const hasFailure = await syntheticsApp.findMonitorConfiguration(monitorListDetails);
|
||||
expect(hasFailure).toBeFalsy();
|
||||
});
|
||||
|
||||
step(`edit ${monitorName}`, async () => {
|
||||
await syntheticsApp.navigateToEditMonitor();
|
||||
await syntheticsApp.findByText(monitorListDetails.location);
|
||||
const hasFailure = await syntheticsApp.findEditMonitorConfiguration(
|
||||
monitorEditDetails,
|
||||
monitorType
|
||||
);
|
||||
expect(hasFailure).toBeFalsy();
|
||||
});
|
||||
|
||||
step('delete monitor', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
await syntheticsApp.findByText('Monitor name');
|
||||
const isSuccessful = await syntheticsApp.deleteMonitors();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Object.values(configuration).forEach((config) => {
|
||||
createMonitorJourney({
|
||||
monitorType: config.monitorType,
|
||||
monitorName: `${config.monitorConfig.name} monitor`,
|
||||
monitorConfig: config.monitorConfig,
|
||||
monitorListDetails: config.monitorListDetails,
|
||||
monitorEditDetails: config.monitorEditDetails as Array<[string, string]>,
|
||||
});
|
||||
});
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './getting_started.journey';
|
||||
export * from './add_monitor.journey';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Page } from '@elastic/synthetics';
|
||||
import { expect, Page } from '@elastic/synthetics';
|
||||
import { getQuerystring } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { DataStream } from '../../common/runtime_types/monitor_management';
|
||||
import { loginPageProvider } from './login';
|
||||
|
@ -107,6 +107,8 @@ export function monitorManagementPageProvider({
|
|||
},
|
||||
|
||||
async clickAddMonitor() {
|
||||
const isEnabled = await this.checkIsEnabled();
|
||||
expect(isEnabled).toBe(true);
|
||||
await page.click('text=Add monitor');
|
||||
},
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Page } from '@elastic/synthetics';
|
||||
import { expect, Page } from '@elastic/synthetics';
|
||||
import { FormMonitorType } from '../../common/runtime_types/monitor_management';
|
||||
import { loginPageProvider } from './login';
|
||||
import { utilsPageProvider } from './utils';
|
||||
|
||||
|
@ -19,8 +20,7 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED);
|
||||
const basePath = isRemote ? remoteKibanaUrl : kibanaUrl;
|
||||
const monitorManagement = `${basePath}/app/synthetics/monitors`;
|
||||
const addMonitor = `${basePath}/app/uptime/add-monitor`;
|
||||
|
||||
const addMonitor = `${basePath}/app/synthetics/add-monitor`;
|
||||
return {
|
||||
...loginPageProvider({
|
||||
page,
|
||||
|
@ -49,25 +49,43 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
},
|
||||
|
||||
async navigateToAddMonitor() {
|
||||
await page.goto(addMonitor, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
await page.goto(addMonitor);
|
||||
},
|
||||
|
||||
async ensureIsOnMonitorConfigPage() {
|
||||
await page.isVisible('[data-test-subj=monitorSettingsSection]');
|
||||
},
|
||||
|
||||
async confirmAndSave(isEditPage?: boolean) {
|
||||
async confirmAndSave() {
|
||||
await this.ensureIsOnMonitorConfigPage();
|
||||
if (isEditPage) {
|
||||
await page.click('text=Update monitor');
|
||||
} else {
|
||||
await page.click('text=Create monitor');
|
||||
}
|
||||
await this.clickByTestSubj('syntheticsMonitorConfigSubmitButton');
|
||||
return await this.findByText('Monitor added successfully.');
|
||||
},
|
||||
|
||||
async deleteMonitors() {
|
||||
let isSuccessful: boolean = false;
|
||||
while (true) {
|
||||
if ((await page.$(this.byTestId('syntheticsMonitorListActions'))) === null) {
|
||||
isSuccessful = true;
|
||||
break;
|
||||
}
|
||||
await page.click(this.byTestId('syntheticsMonitorListActions'), { delay: 800 });
|
||||
await page.click('text=delete', { delay: 800 });
|
||||
await page.waitForSelector('[data-test-subj="confirmModalTitleText"]');
|
||||
await this.clickByTestSubj('confirmModalConfirmButton');
|
||||
isSuccessful = Boolean(await this.findByTestSubj('uptimeDeleteMonitorSuccess'));
|
||||
await this.navigateToMonitorManagement();
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
}
|
||||
return isSuccessful;
|
||||
},
|
||||
|
||||
async navigateToEditMonitor() {
|
||||
await this.clickByTestSubj('syntheticsMonitorListActions');
|
||||
await page.click('text=Edit');
|
||||
await this.findByText('Edit monitor');
|
||||
},
|
||||
|
||||
async selectLocations({ locations }: { locations: string[] }) {
|
||||
for (let i = 0; i < locations.length; i++) {
|
||||
await page.click(
|
||||
|
@ -77,6 +95,13 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
}
|
||||
},
|
||||
|
||||
async selectLocationsAddEdit({ locations }: { locations: string[] }) {
|
||||
for (let i = 0; i < locations.length; i++) {
|
||||
await page.click(this.byTestId('syntheticsMonitorConfigLocations'));
|
||||
await page.click(`text=${locations[i]}`);
|
||||
}
|
||||
},
|
||||
|
||||
async fillFirstMonitorDetails({ url, locations }: { url: string; locations: string[] }) {
|
||||
await this.fillByTestSubj('urls-input', url);
|
||||
await page.click(this.byTestId('comboBoxInput'));
|
||||
|
@ -84,6 +109,162 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
await page.click(this.byTestId('urls-input'));
|
||||
},
|
||||
|
||||
async selectMonitorType(monitorType: string) {
|
||||
await this.clickByTestSubj(monitorType);
|
||||
},
|
||||
|
||||
async findMonitorConfiguration(monitorConfig: Record<string, string>) {
|
||||
const values = Object.values(monitorConfig);
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
await this.findByText(values[i]);
|
||||
}
|
||||
},
|
||||
|
||||
async findEditMonitorConfiguration(
|
||||
monitorConfig: Array<[string, string]>,
|
||||
monitorType: FormMonitorType
|
||||
) {
|
||||
await page.click('text="Advanced options"');
|
||||
|
||||
for (let i = 0; i < monitorConfig.length; i++) {
|
||||
const [selector, expected] = monitorConfig[i];
|
||||
const actual = await page.inputValue(selector);
|
||||
expect(actual).toEqual(expected);
|
||||
}
|
||||
},
|
||||
|
||||
async fillCodeEditor(value: string) {
|
||||
await page.fill('[data-test-subj=codeEditorContainer] textarea', value);
|
||||
},
|
||||
|
||||
async createBasicHTTPMonitorDetails({
|
||||
name,
|
||||
url,
|
||||
apmServiceName,
|
||||
locations,
|
||||
}: {
|
||||
name: string;
|
||||
url: string;
|
||||
apmServiceName: string;
|
||||
locations: string[];
|
||||
}) {
|
||||
await this.selectMonitorType('syntheticsMonitorTypeHTTP');
|
||||
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
|
||||
await this.fillByTestSubj('syntheticsMonitorConfigURL', url);
|
||||
},
|
||||
|
||||
async createBasicTCPMonitorDetails({
|
||||
name,
|
||||
host,
|
||||
apmServiceName,
|
||||
locations,
|
||||
}: {
|
||||
name: string;
|
||||
host: string;
|
||||
apmServiceName: string;
|
||||
locations: string[];
|
||||
}) {
|
||||
await this.selectMonitorType('syntheticsMonitorTypeTCP');
|
||||
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
|
||||
await this.fillByTestSubj('syntheticsMonitorConfigHost', host);
|
||||
},
|
||||
|
||||
async createBasicICMPMonitorDetails({
|
||||
name,
|
||||
host,
|
||||
apmServiceName,
|
||||
locations,
|
||||
}: {
|
||||
name: string;
|
||||
host: string;
|
||||
apmServiceName: string;
|
||||
locations: string[];
|
||||
}) {
|
||||
await this.selectMonitorType('syntheticsMonitorTypeICMP');
|
||||
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
|
||||
await this.fillByTestSubj('syntheticsMonitorConfigHost', host);
|
||||
},
|
||||
|
||||
async createBasicBrowserMonitorDetails({
|
||||
name,
|
||||
inlineScript,
|
||||
recorderScript,
|
||||
params,
|
||||
username,
|
||||
password,
|
||||
apmServiceName,
|
||||
locations,
|
||||
}: {
|
||||
name: string;
|
||||
inlineScript?: string;
|
||||
recorderScript?: string;
|
||||
params?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
apmServiceName: string;
|
||||
locations: string[];
|
||||
}) {
|
||||
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
|
||||
if (inlineScript) {
|
||||
await this.clickByTestSubj('syntheticsSourceTab__inline');
|
||||
await this.fillCodeEditor(inlineScript);
|
||||
return;
|
||||
}
|
||||
if (recorderScript) {
|
||||
// Upload buffer from memory
|
||||
await page.setInputFiles('input[data-test-subj=syntheticsFleetScriptRecorderUploader]', {
|
||||
name: 'file.js',
|
||||
mimeType: 'text/javascript',
|
||||
buffer: Buffer.from(recorderScript),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async createBasicMonitorDetails({
|
||||
name,
|
||||
apmServiceName,
|
||||
locations,
|
||||
}: {
|
||||
name: string;
|
||||
apmServiceName: string;
|
||||
locations: string[];
|
||||
}) {
|
||||
await page.click('text="Advanced options"');
|
||||
await this.fillByTestSubj('syntheticsMonitorConfigName', name);
|
||||
await this.fillByTestSubj('syntheticsMonitorConfigAPMServiceName', apmServiceName);
|
||||
await this.selectLocationsAddEdit({ locations });
|
||||
},
|
||||
|
||||
async createMonitor({
|
||||
monitorConfig,
|
||||
monitorType,
|
||||
}: {
|
||||
monitorConfig: Record<string, string | string[]>;
|
||||
monitorType: FormMonitorType;
|
||||
}) {
|
||||
switch (monitorType) {
|
||||
case FormMonitorType.HTTP:
|
||||
// @ts-ignore
|
||||
await this.createBasicHTTPMonitorDetails(monitorConfig);
|
||||
break;
|
||||
case FormMonitorType.TCP:
|
||||
// @ts-ignore
|
||||
await this.createBasicTCPMonitorDetails(monitorConfig);
|
||||
break;
|
||||
case FormMonitorType.ICMP:
|
||||
// @ts-ignore
|
||||
await this.createBasicICMPMonitorDetails(monitorConfig);
|
||||
break;
|
||||
case FormMonitorType.MULTISTEP:
|
||||
// @ts-ignore
|
||||
await this.createBasicBrowserMonitorDetails(monitorConfig);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
async enableMonitorManagement(shouldEnable: boolean = true) {
|
||||
const isEnabled = await this.checkIsEnabled();
|
||||
if (isEnabled === shouldEnable) {
|
||||
|
|
|
@ -8,13 +8,17 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { useBreadcrumbs } from '../../hooks';
|
||||
import { getServiceLocations } from '../../state';
|
||||
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui';
|
||||
import { SimpleMonitorForm } from './simple_monitor_form';
|
||||
|
||||
export const GettingStartedPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getServiceLocations());
|
||||
|
@ -32,7 +36,13 @@ export const GettingStartedPage = () => {
|
|||
<>
|
||||
<EuiText size="s">
|
||||
{OR_LABEL}{' '}
|
||||
<EuiLink href="/synthetics/monitors/add-new">{SELECT_DIFFERENT_MONITOR}</EuiLink>
|
||||
<EuiLink
|
||||
href={history.createHref({
|
||||
pathname: MONITOR_ADD_ROUTE,
|
||||
})}
|
||||
>
|
||||
{SELECT_DIFFERENT_MONITOR}
|
||||
</EuiLink>
|
||||
{i18n.translate('xpack.synthetics.gettingStarted.createSingle.description', {
|
||||
defaultMessage: ' to get started with Elastic Synthetics Monitoring',
|
||||
})}
|
||||
|
|
|
@ -77,7 +77,13 @@ export const SimpleMonitorForm = () => {
|
|||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton type="submit" fill iconType="plusInCircleFilled" isLoading={loading}>
|
||||
<EuiButton
|
||||
type="submit"
|
||||
fill
|
||||
iconType="plusInCircleFilled"
|
||||
isLoading={loading}
|
||||
data-test-subj="syntheticsMonitorConfigSubmitButton"
|
||||
>
|
||||
{CREATE_MONITOR_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiAccordion, EuiDescribedFormGroup, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { useFormContext, FieldError } from 'react-hook-form';
|
||||
import { FORM_CONFIG } from '../form/form_config';
|
||||
import { Field } from '../form/field';
|
||||
import { ConfigKey, FormMonitorType } from '../types';
|
||||
|
||||
export const AdvancedConfig = () => {
|
||||
const {
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]);
|
||||
|
||||
return FORM_CONFIG[type]?.advanced ? (
|
||||
<EuiPanel hasBorder>
|
||||
<EuiAccordion
|
||||
id="syntheticsAdvancedPanel"
|
||||
buttonContent={i18n.translate('xpack.synthetics.monitorConfig.advancedOptions.title', {
|
||||
defaultMessage: 'Advanced options',
|
||||
})}
|
||||
>
|
||||
<EuiSpacer />
|
||||
{FORM_CONFIG[type].advanced?.map((configGroup) => {
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
description={configGroup.description}
|
||||
title={<h4>{configGroup.title}</h4>}
|
||||
fullWidth
|
||||
key={configGroup.title}
|
||||
>
|
||||
{configGroup.components.map((field) => {
|
||||
return (
|
||||
<Field
|
||||
{...field}
|
||||
key={field.fieldKey}
|
||||
fieldError={errors[field.fieldKey] as FieldError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
})}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
) : null;
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 '../../../../../common/constants/monitor_management';
|
||||
export * from '../../../../../common/constants/monitor_defaults';
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { CodeEditor as MonacoCodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { MonacoEditorLangId } from '../types';
|
||||
|
||||
const CodeEditorContainer = styled(EuiPanel)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
ariaLabel: string;
|
||||
id: string;
|
||||
languageId: MonacoEditorLangId;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value, placeholder }: Props) => {
|
||||
return (
|
||||
<CodeEditorContainer borderRadius="none" hasShadow={false} hasBorder={true}>
|
||||
<MonacoCodeContainer
|
||||
id={`${id}-editor`}
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj="codeEditorContainer"
|
||||
>
|
||||
<MonacoCodeEditor
|
||||
languageId={languageId}
|
||||
width="100%"
|
||||
height="250px"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={{
|
||||
renderValidationDecorations: value ? 'on' : 'off',
|
||||
}}
|
||||
isCopyable={true}
|
||||
allowFullScreen={true}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</MonacoCodeContainer>
|
||||
</CodeEditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const MonacoCodeContainer = euiStyled.div`
|
||||
& > .kibanaCodeEditor {
|
||||
z-index: 0;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { ComboBox } from './combo_box';
|
||||
|
||||
describe('<ComboBox />', () => {
|
||||
const onChange = jest.fn();
|
||||
const selectedOptions: string[] = [];
|
||||
|
||||
it('renders ComboBox', () => {
|
||||
const { getByTestId } = render(
|
||||
<ComboBox selectedOptions={selectedOptions} onChange={onChange} />
|
||||
);
|
||||
|
||||
expect(getByTestId('syntheticsFleetComboBox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onBlur', () => {
|
||||
const onBlur = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<ComboBox selectedOptions={selectedOptions} onChange={onChange} onBlur={onBlur} />
|
||||
);
|
||||
|
||||
const combobox = getByTestId('syntheticsFleetComboBox');
|
||||
fireEvent.focus(combobox);
|
||||
fireEvent.blur(combobox);
|
||||
|
||||
expect(onBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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, { useState, useCallback } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
export interface Props {
|
||||
onChange: (value: string[]) => void;
|
||||
onBlur?: () => void;
|
||||
selectedOptions: string[];
|
||||
}
|
||||
|
||||
export const ComboBox = ({ onChange, onBlur, selectedOptions, ...props }: Props) => {
|
||||
const [formattedSelectedOptions, setSelectedOptions] = useState<
|
||||
Array<EuiComboBoxOptionOption<string>>
|
||||
>(selectedOptions.map((option) => ({ label: option, key: option })));
|
||||
const [isInvalid, setInvalid] = useState(false);
|
||||
|
||||
const onOptionsChange = useCallback(
|
||||
(options: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setSelectedOptions(options);
|
||||
const formattedTags = options.map((option) => option.label);
|
||||
onChange(formattedTags);
|
||||
setInvalid(false);
|
||||
},
|
||||
[onChange, setSelectedOptions, setInvalid]
|
||||
);
|
||||
|
||||
const onCreateOption = useCallback(
|
||||
(tag: string) => {
|
||||
const formattedTag = tag.trim();
|
||||
const newOption = {
|
||||
label: formattedTag,
|
||||
};
|
||||
|
||||
onChange([...selectedOptions, formattedTag]);
|
||||
|
||||
// Select the option.
|
||||
setSelectedOptions([...formattedSelectedOptions, newOption]);
|
||||
},
|
||||
[onChange, formattedSelectedOptions, selectedOptions, setSelectedOptions]
|
||||
);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(searchValue: string) => {
|
||||
if (!searchValue) {
|
||||
setInvalid(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInvalid(!isValid(searchValue));
|
||||
},
|
||||
[setInvalid]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiComboBox<string>
|
||||
data-test-subj="syntheticsFleetComboBox"
|
||||
noSuggestions
|
||||
selectedOptions={formattedSelectedOptions}
|
||||
onCreateOption={onCreateOption}
|
||||
onChange={onOptionsChange}
|
||||
onBlur={() => onBlur?.()}
|
||||
onSearchChange={onSearchChange}
|
||||
isInvalid={isInvalid}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const isValid = (value: string) => {
|
||||
// Ensure that the tag is more than whitespace
|
||||
return value.match(/\S+/) !== null;
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { HeaderField, contentTypes } from './header_field';
|
||||
import { Mode } from '../types';
|
||||
|
||||
describe('<HeaderField />', () => {
|
||||
const onChange = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
const defaultValue = {};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders HeaderField', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<HeaderField defaultValue={{ sample: 'header' }} onChange={onChange} />
|
||||
);
|
||||
|
||||
expect(getByText('Key')).toBeInTheDocument();
|
||||
expect(getByText('Value')).toBeInTheDocument();
|
||||
const key = getByTestId('keyValuePairsKey0') as HTMLInputElement;
|
||||
const value = getByTestId('keyValuePairsValue0') as HTMLInputElement;
|
||||
expect(key.value).toEqual('sample');
|
||||
expect(value.value).toEqual('header');
|
||||
});
|
||||
|
||||
it('calls onBlur', () => {
|
||||
const { getByTestId } = render(
|
||||
<HeaderField defaultValue={{ sample: 'header' }} onChange={onChange} onBlur={onBlur} />
|
||||
);
|
||||
|
||||
const key = getByTestId('keyValuePairsKey0') as HTMLInputElement;
|
||||
const value = getByTestId('keyValuePairsValue0') as HTMLInputElement;
|
||||
|
||||
fireEvent.blur(key);
|
||||
fireEvent.blur(value);
|
||||
|
||||
expect(onBlur).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('formats headers and handles onChange', async () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<HeaderField defaultValue={defaultValue} onChange={onChange} />
|
||||
);
|
||||
const addHeader = getByText('Add header');
|
||||
fireEvent.click(addHeader);
|
||||
const key = getByTestId('keyValuePairsKey0') as HTMLInputElement;
|
||||
const value = getByTestId('keyValuePairsValue0') as HTMLInputElement;
|
||||
const newKey = 'sampleKey';
|
||||
const newValue = 'sampleValue';
|
||||
fireEvent.change(key, { target: { value: newKey } });
|
||||
fireEvent.change(value, { target: { value: newValue } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toBeCalledWith({
|
||||
[newKey]: newValue,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles deleting headers', async () => {
|
||||
const { getByTestId, getByText, getByLabelText } = render(
|
||||
<HeaderField defaultValue={{ sampleKey: 'sampleValue' }} onChange={onChange} />
|
||||
);
|
||||
const addHeader = getByText('Add header');
|
||||
|
||||
fireEvent.click(addHeader);
|
||||
|
||||
const key = getByTestId('keyValuePairsKey0') as HTMLInputElement;
|
||||
const value = getByTestId('keyValuePairsValue0') as HTMLInputElement;
|
||||
const newKey = 'sampleKey';
|
||||
const newValue = 'sampleValue';
|
||||
fireEvent.change(key, { target: { value: newKey } });
|
||||
fireEvent.change(value, { target: { value: newValue } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toBeCalledWith({
|
||||
[newKey]: newValue,
|
||||
});
|
||||
});
|
||||
|
||||
const deleteBtn = getByLabelText('Delete item number 2, sampleKey:sampleValue');
|
||||
|
||||
// uncheck
|
||||
fireEvent.click(deleteBtn);
|
||||
});
|
||||
|
||||
it('handles content mode', async () => {
|
||||
const contentMode: Mode = Mode.PLAINTEXT;
|
||||
render(
|
||||
<HeaderField defaultValue={defaultValue} onChange={onChange} contentMode={contentMode} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toBeCalledWith({
|
||||
'Content-Type': contentTypes[Mode.PLAINTEXT],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ContentType, Mode } from '../types';
|
||||
|
||||
import { KeyValuePairsField, Pair } from './key_value_field';
|
||||
|
||||
interface Props {
|
||||
contentMode?: Mode;
|
||||
defaultValue: Record<string, string>;
|
||||
onChange: (value: Record<string, string>) => void;
|
||||
onBlur?: () => void;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const HeaderField = ({
|
||||
contentMode,
|
||||
defaultValue,
|
||||
onChange,
|
||||
onBlur,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: Props) => {
|
||||
const defaultValueKeys = Object.keys(defaultValue).filter((key) => key !== 'Content-Type'); // Content-Type is a secret header we hide from the user
|
||||
const formattedDefaultValues: Pair[] = [
|
||||
...defaultValueKeys.map<Pair>((key) => {
|
||||
return [key || '', defaultValue[key] || '']; // key, value
|
||||
}),
|
||||
];
|
||||
const [headers, setHeaders] = useState<Pair[]>(formattedDefaultValues);
|
||||
|
||||
useEffect(() => {
|
||||
const formattedHeaders = headers.reduce((acc: Record<string, string>, header) => {
|
||||
const [key, value] = header;
|
||||
if (key) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (contentMode) {
|
||||
onChange({ 'Content-Type': contentTypes[contentMode], ...formattedHeaders });
|
||||
} else {
|
||||
onChange(formattedHeaders);
|
||||
}
|
||||
}, [contentMode, headers, onChange]);
|
||||
|
||||
return (
|
||||
<KeyValuePairsField
|
||||
addPairControlLabel={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.createPackagePolicy.stepConfigure.headerField.addHeader.label"
|
||||
defaultMessage="Add header"
|
||||
/>
|
||||
}
|
||||
defaultPairs={headers}
|
||||
onChange={setHeaders}
|
||||
onBlur={() => onBlur?.()}
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const contentTypes: Record<Mode, ContentType> = {
|
||||
[Mode.JSON]: ContentType.JSON,
|
||||
[Mode.PLAINTEXT]: ContentType.TEXT,
|
||||
[Mode.XML]: ContentType.XML,
|
||||
[Mode.FORM]: ContentType.FORM,
|
||||
};
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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 { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { ResponseBodyIndexField } from './index_response_body_field';
|
||||
import { ResponseBodyIndexPolicy } from '../types';
|
||||
|
||||
describe('<ResponseBodyIndexField/>', () => {
|
||||
const defaultDefaultValue = ResponseBodyIndexPolicy.ON_ERROR;
|
||||
const onChange = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
const WrappedComponent = ({ defaultValue = defaultDefaultValue }) => {
|
||||
return (
|
||||
<ResponseBodyIndexField defaultValue={defaultValue} onChange={onChange} onBlur={onBlur} />
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders ResponseBodyIndexField', () => {
|
||||
const { getByText, getByTestId } = render(<WrappedComponent />);
|
||||
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
|
||||
expect(select.value).toEqual(defaultDefaultValue);
|
||||
expect(getByText('On error')).toBeInTheDocument();
|
||||
expect(getByText('Index response body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles select change', async () => {
|
||||
const { getByText, getByTestId } = render(<WrappedComponent />);
|
||||
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
|
||||
const newPolicy = ResponseBodyIndexPolicy.ALWAYS;
|
||||
expect(select.value).toEqual(defaultDefaultValue);
|
||||
|
||||
fireEvent.change(select, { target: { value: newPolicy } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(select.value).toBe(newPolicy);
|
||||
expect(getByText('Always')).toBeInTheDocument();
|
||||
expect(onChange).toBeCalledWith(newPolicy);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onBlur', async () => {
|
||||
const { getByTestId } = render(<WrappedComponent />);
|
||||
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
|
||||
const newPolicy = ResponseBodyIndexPolicy.ALWAYS;
|
||||
|
||||
fireEvent.change(select, { target: { value: newPolicy } });
|
||||
fireEvent.blur(select);
|
||||
|
||||
expect(onBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles checkbox change', async () => {
|
||||
const { getByTestId, getByLabelText } = render(<WrappedComponent />);
|
||||
const checkbox = getByLabelText('Index response body') as HTMLInputElement;
|
||||
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
|
||||
const newPolicy = ResponseBodyIndexPolicy.NEVER;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkbox.checked).toBe(false);
|
||||
expect(select).not.toBeInTheDocument();
|
||||
expect(onChange).toBeCalledWith(newPolicy);
|
||||
});
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkbox.checked).toBe(true);
|
||||
expect(select).not.toBeInTheDocument();
|
||||
expect(onChange).toBeCalledWith(defaultDefaultValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles ResponseBodyIndexPolicy.NEVER as a default value', async () => {
|
||||
const { queryByTestId, getByTestId, getByLabelText } = render(
|
||||
<WrappedComponent defaultValue={ResponseBodyIndexPolicy.NEVER} />
|
||||
);
|
||||
const checkbox = getByLabelText('Index response body') as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(false);
|
||||
expect(
|
||||
queryByTestId('indexResponseBodyFieldSelect') as HTMLInputElement
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkbox.checked).toBe(true);
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select.value).toEqual(ResponseBodyIndexPolicy.ON_ERROR);
|
||||
// switches back to on error policy when checkbox is checked
|
||||
expect(onChange).toBeCalledWith(ResponseBodyIndexPolicy.ON_ERROR);
|
||||
});
|
||||
|
||||
const newPolicy = ResponseBodyIndexPolicy.ALWAYS;
|
||||
fireEvent.change(select, { target: { value: newPolicy } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(select.value).toEqual(newPolicy);
|
||||
expect(onChange).toBeCalledWith(newPolicy);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
|
||||
import { ResponseBodyIndexPolicy } from '../types';
|
||||
|
||||
interface Props {
|
||||
defaultValue: ResponseBodyIndexPolicy;
|
||||
onChange: (responseBodyIndexPolicy: ResponseBodyIndexPolicy) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export const ResponseBodyIndexField = ({ defaultValue, onChange, onBlur }: Props) => {
|
||||
const [policy, setPolicy] = useState<ResponseBodyIndexPolicy>(
|
||||
defaultValue !== ResponseBodyIndexPolicy.NEVER ? defaultValue : ResponseBodyIndexPolicy.ON_ERROR
|
||||
);
|
||||
const [checked, setChecked] = useState<boolean>(defaultValue !== ResponseBodyIndexPolicy.NEVER);
|
||||
|
||||
useEffect(() => {
|
||||
if (checked) {
|
||||
setPolicy(policy);
|
||||
onChange(policy);
|
||||
} else {
|
||||
onChange(ResponseBodyIndexPolicy.NEVER);
|
||||
}
|
||||
}, [checked, policy, setPolicy, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(policy);
|
||||
}, [onChange, policy]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="syntheticsIndexResponseBody">
|
||||
<EuiCheckbox
|
||||
id="uptimeFleetIndexResponseBody"
|
||||
checked={checked}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfig.indexResponseBody"
|
||||
defaultMessage="Index response body"
|
||||
/>
|
||||
}
|
||||
onChange={(event) => {
|
||||
const checkedEvent = event.target.checked;
|
||||
setChecked(checkedEvent);
|
||||
}}
|
||||
onBlur={() => onBlur?.()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{checked && (
|
||||
<EuiFlexItem>
|
||||
<EuiSelect
|
||||
aria-label={i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfig.responseBodyIndexPolicy',
|
||||
{
|
||||
defaultMessage: 'Response body index policy',
|
||||
}
|
||||
)}
|
||||
data-test-subj="indexResponseBodyFieldSelect"
|
||||
options={responseBodyIndexPolicyOptions}
|
||||
value={policy}
|
||||
onChange={(event) => {
|
||||
setPolicy(event.target.value as ResponseBodyIndexPolicy);
|
||||
}}
|
||||
onBlur={() => onBlur?.()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const responseBodyIndexPolicyOptions = [
|
||||
{
|
||||
value: ResponseBodyIndexPolicy.ALWAYS,
|
||||
text: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.responseBodyIndex.always',
|
||||
{
|
||||
defaultMessage: 'Always',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ResponseBodyIndexPolicy.ON_ERROR,
|
||||
text: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.responseBodyIndex.onError',
|
||||
{
|
||||
defaultMessage: 'On error',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { KeyValuePairsField, Pair } from './key_value_field';
|
||||
|
||||
describe('<KeyValuePairsField />', () => {
|
||||
const onChange = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
const defaultDefaultValue = [['', '']] as Pair[];
|
||||
const WrappedComponent = ({
|
||||
defaultValue = defaultDefaultValue,
|
||||
addPairControlLabel = 'Add pair',
|
||||
}) => {
|
||||
return (
|
||||
<KeyValuePairsField
|
||||
defaultPairs={defaultValue}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
addPairControlLabel={addPairControlLabel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders KeyValuePairsField', () => {
|
||||
const { getByText } = render(<WrappedComponent />);
|
||||
expect(getByText('Key')).toBeInTheDocument();
|
||||
expect(getByText('Value')).toBeInTheDocument();
|
||||
|
||||
expect(getByText('Add pair')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onBlur', () => {
|
||||
const { getByText, getByTestId } = render(<WrappedComponent />);
|
||||
const addPair = getByText('Add pair');
|
||||
fireEvent.click(addPair);
|
||||
|
||||
const keyInput = getByTestId('keyValuePairsKey0') as HTMLInputElement;
|
||||
const valueInput = getByTestId('keyValuePairsValue0') as HTMLInputElement;
|
||||
|
||||
userEvent.type(keyInput, 'some-key');
|
||||
userEvent.type(valueInput, 'some-value');
|
||||
fireEvent.blur(valueInput);
|
||||
|
||||
expect(onBlur).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handles adding and editing a new row', async () => {
|
||||
const { getByTestId, queryByTestId, getByText } = render(
|
||||
<WrappedComponent defaultValue={[]} />
|
||||
);
|
||||
|
||||
expect(queryByTestId('keyValuePairsKey0')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('keyValuePairsValue0')).not.toBeInTheDocument(); // check that only one row exists
|
||||
|
||||
const addPair = getByText('Add pair');
|
||||
|
||||
fireEvent.click(addPair);
|
||||
|
||||
const newRowKey = getByTestId('keyValuePairsKey0') as HTMLInputElement;
|
||||
const newRowValue = getByTestId('keyValuePairsValue0') as HTMLInputElement;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(newRowKey.value).toEqual('');
|
||||
expect(newRowValue.value).toEqual('');
|
||||
expect(onChange).toBeCalledWith([[newRowKey.value, newRowValue.value]]);
|
||||
});
|
||||
|
||||
fireEvent.change(newRowKey, { target: { value: 'newKey' } });
|
||||
fireEvent.change(newRowValue, { target: { value: 'newValue' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(newRowKey.value).toEqual('newKey');
|
||||
expect(newRowValue.value).toEqual('newValue');
|
||||
expect(onChange).toBeCalledWith([[newRowKey.value, newRowValue.value]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormControlLayoutDelimited,
|
||||
EuiFormLabel,
|
||||
EuiFormFieldset,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
const StyledFieldset = styled(EuiFormFieldset)`
|
||||
&&& {
|
||||
legend {
|
||||
width: calc(100% - 52px); // right margin + flex item padding
|
||||
margin-right: 40px;
|
||||
}
|
||||
.euiFlexGroup {
|
||||
margin-left: 0;
|
||||
}
|
||||
.euiFlexItem {
|
||||
margin-left: 0;
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledField = styled(EuiFieldText)`
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
export type Pair = [
|
||||
string, // key
|
||||
string // value
|
||||
];
|
||||
|
||||
interface Props {
|
||||
addPairControlLabel: string | React.ReactElement;
|
||||
defaultPairs: Pair[];
|
||||
onChange: (pairs: Pair[]) => void;
|
||||
onBlur?: () => void;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const KeyValuePairsField = ({
|
||||
addPairControlLabel,
|
||||
defaultPairs,
|
||||
onChange,
|
||||
onBlur,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: Props) => {
|
||||
const [pairs, setPairs] = useState<Pair[]>(defaultPairs);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>, index: number, isKey: boolean) => {
|
||||
const targetValue = event.target.value;
|
||||
|
||||
setPairs((prevPairs) => {
|
||||
const newPairs = [...prevPairs];
|
||||
const [prevKey, prevValue] = prevPairs[index];
|
||||
newPairs[index] = isKey ? [targetValue, prevValue] : [prevKey, targetValue];
|
||||
return newPairs;
|
||||
});
|
||||
},
|
||||
[setPairs]
|
||||
);
|
||||
|
||||
const handleAddPair = useCallback(() => {
|
||||
setPairs((prevPairs) => [['', ''], ...prevPairs]);
|
||||
}, [setPairs]);
|
||||
|
||||
const handleDeletePair = useCallback(
|
||||
(index: number) => {
|
||||
setPairs((prevPairs) => {
|
||||
const newPairs = [...prevPairs];
|
||||
newPairs.splice(index, 1);
|
||||
return [...newPairs];
|
||||
});
|
||||
},
|
||||
[setPairs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(pairs);
|
||||
}, [onChange, pairs]);
|
||||
|
||||
return (
|
||||
<div data-test-subj={dataTestSubj}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="plus"
|
||||
onClick={handleAddPair}
|
||||
data-test-subj={`${dataTestSubj}__button`}
|
||||
>
|
||||
{addPairControlLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<StyledFieldset
|
||||
legend={
|
||||
!!pairs.length
|
||||
? {
|
||||
children: (
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.keyValuePairsField.key.label"
|
||||
defaultMessage="Key"
|
||||
/>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.keyValuePairsField.value.label"
|
||||
defaultMessage="Value"
|
||||
/>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{pairs.map((pair, index) => {
|
||||
const [key, value] = pair;
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFormControlLayoutDelimited
|
||||
fullWidth
|
||||
append={
|
||||
<EuiFormLabel>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.synthetics.keyValuePairsField.deleteItem.label',
|
||||
{
|
||||
defaultMessage: 'Delete item number {index}, {key}:{value}',
|
||||
values: { index: index + 1, key, value },
|
||||
}
|
||||
)}
|
||||
onClick={() => handleDeletePair(index)}
|
||||
/>
|
||||
</EuiFormLabel>
|
||||
}
|
||||
startControl={
|
||||
<StyledField
|
||||
aria-label={i18n.translate(
|
||||
'xpack.synthetics.keyValuePairsField.key.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Key',
|
||||
}
|
||||
)}
|
||||
data-test-subj={`keyValuePairsKey${index}`}
|
||||
value={key}
|
||||
onChange={(event) => handleOnChange(event, index, true)}
|
||||
onBlur={() => onBlur?.()}
|
||||
/>
|
||||
}
|
||||
endControl={
|
||||
<StyledField
|
||||
aria-label={i18n.translate(
|
||||
'xpack.synthetics.keyValuePairsField.value.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Value',
|
||||
}
|
||||
)}
|
||||
data-test-subj={`keyValuePairsValue${index}`}
|
||||
value={value}
|
||||
onChange={(event) => handleOnChange(event, index, false)}
|
||||
onBlur={() => onBlur?.()}
|
||||
/>
|
||||
}
|
||||
delimiter=":"
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</StyledFieldset>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiKeyPadMenu,
|
||||
EuiKeyPadMenuItem,
|
||||
EuiIcon,
|
||||
EuiKeyPadMenuItemProps,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export type MonitorTypeRadioOption = EuiKeyPadMenuItemProps & {
|
||||
icon: string;
|
||||
description: string;
|
||||
descriptionTitle: string;
|
||||
link: string;
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
onChange: (id: string, value: string) => void;
|
||||
name: string;
|
||||
'data-test-subj': string;
|
||||
};
|
||||
|
||||
export const MonitorType = ({
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
icon,
|
||||
onChange,
|
||||
name,
|
||||
isSelected,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: MonitorTypeRadioOption) => {
|
||||
return (
|
||||
<EuiKeyPadMenuItem
|
||||
checkable="single"
|
||||
label={label}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
name={name}
|
||||
isSelected={isSelected}
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiIcon type={icon} />
|
||||
</EuiKeyPadMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MonitorTypeRadioGroup = ({
|
||||
options,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
ariaLegend,
|
||||
...props
|
||||
}: EuiKeyPadMenuItemProps & {
|
||||
options: MonitorTypeRadioOption[];
|
||||
onChange: React.ChangeEvent<HTMLInputElement>;
|
||||
name: string;
|
||||
value: string;
|
||||
ariaLegend: string;
|
||||
}) => {
|
||||
const selectedOption = options.find((radio) => radio.value === value);
|
||||
return (
|
||||
<>
|
||||
<EuiKeyPadMenu checkable={{ ariaLegend }} css={{ width: '100%' }}>
|
||||
{options.map((radio) => {
|
||||
return (
|
||||
<MonitorType
|
||||
{...props}
|
||||
{...radio}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
isSelected={radio.value === value}
|
||||
data-test-subj={radio['data-test-subj']}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiKeyPadMenu>
|
||||
<EuiSpacer />
|
||||
{selectedOption && (
|
||||
<EuiPanel color="primary">
|
||||
<EuiText size="s">
|
||||
<h4>{selectedOption.descriptionTitle}</h4>
|
||||
</EuiText>
|
||||
<EuiText size="s" color="subdued">
|
||||
<span>{`${selectedOption.description} `}</span>
|
||||
<EuiLink href={selectedOption.link} target="_blank">
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.monitorType.learnMoreLink', {
|
||||
defaultMessage: 'Learn more',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { EuiText } from '@elastic/eui';
|
||||
|
||||
export const OptionalLabel = () => {
|
||||
return (
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel"
|
||||
defaultMessage="Optional"
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 'jest-canvas-mock';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { RequestBodyField } from './request_body_field';
|
||||
import { Mode } from '../types';
|
||||
|
||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
|
||||
htmlIdGenerator: () => () => `id-${Math.random()}`,
|
||||
}));
|
||||
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('<RequestBodyField />', () => {
|
||||
const defaultMode = Mode.PLAINTEXT;
|
||||
const defaultValue = 'sample value';
|
||||
const WrappedComponent = () => {
|
||||
const [config, setConfig] = useState({
|
||||
type: defaultMode,
|
||||
value: defaultValue,
|
||||
});
|
||||
|
||||
return (
|
||||
<RequestBodyField
|
||||
value={{
|
||||
value: config.value,
|
||||
type: config.type,
|
||||
}}
|
||||
onChange={useCallback(
|
||||
(code) => setConfig({ type: code.type as Mode, value: code.value }),
|
||||
[setConfig]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders RequestBodyField', () => {
|
||||
const { getByText, getByLabelText } = render(<WrappedComponent />);
|
||||
|
||||
expect(getByText('Form')).toBeInTheDocument();
|
||||
expect(getByText('Text')).toBeInTheDocument();
|
||||
expect(getByText('XML')).toBeInTheDocument();
|
||||
expect(getByText('JSON')).toBeInTheDocument();
|
||||
expect(getByLabelText('Text code editor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles changing code editor mode', async () => {
|
||||
const { getByText, getByLabelText, queryByText, queryByLabelText } = render(
|
||||
<WrappedComponent />
|
||||
);
|
||||
|
||||
// currently text code editor is displayed
|
||||
expect(getByLabelText('Text code editor')).toBeInTheDocument();
|
||||
expect(queryByText('Key')).not.toBeInTheDocument();
|
||||
|
||||
const formButton = getByText('Form').closest('button');
|
||||
if (formButton) {
|
||||
fireEvent.click(formButton);
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(getByText('Add form field')).toBeInTheDocument();
|
||||
expect(queryByLabelText('Text code editor')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { stringify, parse } from 'query-string';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
import { Mode, MonacoEditorLangId } from '../types';
|
||||
import { KeyValuePairsField, Pair } from './key_value_field';
|
||||
import { CodeEditor } from './code_editor';
|
||||
|
||||
interface Props {
|
||||
onChange: (requestBody: { type: Mode; value: string }) => void;
|
||||
onBlur?: () => void;
|
||||
value: {
|
||||
type: Mode;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum ResponseBodyType {
|
||||
CODE = 'code',
|
||||
FORM = 'form',
|
||||
}
|
||||
|
||||
// TO DO: Look into whether or not code editor reports errors, in order to prevent form submission on an error
|
||||
export const RequestBodyField = ({ onChange, onBlur, value: { type, value } }: Props) => {
|
||||
const [values, setValues] = useState<Record<ResponseBodyType, string>>({
|
||||
[ResponseBodyType.FORM]: type === Mode.FORM ? value : '',
|
||||
[ResponseBodyType.CODE]: type !== Mode.FORM ? value : '',
|
||||
});
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
type,
|
||||
value: type === Mode.FORM ? values[ResponseBodyType.FORM] : values[ResponseBodyType.CODE],
|
||||
});
|
||||
}, [onChange, type, values]);
|
||||
|
||||
const handleSetMode = useCallback(
|
||||
(currentMode: Mode) => {
|
||||
onChange({
|
||||
type: currentMode,
|
||||
value:
|
||||
currentMode === Mode.FORM ? values[ResponseBodyType.FORM] : values[ResponseBodyType.CODE],
|
||||
});
|
||||
},
|
||||
[onChange, values]
|
||||
);
|
||||
|
||||
const onChangeFormFields = useCallback(
|
||||
(pairs: Pair[]) => {
|
||||
const formattedPairs = pairs.reduce((acc: Record<string, string>, header) => {
|
||||
const [key, pairValue] = header;
|
||||
if (key) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: pairValue,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return setValues((prevValues) => ({
|
||||
...prevValues,
|
||||
[Mode.FORM]: stringify(formattedPairs),
|
||||
}));
|
||||
},
|
||||
[setValues]
|
||||
);
|
||||
|
||||
const defaultFormPairs: Pair[] = useMemo(() => {
|
||||
const pairs = parse(values[Mode.FORM]);
|
||||
const keys = Object.keys(pairs);
|
||||
const formattedPairs: Pair[] = keys.map((key: string) => {
|
||||
// key, value, checked;
|
||||
return [key, `${pairs[key]}`];
|
||||
});
|
||||
return formattedPairs;
|
||||
}, [values]);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: Mode.PLAINTEXT,
|
||||
name: modeLabels[Mode.PLAINTEXT],
|
||||
'data-test-subj': `syntheticsRequestBodyTab__${Mode.PLAINTEXT}`,
|
||||
content: (
|
||||
<CodeEditor
|
||||
ariaLabel={i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBody.codeEditor.text.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Text code editor',
|
||||
}
|
||||
)}
|
||||
id={Mode.PLAINTEXT}
|
||||
languageId={MonacoEditorLangId.PLAINTEXT}
|
||||
onChange={(code) => {
|
||||
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }));
|
||||
onBlur?.();
|
||||
}}
|
||||
value={values[ResponseBodyType.CODE]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: Mode.JSON,
|
||||
name: modeLabels[Mode.JSON],
|
||||
'data-test-subj': `syntheticsRequestBodyTab__${Mode.JSON}`,
|
||||
content: (
|
||||
<CodeEditor
|
||||
ariaLabel={i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBody.codeEditor.json.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'JSON code editor',
|
||||
}
|
||||
)}
|
||||
id={Mode.JSON}
|
||||
languageId={MonacoEditorLangId.JSON}
|
||||
onChange={(code) => {
|
||||
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }));
|
||||
onBlur?.();
|
||||
}}
|
||||
value={values[ResponseBodyType.CODE]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: Mode.XML,
|
||||
name: modeLabels[Mode.XML],
|
||||
'data-test-subj': `syntheticsRequestBodyTab__${Mode.XML}`,
|
||||
content: (
|
||||
<CodeEditor
|
||||
ariaLabel={i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBody.codeEditor.xml.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'XML code editor',
|
||||
}
|
||||
)}
|
||||
id={Mode.XML}
|
||||
languageId={MonacoEditorLangId.XML}
|
||||
onChange={(code) => {
|
||||
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }));
|
||||
onBlur?.();
|
||||
}}
|
||||
value={values[ResponseBodyType.CODE]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: Mode.FORM,
|
||||
name: modeLabels[Mode.FORM],
|
||||
'data-test-subj': `syntheticsRequestBodyTab__${Mode.FORM}`,
|
||||
content: (
|
||||
<KeyValuePairsField
|
||||
addPairControlLabel={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.createPackagePolicy.stepConfigure.requestBody.formField.addFormField.label"
|
||||
defaultMessage="Add form field"
|
||||
/>
|
||||
}
|
||||
defaultPairs={defaultFormPairs}
|
||||
onChange={onChangeFormFields}
|
||||
onBlur={() => onBlur?.()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs.find((tab) => tab.id === type)}
|
||||
autoFocus="selected"
|
||||
onTabClick={(tab) => {
|
||||
handleSetMode(tab.id as Mode);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const modeLabels = {
|
||||
[Mode.FORM]: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.form',
|
||||
{
|
||||
defaultMessage: 'Form',
|
||||
}
|
||||
),
|
||||
[Mode.PLAINTEXT]: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.text',
|
||||
{
|
||||
defaultMessage: 'Text',
|
||||
}
|
||||
),
|
||||
[Mode.JSON]: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.JSON',
|
||||
{
|
||||
defaultMessage: 'JSON',
|
||||
}
|
||||
),
|
||||
[Mode.XML]: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBodyType.XML',
|
||||
{
|
||||
defaultMessage: 'XML',
|
||||
}
|
||||
),
|
||||
};
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { ScriptRecorderFields } from './script_recorder_fields';
|
||||
|
||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
|
||||
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
|
||||
htmlIdGenerator: () => () => `id-${Math.random()}`,
|
||||
}));
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
describe('<ScriptRecorderFields />', () => {
|
||||
let file: File;
|
||||
const testScript = 'step(() => {})';
|
||||
const WrappedComponent = ({
|
||||
isEditable = true,
|
||||
script = '',
|
||||
fileName = '',
|
||||
}: {
|
||||
isEditable?: boolean;
|
||||
script?: string;
|
||||
fileName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ScriptRecorderFields
|
||||
script={script}
|
||||
fileName={fileName}
|
||||
onChange={onChange}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
file = new File([testScript], 'samplescript.js', { type: 'text/javascript' });
|
||||
});
|
||||
|
||||
it('renders ScriptRecorderFields', () => {
|
||||
const { queryByText } = render(<WrappedComponent />);
|
||||
|
||||
expect(queryByText('Show script')).not.toBeInTheDocument();
|
||||
expect(queryByText('Remove script')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles uploading files', async () => {
|
||||
const { getByTestId } = render(<WrappedComponent />);
|
||||
|
||||
const uploader = getByTestId('syntheticsFleetScriptRecorderUploader');
|
||||
|
||||
fireEvent.change(uploader, {
|
||||
target: { files: [file] },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toBeCalledWith({ scriptText: testScript, fileName: 'samplescript.js' });
|
||||
});
|
||||
});
|
||||
|
||||
it('shows user errors for invalid file types', async () => {
|
||||
const { getByTestId, getByText } = render(<WrappedComponent />);
|
||||
file = new File(['journey(() => {})'], 'samplescript.js', { type: 'text/javascript' });
|
||||
|
||||
let uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(uploader, {
|
||||
target: { files: [file] },
|
||||
});
|
||||
|
||||
uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).not.toBeCalled();
|
||||
expect(
|
||||
getByText(
|
||||
'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows show script button when script is available', () => {
|
||||
const { getByText, queryByText } = render(<WrappedComponent script={testScript} />);
|
||||
|
||||
const showScriptBtn = getByText('Show script');
|
||||
|
||||
expect(queryByText(testScript)).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(showScriptBtn);
|
||||
|
||||
expect(getByText(testScript)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows show remove script button when script is available and isEditable is true', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<WrappedComponent script={testScript} isEditable={true} />
|
||||
);
|
||||
|
||||
const showScriptBtn = getByText('Show script');
|
||||
fireEvent.click(showScriptBtn);
|
||||
|
||||
expect(getByText(testScript)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByTestId('euiFlyoutCloseButton'));
|
||||
|
||||
const removeScriptBtn = getByText('Remove script');
|
||||
|
||||
fireEvent.click(removeScriptBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toBeCalledWith({ scriptText: '', fileName: '' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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, { useState, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
EuiCodeBlock,
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { Uploader } from './uploader';
|
||||
|
||||
interface Props {
|
||||
onChange: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void;
|
||||
script: string;
|
||||
fileName?: string;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
export function ScriptRecorderFields({ onChange, script, fileName, isEditable }: Props) {
|
||||
const [showScript, setShowScript] = useState(false);
|
||||
|
||||
const handleUpload = useCallback(
|
||||
({ scriptText, fileName: fileNameT }: { scriptText: string; fileName: string }) => {
|
||||
onChange({ scriptText, fileName: fileNameT });
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
{isEditable && script ? (
|
||||
<EuiFormRow aria-label="Testing script" fullWidth>
|
||||
<EuiText size="s">
|
||||
<strong>{fileName}</strong>
|
||||
</EuiText>
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
<Uploader onUpload={handleUpload} />
|
||||
)}
|
||||
{script && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => setShowScript(true)}
|
||||
iconType="editorCodeBlock"
|
||||
iconSide="right"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.zipUrl.showScriptLabel"
|
||||
defaultMessage="Show script"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isEditable && (
|
||||
<EuiButton
|
||||
onClick={() => onChange({ scriptText: '', fileName: '' })}
|
||||
iconType="trash"
|
||||
iconSide="right"
|
||||
color="danger"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.zipUrl.removeScriptLabel"
|
||||
defaultMessage="Remove script"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
{showScript && (
|
||||
<EuiFlyout
|
||||
ownFocus
|
||||
onClose={() => setShowScript(false)}
|
||||
aria-labelledby="syntheticsBrowserScriptBlockHeader"
|
||||
closeButtonAriaLabel={CLOSE_BUTTON_LABEL}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<span id="syntheticsBrowserScriptBlockHeader">
|
||||
{fileName || PLACEHOLDER_FILE_NAME}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<div style={{ height: '100%' }}>
|
||||
<EuiCodeBlock language="js" overflowHeight={'100%'} fontSize="m" isCopyable>
|
||||
{script}
|
||||
</EuiCodeBlock>
|
||||
</div>
|
||||
</EuiFlyout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PLACEHOLDER_FILE_NAME = i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.mockFileName',
|
||||
{
|
||||
defaultMessage: 'test_script.js',
|
||||
}
|
||||
);
|
||||
|
||||
const CLOSE_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Close script flyout',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 userEvent from '@testing-library/user-event';
|
||||
import { render } from '../../../utils/testing/rtl_helpers';
|
||||
import { SourceField } from './source_field';
|
||||
|
||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
|
||||
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
|
||||
htmlIdGenerator: () => () => `id-${Math.random()}`,
|
||||
}));
|
||||
|
||||
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 onChange = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
|
||||
describe('<ScriptRecorderFields />', () => {
|
||||
const WrappedComponent = ({
|
||||
script = '',
|
||||
fileName = '',
|
||||
type = 'recorder',
|
||||
isEdit = false,
|
||||
}: {
|
||||
isEditable?: boolean;
|
||||
script?: string;
|
||||
fileName?: string;
|
||||
type?: 'recorder' | 'inline';
|
||||
isEdit?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<SourceField
|
||||
value={{
|
||||
script,
|
||||
fileName,
|
||||
type,
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
isEditFlow={isEdit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders ScriptRecorderFields as the default tab', () => {
|
||||
const { getByText } = render(<WrappedComponent />);
|
||||
|
||||
expect(getByText('Select or drag and drop a .js file')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes to code editor when selected', async () => {
|
||||
const script = 'test script';
|
||||
const { getByTestId } = render(<WrappedComponent type="inline" script={script} />);
|
||||
expect(getByTestId('codeEditorContainer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays code editor by default in edit flow', async () => {
|
||||
const fileName = 'fileName';
|
||||
const script = 'test script';
|
||||
const { getByTestId } = render(
|
||||
<WrappedComponent fileName={fileName} type="recorder" script={script} isEdit={true} />
|
||||
);
|
||||
expect(getByTestId('codeEditorContainer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays filename of existing script in edit flow', async () => {
|
||||
const fileName = 'fileName';
|
||||
const script = 'test script';
|
||||
const { getByText } = render(
|
||||
<WrappedComponent fileName={fileName} type="recorder" script={script} isEdit={true} />
|
||||
);
|
||||
userEvent.click(getByText(/Upload new script/));
|
||||
expect(getByText(fileName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiTabbedContent,
|
||||
EuiFormRow,
|
||||
EuiBetaBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { CodeEditor } from './code_editor';
|
||||
import { ScriptRecorderFields } from './script_recorder_fields';
|
||||
import { ConfigKey, MonacoEditorLangId } from '../types';
|
||||
|
||||
enum SourceType {
|
||||
INLINE = 'syntheticsBrowserInlineConfig',
|
||||
SCRIPT_RECORDER = 'syntheticsBrowserScriptRecorderConfig',
|
||||
ZIP = 'syntheticsBrowserZipURLConfig',
|
||||
}
|
||||
|
||||
interface SourceConfig {
|
||||
script: string;
|
||||
type: 'recorder' | 'inline';
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onChange: (sourceConfig: SourceConfig) => void;
|
||||
onBlur: (field: ConfigKey) => void;
|
||||
value: SourceConfig;
|
||||
isEditFlow?: boolean;
|
||||
}
|
||||
|
||||
export const SourceField = ({ onChange, onBlur, value, isEditFlow = false }: Props) => {
|
||||
const [sourceType, setSourceType] = useState<SourceType>(
|
||||
value.type === 'inline' ? SourceType.INLINE : SourceType.SCRIPT_RECORDER
|
||||
);
|
||||
const [config, setConfig] = useState<SourceConfig>(value);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(config);
|
||||
}, [config, onChange]);
|
||||
|
||||
const allTabs = [
|
||||
{
|
||||
id: 'syntheticsBrowserScriptRecorderConfig',
|
||||
name: (
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isEditFlow ? (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.scriptRecorderEdit.label"
|
||||
defaultMessage="Upload new script"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.scriptRecorder.label"
|
||||
defaultMessage="Upload script"
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<StyledBetaBadgeWrapper grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalLabel',
|
||||
{
|
||||
defaultMessage: 'Tech preview',
|
||||
}
|
||||
)}
|
||||
iconType="beaker"
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Preview the quickest way to create Elastic Synthetics monitoring scripts with our Elastic Synthetics Recorder',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</StyledBetaBadgeWrapper>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': 'syntheticsSourceTab__scriptRecorder',
|
||||
content: (
|
||||
<ScriptRecorderFields
|
||||
onChange={({ scriptText, fileName }) => {
|
||||
setConfig((prevConfig) => ({
|
||||
...prevConfig,
|
||||
script: scriptText,
|
||||
type: 'recorder',
|
||||
fileName,
|
||||
}));
|
||||
}}
|
||||
script={config.script}
|
||||
isEditable={isEditFlow}
|
||||
fileName={config.fileName}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'syntheticsBrowserInlineConfig',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.addEditMonitor.scriptEditor.label"
|
||||
defaultMessage="Script editor"
|
||||
/>
|
||||
),
|
||||
'data-test-subj': `syntheticsSourceTab__inline`,
|
||||
content: (
|
||||
<EuiFormRow
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.addEditMonitor.scriptEditor.helpText"
|
||||
defaultMessage="Runs Synthetic test scripts that are defined inline."
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<CodeEditor
|
||||
ariaLabel={i18n.translate('xpack.synthetics.addEditMonitor.scriptEditor.ariaLabel', {
|
||||
defaultMessage: 'JavaScript code editor',
|
||||
})}
|
||||
id="javascript"
|
||||
languageId={MonacoEditorLangId.JAVASCRIPT}
|
||||
onChange={(code) => {
|
||||
setConfig((prevConfig) => ({ ...prevConfig, script: code }));
|
||||
onBlur(ConfigKey.SOURCE_INLINE);
|
||||
}}
|
||||
value={config.script}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.synthetics.addEditMonitor.scriptEditor.placeholder',
|
||||
{
|
||||
defaultMessage: '// Paste your Playwright script here...',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (isEditFlow) {
|
||||
allTabs.reverse();
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
tabs={allTabs}
|
||||
initialSelectedTab={
|
||||
isEditFlow
|
||||
? allTabs.find((tab) => tab.id === SourceType.INLINE)
|
||||
: allTabs.find((tab) => tab.id === sourceType)
|
||||
}
|
||||
autoFocus="selected"
|
||||
onTabClick={(tab) => {
|
||||
if (tab.id !== sourceType) {
|
||||
setConfig({
|
||||
script: '',
|
||||
type: tab.id === SourceType.INLINE ? 'inline' : 'recorder',
|
||||
fileName: '',
|
||||
});
|
||||
}
|
||||
setSourceType(tab.id as SourceType);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledBetaBadgeWrapper = styled(EuiFlexItem)`
|
||||
.euiToolTipAnchor {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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, { useState, useRef } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFormRow, EuiFilePicker } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
onUpload: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void;
|
||||
}
|
||||
|
||||
export function Uploader({ onUpload }: Props) {
|
||||
const fileReader = useRef<null | FileReader>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const filePickerRef = useRef<EuiFilePicker>(null);
|
||||
|
||||
const handleFileRead = (fileName: string) => {
|
||||
const content = fileReader?.current?.result as string;
|
||||
|
||||
if (content?.trim().slice(0, 4) !== 'step') {
|
||||
setError(PARSING_ERROR);
|
||||
filePickerRef.current?.removeFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
onUpload({ scriptText: content, fileName });
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleFileChosen = (files: FileList | null) => {
|
||||
if (!files || !files.length) {
|
||||
onUpload({ scriptText: '', fileName: '' });
|
||||
return;
|
||||
}
|
||||
if (files.length && !files[0].type.includes('javascript')) {
|
||||
setError(INVALID_FILE_ERROR);
|
||||
filePickerRef.current?.removeFiles();
|
||||
return;
|
||||
}
|
||||
fileReader.current = new FileReader();
|
||||
fileReader.current.onloadend = () => handleFileRead(files[0].name);
|
||||
fileReader.current.readAsText(files[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
isInvalid={Boolean(error)}
|
||||
error={error}
|
||||
aria-label={TESTING_SCRIPT_LABEL}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFilePicker
|
||||
id="syntheticsFleetScriptRecorderUploader"
|
||||
data-test-subj="syntheticsFleetScriptRecorderUploader"
|
||||
ref={filePickerRef}
|
||||
initialPromptText={PROMPT_TEXT}
|
||||
onChange={handleFileChosen}
|
||||
display={'large'}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
const TESTING_SCRIPT_LABEL = i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.fieldLabel',
|
||||
{
|
||||
defaultMessage: 'Testing script',
|
||||
}
|
||||
);
|
||||
|
||||
const PROMPT_TEXT = i18n.translate('xpack.synthetics.monitorConfig.uploader.label', {
|
||||
defaultMessage: 'Select or drag and drop a .js file',
|
||||
});
|
||||
|
||||
const INVALID_FILE_ERROR = i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.invalidFileError',
|
||||
{
|
||||
defaultMessage:
|
||||
'Invalid file type. Please upload a .js file generated by the Elastic Synthetics Recorder.',
|
||||
}
|
||||
);
|
||||
|
||||
const PARSING_ERROR = i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.parsingError',
|
||||
{
|
||||
defaultMessage:
|
||||
'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiFormRowProps } from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
UseFormReturn,
|
||||
ControllerRenderProps,
|
||||
ControllerFieldState,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { useKibanaSpace, useIsEditFlow } from '../hooks';
|
||||
import { selectServiceLocationsState } from '../../../state';
|
||||
import { FieldMeta } from '../types';
|
||||
|
||||
type Props = FieldMeta & {
|
||||
component: React.ComponentType<any>;
|
||||
field: ControllerRenderProps;
|
||||
fieldState: ControllerFieldState;
|
||||
formRowProps: Partial<EuiFormRowProps>;
|
||||
error: React.ReactNode;
|
||||
dependenciesValues: unknown[];
|
||||
dependenciesFieldMeta: Record<string, ControllerFieldState>;
|
||||
};
|
||||
|
||||
const setFieldValue = (key: string, setValue: UseFormReturn['setValue']) => (value: any) => {
|
||||
setValue(key, value);
|
||||
};
|
||||
|
||||
export const ControlledField = ({
|
||||
component: FieldComponent,
|
||||
props,
|
||||
fieldKey,
|
||||
shouldUseSetValue,
|
||||
field,
|
||||
formRowProps,
|
||||
fieldState,
|
||||
customHook,
|
||||
error,
|
||||
dependenciesValues,
|
||||
dependenciesFieldMeta,
|
||||
}: Props) => {
|
||||
const { setValue, reset, formState } = useFormContext();
|
||||
const noop = () => {};
|
||||
let hook: Function = noop;
|
||||
let hookProps;
|
||||
const { locations } = useSelector(selectServiceLocationsState);
|
||||
const { space } = useKibanaSpace();
|
||||
const isEdit = useIsEditFlow();
|
||||
if (customHook) {
|
||||
hookProps = customHook(field.value);
|
||||
hook = hookProps.func;
|
||||
}
|
||||
const { [hookProps?.fieldKey as string]: hookResult } = hook(hookProps?.params) || {};
|
||||
const onChange = shouldUseSetValue ? setFieldValue(fieldKey, setValue) : field.onChange;
|
||||
const generatedProps = props
|
||||
? props({
|
||||
field,
|
||||
setValue,
|
||||
reset,
|
||||
locations,
|
||||
dependencies: dependenciesValues,
|
||||
dependenciesFieldMeta,
|
||||
space: space?.id,
|
||||
isEdit,
|
||||
formState,
|
||||
})
|
||||
: {};
|
||||
const isInvalid = hookResult || Boolean(fieldState.error);
|
||||
const hookError = hookResult ? hookProps?.error : undefined;
|
||||
return (
|
||||
<EuiFormRow
|
||||
{...formRowProps}
|
||||
isInvalid={isInvalid}
|
||||
error={isInvalid ? hookError || fieldState.error?.message || error : undefined}
|
||||
>
|
||||
<FieldComponent
|
||||
{...field}
|
||||
checked={field.value}
|
||||
defaultValue={field.value}
|
||||
onChange={onChange}
|
||||
{...generatedProps}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 { ConfigKey, DataStream, FormMonitorType, SyntheticsMonitor } from '../types';
|
||||
import { DEFAULT_FIELDS } from '../constants';
|
||||
import { formatDefaultFormValues } from './defaults';
|
||||
|
||||
describe('defaults', () => {
|
||||
const testScript = 'testScript';
|
||||
const monitorValues = {
|
||||
__ui: {
|
||||
script_source: {
|
||||
file_name: '',
|
||||
is_generated_script: false,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
'filter_journeys.match': '',
|
||||
'filter_journeys.tags': [],
|
||||
form_monitor_type: 'multistep',
|
||||
ignore_https_errors: false,
|
||||
journey_id: '',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
name: 'Browser monitor',
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
params: '',
|
||||
playwright_options: '',
|
||||
playwright_text_assertion: '',
|
||||
project_id: '',
|
||||
schedule: {
|
||||
number: '10',
|
||||
unit: 'm',
|
||||
},
|
||||
screenshots: 'on',
|
||||
'service.name': '',
|
||||
'source.inline.script': testScript,
|
||||
'source.project.content': '',
|
||||
'source.zip_url.folder': '',
|
||||
'source.zip_url.password': '',
|
||||
'source.zip_url.proxy_url': '',
|
||||
'source.zip_url.ssl.certificate': undefined,
|
||||
'source.zip_url.ssl.certificate_authorities': undefined,
|
||||
'source.zip_url.ssl.key': undefined,
|
||||
'source.zip_url.ssl.key_passphrase': undefined,
|
||||
'source.zip_url.ssl.supported_protocols': undefined,
|
||||
'source.zip_url.ssl.verification_mode': undefined,
|
||||
'source.zip_url.url': '',
|
||||
'source.zip_url.username': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
'ssl.verification_mode': 'full',
|
||||
synthetics_args: [],
|
||||
tags: [],
|
||||
'throttling.config': '5d/3u/20l',
|
||||
'throttling.download_speed': '5',
|
||||
'throttling.is_enabled': true,
|
||||
'throttling.latency': '20',
|
||||
'throttling.upload_speed': '3',
|
||||
timeout: '16',
|
||||
type: 'browser',
|
||||
'url.port': null,
|
||||
urls: '',
|
||||
} as SyntheticsMonitor;
|
||||
|
||||
it('correctly formats monitor type to form type', () => {
|
||||
expect(formatDefaultFormValues(monitorValues)).toEqual({
|
||||
__ui: {
|
||||
script_source: {
|
||||
file_name: '',
|
||||
is_generated_script: false,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
'filter_journeys.match': '',
|
||||
'filter_journeys.tags': [],
|
||||
form_monitor_type: 'multistep',
|
||||
ignore_https_errors: false,
|
||||
journey_id: '',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
name: 'Browser monitor',
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
params: '',
|
||||
playwright_options: '',
|
||||
playwright_text_assertion: '',
|
||||
project_id: '',
|
||||
schedule: {
|
||||
number: '10',
|
||||
unit: 'm',
|
||||
},
|
||||
screenshots: 'on',
|
||||
'service.name': '',
|
||||
'source.inline': {
|
||||
fileName: '',
|
||||
script: 'testScript',
|
||||
type: 'inline',
|
||||
},
|
||||
'source.inline.script': 'testScript',
|
||||
'source.project.content': '',
|
||||
'source.zip_url.folder': '',
|
||||
'source.zip_url.password': '',
|
||||
'source.zip_url.proxy_url': '',
|
||||
'source.zip_url.ssl.certificate': undefined,
|
||||
'source.zip_url.ssl.certificate_authorities': undefined,
|
||||
'source.zip_url.ssl.key': undefined,
|
||||
'source.zip_url.ssl.key_passphrase': undefined,
|
||||
'source.zip_url.ssl.supported_protocols': undefined,
|
||||
'source.zip_url.ssl.verification_mode': undefined,
|
||||
'source.zip_url.url': '',
|
||||
'source.zip_url.username': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
'ssl.verification_mode': 'full',
|
||||
synthetics_args: [],
|
||||
tags: [],
|
||||
'throttling.config': '5d/3u/20l',
|
||||
'throttling.download_speed': '5',
|
||||
'throttling.is_enabled': true,
|
||||
'throttling.latency': '20',
|
||||
'throttling.upload_speed': '3',
|
||||
timeout: '16',
|
||||
type: 'browser',
|
||||
'url.port': null,
|
||||
urls: '',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[DataStream.HTTP, 'testCA'],
|
||||
[DataStream.HTTP, ''],
|
||||
[DataStream.TCP, 'testCA'],
|
||||
[DataStream.TCP, ''],
|
||||
])('correctly formats isTLSEnabled', (formType, testCA) => {
|
||||
const monitor = {
|
||||
...DEFAULT_FIELDS[formType as DataStream],
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: formType as unknown as FormMonitorType,
|
||||
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: testCA,
|
||||
};
|
||||
expect(formatDefaultFormValues(monitor)).toEqual({
|
||||
...monitor,
|
||||
isTLSEnabled: Boolean(testCA),
|
||||
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: testCA,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[DataStream.HTTP, FormMonitorType.HTTP],
|
||||
[DataStream.TCP, FormMonitorType.TCP],
|
||||
[DataStream.ICMP, FormMonitorType.ICMP],
|
||||
[DataStream.BROWSER, FormMonitorType.MULTISTEP],
|
||||
])(
|
||||
'correctly formats legacy uptime monitors to include ConfigKey.FORM_MONITOR_TYPE',
|
||||
(dataStream, formType) => {
|
||||
const monitor = {
|
||||
...DEFAULT_FIELDS[dataStream],
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: undefined,
|
||||
};
|
||||
expect(formatDefaultFormValues(monitor as unknown as SyntheticsMonitor)).toEqual(
|
||||
expect.objectContaining({
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: formType,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { DEFAULT_FIELDS, DEFAULT_TLS_FIELDS } from '../constants';
|
||||
import {
|
||||
ConfigKey,
|
||||
DataStream,
|
||||
FormMonitorType,
|
||||
SyntheticsMonitor,
|
||||
BrowserFields,
|
||||
TLSFields,
|
||||
} from '../types';
|
||||
|
||||
export const getDefaultFormFields = (
|
||||
spaceId: string = 'default'
|
||||
): Record<FormMonitorType, Record<string, any>> => {
|
||||
return {
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
...DEFAULT_FIELDS[DataStream.BROWSER],
|
||||
'source.inline': {
|
||||
type: 'recorder',
|
||||
script: '',
|
||||
fileName: '',
|
||||
},
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
[ConfigKey.NAMESPACE]: spaceId,
|
||||
},
|
||||
[FormMonitorType.SINGLE]: {
|
||||
...DEFAULT_FIELDS[DataStream.BROWSER],
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.SINGLE,
|
||||
[ConfigKey.NAMESPACE]: spaceId,
|
||||
},
|
||||
[FormMonitorType.HTTP]: {
|
||||
...DEFAULT_FIELDS[DataStream.HTTP],
|
||||
isTLSEnabled: false,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
|
||||
[ConfigKey.NAMESPACE]: spaceId,
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
...DEFAULT_FIELDS[DataStream.TCP],
|
||||
isTLSEnabled: false,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
|
||||
[ConfigKey.NAMESPACE]: spaceId,
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
...DEFAULT_FIELDS[DataStream.ICMP],
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP,
|
||||
[ConfigKey.NAMESPACE]: spaceId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const formatDefaultFormValues = (monitor?: SyntheticsMonitor) => {
|
||||
if (!monitor) return undefined;
|
||||
|
||||
let formMonitorType = monitor[ConfigKey.FORM_MONITOR_TYPE];
|
||||
const monitorType = monitor[ConfigKey.MONITOR_TYPE];
|
||||
const monitorWithFormMonitorType = {
|
||||
...monitor,
|
||||
};
|
||||
|
||||
// handle default monitor types from Uptime, which don't contain `ConfigKey.FORM_MONITOR_TYPE`
|
||||
if (!formMonitorType) {
|
||||
formMonitorType =
|
||||
monitorType === DataStream.BROWSER
|
||||
? FormMonitorType.MULTISTEP
|
||||
: (monitorType as Omit<DataStream, DataStream.BROWSER> as FormMonitorType);
|
||||
monitorWithFormMonitorType[ConfigKey.FORM_MONITOR_TYPE] = formMonitorType;
|
||||
}
|
||||
|
||||
switch (formMonitorType) {
|
||||
case FormMonitorType.MULTISTEP:
|
||||
const browserMonitor = monitor as BrowserFields;
|
||||
return {
|
||||
...monitorWithFormMonitorType,
|
||||
'source.inline': {
|
||||
type: browserMonitor[ConfigKey.METADATA]?.script_source?.is_generated_script
|
||||
? 'recorder'
|
||||
: 'inline',
|
||||
script: browserMonitor[ConfigKey.SOURCE_INLINE],
|
||||
fileName: browserMonitor[ConfigKey.METADATA]?.script_source?.file_name,
|
||||
},
|
||||
};
|
||||
case FormMonitorType.SINGLE:
|
||||
case FormMonitorType.ICMP:
|
||||
return {
|
||||
...monitorWithFormMonitorType,
|
||||
};
|
||||
case FormMonitorType.HTTP:
|
||||
case FormMonitorType.TCP:
|
||||
return {
|
||||
...monitorWithFormMonitorType,
|
||||
isTLSEnabled: isCustomTLSEnabled(monitor),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomTLSEnabled = (monitor: SyntheticsMonitor) => {
|
||||
const sslKeys = Object.keys(monitor).filter((key) => key.includes('ssl')) as unknown as Array<
|
||||
keyof TLSFields
|
||||
>;
|
||||
const sslValues: Record<string, unknown> = {};
|
||||
sslKeys.map((key) => (sslValues[key] = (monitor as TLSFields)[key]));
|
||||
return !isEqual(sslValues, DEFAULT_TLS_FIELDS);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { render } from '../../../utils/testing/rtl_helpers';
|
||||
import * as formContext from 'react-hook-form';
|
||||
import { Disclaimer } from './disclaimer';
|
||||
import { ServiceLocations } from '../types';
|
||||
|
||||
export const mockLocation = {
|
||||
label: 'US Central',
|
||||
id: 'us_central',
|
||||
geo: {
|
||||
lat: 1,
|
||||
lon: 1,
|
||||
},
|
||||
url: 'url',
|
||||
isServiceManaged: true,
|
||||
};
|
||||
describe('<Disclaimer />', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(formContext, 'useFormContext').mockReturnValue({
|
||||
watch: () => [[mockLocation] as ServiceLocations],
|
||||
} as unknown as formContext.UseFormReturn);
|
||||
});
|
||||
|
||||
it('shows disclaimer when ', () => {
|
||||
const { getByText } = render(<Disclaimer />);
|
||||
|
||||
expect(getByText(/You consent/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show disclaimer when locations are not service managed', () => {
|
||||
jest.spyOn(formContext, 'useFormContext').mockReturnValue({
|
||||
watch: () => [[{ ...mockLocation, isServiceManaged: false }] as ServiceLocations],
|
||||
} as unknown as formContext.UseFormReturn);
|
||||
const { queryByText } = render(<Disclaimer />);
|
||||
|
||||
expect(queryByText(/You consent/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { ConfigKey, MonitorServiceLocation } from '../types';
|
||||
|
||||
export const Disclaimer: React.FC = () => {
|
||||
const { watch } = useFormContext();
|
||||
const [locations]: [locations: MonitorServiceLocation[]] = watch([ConfigKey.LOCATIONS]);
|
||||
|
||||
const includesServiceLocation = locations.find((location) => location.isServiceManaged === true);
|
||||
|
||||
return includesServiceLocation ? (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.locations.disclaimer', {
|
||||
defaultMessage:
|
||||
'You consent to the transfer of testing instructions and the output of such instructions (including any data shown therein) to your selected testing location, on infrastructure provided by a cloud service provider chosen by Elastic.',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
) : null;
|
||||
};
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Controller, useFormContext, FieldError, ControllerFieldState } from 'react-hook-form';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { selectServiceLocationsState } from '../../../state';
|
||||
import { useKibanaSpace, useIsEditFlow } from '../hooks';
|
||||
import { ControlledField } from './controlled_field';
|
||||
import { FieldMeta } from '../types';
|
||||
|
||||
type Props = FieldMeta & { fieldError?: FieldError };
|
||||
|
||||
export const Field = memo<Props>(
|
||||
({
|
||||
component: Component,
|
||||
helpText,
|
||||
label,
|
||||
ariaLabel,
|
||||
props,
|
||||
fieldKey,
|
||||
controlled,
|
||||
showWhen,
|
||||
shouldUseSetValue,
|
||||
required,
|
||||
validation,
|
||||
error,
|
||||
fieldError,
|
||||
dependencies,
|
||||
customHook,
|
||||
}: Props) => {
|
||||
const { register, watch, control, setValue, reset, getFieldState, formState } =
|
||||
useFormContext();
|
||||
const { locations } = useSelector(selectServiceLocationsState);
|
||||
const { space } = useKibanaSpace();
|
||||
const isEdit = useIsEditFlow();
|
||||
const [dependenciesFieldMeta, setDependenciesFieldMeta] = useState<
|
||||
Record<string, ControllerFieldState>
|
||||
>({});
|
||||
let show = true;
|
||||
let dependenciesValues: unknown[] = [];
|
||||
if (showWhen) {
|
||||
const [showKey, expectedValue] = showWhen;
|
||||
const [actualValue] = watch([showKey]);
|
||||
show = actualValue === expectedValue;
|
||||
}
|
||||
if (dependencies) {
|
||||
dependenciesValues = watch(dependencies);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (dependencies) {
|
||||
dependencies.forEach((dependency) => {
|
||||
setDependenciesFieldMeta((prevState) => ({
|
||||
...prevState,
|
||||
[dependency]: getFieldState(dependency),
|
||||
}));
|
||||
});
|
||||
}
|
||||
// run effect when dependencies values change, to get the most up to date meta state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(dependenciesValues || []), dependencies, getFieldState]);
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formRowProps = {
|
||||
label,
|
||||
'aria-label': ariaLabel,
|
||||
helpText,
|
||||
fullWidth: true,
|
||||
};
|
||||
|
||||
return controlled ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={fieldKey}
|
||||
rules={{
|
||||
required,
|
||||
...(validation ? validation(dependenciesValues) : {}),
|
||||
}}
|
||||
render={({ field, fieldState: fieldStateT }) => {
|
||||
return (
|
||||
<ControlledField
|
||||
field={field}
|
||||
component={Component}
|
||||
props={props}
|
||||
shouldUseSetValue={shouldUseSetValue}
|
||||
fieldKey={fieldKey}
|
||||
customHook={customHook}
|
||||
formRowProps={formRowProps}
|
||||
fieldState={fieldStateT}
|
||||
error={error}
|
||||
dependenciesValues={dependenciesValues}
|
||||
dependenciesFieldMeta={dependenciesFieldMeta}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EuiFormRow
|
||||
{...formRowProps}
|
||||
isInvalid={Boolean(fieldError)}
|
||||
error={fieldError?.message || error}
|
||||
>
|
||||
<Component
|
||||
{...register(fieldKey, {
|
||||
required,
|
||||
...(validation ? validation(dependenciesValues) : {}),
|
||||
})}
|
||||
{...(props
|
||||
? props({
|
||||
field: undefined,
|
||||
formState,
|
||||
setValue,
|
||||
reset,
|
||||
locations,
|
||||
dependencies: dependenciesValues,
|
||||
dependenciesFieldMeta,
|
||||
space: space?.id,
|
||||
isEdit,
|
||||
})
|
||||
: {})}
|
||||
isInvalid={Boolean(fieldError)}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,980 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { UseFormReturn, ControllerRenderProps, FormState } from 'react-hook-form';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
EuiCheckbox,
|
||||
EuiCode,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiComboBoxProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFieldText,
|
||||
EuiFieldNumber,
|
||||
EuiFieldPassword,
|
||||
EuiSelect,
|
||||
EuiSuperSelect,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiTextArea,
|
||||
} from '@elastic/eui';
|
||||
import { useMonitorName } from '../hooks/use_monitor_name';
|
||||
import { MonitorTypeRadioGroup } from '../fields/monitor_type_radio_group';
|
||||
import {
|
||||
ConfigKey,
|
||||
DataStream,
|
||||
FormMonitorType,
|
||||
HTTPMethod,
|
||||
MonitorFields,
|
||||
MonitorServiceLocations,
|
||||
ScreenshotOption,
|
||||
ServiceLocations,
|
||||
SyntheticsMonitor,
|
||||
TLSVersion,
|
||||
VerificationMode,
|
||||
FieldMeta,
|
||||
} from '../types';
|
||||
import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../constants';
|
||||
import { HeaderField } from '../fields/header_field';
|
||||
import { RequestBodyField } from '../fields/request_body_field';
|
||||
import { ResponseBodyIndexField } from '../fields/index_response_body_field';
|
||||
import { ComboBox } from '../fields/combo_box';
|
||||
import { SourceField } from '../fields/source_field';
|
||||
import { getDefaultFormFields } from './defaults';
|
||||
import { validate, validateHeaders, WHOLE_NUMBERS_ONLY, FLOATS_ONLY } from './validation';
|
||||
|
||||
const getScheduleContent = (value: number) => {
|
||||
if (value > 60) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.schedule.label', {
|
||||
defaultMessage: 'Every {value, number} {value, plural, one {hour} other {hours}}',
|
||||
values: {
|
||||
value: value / 60,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.schedule.minutes.label', {
|
||||
defaultMessage: 'Every {value, number} {value, plural, one {minute} other {minutes}}',
|
||||
values: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getScheduleConfig = (schedules: number[]) => {
|
||||
return schedules.map((value) => ({
|
||||
value: `${value}`,
|
||||
text: getScheduleContent(value),
|
||||
}));
|
||||
};
|
||||
|
||||
const BROWSER_SCHEDULES = getScheduleConfig([3, 5, 10, 15, 30, 60, 120, 240]);
|
||||
|
||||
const LIGHTWEIGHT_SCHEDULES = getScheduleConfig([1, 3, 5, 10, 15, 30, 60]);
|
||||
|
||||
export const MONITOR_TYPE_CONFIG = {
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
id: 'syntheticsMonitorTypeMultistep',
|
||||
'data-test-subj': 'syntheticsMonitorTypeMultistep',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.multiStep.label', {
|
||||
defaultMessage: 'Multistep',
|
||||
}),
|
||||
value: FormMonitorType.MULTISTEP,
|
||||
descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.multiStep.title', {
|
||||
defaultMessage: 'Multistep Browser Journey',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.monitorType.multiStep.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Navigate through multiple steps or pages to test key user flows from a real browser.',
|
||||
}
|
||||
),
|
||||
link: '#',
|
||||
icon: 'videoPlayer',
|
||||
beta: true,
|
||||
},
|
||||
[FormMonitorType.SINGLE]: {
|
||||
id: 'syntheticsMonitorTypeSingle',
|
||||
'data-test-subj': 'syntheticsMonitorTypeSingle',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.singlePage.label', {
|
||||
defaultMessage: 'Single Page',
|
||||
}),
|
||||
value: FormMonitorType.SINGLE,
|
||||
descriptionTitle: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.monitorType.singlePage.title',
|
||||
{
|
||||
defaultMessage: 'Single Page Browser Test',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.monitorType.singlePage.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Test a single page load including all objects on the page from a real web browser.',
|
||||
}
|
||||
),
|
||||
link: '#',
|
||||
icon: 'videoPlayer',
|
||||
beta: true,
|
||||
},
|
||||
[FormMonitorType.HTTP]: {
|
||||
id: 'syntheticsMonitorTypeHTTP',
|
||||
'data-test-subj': 'syntheticsMonitorTypeHTTP',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.http.label', {
|
||||
defaultMessage: 'HTTP Ping',
|
||||
}),
|
||||
value: FormMonitorType.HTTP,
|
||||
descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.http.title', {
|
||||
defaultMessage: 'HTTP Ping',
|
||||
}),
|
||||
description: i18n.translate('xpack.synthetics.monitorConfig.monitorType.http.description', {
|
||||
defaultMessage:
|
||||
'A lightweight API check to validate the availability of a web service or endpoint.',
|
||||
}),
|
||||
link: '#',
|
||||
icon: 'online',
|
||||
beta: false,
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
id: 'syntheticsMonitorTypeTCP',
|
||||
'data-test-subj': 'syntheticsMonitorTypeTCP',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.tcp.label', {
|
||||
defaultMessage: 'TCP Ping',
|
||||
}),
|
||||
value: FormMonitorType.TCP,
|
||||
descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.tcp.title', {
|
||||
defaultMessage: 'TCP Ping',
|
||||
}),
|
||||
description: i18n.translate('xpack.synthetics.monitorConfig.monitorType.tcp.description', {
|
||||
defaultMessage:
|
||||
'A lightweight API check to validate the availability of a web service or endpoint.',
|
||||
}),
|
||||
link: '#',
|
||||
icon: 'online',
|
||||
beta: false,
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
id: 'syntheticsMonitorTypeICMP',
|
||||
'data-test-subj': 'syntheticsMonitorTypeICMP',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.monitorType.icmp.label', {
|
||||
defaultMessage: 'ICMP Ping',
|
||||
}),
|
||||
value: FormMonitorType.ICMP,
|
||||
descriptionTitle: i18n.translate('xpack.synthetics.monitorConfig.monitorType.icmp.title', {
|
||||
defaultMessage: 'ICMP Ping',
|
||||
}),
|
||||
description: i18n.translate('xpack.synthetics.monitorConfig.monitorType.icmp.description', {
|
||||
defaultMessage:
|
||||
'A lightweight API check to validate the availability of a web service or endpoint.',
|
||||
}),
|
||||
link: '#',
|
||||
icon: 'online',
|
||||
beta: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const FIELD: Record<string, FieldMeta> = {
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: {
|
||||
fieldKey: ConfigKey.FORM_MONITOR_TYPE,
|
||||
required: true,
|
||||
component: MonitorTypeRadioGroup,
|
||||
ariaLabel: i18n.translate('xpack.synthetics.monitorConfig.monitorType.label', {
|
||||
defaultMessage: 'Monitor type',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field, reset, space }) => ({
|
||||
onChange: (_: string, monitorType: FormMonitorType) => {
|
||||
const defaultFields = getDefaultFormFields(space)[monitorType];
|
||||
reset(defaultFields);
|
||||
},
|
||||
selectedOption: field?.value,
|
||||
options: Object.values(MONITOR_TYPE_CONFIG),
|
||||
}),
|
||||
validation: () => ({
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
[`${ConfigKey.URLS}__single`]: {
|
||||
fieldKey: ConfigKey.URLS,
|
||||
required: true,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.urlsSingle.label', {
|
||||
defaultMessage: 'Website URL',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.urlsSingle.helpText', {
|
||||
defaultMessage: 'For example, https://www.elastic.co.',
|
||||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => {
|
||||
return {
|
||||
'data-test-subj': 'syntheticsMonitorConfigURL',
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.URLS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
[`${ConfigKey.URLS}__http`]: {
|
||||
fieldKey: ConfigKey.URLS,
|
||||
required: true,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.urls.label', {
|
||||
defaultMessage: 'URL',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.urls.helpText', {
|
||||
defaultMessage: 'For example, your service endpoint.',
|
||||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => {
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.URLS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
}
|
||||
},
|
||||
'data-test-subj': 'syntheticsMonitorConfigURL',
|
||||
};
|
||||
},
|
||||
},
|
||||
[`${ConfigKey.HOSTS}__tcp`]: {
|
||||
fieldKey: ConfigKey.HOSTS,
|
||||
required: true,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.hostsTCP.label', {
|
||||
defaultMessage: 'Host:Port',
|
||||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => {
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.HOSTS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
}
|
||||
},
|
||||
'data-test-subj': 'syntheticsMonitorConfigHost',
|
||||
};
|
||||
},
|
||||
},
|
||||
[`${ConfigKey.HOSTS}__icmp`]: {
|
||||
fieldKey: ConfigKey.HOSTS,
|
||||
required: true,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.hostsICMP.label', {
|
||||
defaultMessage: 'Host',
|
||||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }) => {
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.HOSTS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
}
|
||||
},
|
||||
'data-test-subj': 'syntheticsMonitorConfigHost',
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.NAME]: {
|
||||
fieldKey: ConfigKey.NAME,
|
||||
required: true,
|
||||
component: EuiFieldText,
|
||||
controlled: true,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.name.label', {
|
||||
defaultMessage: 'Monitor name',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.name.helpText', {
|
||||
defaultMessage: 'Choose a name to help identify this monitor in the future.',
|
||||
}),
|
||||
dependencies: [ConfigKey.URLS, ConfigKey.HOSTS],
|
||||
customHook: (value: unknown) => ({
|
||||
fieldKey: 'nameAlreadyExists',
|
||||
func: useMonitorName,
|
||||
params: { search: value as string },
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.name.existsError', {
|
||||
defaultMessage: 'Monitor name already exists',
|
||||
}),
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: {
|
||||
notEmpty: (value) => Boolean(value.trim()),
|
||||
},
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.name.error', {
|
||||
defaultMessage: 'Monitor name is required',
|
||||
}),
|
||||
props: () => ({
|
||||
'data-test-subj': 'syntheticsMonitorConfigName',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.SCHEDULE]: {
|
||||
fieldKey: `${ConfigKey.SCHEDULE}.number`,
|
||||
required: true,
|
||||
component: EuiSelect,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.frequency.label', {
|
||||
defaultMessage: 'Frequency',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.frequency.helpText', {
|
||||
defaultMessage:
|
||||
'How often do you want to run this test? Higher frequencies will increase your total cost.',
|
||||
}),
|
||||
dependencies: [ConfigKey.MONITOR_TYPE],
|
||||
props: ({ dependencies }) => {
|
||||
const [monitorType] = dependencies;
|
||||
return {
|
||||
'data-test-subj': 'syntheticsMonitorConfigSchedule',
|
||||
options: monitorType === DataStream.BROWSER ? BROWSER_SCHEDULES : LIGHTWEIGHT_SCHEDULES,
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.LOCATIONS]: {
|
||||
fieldKey: ConfigKey.LOCATIONS,
|
||||
required: true,
|
||||
controlled: true,
|
||||
component: EuiComboBox as React.ComponentType<EuiComboBoxProps<string>>,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.locations.label', {
|
||||
defaultMessage: 'Locations',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.locations.helpText', {
|
||||
defaultMessage:
|
||||
'Where do you want to run this test from? Additional locations will increase your total cost.',
|
||||
}),
|
||||
props: ({
|
||||
field,
|
||||
setValue,
|
||||
locations,
|
||||
formState,
|
||||
}: {
|
||||
field?: ControllerRenderProps;
|
||||
setValue: UseFormReturn['setValue'];
|
||||
locations: ServiceLocations;
|
||||
formState: FormState<SyntheticsMonitor>;
|
||||
}) => {
|
||||
return {
|
||||
options: Object.values(locations).map((location) => ({
|
||||
label: locations?.find((loc) => location.id === loc.id)?.label,
|
||||
id: location.id,
|
||||
isServiceManaged: location.isServiceManaged,
|
||||
})),
|
||||
selectedOptions: Object.values(field?.value as ServiceLocations).map((location) => ({
|
||||
label: locations?.find((loc) => location.id === loc.id)?.label,
|
||||
id: location.id,
|
||||
isServiceManaged: location.isServiceManaged,
|
||||
})),
|
||||
'data-test-subj': 'syntheticsMonitorConfigLocations',
|
||||
onChange: (updatedValues: ServiceLocations) => {
|
||||
setValue(
|
||||
ConfigKey.LOCATIONS,
|
||||
updatedValues.map((location) => ({
|
||||
id: location.id,
|
||||
isServiceManaged: location.isServiceManaged,
|
||||
})) as MonitorServiceLocations,
|
||||
{ shouldValidate: Boolean(formState.submitCount > 0) }
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.TAGS]: {
|
||||
fieldKey: ConfigKey.TAGS,
|
||||
component: ComboBox,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.tags.label', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.tags.helpText', {
|
||||
defaultMessage:
|
||||
'A list of tags that will be sent with each monitor event. Useful for searching and segmenting data.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
selectedOptions: field?.value,
|
||||
}),
|
||||
},
|
||||
[ConfigKey.TIMEOUT]: {
|
||||
fieldKey: ConfigKey.TIMEOUT,
|
||||
component: EuiFieldNumber,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.timeout.label', {
|
||||
defaultMessage: 'Timeout in seconds',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.timeout.helpText', {
|
||||
defaultMessage: 'The total time allowed for testing the connection and exchanging data.',
|
||||
}),
|
||||
props: () => ({
|
||||
min: 1,
|
||||
step: 'any',
|
||||
}),
|
||||
dependencies: [ConfigKey.SCHEDULE],
|
||||
validation: ([schedule]) => {
|
||||
return {
|
||||
validate: (value) => {
|
||||
switch (true) {
|
||||
case value < 0:
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.timeout.greaterThan0Error', {
|
||||
defaultMessage: 'Timeout must be greater than or equal to 0.',
|
||||
});
|
||||
case value > parseFloat((schedule as MonitorFields[ConfigKey.SCHEDULE]).number) * 60:
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.timeout.scheduleError', {
|
||||
defaultMessage: 'Timemout must be less than the monitor frequency.',
|
||||
});
|
||||
case !Boolean(`${value}`.match(FLOATS_ONLY)):
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.timeout.formatError', {
|
||||
defaultMessage: 'Timeout is invalid.',
|
||||
});
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.APM_SERVICE_NAME]: {
|
||||
fieldKey: ConfigKey.APM_SERVICE_NAME,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.apmServiceName.label', {
|
||||
defaultMessage: 'APM service name',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.apmServiceName.helpText', {
|
||||
defaultMessage:
|
||||
'Corrseponds to the service.name ECS field from APM. Set this to enable integrations between APM and Synthetics data.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
selectedOptions: field?.value,
|
||||
'data-test-subj': 'syntheticsMonitorConfigAPMServiceName',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.NAMESPACE]: {
|
||||
fieldKey: ConfigKey.NAMESPACE,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.namespace.label', {
|
||||
defaultMessage: 'Data stream namespace',
|
||||
}),
|
||||
helpText: (
|
||||
<span>
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.namespace.helpText', {
|
||||
defaultMessage:
|
||||
"Change the default namespace. This setting changes the name of the monitor's data stream. ",
|
||||
})}
|
||||
<EuiLink href="#" target="_blank">
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.namespace.learnMore', {
|
||||
defaultMessage: 'Learn more',
|
||||
})}
|
||||
</EuiLink>
|
||||
</span>
|
||||
),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
selectedOptions: field,
|
||||
}),
|
||||
},
|
||||
[ConfigKey.MAX_REDIRECTS]: {
|
||||
fieldKey: ConfigKey.MAX_REDIRECTS,
|
||||
component: EuiFieldNumber,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.label', {
|
||||
defaultMessage: 'Max redirects',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.helpText', {
|
||||
defaultMessage: 'The total number of redirects to follow.',
|
||||
}),
|
||||
props: () => ({
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
}),
|
||||
validation: () => ({
|
||||
min: 0,
|
||||
pattern: WHOLE_NUMBERS_ONLY,
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.error', {
|
||||
defaultMessage: 'Max redirects is invalid.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.WAIT]: {
|
||||
fieldKey: ConfigKey.WAIT,
|
||||
component: EuiFieldNumber,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.wait.label', {
|
||||
defaultMessage: 'Wait',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.wait.helpText', {
|
||||
defaultMessage:
|
||||
'The duration to wait before emitting another ICMP Echo Request if no response is received.',
|
||||
}),
|
||||
props: () => ({
|
||||
min: 1,
|
||||
step: 1,
|
||||
}),
|
||||
validation: () => ({
|
||||
min: 1,
|
||||
pattern: WHOLE_NUMBERS_ONLY,
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.wait.error', {
|
||||
defaultMessage: 'Wait duration is invalid.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.USERNAME]: {
|
||||
fieldKey: ConfigKey.USERNAME,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.username.label', {
|
||||
defaultMessage: 'Username',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.username.helpText', {
|
||||
defaultMessage: 'Username for authenticating with the server.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.PASSWORD]: {
|
||||
fieldKey: ConfigKey.PASSWORD,
|
||||
component: EuiFieldPassword,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.password.label', {
|
||||
defaultMessage: 'Password',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.password.helpText', {
|
||||
defaultMessage: 'Password for authenticating with the server.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.PROXY_URL]: {
|
||||
fieldKey: ConfigKey.PROXY_URL,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.proxyUrl.label', {
|
||||
defaultMessage: 'Proxy URL',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.proxyUrl.helpText', {
|
||||
defaultMessage: 'HTTP proxy URL',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.REQUEST_METHOD_CHECK]: {
|
||||
fieldKey: ConfigKey.REQUEST_METHOD_CHECK,
|
||||
component: EuiSelect,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.requestMethod.label', {
|
||||
defaultMessage: 'Request method',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.requestMethod.helpText', {
|
||||
defaultMessage: 'The HTTP method to use.',
|
||||
}),
|
||||
props: () => ({
|
||||
options: Object.keys(HTTPMethod).map((method) => ({
|
||||
value: method,
|
||||
text: method,
|
||||
})),
|
||||
}),
|
||||
},
|
||||
[ConfigKey.REQUEST_HEADERS_CHECK]: {
|
||||
fieldKey: ConfigKey.REQUEST_HEADERS_CHECK,
|
||||
component: HeaderField,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.label', {
|
||||
defaultMessage: 'Request headers',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.helpText', {
|
||||
defaultMessage:
|
||||
'A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself.',
|
||||
}),
|
||||
controlled: true,
|
||||
validation: () => ({
|
||||
validate: (headers) => !validateHeaders(headers),
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.error', {
|
||||
defaultMessage: 'Header key must be a valid HTTP token.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.REQUEST_BODY_CHECK]: {
|
||||
fieldKey: ConfigKey.REQUEST_BODY_CHECK,
|
||||
component: RequestBodyField,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.requestBody.label', {
|
||||
defaultMessage: 'Request body',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.requestBody.helpText', {
|
||||
defaultMessage: 'Request body content.',
|
||||
}),
|
||||
controlled: true,
|
||||
},
|
||||
[ConfigKey.RESPONSE_HEADERS_INDEX]: {
|
||||
fieldKey: ConfigKey.RESPONSE_HEADERS_INDEX,
|
||||
component: EuiCheckbox,
|
||||
helpText: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.indexResponseHeaders.helpText"
|
||||
defaultMessage="Controls the indexing of the HTTP response headers to "
|
||||
/>
|
||||
<EuiCode>http.response.body.headers</EuiCode>
|
||||
</>
|
||||
),
|
||||
props: () => ({
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.indexResponseHeaders.label', {
|
||||
defaultMessage: 'Index response headers',
|
||||
}),
|
||||
id: 'syntheticsMonitorConfigResponseHeadersIndex', // checkbox needs an id or it won't work
|
||||
}),
|
||||
controlled: true,
|
||||
},
|
||||
[ConfigKey.RESPONSE_BODY_INDEX]: {
|
||||
fieldKey: ConfigKey.RESPONSE_BODY_INDEX,
|
||||
component: ResponseBodyIndexField,
|
||||
helpText: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.indexResponseBody.helpText"
|
||||
defaultMessage="Controls the indexing of the HTTP response body contents to"
|
||||
/>
|
||||
<EuiCode>http.response.body.contents</EuiCode>
|
||||
</>
|
||||
),
|
||||
props: () => ({
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.indexResponseBody.label', {
|
||||
defaultMessage: 'Index response body',
|
||||
}),
|
||||
}),
|
||||
controlled: true,
|
||||
},
|
||||
[ConfigKey.RESPONSE_STATUS_CHECK]: {
|
||||
fieldKey: ConfigKey.RESPONSE_STATUS_CHECK,
|
||||
component: ComboBox,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.label', {
|
||||
defaultMessage: 'Check response status equals',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.helpText', {
|
||||
defaultMessage:
|
||||
'A list of expected status codes. Press enter to add a new code. 4xx and 5xx codes are considered down by default. Other codes are considered up.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
selectedOptions: field?.value,
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: (value) => {
|
||||
const validateFn = validate[DataStream.HTTP][ConfigKey.RESPONSE_STATUS_CHECK];
|
||||
if (validateFn) {
|
||||
return !validateFn({
|
||||
[ConfigKey.RESPONSE_STATUS_CHECK]: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.error', {
|
||||
defaultMessage: 'Status code must contain digits only.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.RESPONSE_HEADERS_CHECK]: {
|
||||
fieldKey: ConfigKey.RESPONSE_HEADERS_CHECK,
|
||||
component: HeaderField,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.label', {
|
||||
defaultMessage: 'Check response headers contain',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.helpText', {
|
||||
defaultMessage: 'A list of expected response headers.',
|
||||
}),
|
||||
controlled: true,
|
||||
validation: () => ({
|
||||
validate: (headers) => !validateHeaders(headers),
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.error', {
|
||||
defaultMessage: 'Header key must be a valid HTTP token.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: {
|
||||
fieldKey: ConfigKey.RESPONSE_BODY_CHECK_POSITIVE,
|
||||
component: ComboBox,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheck.label', {
|
||||
defaultMessage: 'Check response body contains',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheck.helpText', {
|
||||
defaultMessage:
|
||||
'A list of regular expressions to match the body output. Press enter to add a new expression. Only a single expression needs to match.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
selectedOptions: field?.value,
|
||||
}),
|
||||
},
|
||||
[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: {
|
||||
fieldKey: ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE,
|
||||
component: ComboBox,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheckNegative.label', {
|
||||
defaultMessage: 'Check response body does not contain',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.responseBodyCheckNegative.helpText', {
|
||||
defaultMessage:
|
||||
'A list of regular expressions to match the the body output negatively. Press enter to add a new expression. Return match failed if single expression matches.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
selectedOptions: field?.value,
|
||||
}),
|
||||
},
|
||||
[ConfigKey.RESPONSE_RECEIVE_CHECK]: {
|
||||
fieldKey: ConfigKey.RESPONSE_RECEIVE_CHECK,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.responseReceiveCheck.label', {
|
||||
defaultMessage: 'Check response contains',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.responseReceiveCheck.helpText', {
|
||||
defaultMessage: 'The expected remote host response.',
|
||||
}),
|
||||
},
|
||||
[`${ConfigKey.PROXY_URL}__tcp`]: {
|
||||
fieldKey: ConfigKey.PROXY_URL,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.proxyURLTCP.label', {
|
||||
defaultMessage: 'Proxy URL',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.proxyURLTCP.helpText', {
|
||||
defaultMessage:
|
||||
'The URL of the SOCKS5 proxy to use when connecting to the server. The value must be a URL with a scheme of socks5://.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.REQUEST_SEND_CHECK]: {
|
||||
fieldKey: ConfigKey.REQUEST_SEND_CHECK,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.requestSendCheck.label', {
|
||||
defaultMessage: 'Request payload',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.requestSendCheck.helpText', {
|
||||
defaultMessage: 'A payload string to send to the remote host.',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.SOURCE_INLINE]: {
|
||||
fieldKey: 'source.inline',
|
||||
required: true,
|
||||
component: SourceField,
|
||||
ariaLabel: i18n.translate('xpack.synthetics.monitorConfig.monitorScript.label', {
|
||||
defaultMessage: 'Monitor script',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ isEdit }) => ({
|
||||
isEditFlow: isEdit,
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: (value) => Boolean(value.script),
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.monitorScript.error', {
|
||||
defaultMessage: 'Monitor script is required',
|
||||
}),
|
||||
},
|
||||
isTLSEnabled: {
|
||||
fieldKey: 'isTLSEnabled',
|
||||
component: EuiSwitch,
|
||||
controlled: true,
|
||||
props: ({ setValue }) => {
|
||||
return {
|
||||
id: 'syntheticsMontiorConfigIsTLSEnabledSwitch',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.customTLS.label', {
|
||||
defaultMessage: 'Use custom TLS configuration',
|
||||
}),
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue('isTLSEnabled', event.target.checked);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.TLS_VERIFICATION_MODE]: {
|
||||
fieldKey: ConfigKey.TLS_VERIFICATION_MODE,
|
||||
component: EuiSelect,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.verificationMode.label', {
|
||||
defaultMessage: 'Verification mode',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.verificationMode.helpText', {
|
||||
defaultMessage:
|
||||
'Verifies that the provided certificate is signed by a trusted authority (CA) and also verifies that the server’s hostname (or IP address) matches the names identified within the certificate. If the Subject Alternative Name is empty, it returns an error.',
|
||||
}),
|
||||
showWhen: ['isTLSEnabled', true],
|
||||
props: () => ({
|
||||
options: Object.values(VerificationMode).map((method) => ({
|
||||
value: method,
|
||||
text: method.toUpperCase(),
|
||||
})),
|
||||
}),
|
||||
},
|
||||
[ConfigKey.TLS_VERSION]: {
|
||||
fieldKey: ConfigKey.TLS_VERSION,
|
||||
component: EuiComboBox as React.ComponentType<EuiComboBoxProps<string>>,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.tlsVersion.label', {
|
||||
defaultMessage: 'Supported TLS protocols',
|
||||
}),
|
||||
controlled: true,
|
||||
showWhen: ['isTLSEnabled', true],
|
||||
props: ({
|
||||
field,
|
||||
setValue,
|
||||
}: {
|
||||
field?: ControllerRenderProps;
|
||||
setValue: UseFormReturn['setValue'];
|
||||
}) => {
|
||||
return {
|
||||
options: Object.values(TLSVersion).map((version) => ({
|
||||
label: version,
|
||||
})),
|
||||
selectedOptions: Object.values(field?.value).map((version) => ({
|
||||
label: version,
|
||||
})),
|
||||
onChange: (updatedValues: Array<EuiComboBoxOptionOption<TLSVersion>>) => {
|
||||
setValue(
|
||||
ConfigKey.TLS_VERSION,
|
||||
updatedValues.map((option) => option.label as TLSVersion)
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: {
|
||||
fieldKey: ConfigKey.TLS_CERTIFICATE_AUTHORITIES,
|
||||
component: EuiTextArea,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.certificateAuthorities.label', {
|
||||
defaultMessage: 'Certificate authorities',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.certificateAuthorities.helpText', {
|
||||
defaultMessage: 'PEM-formatted custom certificate authorities.',
|
||||
}),
|
||||
showWhen: ['isTLSEnabled', true],
|
||||
},
|
||||
[ConfigKey.TLS_CERTIFICATE]: {
|
||||
fieldKey: ConfigKey.TLS_CERTIFICATE,
|
||||
component: EuiTextArea,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.clientCertificate.label', {
|
||||
defaultMessage: 'Client certificate',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.clientCertificate.helpText', {
|
||||
defaultMessage: 'PEM-formatted certificate for TLS client authentication.',
|
||||
}),
|
||||
showWhen: ['isTLSEnabled', true],
|
||||
},
|
||||
[ConfigKey.TLS_KEY]: {
|
||||
fieldKey: ConfigKey.TLS_KEY,
|
||||
component: EuiTextArea,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.clientKey.label', {
|
||||
defaultMessage: 'Client key',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.clientKey.helpText', {
|
||||
defaultMessage: 'PEM-formatted certificate key for TLS client authentication.',
|
||||
}),
|
||||
showWhen: ['isTLSEnabled', true],
|
||||
},
|
||||
[ConfigKey.TLS_KEY_PASSPHRASE]: {
|
||||
fieldKey: ConfigKey.TLS_KEY_PASSPHRASE,
|
||||
component: EuiFieldPassword,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.clientKeyPassphrase.label', {
|
||||
defaultMessage: 'Client key passphrase',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.clientKeyPassphrase.helpText', {
|
||||
defaultMessage: 'Certificate key passphrase for TLS client authentication.',
|
||||
}),
|
||||
showWhen: ['isTLSEnabled', true],
|
||||
},
|
||||
[ConfigKey.SCREENSHOTS]: {
|
||||
fieldKey: ConfigKey.SCREENSHOTS,
|
||||
component: EuiButtonGroup,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.screenshotOptions.label', {
|
||||
defaultMessage: 'Screenshot options',
|
||||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.screenshotOptions.helpText', {
|
||||
defaultMessage: 'Set this option to manage the screenshots captured by the synthetics agent.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({
|
||||
field,
|
||||
setValue,
|
||||
}: {
|
||||
field?: ControllerRenderProps;
|
||||
setValue: UseFormReturn['setValue'];
|
||||
}) => ({
|
||||
type: 'single',
|
||||
idSelected: field?.value,
|
||||
onChange: (option: ScreenshotOption) => setValue(ConfigKey.SCREENSHOTS, option),
|
||||
options: Object.values(ScreenshotOption).map((option) => ({
|
||||
id: option,
|
||||
label: option.replace(/-/g, ' '),
|
||||
})),
|
||||
css: {
|
||||
'text-transform': 'capitalize',
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ConfigKey.TEXT_ASSERTION]: {
|
||||
fieldKey: ConfigKey.TEXT_ASSERTION,
|
||||
component: EuiFieldText,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.textAssertion.label', {
|
||||
defaultMessage: 'Text assertion',
|
||||
}),
|
||||
required: true,
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.textAssertion.helpText', {
|
||||
defaultMessage: 'Consider the page loaded when the specified text is rendered.',
|
||||
}),
|
||||
validation: () => ({
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
[ConfigKey.THROTTLING_CONFIG]: {
|
||||
fieldKey: ConfigKey.THROTTLING_CONFIG,
|
||||
component: EuiSuperSelect,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.throttling.label', {
|
||||
defaultMessage: 'Connection profile',
|
||||
}),
|
||||
required: true,
|
||||
controlled: true,
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.throttling.helpText', {
|
||||
defaultMessage:
|
||||
'Simulate network throttling (download, upload, latency). More options will be added in a future version.',
|
||||
}),
|
||||
props: () => ({
|
||||
options: [
|
||||
{
|
||||
value: DEFAULT_BROWSER_ADVANCED_FIELDS[ConfigKey.THROTTLING_CONFIG],
|
||||
inputDisplay: (
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.throttling.options.default', {
|
||||
defaultMessage: 'Default',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{'(5 Mbps, 3 Mbps, 20 ms)'}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
],
|
||||
disabled: true, // currently disabled through 1.0 until we define connection profiles
|
||||
}),
|
||||
validation: () => ({
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ConfigKey, FormMonitorType, FieldMeta } from '../types';
|
||||
|
||||
import { FIELD } from './field_config';
|
||||
|
||||
const DEFAULT_DATA_OPTIONS = {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.dataOptions.title', {
|
||||
defaultMessage: 'Data options',
|
||||
}),
|
||||
description: i18n.translate('xpack.synthetics.monitorConfig.section.dataOptions.description', {
|
||||
defaultMessage: 'Configure data options to add context to the data coming from your monitors.',
|
||||
}),
|
||||
components: [
|
||||
FIELD[ConfigKey.TAGS],
|
||||
FIELD[ConfigKey.APM_SERVICE_NAME],
|
||||
FIELD[ConfigKey.NAMESPACE],
|
||||
],
|
||||
};
|
||||
|
||||
const HTTP_ADVANCED = {
|
||||
requestConfig: {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.requestConfiguration.title', {
|
||||
defaultMessage: 'Request configuration',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.section.requestConfiguration.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Configure an optional request to send to the remote host including method, body, and headers.',
|
||||
}
|
||||
),
|
||||
components: [
|
||||
FIELD[ConfigKey.USERNAME],
|
||||
FIELD[ConfigKey.PASSWORD],
|
||||
FIELD[ConfigKey.PROXY_URL],
|
||||
FIELD[ConfigKey.REQUEST_METHOD_CHECK],
|
||||
FIELD[ConfigKey.REQUEST_HEADERS_CHECK],
|
||||
FIELD[ConfigKey.REQUEST_BODY_CHECK],
|
||||
],
|
||||
},
|
||||
responseConfig: {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.responseConfiguration.title', {
|
||||
defaultMessage: 'Response configuration',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.section.responseConfiguration.description',
|
||||
{
|
||||
defaultMessage: 'Control the indexing of the HTTP response contents.',
|
||||
}
|
||||
),
|
||||
components: [FIELD[ConfigKey.RESPONSE_HEADERS_INDEX], FIELD[ConfigKey.RESPONSE_BODY_INDEX]],
|
||||
},
|
||||
responseChecks: {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.responseChecks.title', {
|
||||
defaultMessage: 'Response checks',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.section.responseChecks.description',
|
||||
{
|
||||
defaultMessage: 'Configure the expected HTTP response.',
|
||||
}
|
||||
),
|
||||
components: [
|
||||
FIELD[ConfigKey.RESPONSE_STATUS_CHECK],
|
||||
FIELD[ConfigKey.RESPONSE_HEADERS_CHECK],
|
||||
FIELD[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE],
|
||||
FIELD[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const TCP_ADVANCED = {
|
||||
requestConfig: {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.requestConfigTCP.title', {
|
||||
defaultMessage: 'Request configuration',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.section.requestConfigTCP.description',
|
||||
{
|
||||
defaultMessage: 'Configure the payload sent to the remote host.',
|
||||
}
|
||||
),
|
||||
components: [FIELD[`${ConfigKey.PROXY_URL}__tcp`], FIELD[ConfigKey.REQUEST_SEND_CHECK]],
|
||||
},
|
||||
responseChecks: {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.responseChecksTCP.title', {
|
||||
defaultMessage: 'Response checks',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.section.responseChecksTCP.description',
|
||||
{
|
||||
defaultMessage: 'Configure the expected response from the remote host.',
|
||||
}
|
||||
),
|
||||
components: [FIELD[ConfigKey.RESPONSE_RECEIVE_CHECK]],
|
||||
},
|
||||
};
|
||||
|
||||
interface AdvancedFieldGroup {
|
||||
title: string;
|
||||
description: string;
|
||||
components: FieldMeta[];
|
||||
}
|
||||
|
||||
type FieldConfig = Record<
|
||||
FormMonitorType,
|
||||
{
|
||||
step1: FieldMeta[];
|
||||
step2: FieldMeta[];
|
||||
step3?: FieldMeta[];
|
||||
scriptEdit?: FieldMeta[];
|
||||
advanced?: AdvancedFieldGroup[];
|
||||
}
|
||||
>;
|
||||
|
||||
const TLS_OPTIONS = {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.section.tlsOptions.title', {
|
||||
defaultMessage: 'TLS options',
|
||||
}),
|
||||
description: i18n.translate('xpack.synthetics.monitorConfig.section.tlsOptions.description', {
|
||||
defaultMessage:
|
||||
'Configure TLS options, including verification mode, certificate authorities, and client certificates.',
|
||||
}),
|
||||
components: [
|
||||
FIELD.isTLSEnabled,
|
||||
FIELD[ConfigKey.TLS_VERIFICATION_MODE],
|
||||
FIELD[ConfigKey.TLS_VERSION],
|
||||
FIELD[ConfigKey.TLS_CERTIFICATE_AUTHORITIES],
|
||||
FIELD[ConfigKey.TLS_CERTIFICATE],
|
||||
FIELD[ConfigKey.TLS_KEY],
|
||||
FIELD[ConfigKey.TLS_KEY_PASSPHRASE],
|
||||
],
|
||||
};
|
||||
|
||||
export const FORM_CONFIG: FieldConfig = {
|
||||
[FormMonitorType.HTTP]: {
|
||||
step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]],
|
||||
step2: [
|
||||
FIELD[`${ConfigKey.URLS}__http`],
|
||||
FIELD[ConfigKey.NAME],
|
||||
FIELD[ConfigKey.LOCATIONS],
|
||||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.MAX_REDIRECTS],
|
||||
FIELD[ConfigKey.TIMEOUT],
|
||||
],
|
||||
advanced: [
|
||||
DEFAULT_DATA_OPTIONS,
|
||||
HTTP_ADVANCED.requestConfig,
|
||||
HTTP_ADVANCED.responseConfig,
|
||||
HTTP_ADVANCED.responseChecks,
|
||||
TLS_OPTIONS,
|
||||
],
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]],
|
||||
step2: [
|
||||
FIELD[`${ConfigKey.HOSTS}__tcp`],
|
||||
FIELD[ConfigKey.NAME],
|
||||
FIELD[ConfigKey.LOCATIONS],
|
||||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.TIMEOUT],
|
||||
],
|
||||
advanced: [
|
||||
DEFAULT_DATA_OPTIONS,
|
||||
TCP_ADVANCED.requestConfig,
|
||||
TCP_ADVANCED.responseChecks,
|
||||
TLS_OPTIONS,
|
||||
],
|
||||
},
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]],
|
||||
step2: [
|
||||
FIELD[ConfigKey.NAME],
|
||||
FIELD[ConfigKey.LOCATIONS],
|
||||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.THROTTLING_CONFIG],
|
||||
],
|
||||
step3: [FIELD[ConfigKey.SOURCE_INLINE]],
|
||||
scriptEdit: [FIELD[ConfigKey.SOURCE_INLINE]],
|
||||
advanced: [
|
||||
{
|
||||
...DEFAULT_DATA_OPTIONS,
|
||||
components: [
|
||||
FIELD[ConfigKey.TAGS],
|
||||
FIELD[ConfigKey.APM_SERVICE_NAME],
|
||||
FIELD[ConfigKey.SCREENSHOTS],
|
||||
FIELD[ConfigKey.NAMESPACE],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
[FormMonitorType.SINGLE]: {
|
||||
step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]],
|
||||
step2: [
|
||||
FIELD[`${ConfigKey.URLS}__single`],
|
||||
FIELD[ConfigKey.NAME],
|
||||
FIELD[ConfigKey.TEXT_ASSERTION],
|
||||
FIELD[ConfigKey.LOCATIONS],
|
||||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.THROTTLING_CONFIG],
|
||||
],
|
||||
advanced: [
|
||||
{
|
||||
...DEFAULT_DATA_OPTIONS,
|
||||
components: [
|
||||
FIELD[ConfigKey.TAGS],
|
||||
FIELD[ConfigKey.APM_SERVICE_NAME],
|
||||
FIELD[ConfigKey.SCREENSHOTS],
|
||||
FIELD[ConfigKey.NAMESPACE],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
step1: [FIELD[ConfigKey.FORM_MONITOR_TYPE]],
|
||||
step2: [
|
||||
FIELD[`${ConfigKey.HOSTS}__icmp`],
|
||||
FIELD[ConfigKey.NAME],
|
||||
FIELD[ConfigKey.LOCATIONS],
|
||||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.WAIT],
|
||||
FIELD[ConfigKey.TIMEOUT],
|
||||
],
|
||||
advanced: [DEFAULT_DATA_OPTIONS],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* 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 { format } from './formatter';
|
||||
|
||||
describe('format', () => {
|
||||
const formValues = {
|
||||
type: 'http',
|
||||
form_monitor_type: 'http',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
number: '3',
|
||||
unit: 'm',
|
||||
},
|
||||
'service.name': '',
|
||||
tags: [],
|
||||
timeout: '16',
|
||||
name: 'Sample name',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
__ui: {
|
||||
is_tls_enabled: false,
|
||||
},
|
||||
urls: 'sample url',
|
||||
max_redirects: '0',
|
||||
password: '',
|
||||
proxy_url: '',
|
||||
'check.response.body.negative': [],
|
||||
'check.response.body.positive': [],
|
||||
'response.include_body': 'on_error',
|
||||
'check.response.headers': {},
|
||||
'response.include_headers': true,
|
||||
'check.response.status': [],
|
||||
'check.request.body': {
|
||||
value: '',
|
||||
type: 'text',
|
||||
},
|
||||
'check.request.headers': {},
|
||||
'check.request.method': 'GET',
|
||||
username: '',
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.verification_mode': 'full',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
isTLSEnabled: false,
|
||||
service: {
|
||||
name: '',
|
||||
},
|
||||
check: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
body: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status: [],
|
||||
headers: {},
|
||||
body: {
|
||||
positive: [],
|
||||
negative: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
include_headers: true,
|
||||
include_body: 'on_error',
|
||||
},
|
||||
};
|
||||
|
||||
it('correctly formats form fields to monitor type', () => {
|
||||
expect(format(formValues)).toEqual({
|
||||
__ui: {
|
||||
is_tls_enabled: false,
|
||||
},
|
||||
config_id: '',
|
||||
'check.request.body': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
'check.request.headers': {},
|
||||
'check.request.method': 'GET',
|
||||
'check.response.body.negative': [],
|
||||
'check.response.body.positive': [],
|
||||
'check.response.headers': {},
|
||||
'check.response.status': [],
|
||||
enabled: true,
|
||||
form_monitor_type: 'http',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
max_redirects: '0',
|
||||
name: 'Sample name',
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
password: '',
|
||||
proxy_url: '',
|
||||
'response.include_body': 'on_error',
|
||||
'response.include_headers': true,
|
||||
schedule: {
|
||||
number: '3',
|
||||
unit: 'm',
|
||||
},
|
||||
'service.name': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
'ssl.verification_mode': 'full',
|
||||
tags: [],
|
||||
timeout: '16',
|
||||
type: 'http',
|
||||
urls: 'sample url',
|
||||
username: '',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['recorder', true, 'testScriptRecorder', 'fileName'],
|
||||
['inline', false, 'testScript', ''],
|
||||
])(
|
||||
'correctly formats form fields to monitor type',
|
||||
(scriptType, isGeneratedScript, script, fileName) => {
|
||||
const browserFormFields = {
|
||||
type: 'browser',
|
||||
form_monitor_type: 'multistep',
|
||||
config_id: '',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
unit: 'm',
|
||||
number: '10',
|
||||
},
|
||||
'service.name': '',
|
||||
tags: [],
|
||||
timeout: '16',
|
||||
name: 'Browser monitor',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
journey_id: '',
|
||||
project_id: '',
|
||||
playwright_options: '',
|
||||
__ui: {
|
||||
script_source: {
|
||||
is_generated_script: false,
|
||||
file_name: '',
|
||||
},
|
||||
is_zip_url_tls_enabled: false,
|
||||
},
|
||||
params: '',
|
||||
'source.inline.script': '',
|
||||
'source.project.content': '',
|
||||
'source.zip_url.url': '',
|
||||
'source.zip_url.username': '',
|
||||
'source.zip_url.password': '',
|
||||
'source.zip_url.folder': '',
|
||||
'source.zip_url.proxy_url': '',
|
||||
playwright_text_assertion: '',
|
||||
urls: '',
|
||||
screenshots: 'on',
|
||||
synthetics_args: [],
|
||||
'filter_journeys.match': '',
|
||||
'filter_journeys.tags': [],
|
||||
ignore_https_errors: false,
|
||||
'throttling.is_enabled': true,
|
||||
'throttling.download_speed': '5',
|
||||
'throttling.upload_speed': '3',
|
||||
'throttling.latency': '20',
|
||||
'throttling.config': '5d/3u/20l',
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.verification_mode': 'full',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
'source.inline': {
|
||||
type: 'recorder',
|
||||
script: '',
|
||||
fileName: '',
|
||||
},
|
||||
throttling: {
|
||||
config: '5d/3u/20l',
|
||||
},
|
||||
source: {
|
||||
inline: {
|
||||
type: scriptType,
|
||||
script,
|
||||
fileName,
|
||||
},
|
||||
},
|
||||
service: {
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
expect(format(browserFormFields)).toEqual({
|
||||
__ui: {
|
||||
script_source: {
|
||||
file_name: fileName,
|
||||
is_generated_script: isGeneratedScript,
|
||||
},
|
||||
},
|
||||
config_id: '',
|
||||
enabled: true,
|
||||
'filter_journeys.match': '',
|
||||
'filter_journeys.tags': [],
|
||||
form_monitor_type: 'multistep',
|
||||
ignore_https_errors: false,
|
||||
journey_id: '',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
name: 'Browser monitor',
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
params: '',
|
||||
playwright_options: '',
|
||||
playwright_text_assertion: '',
|
||||
project_id: '',
|
||||
schedule: {
|
||||
number: '10',
|
||||
unit: 'm',
|
||||
},
|
||||
screenshots: 'on',
|
||||
'service.name': '',
|
||||
'source.inline.script': script,
|
||||
'source.project.content': '',
|
||||
'source.zip_url.folder': '',
|
||||
'source.zip_url.password': '',
|
||||
'source.zip_url.proxy_url': '',
|
||||
'source.zip_url.ssl.certificate': undefined,
|
||||
'source.zip_url.ssl.certificate_authorities': undefined,
|
||||
'source.zip_url.ssl.key': undefined,
|
||||
'source.zip_url.ssl.key_passphrase': undefined,
|
||||
'source.zip_url.ssl.supported_protocols': undefined,
|
||||
'source.zip_url.ssl.verification_mode': undefined,
|
||||
'source.zip_url.url': '',
|
||||
'source.zip_url.username': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
'ssl.verification_mode': 'full',
|
||||
synthetics_args: [],
|
||||
tags: [],
|
||||
'throttling.config': '5d/3u/20l',
|
||||
'throttling.download_speed': '5',
|
||||
'throttling.is_enabled': true,
|
||||
'throttling.latency': '20',
|
||||
'throttling.upload_speed': '3',
|
||||
timeout: '16',
|
||||
type: 'browser',
|
||||
'url.port': null,
|
||||
urls: '',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['testCA', true],
|
||||
['', false],
|
||||
])('correctly formats form fields to monitor type', (certificateAuthorities, isTLSEnabled) => {
|
||||
expect(
|
||||
format({
|
||||
...formValues,
|
||||
ssl: {
|
||||
// @ts-ignore next
|
||||
...formValues.ssl,
|
||||
certificate_authorities: certificateAuthorities,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
__ui: {
|
||||
is_tls_enabled: isTLSEnabled,
|
||||
},
|
||||
config_id: '',
|
||||
'check.request.body': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
'check.request.headers': {},
|
||||
'check.request.method': 'GET',
|
||||
'check.response.body.negative': [],
|
||||
'check.response.body.positive': [],
|
||||
'check.response.headers': {},
|
||||
'check.response.status': [],
|
||||
enabled: true,
|
||||
form_monitor_type: 'http',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
max_redirects: '0',
|
||||
name: 'Sample name',
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
password: '',
|
||||
proxy_url: '',
|
||||
'response.include_body': 'on_error',
|
||||
'response.include_headers': true,
|
||||
schedule: {
|
||||
number: '3',
|
||||
unit: 'm',
|
||||
},
|
||||
'service.name': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.certificate_authorities': certificateAuthorities,
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
'ssl.verification_mode': 'full',
|
||||
tags: [],
|
||||
timeout: '16',
|
||||
type: 'http',
|
||||
urls: 'sample url',
|
||||
username: '',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { get, isEqual } from 'lodash';
|
||||
import { ConfigKey, DataStream, FormMonitorType, MonitorFields } from '../types';
|
||||
import { DEFAULT_FIELDS, DEFAULT_TLS_FIELDS } from '../constants';
|
||||
|
||||
export const formatter = (fields: Record<string, any>) => {
|
||||
const monitorType = fields[ConfigKey.MONITOR_TYPE] as DataStream;
|
||||
const monitorFields: Record<string, any> = {};
|
||||
const defaults = DEFAULT_FIELDS[monitorType] as MonitorFields;
|
||||
Object.keys(defaults).map((key) => {
|
||||
/* split key names on dot to handle dot notation fields,
|
||||
* which are changed to nested fields by react-hook-form */
|
||||
monitorFields[key] = get(fields, key.split('.')) || defaults[key as ConfigKey];
|
||||
});
|
||||
return monitorFields as MonitorFields;
|
||||
};
|
||||
|
||||
export const format = (fields: Record<string, unknown>) => {
|
||||
const formattedFields = formatter(fields) as MonitorFields;
|
||||
const formattedMap = {
|
||||
[FormMonitorType.SINGLE]: {
|
||||
...formattedFields,
|
||||
[ConfigKey.SOURCE_INLINE]: `step('Go to ${formattedFields[ConfigKey.URLS]}', async () => {
|
||||
await page.goto('${formattedFields[ConfigKey.URLS]}');
|
||||
expect(await page.isVisible('text=${
|
||||
formattedFields[ConfigKey.TEXT_ASSERTION]
|
||||
}')).toBeTruthy();
|
||||
});`,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.SINGLE,
|
||||
},
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
...formattedFields,
|
||||
[ConfigKey.METADATA]: {
|
||||
script_source: {
|
||||
is_generated_script: get(fields, 'source.inline.type') === 'recorder' ? true : false,
|
||||
file_name: get(fields, 'source.inline.fileName'),
|
||||
},
|
||||
},
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
},
|
||||
[FormMonitorType.HTTP]: {
|
||||
...formattedFields,
|
||||
[ConfigKey.METADATA]: {
|
||||
is_tls_enabled: isCustomTLSEnabled(formattedFields),
|
||||
},
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
...formattedFields,
|
||||
[ConfigKey.METADATA]: {
|
||||
is_tls_enabled: isCustomTLSEnabled(formattedFields),
|
||||
},
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
...formattedFields,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP,
|
||||
},
|
||||
};
|
||||
return formattedMap[fields[ConfigKey.FORM_MONITOR_TYPE] as FormMonitorType];
|
||||
};
|
||||
|
||||
const isCustomTLSEnabled = (fields: MonitorFields) => {
|
||||
const tlsFields: Record<string, unknown> = {};
|
||||
Object.keys(DEFAULT_TLS_FIELDS).map((key) => {
|
||||
tlsFields[key] = fields[key as keyof MonitorFields];
|
||||
});
|
||||
return !isEqual(tlsFields, DEFAULT_TLS_FIELDS);
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { EuiForm, EuiSpacer } from '@elastic/eui';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { useFormWrapped } from '../hooks/use_form_wrapped';
|
||||
import { FormMonitorType, SyntheticsMonitor } from '../types';
|
||||
import { getDefaultFormFields, formatDefaultFormValues } from './defaults';
|
||||
import { ActionBar } from './submit';
|
||||
import { Disclaimer } from './disclaimer';
|
||||
|
||||
export const MonitorForm: React.FC<{ defaultValues?: SyntheticsMonitor; space?: string }> = ({
|
||||
children,
|
||||
defaultValues,
|
||||
space,
|
||||
}) => {
|
||||
const methods = useFormWrapped({
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues:
|
||||
formatDefaultFormValues(defaultValues as SyntheticsMonitor) ||
|
||||
getDefaultFormFields(space)[FormMonitorType.MULTISTEP],
|
||||
shouldFocusError: true,
|
||||
});
|
||||
|
||||
/* React hook form doesn't seem to register a field
|
||||
* as dirty until validation unless dirtyFields is subscribed to */
|
||||
const {
|
||||
formState: { isSubmitted, errors, dirtyFields: _ },
|
||||
} = methods;
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm
|
||||
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
|
||||
component="form"
|
||||
noValidate
|
||||
>
|
||||
{children}
|
||||
<EuiSpacer />
|
||||
<ActionBar />
|
||||
</EuiForm>
|
||||
<Disclaimer />
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { Redirect, useParams, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { EuiButton, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFetcher, FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import { SyntheticsMonitor } from '../types';
|
||||
import { format } from './formatter';
|
||||
import { createMonitorAPI, updateMonitorAPI } from '../../../state/monitor_management/api';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
|
||||
import { MONITORS_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../../common/constants';
|
||||
|
||||
export const ActionBar = () => {
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
const history = useHistory();
|
||||
const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE });
|
||||
const isEdit = editRouteMatch?.isExact;
|
||||
const { handleSubmit } = useFormContext();
|
||||
|
||||
const [monitorData, setMonitorData] = useState<SyntheticsMonitor | undefined>(undefined);
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (!monitorData) {
|
||||
return null;
|
||||
}
|
||||
if (isEdit) {
|
||||
return updateMonitorAPI({
|
||||
id: monitorId,
|
||||
monitor: monitorData,
|
||||
});
|
||||
} else {
|
||||
return createMonitorAPI({
|
||||
monitor: monitorData,
|
||||
});
|
||||
}
|
||||
}, [monitorData]);
|
||||
|
||||
const loading = status === FETCH_STATUS.LOADING;
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FETCH_STATUS.FAILURE) {
|
||||
kibanaService.toasts.addDanger({
|
||||
title: MONITOR_FAILURE_LABEL,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
} else if (status === FETCH_STATUS.SUCCESS && !loading) {
|
||||
kibanaService.toasts.addSuccess({
|
||||
title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
}
|
||||
}, [data, status, monitorId, loading]);
|
||||
|
||||
const formSubmitter = (formData: Record<string, any>) => {
|
||||
setMonitorData(format(formData) as SyntheticsMonitor);
|
||||
};
|
||||
|
||||
return status === FETCH_STATUS.SUCCESS ? (
|
||||
<Redirect to={MONITORS_ROUTE} />
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiLink href={history.createHref({ pathname: MONITORS_ROUTE })}>{CANCEL_LABEL}</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="plusInCircleFilled"
|
||||
onClick={handleSubmit(formSubmitter)}
|
||||
data-test-subj="syntheticsMonitorConfigSubmitButton"
|
||||
>
|
||||
{isEdit ? UPDATE_MONITOR_LABEL : CREATE_MONITOR_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.discardLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
const CREATE_MONITOR_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.addEdit.createMonitorLabel',
|
||||
{
|
||||
defaultMessage: 'Create monitor',
|
||||
}
|
||||
);
|
||||
|
||||
const UPDATE_MONITOR_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.updateMonitorLabel',
|
||||
{
|
||||
defaultMessage: 'Update monitor',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_SUCCESS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor added successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor updated successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const MONITOR_FAILURE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.monitorFailureMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor was unable to be saved. Please try again later.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 {
|
||||
ConfigKey,
|
||||
DataStream,
|
||||
HTTPFields,
|
||||
BrowserFields,
|
||||
MonitorFields,
|
||||
ScheduleUnit,
|
||||
} from '../types';
|
||||
import { validate } from './validation';
|
||||
|
||||
describe('[Monitor Management] validation', () => {
|
||||
const commonPropsValid: Partial<MonitorFields> = {
|
||||
[ConfigKey.SCHEDULE]: { number: '5', unit: ScheduleUnit.MINUTES },
|
||||
[ConfigKey.TIMEOUT]: '3m',
|
||||
};
|
||||
|
||||
describe('HTTP', () => {
|
||||
const httpPropsValid: Partial<HTTPFields> = {
|
||||
...commonPropsValid,
|
||||
[ConfigKey.RESPONSE_STATUS_CHECK]: ['200', '204'],
|
||||
[ConfigKey.RESPONSE_HEADERS_CHECK]: { 'Content-Type': 'application/json' },
|
||||
[ConfigKey.REQUEST_HEADERS_CHECK]: { 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8' },
|
||||
[ConfigKey.MAX_REDIRECTS]: '3',
|
||||
[ConfigKey.URLS]: 'https:// example-url.com',
|
||||
};
|
||||
|
||||
it('should return false for all valid props', () => {
|
||||
const validators = validate[DataStream.HTTP];
|
||||
const keysToValidate = [
|
||||
ConfigKey.SCHEDULE,
|
||||
ConfigKey.TIMEOUT,
|
||||
ConfigKey.RESPONSE_STATUS_CHECK,
|
||||
ConfigKey.RESPONSE_HEADERS_CHECK,
|
||||
ConfigKey.REQUEST_HEADERS_CHECK,
|
||||
ConfigKey.MAX_REDIRECTS,
|
||||
ConfigKey.URLS,
|
||||
];
|
||||
const validatorFns = keysToValidate.map((key) => validators[key]);
|
||||
const result = validatorFns.map((fn) => fn?.(httpPropsValid) ?? true);
|
||||
|
||||
expect(result).not.toEqual(expect.arrayContaining([true]));
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[ConfigKey.SOURCE_INLINE, 'step(() => {});'],
|
||||
[ConfigKey.SOURCE_ZIP_URL, 'https://test.zip'],
|
||||
])('Browser', (configKey, value) => {
|
||||
const browserProps: Partial<BrowserFields> = {
|
||||
...commonPropsValid,
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.BROWSER,
|
||||
[ConfigKey.TIMEOUT]: null,
|
||||
[ConfigKey.URLS]: null,
|
||||
[ConfigKey.PORT]: null,
|
||||
[configKey]: value,
|
||||
};
|
||||
|
||||
it('should return false for all valid props', () => {
|
||||
const validators = validate[DataStream.BROWSER];
|
||||
const keysToValidate = [ConfigKey.SCHEDULE, ConfigKey.TIMEOUT, configKey];
|
||||
const validatorFns = keysToValidate.map((key) => validators[key]);
|
||||
const result = validatorFns.map((fn) => fn?.(browserProps as Partial<MonitorFields>) ?? true);
|
||||
|
||||
expect(result).not.toEqual(expect.arrayContaining([true]));
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add test for other monitor types if needed
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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 {
|
||||
ConfigKey,
|
||||
DataStream,
|
||||
ScheduleUnit,
|
||||
MonitorFields,
|
||||
Validator,
|
||||
Validation,
|
||||
} from '../types';
|
||||
|
||||
export const DIGITS_ONLY = /^[0-9]*$/g;
|
||||
export const INCLUDES_VALID_PORT = /[^\:]+:[0-9]{1,5}$/g;
|
||||
export const WHOLE_NUMBERS_ONLY = /^[0-9]+(.?[0]+)?$/;
|
||||
export const FLOATS_ONLY = /^[0-9]+(.?[0-9]+)?$/;
|
||||
|
||||
type ValidationLibrary = Record<string, Validator>;
|
||||
|
||||
// returns true if invalid
|
||||
export function validateHeaders<T>(headers: T): boolean {
|
||||
return Object.keys(headers).some((key) => {
|
||||
if (key) {
|
||||
const whiteSpaceRegEx = /[\s]/g;
|
||||
return whiteSpaceRegEx.test(key);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// returns true if invalid
|
||||
export const validateTimeout = ({
|
||||
scheduleNumber,
|
||||
scheduleUnit,
|
||||
timeout,
|
||||
}: {
|
||||
scheduleNumber: string;
|
||||
scheduleUnit: ScheduleUnit;
|
||||
timeout: string;
|
||||
}): boolean => {
|
||||
let schedule: number;
|
||||
switch (scheduleUnit) {
|
||||
case ScheduleUnit.SECONDS:
|
||||
schedule = parseFloat(scheduleNumber);
|
||||
break;
|
||||
case ScheduleUnit.MINUTES:
|
||||
schedule = parseFloat(scheduleNumber) * 60;
|
||||
break;
|
||||
default:
|
||||
schedule = parseFloat(scheduleNumber);
|
||||
}
|
||||
|
||||
return parseFloat(timeout) > schedule;
|
||||
};
|
||||
|
||||
// validation functions return true when invalid
|
||||
const validateCommon: ValidationLibrary = {
|
||||
[ConfigKey.SCHEDULE]: ({ [ConfigKey.SCHEDULE]: value }) => {
|
||||
const { number, unit } = value as MonitorFields[ConfigKey.SCHEDULE];
|
||||
const parsedFloat = parseFloat(number);
|
||||
return !parsedFloat || !unit || parsedFloat < 1;
|
||||
},
|
||||
[ConfigKey.TIMEOUT]: ({
|
||||
[ConfigKey.MONITOR_TYPE]: monitorType,
|
||||
[ConfigKey.TIMEOUT]: timeout,
|
||||
[ConfigKey.SCHEDULE]: schedule,
|
||||
}) => {
|
||||
const { number, unit } = schedule as MonitorFields[ConfigKey.SCHEDULE];
|
||||
|
||||
// Timeout is not currently supported by browser monitors
|
||||
if (monitorType === DataStream.BROWSER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!timeout ||
|
||||
parseFloat(timeout) < 0 ||
|
||||
validateTimeout({
|
||||
timeout,
|
||||
scheduleNumber: number,
|
||||
scheduleUnit: unit,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const validateHTTP: ValidationLibrary = {
|
||||
[ConfigKey.RESPONSE_STATUS_CHECK]: ({ [ConfigKey.RESPONSE_STATUS_CHECK]: value }) => {
|
||||
const statusCodes = value as MonitorFields[ConfigKey.RESPONSE_STATUS_CHECK];
|
||||
return statusCodes.length ? statusCodes.some((code) => !`${code}`.match(DIGITS_ONLY)) : false;
|
||||
},
|
||||
[ConfigKey.RESPONSE_HEADERS_CHECK]: ({ [ConfigKey.RESPONSE_HEADERS_CHECK]: value }) => {
|
||||
const headers = value as MonitorFields[ConfigKey.RESPONSE_HEADERS_CHECK];
|
||||
return validateHeaders<MonitorFields[ConfigKey.RESPONSE_HEADERS_CHECK]>(headers);
|
||||
},
|
||||
[ConfigKey.REQUEST_HEADERS_CHECK]: ({ [ConfigKey.REQUEST_HEADERS_CHECK]: value }) => {
|
||||
const headers = value as MonitorFields[ConfigKey.REQUEST_HEADERS_CHECK];
|
||||
return validateHeaders<MonitorFields[ConfigKey.REQUEST_HEADERS_CHECK]>(headers);
|
||||
},
|
||||
[ConfigKey.MAX_REDIRECTS]: ({ [ConfigKey.MAX_REDIRECTS]: value }) =>
|
||||
(!!value && !`${value}`.match(DIGITS_ONLY)) ||
|
||||
parseFloat(value as MonitorFields[ConfigKey.MAX_REDIRECTS]) < 0,
|
||||
[ConfigKey.URLS]: ({ [ConfigKey.URLS]: value }) => !value,
|
||||
...validateCommon,
|
||||
};
|
||||
|
||||
const validateTCP: Record<string, Validator> = {
|
||||
[ConfigKey.HOSTS]: ({ [ConfigKey.HOSTS]: value }) => {
|
||||
return !value || !`${value}`.match(INCLUDES_VALID_PORT);
|
||||
},
|
||||
...validateCommon,
|
||||
};
|
||||
|
||||
const validateICMP: ValidationLibrary = {
|
||||
[ConfigKey.HOSTS]: ({ [ConfigKey.HOSTS]: value }) => !value,
|
||||
[ConfigKey.WAIT]: ({ [ConfigKey.WAIT]: value }) =>
|
||||
!!value &&
|
||||
!DIGITS_ONLY.test(`${value}`) &&
|
||||
parseFloat(value as MonitorFields[ConfigKey.WAIT]) < 0,
|
||||
...validateCommon,
|
||||
};
|
||||
|
||||
const validateThrottleValue = (speed: string | undefined, allowZero?: boolean) => {
|
||||
if (speed === undefined || speed === '') return false;
|
||||
const throttleValue = parseFloat(speed);
|
||||
return isNaN(throttleValue) || (allowZero ? throttleValue < 0 : throttleValue <= 0);
|
||||
};
|
||||
|
||||
const validateBrowser: ValidationLibrary = {
|
||||
...validateCommon,
|
||||
[ConfigKey.SOURCE_ZIP_URL]: ({
|
||||
[ConfigKey.SOURCE_ZIP_URL]: zipUrl,
|
||||
[ConfigKey.SOURCE_INLINE]: inlineScript,
|
||||
}) => !zipUrl && !inlineScript,
|
||||
[ConfigKey.SOURCE_INLINE]: ({
|
||||
[ConfigKey.SOURCE_ZIP_URL]: zipUrl,
|
||||
[ConfigKey.SOURCE_INLINE]: inlineScript,
|
||||
}) => !zipUrl && !inlineScript,
|
||||
[ConfigKey.DOWNLOAD_SPEED]: ({ [ConfigKey.DOWNLOAD_SPEED]: downloadSpeed }) =>
|
||||
validateThrottleValue(downloadSpeed),
|
||||
[ConfigKey.UPLOAD_SPEED]: ({ [ConfigKey.UPLOAD_SPEED]: uploadSpeed }) =>
|
||||
validateThrottleValue(uploadSpeed),
|
||||
[ConfigKey.LATENCY]: ({ [ConfigKey.LATENCY]: latency }) => validateThrottleValue(latency, true),
|
||||
};
|
||||
|
||||
export type ValidateDictionary = Record<DataStream, Validation>;
|
||||
|
||||
export const validate: ValidateDictionary = {
|
||||
[DataStream.HTTP]: validateHTTP,
|
||||
[DataStream.TCP]: validateTCP,
|
||||
[DataStream.ICMP]: validateICMP,
|
||||
[DataStream.BROWSER]: validateBrowser,
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 './use_is_edit_flow';
|
||||
export * from './use_kibana_space';
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { FieldValues, useForm, UseFormProps } from 'react-hook-form';
|
||||
|
||||
export function useFormWrapped<TFieldValues extends FieldValues = FieldValues, TContext = any>(
|
||||
props?: UseFormProps<TFieldValues, TContext>
|
||||
) {
|
||||
const { register, ...restOfForm } = useForm(props);
|
||||
|
||||
const euiRegister = useCallback(
|
||||
(name, ...registerArgs) => {
|
||||
const { ref, ...restOfRegister } = register(name, ...registerArgs);
|
||||
|
||||
return {
|
||||
inputRef: ref,
|
||||
ref,
|
||||
...restOfRegister,
|
||||
};
|
||||
},
|
||||
[register]
|
||||
);
|
||||
|
||||
return {
|
||||
register: euiRegister,
|
||||
...restOfForm,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants';
|
||||
|
||||
export const useIsEditFlow = () => {
|
||||
const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE });
|
||||
return editRouteMatch?.isExact || false;
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
export const useKibanaSpace = () => {
|
||||
const { services } = useKibana<ClientPluginsStart>();
|
||||
|
||||
const {
|
||||
data: space,
|
||||
loading,
|
||||
error,
|
||||
} = useFetcher(() => {
|
||||
return services.spaces?.getActiveSpace();
|
||||
}, [services.spaces]);
|
||||
|
||||
return {
|
||||
space,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { defaultCore, WrappedHelper } from '../../../utils/testing/rtl_helpers';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useMonitorName } from './use_monitor_name';
|
||||
|
||||
import * as reactRouter from 'react-router-dom';
|
||||
|
||||
const mockRouter = {
|
||||
...reactRouter,
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
describe('useMonitorName', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useMonitorName({}), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: '' });
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
monitorNames: {
|
||||
terms: { field: 'synthetics-monitor.attributes.name.keyword', size: 10000 },
|
||||
},
|
||||
},
|
||||
perPage: 0,
|
||||
type: 'synthetics-monitor',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns expected results after data', async () => {
|
||||
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
|
||||
aggregations: {
|
||||
monitorNames: {
|
||||
buckets: [{ key: 'Test' }, { key: 'Test 1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMonitorName({ search: 'Test' }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: true, validName: '' });
|
||||
});
|
||||
|
||||
it('returns expected results after data while editing monitor', async () => {
|
||||
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
|
||||
aggregations: {
|
||||
monitorNames: {
|
||||
buckets: [{ key: 'Test' }, { key: 'Test 1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(mockRouter, 'useParams').mockReturnValue({ monitorId: 'test-id' });
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMonitorName({ search: 'Test' }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { syntheticsMonitorType } from '../../../../../../common/types/saved_objects';
|
||||
|
||||
interface AggsResponse {
|
||||
monitorNames: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const useMonitorName = ({ search = '' }: { search?: string }) => {
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data } = useFetcher(() => {
|
||||
const aggs = {
|
||||
monitorNames: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.name.keyword`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
return savedObjects?.client.find<unknown, typeof aggs>({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 0,
|
||||
aggs,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.aggregations) {
|
||||
const newValues = (data.aggregations as AggsResponse)?.monitorNames.buckets.map(({ key }) =>
|
||||
key.toLowerCase()
|
||||
);
|
||||
if (monitorId && newValues.includes(search.toLowerCase())) {
|
||||
setValues(newValues.filter((val) => val !== search.toLowerCase()));
|
||||
} else {
|
||||
setValues(newValues);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, monitorId]);
|
||||
|
||||
const hasMonitor = Boolean(
|
||||
search && values && values.length > 0 && values?.includes(search.trim().toLowerCase())
|
||||
);
|
||||
|
||||
return { nameAlreadyExists: hasMonitor, validName: hasMonitor ? '' : search };
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs';
|
||||
|
||||
export const MonitorAddEditPage: React.FC = () => {
|
||||
useTrackPageview({ app: 'synthetics', path: 'add-monitor' });
|
||||
useTrackPageview({ app: 'synthetics', path: 'add-monitor', delay: 15000 });
|
||||
useMonitorAddEditBreadcrumbs();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Monitor Add or Edit page</p>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { useKibanaSpace } from './hooks/use_kibana_space';
|
||||
import { getServiceLocations } from '../../state';
|
||||
import { MonitorSteps } from './steps';
|
||||
import { MonitorForm } from './form';
|
||||
import { ADD_MONITOR_STEPS } from './steps/step_config';
|
||||
import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs';
|
||||
|
||||
export const MonitorAddPage = () => {
|
||||
useTrackPageview({ app: 'synthetics', path: 'add-monitor' });
|
||||
const { space, loading, error } = useKibanaSpace();
|
||||
useTrackPageview({ app: 'synthetics', path: 'add-monitor', delay: 15000 });
|
||||
useMonitorAddEditBreadcrumbs();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getServiceLocations());
|
||||
}, [dispatch]);
|
||||
|
||||
return !loading && !error ? (
|
||||
<MonitorForm space={space?.id}>
|
||||
<MonitorSteps stepMap={ADD_MONITOR_STEPS} />
|
||||
</MonitorForm>
|
||||
) : (
|
||||
<EuiLoadingSpinner />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { useHistory } from 'react-router-dom';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { InPortal } from 'react-reverse-portal';
|
||||
import { MonitorDetailsLinkPortalNode } from './portals';
|
||||
|
||||
export const MonitorDetailsLinkPortal = ({ name, id }: { name: string; id: string }) => {
|
||||
return (
|
||||
<InPortal node={MonitorDetailsLinkPortalNode}>
|
||||
<MonitorDetailsLink name={name} id={id} />
|
||||
</InPortal>
|
||||
);
|
||||
};
|
||||
|
||||
export const MonitorDetailsLink = ({ name, id }: { name: string; id: string }) => {
|
||||
const history = useHistory();
|
||||
const href = history.createHref({
|
||||
pathname: `monitor/${id}`,
|
||||
});
|
||||
return (
|
||||
<EuiButtonEmpty href={href} iconType="arrowLeft" flush="left">
|
||||
{name}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTrackPageview, useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { getServiceLocations } from '../../state';
|
||||
import { MonitorSteps } from './steps';
|
||||
import { MonitorForm } from './form';
|
||||
import { MonitorDetailsLinkPortal } from './monitor_details_portal';
|
||||
import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs';
|
||||
import { getMonitorAPI } from '../../state/monitor_management/api';
|
||||
import { EDIT_MONITOR_STEPS } from './steps/step_config';
|
||||
|
||||
export const MonitorEditPage: React.FC = () => {
|
||||
useTrackPageview({ app: 'synthetics', path: 'edit-monitor' });
|
||||
useTrackPageview({ app: 'synthetics', path: 'edit-monitor', delay: 15000 });
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
useMonitorAddEditBreadcrumbs(true);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getServiceLocations());
|
||||
}, [dispatch]);
|
||||
|
||||
const { data, loading, error } = useFetcher(() => {
|
||||
return getMonitorAPI({ id: monitorId });
|
||||
}, []);
|
||||
|
||||
return data && !loading && !error ? (
|
||||
<MonitorForm defaultValues={data?.attributes}>
|
||||
<MonitorSteps stepMap={EDIT_MONITOR_STEPS} isEditFlow={true} />
|
||||
<MonitorDetailsLinkPortal id={data?.id} name={data?.attributes.name} />
|
||||
</MonitorForm>
|
||||
) : (
|
||||
<EuiLoadingSpinner />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { createHtmlPortalNode } from 'react-reverse-portal';
|
||||
|
||||
export const MonitorTypePortalNode = createHtmlPortalNode();
|
||||
|
||||
export const MonitorDetailsLinkPortalNode = createHtmlPortalNode();
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { EuiSteps, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { ConfigKey, FormMonitorType, StepMap } from '../types';
|
||||
import { AdvancedConfig } from '../advanced';
|
||||
import { MonitorTypePortal } from './monitor_type_portal';
|
||||
|
||||
export const MonitorSteps = ({
|
||||
stepMap,
|
||||
isEditFlow = false,
|
||||
}: {
|
||||
stepMap: StepMap;
|
||||
isEditFlow?: boolean;
|
||||
}) => {
|
||||
const { watch } = useFormContext();
|
||||
const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]);
|
||||
const steps = stepMap[type];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditFlow ? (
|
||||
steps.map((step) => (
|
||||
<>
|
||||
<EuiPanel hasBorder>
|
||||
<EuiText size="s">
|
||||
<h2>{step.title}</h2>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
{step.children}
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<EuiSteps steps={steps} headingElement="h2" />
|
||||
)}
|
||||
<AdvancedConfig />
|
||||
<MonitorTypePortal monitorType={type} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiBetaBadge, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormMonitorType } from '../types';
|
||||
import { MONITOR_TYPE_CONFIG } from '../form/field_config';
|
||||
|
||||
export const MonitorType = ({ monitorType }: { monitorType: FormMonitorType }) => {
|
||||
const config = MONITOR_TYPE_CONFIG[monitorType];
|
||||
return (
|
||||
<>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.monitorType.label', {
|
||||
defaultMessage: 'Monitor type',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiText size="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<strong>{config.descriptionTitle}</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{config.beta && (
|
||||
<EuiBetaBadge
|
||||
label="Beta"
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.monitorType.betaLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'This functionality is in beta and is subject to change. The design and code is less mature than official generally available features and is being provided as-is with no warranties. Beta features are not subject to the support service level agreement of official generally available features.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { InPortal } from 'react-reverse-portal';
|
||||
import { MonitorTypePortalNode } from '../portals';
|
||||
import { FormMonitorType } from '../types';
|
||||
|
||||
import { MonitorType } from './monitor_type';
|
||||
|
||||
export const MonitorTypePortal = ({ monitorType }: { monitorType: FormMonitorType }) => {
|
||||
return (
|
||||
<InPortal node={MonitorTypePortalNode}>
|
||||
<MonitorType monitorType={monitorType} />
|
||||
</InPortal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
description: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Step = ({ description, children }: Props) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiText>{description}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import { FormMonitorType, Step, StepMap } from '../types';
|
||||
import { StepFields } from './step_fields';
|
||||
|
||||
const MONITOR_TYPE_STEP: Step = {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.monitorTypeStep.title', {
|
||||
defaultMessage: 'Select a monitor type',
|
||||
}),
|
||||
children: (
|
||||
<StepFields
|
||||
description={
|
||||
<p>
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.monitorTypeStep.description', {
|
||||
defaultMessage: 'Choose a monitor that best fits your use case',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
stepKey="step1"
|
||||
/>
|
||||
),
|
||||
};
|
||||
const MONITOR_DETAILS_STEP: Step = {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.monitorDetailsStep.title', {
|
||||
defaultMessage: 'Monitor details',
|
||||
}),
|
||||
children: (
|
||||
<StepFields
|
||||
description={
|
||||
<p>
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.monitorDetailsStep.description', {
|
||||
defaultMessage: 'Provide some details about how your monitor should run',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
stepKey="step2"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const SCRIPT_RECORDER_BTNS = (
|
||||
<EuiFlexGroup justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton href={`elastic-synthetics-recorder://`} iconType="popout" iconSide="right">
|
||||
{i18n.translate('xpack.synthetics.monitorConfig.monitorScriptStep.scriptRecorder.launch', {
|
||||
defaultMessage: 'Launch Synthetics Recorder',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href="https://github.com/elastic/synthetics-recorder/releases/"
|
||||
iconType="download"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.monitorScriptStep.scriptRecorder.download',
|
||||
{
|
||||
defaultMessage: 'Download Synthetics Recorder',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const MONITOR_SCRIPT_STEP: Step = {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.monitorScriptStep.title', {
|
||||
defaultMessage: 'Add a script',
|
||||
}),
|
||||
children: (
|
||||
<StepFields
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.monitorScriptStep.description"
|
||||
defaultMessage="Use Elastic Synthetics Recorder to generate a script and then upload it. Alternatively, you can write your own {playwright} script and paste it in the script editor."
|
||||
values={{
|
||||
playwright: (
|
||||
<EuiLink href="https://playwright.dev/" target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.monitorScriptStep.playwrightLink"
|
||||
defaultMessage="Playwright"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{SCRIPT_RECORDER_BTNS}
|
||||
</>
|
||||
}
|
||||
stepKey="step3"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const MONITOR_SCRIPT_STEP_EDIT: Step = {
|
||||
title: i18n.translate('xpack.synthetics.monitorConfig.monitorScriptEditStep.title', {
|
||||
defaultMessage: 'Monitor script',
|
||||
}),
|
||||
children: (
|
||||
<StepFields
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.monitorScriptEditStep.description"
|
||||
defaultMessage="Use Elastic Synthetics Recorder to generate and upload a script. Alternatively, you can edit the existing {playwright} script (or paste a new one) in the script editor."
|
||||
values={{
|
||||
playwright: (
|
||||
<EuiLink href="https://playwright.dev/" target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.monitorScriptEditStep.playwrightLink"
|
||||
defaultMessage="Playwright"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{SCRIPT_RECORDER_BTNS}
|
||||
</>
|
||||
}
|
||||
stepKey="scriptEdit"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const ADD_MONITOR_STEPS: StepMap = {
|
||||
[FormMonitorType.MULTISTEP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP, MONITOR_SCRIPT_STEP],
|
||||
[FormMonitorType.SINGLE]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.HTTP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.ICMP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.TCP]: [MONITOR_TYPE_STEP, MONITOR_DETAILS_STEP],
|
||||
};
|
||||
|
||||
export const EDIT_MONITOR_STEPS: StepMap = {
|
||||
[FormMonitorType.MULTISTEP]: [MONITOR_SCRIPT_STEP_EDIT, MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.SINGLE]: [MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.HTTP]: [MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.ICMP]: [MONITOR_DETAILS_STEP],
|
||||
[FormMonitorType.TCP]: [MONITOR_DETAILS_STEP],
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useFormContext, FieldError } from 'react-hook-form';
|
||||
import { Step } from './step';
|
||||
import { FORM_CONFIG } from '../form/form_config';
|
||||
import { Field } from '../form/field';
|
||||
import { ConfigKey, FormMonitorType, StepKey } from '../types';
|
||||
|
||||
export const StepFields = ({
|
||||
description,
|
||||
stepKey,
|
||||
}: {
|
||||
description: React.ReactNode;
|
||||
stepKey: StepKey;
|
||||
}) => {
|
||||
const {
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]);
|
||||
|
||||
return (
|
||||
<Step description={description}>
|
||||
{FORM_CONFIG[type][stepKey]?.map((field) => {
|
||||
return (
|
||||
<Field
|
||||
{...field}
|
||||
key={field.fieldKey}
|
||||
fieldError={errors[field.fieldKey] as FieldError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Step>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 {
|
||||
UseFormReturn,
|
||||
ControllerRenderProps,
|
||||
ControllerFieldState,
|
||||
FormState,
|
||||
} from 'react-hook-form';
|
||||
import {
|
||||
ServiceLocations,
|
||||
FormMonitorType,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../common/runtime_types/monitor_management';
|
||||
|
||||
export type StepKey = 'step1' | 'step2' | 'step3' | 'scriptEdit';
|
||||
|
||||
export interface Step {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export type StepMap = Record<FormMonitorType, Step[]>;
|
||||
|
||||
export * from '../../../../../common/runtime_types/monitor_management';
|
||||
export * from '../../../../../common/types/monitor_validation';
|
||||
|
||||
export interface FieldMeta {
|
||||
fieldKey: string;
|
||||
component: React.ComponentType<any>;
|
||||
label?: string;
|
||||
ariaLabel?: string;
|
||||
helpText?: string | React.ReactNode;
|
||||
props?: (params: {
|
||||
field?: ControllerRenderProps;
|
||||
formState: FormState<SyntheticsMonitor>;
|
||||
setValue: UseFormReturn['setValue'];
|
||||
reset: UseFormReturn['reset'];
|
||||
locations: ServiceLocations;
|
||||
dependencies: unknown[];
|
||||
dependenciesFieldMeta: Record<string, ControllerFieldState>;
|
||||
space?: string;
|
||||
isEdit?: boolean;
|
||||
}) => Record<string, any>;
|
||||
controlled?: boolean;
|
||||
required?: boolean;
|
||||
shouldUseSetValue?: boolean;
|
||||
customHook?: (value: unknown) => {
|
||||
// custom hooks are only supported for controlled components and only supported for determining error validation
|
||||
func: Function;
|
||||
params: unknown;
|
||||
fieldKey: string;
|
||||
error: string;
|
||||
};
|
||||
onChange?: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
formOnChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
) => void;
|
||||
showWhen?: [string, any]; // show field when another field equals an arbitrary value
|
||||
validation?: (dependencies: unknown[]) => Parameters<UseFormReturn['register']>[1];
|
||||
error?: React.ReactNode;
|
||||
dependencies?: string[]; // fields that another field may depend for or validation. Values are passed to the validation function
|
||||
}
|
|
@ -7,19 +7,24 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants';
|
||||
import { MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE } from '../../../../../common/constants';
|
||||
import { PLUGIN } from '../../../../../common/constants/plugin';
|
||||
|
||||
export const useMonitorAddEditBreadcrumbs = () => {
|
||||
export const useMonitorAddEditBreadcrumbs = (isEdit?: boolean) => {
|
||||
const kibana = useKibana();
|
||||
const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? '';
|
||||
|
||||
useBreadcrumbs([
|
||||
{
|
||||
text: ADD_MONITOR_CRUMB,
|
||||
href: `${appPath}/${MONITOR_ADD_ROUTE}`,
|
||||
},
|
||||
]);
|
||||
const config = isEdit
|
||||
? {
|
||||
text: EDIT_MONITOR_CRUMB,
|
||||
href: `${appPath}/${MONITOR_EDIT_ROUTE}`,
|
||||
}
|
||||
: {
|
||||
text: ADD_MONITOR_CRUMB,
|
||||
href: `${appPath}/${MONITOR_ADD_ROUTE}`,
|
||||
};
|
||||
|
||||
useBreadcrumbs([config]);
|
||||
};
|
||||
|
||||
export const ADD_MONITOR_CRUMB = i18n.translate(
|
||||
|
@ -28,3 +33,10 @@ export const ADD_MONITOR_CRUMB = i18n.translate(
|
|||
defaultMessage: 'Add monitor',
|
||||
}
|
||||
);
|
||||
|
||||
export const EDIT_MONITOR_CRUMB = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.editMonitorCrumb',
|
||||
{
|
||||
defaultMessage: 'Edit monitor',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -98,6 +98,7 @@ export const Actions = ({ euiTheme, id, name, reloadPage, canEditSynthetics }: P
|
|||
iconType="boxesHorizontal"
|
||||
color="primary"
|
||||
iconSide="right"
|
||||
data-test-subj="syntheticsMonitorListActions"
|
||||
onClick={openPopover}
|
||||
/>
|
||||
);
|
||||
|
@ -139,7 +140,7 @@ export const Actions = ({ euiTheme, id, name, reloadPage, canEditSynthetics }: P
|
|||
key="xpack.synthetics.editMonitor"
|
||||
icon="pencil"
|
||||
onClick={closePopover}
|
||||
href={`${basePath}/app/uptime/edit-monitor/${id}`}
|
||||
href={`${basePath}/app/synthetics/edit-monitor/${id}`}
|
||||
disabled={!canEditSynthetics}
|
||||
>
|
||||
{labels.EDIT_LABEL}
|
||||
|
|
|
@ -42,7 +42,7 @@ export const MonitorsPageHeader = () => {
|
|||
fill
|
||||
iconSide="left"
|
||||
iconType="plusInCircleFilled"
|
||||
href={`${basePath}/app/uptime${MONITOR_ADD_ROUTE}`}
|
||||
href={`${basePath}/app/synthetics${MONITOR_ADD_ROUTE}`}
|
||||
isDisabled={!isEnabled}
|
||||
data-test-subj="syntheticsAddMonitorBtn"
|
||||
>
|
||||
|
|
|
@ -7,25 +7,38 @@
|
|||
|
||||
import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { EuiPageTemplateProps, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
import {
|
||||
EuiPageTemplateProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { OutPortal } from 'react-reverse-portal';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
|
||||
import { useInspectorContext } from '@kbn/observability-plugin/public';
|
||||
import { MonitorAddPage } from './components/monitor_add_edit/monitor_add_page';
|
||||
import { MonitorEditPage } from './components/monitor_add_edit/monitor_edit_page';
|
||||
import { RunTestManually } from './components/monitor_summary/run_test_manually';
|
||||
import { MonitorSummaryHeaderContent } from './components/monitor_summary/monitor_summary_header_content';
|
||||
import { MonitorSummaryTitle } from './components/monitor_summary/monitor_summary_title';
|
||||
import { MonitorSummaryPage } from './components/monitor_summary/monitor_summary';
|
||||
import { GettingStartedPage } from './components/getting_started/getting_started_page';
|
||||
import { MonitorAddEditPage } from './components/monitor_add_edit/monitor_add_edit_page';
|
||||
import { MonitorsPageHeader } from './components/monitors_page/management/page_header/monitors_page_header';
|
||||
import { OverviewPage } from './components/monitors_page/overview/overview_page';
|
||||
import { SyntheticsPageTemplateComponent } from './components/common/pages/synthetics_page_template';
|
||||
import { NotFoundPage } from './components/common/pages/not_found';
|
||||
import { ServiceAllowedWrapper } from './components/common/wrappers/service_allowed_wrapper';
|
||||
import {
|
||||
MonitorTypePortalNode,
|
||||
MonitorDetailsLinkPortalNode,
|
||||
} from './components/monitor_add_edit/portals';
|
||||
import {
|
||||
MONITOR_ADD_ROUTE,
|
||||
MONITOR_EDIT_ROUTE,
|
||||
MONITORS_ROUTE,
|
||||
OVERVIEW_ROUTE,
|
||||
GETTING_STARTED_ROUTE,
|
||||
|
@ -185,27 +198,68 @@ const getRoutes = (
|
|||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.synthetics.addMonitorRoute.title', {
|
||||
defaultMessage: 'Add Monitor | {baseTitle}',
|
||||
title: i18n.translate('xpack.synthetics.createMonitorRoute.title', {
|
||||
defaultMessage: 'Create Monitor | {baseTitle}',
|
||||
values: { baseTitle },
|
||||
}),
|
||||
path: MONITOR_ADD_ROUTE,
|
||||
component: () => (
|
||||
<ServiceAllowedWrapper>
|
||||
<MonitorAddEditPage />
|
||||
<MonitorAddPage />
|
||||
</ServiceAllowedWrapper>
|
||||
),
|
||||
dataTestSubj: 'syntheticsMonitorAddPage',
|
||||
pageHeader: {
|
||||
pageTitle: (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.addMonitor.pageHeader.title"
|
||||
defaultMessage="Add Monitor"
|
||||
id="xpack.synthetics.createMonitor.pageHeader.title"
|
||||
defaultMessage="Create Monitor"
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.addMonitor.pageHeader.description"
|
||||
defaultMessage="For more information about available monitor types and other options, see our {docs}."
|
||||
values={{
|
||||
docs: (
|
||||
<EuiLink target="_blank" href="#">
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.addMonitor.pageHeader.docsLink"
|
||||
defaultMessage="documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// bottomBar: <MonitorManagementBottomBar />,
|
||||
bottomBarProps: { paddingSize: 'm' as const },
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.synthetics.editMonitorRoute.title', {
|
||||
defaultMessage: 'Edit Monitor | {baseTitle}',
|
||||
values: { baseTitle },
|
||||
}),
|
||||
path: MONITOR_EDIT_ROUTE,
|
||||
component: () => (
|
||||
<ServiceAllowedWrapper>
|
||||
<MonitorEditPage />
|
||||
</ServiceAllowedWrapper>
|
||||
),
|
||||
dataTestSubj: 'syntheticsMonitorEditPage',
|
||||
pageHeader: {
|
||||
pageTitle: (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.editMonitor.pageHeader.title"
|
||||
defaultMessage="Edit Monitor"
|
||||
/>
|
||||
),
|
||||
rightSideItems: [<OutPortal node={MonitorTypePortalNode} />],
|
||||
breadcrumbs: [
|
||||
{
|
||||
text: <OutPortal node={MonitorDetailsLinkPortalNode} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { apiService } from '../../../../utils/api_service';
|
||||
import {
|
||||
EncryptedSyntheticsMonitor,
|
||||
ServiceLocationErrors,
|
||||
SyntheticsMonitor,
|
||||
SyntheticsMonitorWithId,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { API_URLS } from '../../../../../common/constants';
|
||||
import { DecryptedSyntheticsMonitorSavedObject } from '../../../../../common/types';
|
||||
|
||||
export const createMonitorAPI = async ({
|
||||
monitor,
|
||||
}: {
|
||||
monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor;
|
||||
}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => {
|
||||
return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor);
|
||||
};
|
||||
|
||||
export const updateMonitorAPI = async ({
|
||||
monitor,
|
||||
id,
|
||||
}: {
|
||||
monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor;
|
||||
id: string;
|
||||
}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitorWithId> => {
|
||||
return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor);
|
||||
};
|
||||
|
||||
export const getMonitorAPI = async ({
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
}): Promise<DecryptedSyntheticsMonitorSavedObject> => {
|
||||
return await apiService.get(`${API_URLS.SYNTHETICS_MONITORS}/${id}`);
|
||||
};
|
|
@ -83,6 +83,7 @@ const Application = (props: SyntheticsAppProps) => {
|
|||
triggersActionsUi: startPlugins.triggersActionsUi,
|
||||
observability: startPlugins.observability,
|
||||
cases: startPlugins.cases,
|
||||
spaces: startPlugins.spaces,
|
||||
}}
|
||||
>
|
||||
<Router history={appMountParameters.history}>
|
||||
|
|
|
@ -112,5 +112,6 @@ export const browserNormalizers: BrowserNormalizerMap = {
|
|||
[ConfigKey.PLAYWRIGHT_OPTIONS]: getBrowserNormalizer(ConfigKey.PLAYWRIGHT_OPTIONS),
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: getBrowserNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID),
|
||||
[ConfigKey.ORIGINAL_SPACE]: getBrowserNormalizer(ConfigKey.ORIGINAL_SPACE),
|
||||
[ConfigKey.TEXT_ASSERTION]: getBrowserNormalizer(ConfigKey.TEXT_ASSERTION),
|
||||
...commonNormalizers,
|
||||
};
|
||||
|
|
|
@ -91,4 +91,5 @@ export const commonNormalizers: CommonNormalizerMap = {
|
|||
fields?.[ConfigKey.NAMESPACE]?.value ?? DEFAULT_NAMESPACE_STRING,
|
||||
[ConfigKey.REVISION]: getCommonNormalizer(ConfigKey.REVISION),
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_SOURCE_TYPE),
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: getCommonNormalizer(ConfigKey.FORM_MONITOR_TYPE),
|
||||
};
|
||||
|
|
|
@ -40,6 +40,7 @@ import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
|
|||
import { CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { PLUGIN } from '../common/constants/plugin';
|
||||
import { MONITORS_ROUTE } from '../common/constants/ui';
|
||||
import {
|
||||
|
@ -76,6 +77,7 @@ export interface ClientPluginsStart {
|
|||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
cases: CasesUiStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
spaces: SpacesPluginStart;
|
||||
cloud?: CloudStart;
|
||||
}
|
||||
|
||||
|
@ -194,7 +196,6 @@ export class UptimePlugin
|
|||
],
|
||||
mount: async (params: AppMountParameters) => {
|
||||
const [coreStart, corePlugins] = await core.getStartServices();
|
||||
|
||||
const { renderApp } = await import('./legacy_uptime/app/render_app');
|
||||
return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev);
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mergeWith } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
SavedObjectsUpdateResponse,
|
||||
|
@ -20,6 +20,7 @@ import {
|
|||
SyntheticsMonitorWithSecrets,
|
||||
SyntheticsMonitor,
|
||||
ConfigKey,
|
||||
FormMonitorType,
|
||||
} from '../../../common/runtime_types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
|
@ -77,10 +78,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
);
|
||||
const normalizedPreviousMonitor = normalizeSecrets(decryptedPreviousMonitor).attributes;
|
||||
|
||||
const editedMonitor = {
|
||||
...normalizedPreviousMonitor,
|
||||
...monitor,
|
||||
};
|
||||
const editedMonitor = mergeWith(normalizedPreviousMonitor, monitor, customizer);
|
||||
|
||||
const validationResult = validateMonitor(editedMonitor as MonitorFields);
|
||||
|
||||
|
@ -94,12 +92,15 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1,
|
||||
};
|
||||
const formattedMonitor = formatSecrets(monitorWithRevision);
|
||||
const isMultiStepMonitor =
|
||||
monitor.type === 'browser' &&
|
||||
monitor[ConfigKey.FORM_MONITOR_TYPE] !== FormMonitorType.SINGLE;
|
||||
|
||||
const editedMonitorSavedObject: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor> =
|
||||
await savedObjectsClient.update<MonitorFields>(
|
||||
syntheticsMonitorType,
|
||||
monitorId,
|
||||
monitor.type === 'browser' ? { ...formattedMonitor, urls: '' } : formattedMonitor
|
||||
isMultiStepMonitor ? { ...formattedMonitor, urls: '' } : formattedMonitor
|
||||
);
|
||||
|
||||
const errors = await syncEditedMonitor({
|
||||
|
@ -185,3 +186,10 @@ export const syncEditedMonitor = async ({
|
|||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure that METADATA is merged deeply, to protect AAD and prevent decryption errors
|
||||
const customizer = (_: any, srcValue: any, key: string) => {
|
||||
if (key !== ConfigKey.METADATA) {
|
||||
return srcValue;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
CommonFields,
|
||||
ConfigKey,
|
||||
DataStream,
|
||||
FormMonitorType,
|
||||
HTTPAdvancedFields,
|
||||
HTTPFields,
|
||||
HTTPSimpleFields,
|
||||
|
@ -75,6 +76,7 @@ describe('validateMonitor', () => {
|
|||
},
|
||||
],
|
||||
[ConfigKey.NAMESPACE]: 'testnamespace',
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
};
|
||||
testMetaData = {
|
||||
is_tls_enabled: false,
|
||||
|
@ -90,6 +92,7 @@ describe('validateMonitor', () => {
|
|||
[ConfigKey.HOSTS]: 'test-hosts',
|
||||
[ConfigKey.WAIT]: '',
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP,
|
||||
};
|
||||
|
||||
testTLSFields = {
|
||||
|
@ -105,6 +108,7 @@ describe('validateMonitor', () => {
|
|||
...testCommonFields,
|
||||
[ConfigKey.METADATA]: testMetaData,
|
||||
[ConfigKey.HOSTS]: 'https://host1.com',
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
|
||||
};
|
||||
|
||||
testTCPAdvancedFields = {
|
||||
|
@ -126,6 +130,7 @@ describe('validateMonitor', () => {
|
|||
[ConfigKey.METADATA]: testMetaData,
|
||||
[ConfigKey.MAX_REDIRECTS]: '3',
|
||||
[ConfigKey.URLS]: 'https://example.com',
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
|
||||
};
|
||||
|
||||
testHTTPAdvancedFields = {
|
||||
|
@ -162,6 +167,7 @@ describe('validateMonitor', () => {
|
|||
testBrowserSimpleFields = {
|
||||
...testZipUrlTLSFields,
|
||||
...testCommonFields,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT,
|
||||
[ConfigKey.JOURNEY_ID]: '',
|
||||
[ConfigKey.PROJECT_ID]: '',
|
||||
|
@ -414,6 +420,7 @@ function getJsonPayload() {
|
|||
' "response.include_body": "never",' +
|
||||
' "check.response.headers": {},' +
|
||||
' "response.include_headers": true,' +
|
||||
' "form_monitor_type": "http",' +
|
||||
' "check.response.status": [' +
|
||||
' "200",' +
|
||||
' "201"' +
|
||||
|
|
|
@ -72,5 +72,6 @@ export const browserFormatters: BrowserFormatMap = {
|
|||
stringToObjectFormatter(fields[ConfigKey.PLAYWRIGHT_OPTIONS] || ''),
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: null,
|
||||
[ConfigKey.ORIGINAL_SPACE]: null,
|
||||
[ConfigKey.TEXT_ASSERTION]: null,
|
||||
...commonFormatters,
|
||||
};
|
||||
|
|
|
@ -29,6 +29,7 @@ export const commonFormatters: CommonFormatMap = {
|
|||
[ConfigKey.REVISION]: null,
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: (fields) =>
|
||||
fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: null,
|
||||
};
|
||||
|
||||
export const arrayFormatter = (value: string[] = []) => (value.length ? value : null);
|
||||
|
|
|
@ -25,6 +25,8 @@ const UI_KEYS_TO_SKIP = [
|
|||
ConfigKey.IS_THROTTLING_ENABLED,
|
||||
ConfigKey.REVISION,
|
||||
ConfigKey.CUSTOM_HEARTBEAT_ID,
|
||||
ConfigKey.FORM_MONITOR_TYPE,
|
||||
ConfigKey.TEXT_ASSERTION,
|
||||
'secrets',
|
||||
];
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
BrowserFields,
|
||||
ConfigKey,
|
||||
DataStream,
|
||||
FormMonitorType,
|
||||
Locations,
|
||||
ProjectBrowserMonitor,
|
||||
ScheduleUnit,
|
||||
|
@ -58,6 +59,7 @@ export const normalizeProjectMonitor = ({
|
|||
const defaultFields = DEFAULT_FIELDS[DataStream.BROWSER];
|
||||
const normalizedFields: NormalizedPublicFields = {
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.BROWSER,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT,
|
||||
[ConfigKey.NAME]: monitor.name || '',
|
||||
[ConfigKey.SCHEDULE]: {
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
before(async () => {
|
||||
await supertestAPI.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
|
||||
await supertestAPI
|
||||
.post('/api/fleet/epm/packages/synthetics/0.9.4')
|
||||
.post('/api/fleet/epm/packages/synthetics/0.10.2')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
|
@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(packagePolicy.policy_id).eql(testFleetPolicyID);
|
||||
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name));
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId));
|
||||
});
|
||||
|
||||
let testFleetPolicyID2: string;
|
||||
|
@ -154,7 +154,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(packagePolicy.policy_id).eql(testFleetPolicyID);
|
||||
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name));
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId));
|
||||
|
||||
packagePolicy = apiResponsePolicy.body.items.find(
|
||||
(pkgPolicy: PackagePolicy) =>
|
||||
|
@ -162,7 +162,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
);
|
||||
|
||||
expect(packagePolicy.policy_id).eql(testFleetPolicyID2);
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name));
|
||||
comparePolicies(
|
||||
packagePolicy,
|
||||
getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId, 'Test private location 1')
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes integration for a removed location from monitor', async () => {
|
||||
|
@ -187,7 +190,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(packagePolicy.policy_id).eql(testFleetPolicyID);
|
||||
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name));
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(httpMonitorJson.name, newMonitorId));
|
||||
|
||||
packagePolicy = apiResponsePolicy.body.items.find(
|
||||
(pkgPolicy: PackagePolicy) =>
|
||||
|
@ -274,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(packagePolicy.policy_id).eql(testFleetPolicyID);
|
||||
expect(packagePolicy.name).eql(`${monitor.name}-Test private location 0-${SPACE_ID}`);
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(monitor.name));
|
||||
comparePolicies(packagePolicy, getTestSyntheticsPolicy(monitor.name, monitorId));
|
||||
} finally {
|
||||
await security.user.delete(username);
|
||||
await security.role.delete(roleName);
|
||||
|
|
|
@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
before(async () => {
|
||||
await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
|
||||
await supertest
|
||||
.post('/api/fleet/epm/packages/synthetics/0.9.4')
|
||||
.post('/api/fleet/epm/packages/synthetics/0.10.2')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
|
@ -1011,7 +1011,18 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
);
|
||||
expect(packagePolicy.policy_id).eql(testPolicyId);
|
||||
|
||||
comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy());
|
||||
const configId = monitorsResponse.body.monitors[0].id;
|
||||
const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID];
|
||||
|
||||
comparePolicies(
|
||||
packagePolicy,
|
||||
getTestProjectSyntheticsPolicy({
|
||||
inputs: {},
|
||||
name: 'check if title is present-Test private location 0',
|
||||
id,
|
||||
configId,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
await deleteMonitor(projectMonitors.monitors[0].id, projectMonitors.project);
|
||||
|
||||
|
@ -1056,7 +1067,18 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(packagePolicy.policy_id).eql(testPolicyId);
|
||||
|
||||
comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy());
|
||||
const configId = monitorsResponse.body.monitors[0].id;
|
||||
const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID];
|
||||
|
||||
comparePolicies(
|
||||
packagePolicy,
|
||||
getTestProjectSyntheticsPolicy({
|
||||
inputs: {},
|
||||
name: 'check if title is present-Test private location 0',
|
||||
id,
|
||||
configId,
|
||||
})
|
||||
);
|
||||
|
||||
await supertest
|
||||
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
|
||||
|
@ -1124,7 +1146,18 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(packagePolicy.policy_id).eql(testPolicyId);
|
||||
|
||||
comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy());
|
||||
const configId = monitorsResponse.body.monitors[0].id;
|
||||
const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID];
|
||||
|
||||
comparePolicies(
|
||||
packagePolicy,
|
||||
getTestProjectSyntheticsPolicy({
|
||||
inputs: {},
|
||||
name: 'check if title is present-Test private location 0',
|
||||
id,
|
||||
configId,
|
||||
})
|
||||
);
|
||||
|
||||
await supertest
|
||||
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
|
||||
|
@ -1199,17 +1232,25 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
|
||||
);
|
||||
|
||||
const configId = monitorsResponse.body.monitors[0].id;
|
||||
const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID];
|
||||
const policyId = `${id}-${testPolicyId}`;
|
||||
|
||||
const packagePolicy = apiResponsePolicy.body.items.find(
|
||||
(pkgPolicy: PackagePolicy) =>
|
||||
pkgPolicy.id ===
|
||||
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] +
|
||||
'-' +
|
||||
testPolicyId
|
||||
(pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId
|
||||
);
|
||||
|
||||
expect(packagePolicy.policy_id).eql(testPolicyId);
|
||||
|
||||
comparePolicies(packagePolicy, getTestProjectSyntheticsPolicy());
|
||||
comparePolicies(
|
||||
packagePolicy,
|
||||
getTestProjectSyntheticsPolicy({
|
||||
inputs: {},
|
||||
name: 'check if title is present-Test private location 0',
|
||||
id,
|
||||
configId,
|
||||
})
|
||||
);
|
||||
|
||||
await supertest
|
||||
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
|
||||
|
@ -1229,18 +1270,21 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
|
||||
);
|
||||
|
||||
const configId2 = monitorsResponse.body.monitors[0].id;
|
||||
const id2 = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID];
|
||||
const policyId2 = `${id}-${testPolicyId}`;
|
||||
|
||||
const packagePolicy2 = apiResponsePolicy2.body.items.find(
|
||||
(pkgPolicy: PackagePolicy) =>
|
||||
pkgPolicy.id ===
|
||||
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] +
|
||||
'-' +
|
||||
testPolicyId
|
||||
(pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId2
|
||||
);
|
||||
|
||||
comparePolicies(
|
||||
packagePolicy2,
|
||||
getTestProjectSyntheticsPolicy({
|
||||
inputs: { enabled: { value: false, type: 'bool' } },
|
||||
name: 'check if title is present-Test private location 0',
|
||||
id: id2,
|
||||
configId: configId2,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
_httpMonitorJson = getFixtureJson('http_monitor');
|
||||
await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
|
||||
await supertest
|
||||
.post('/api/fleet/epm/packages/synthetics/0.9.4')
|
||||
.post('/api/fleet/epm/packages/synthetics/0.10.2')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
|
|
|
@ -66,9 +66,37 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const updates: Partial<HTTPFields> = {
|
||||
[ConfigKey.URLS]: 'https://modified-host.com',
|
||||
[ConfigKey.NAME]: 'Modified name',
|
||||
[ConfigKey.LOCATIONS]: [
|
||||
{
|
||||
id: 'eu-west-01',
|
||||
label: 'Europe West',
|
||||
geo: {
|
||||
lat: 33.2343132435,
|
||||
lon: 73.2342343434,
|
||||
},
|
||||
url: 'https://example-url.com',
|
||||
isServiceManaged: true,
|
||||
},
|
||||
],
|
||||
[ConfigKey.REQUEST_HEADERS_CHECK]: {
|
||||
sampleHeader2: 'sampleValue2',
|
||||
},
|
||||
[ConfigKey.METADATA]: {
|
||||
script_source: {
|
||||
is_generated_script: false,
|
||||
file_name: 'test-file.name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const modifiedMonitor = { ...newMonitor, ...updates };
|
||||
const modifiedMonitor = {
|
||||
...newMonitor,
|
||||
...updates,
|
||||
[ConfigKey.METADATA]: {
|
||||
...newMonitor[ConfigKey.METADATA],
|
||||
...updates[ConfigKey.METADATA],
|
||||
},
|
||||
};
|
||||
|
||||
const editResponse = await supertest
|
||||
.put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"name": "Test HTTP Monitor 03",
|
||||
"namespace": "testnamespace",
|
||||
"origin": "ui",
|
||||
"form_monitor_type": "multistep",
|
||||
"urls": "",
|
||||
"url.port": null
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@
|
|||
"check.response.body.negative": [],
|
||||
"check.response.body.positive": [],
|
||||
"response.include_body": "never",
|
||||
"check.response.headers": {},
|
||||
"check.request.headers": {
|
||||
"sampleHeader": "sampleHeaderValue"
|
||||
},
|
||||
"response.include_headers": true,
|
||||
"check.response.status": [
|
||||
"200",
|
||||
|
@ -37,7 +39,7 @@
|
|||
"value": "testValue",
|
||||
"type": "json"
|
||||
},
|
||||
"check.request.headers": {},
|
||||
"check.response.headers": {},
|
||||
"check.request.method": "",
|
||||
"username": "test-username",
|
||||
"ssl.certificate_authorities": "t.string",
|
||||
|
@ -74,5 +76,6 @@
|
|||
],
|
||||
"namespace": "testnamespace",
|
||||
"revision": 1,
|
||||
"origin": "ui"
|
||||
"origin": "ui",
|
||||
"form_monitor_type": "http"
|
||||
}
|
||||
|
|
|
@ -34,5 +34,6 @@
|
|||
],
|
||||
"name": "Test HTTP Monitor 04",
|
||||
"namespace": "testnamespace",
|
||||
"origin": "ui"
|
||||
"origin": "ui",
|
||||
"form_monitor_type": "icmp"
|
||||
}
|
||||
|
|
|
@ -30,5 +30,6 @@
|
|||
],
|
||||
"name": "Test HTTP Monitor 04",
|
||||
"namespace": "testnamespace",
|
||||
"origin": "ui"
|
||||
"origin": "ui",
|
||||
"form_monitor_type": "tcp"
|
||||
}
|
||||
|
|
|
@ -9,14 +9,18 @@ import { omit, sortBy } from 'lodash';
|
|||
import expect from '@kbn/expect';
|
||||
import { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
|
||||
export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
||||
id: '5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
version: 'WzMyNTcsMV0=',
|
||||
name: '5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
export const getTestSyntheticsPolicy = (
|
||||
name: string,
|
||||
id: string,
|
||||
locationName?: string
|
||||
): PackagePolicy => ({
|
||||
id: '2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
version: 'WzE2MjYsMV0=',
|
||||
name: 'test-monitor-name-Test private location 0-default',
|
||||
namespace: 'default',
|
||||
package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.9.4' },
|
||||
package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.10.2' },
|
||||
enabled: true,
|
||||
policy_id: '5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
policy_id: '27337270-22ed-11ed-8c6b-09a2d21dfbc3',
|
||||
output_id: '',
|
||||
inputs: [
|
||||
{
|
||||
|
@ -48,7 +52,10 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'response.include_headers': { value: true, type: 'bool' },
|
||||
'response.include_body': { value: 'never', type: 'text' },
|
||||
'check.request.method': { value: '', type: 'text' },
|
||||
'check.request.headers': { value: null, type: 'yaml' },
|
||||
'check.request.headers': {
|
||||
value: '{"sampleHeader":"sampleHeaderValue"}',
|
||||
type: 'yaml',
|
||||
},
|
||||
'check.request.body': { value: '"testValue"', type: 'yaml' },
|
||||
'check.response.status': { value: '["200","201"]', type: 'yaml' },
|
||||
'check.response.headers': { value: null, type: 'yaml' },
|
||||
|
@ -60,8 +67,13 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'ssl.key_passphrase': { value: 't.string', type: 'text' },
|
||||
'ssl.verification_mode': { value: 'certificate', type: 'text' },
|
||||
'ssl.supported_protocols': { value: '["TLSv1.1","TLSv1.2"]', type: 'yaml' },
|
||||
location_name: { value: locationName || 'Test private location 0', type: 'text' },
|
||||
id: { value: id, type: 'text' },
|
||||
config_id: { value: id, type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { value: 'ui', type: 'text' },
|
||||
},
|
||||
id: 'synthetics/http-http-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
id: 'synthetics/http-http-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
compiled_stream: {
|
||||
__ui: {
|
||||
is_tls_enabled: false,
|
||||
|
@ -70,6 +82,8 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
},
|
||||
type: 'http',
|
||||
name,
|
||||
id,
|
||||
origin: 'ui',
|
||||
enabled: true,
|
||||
urls: 'https://nextjs-test-synthetics.vercel.app/api/users',
|
||||
schedule: '@every 5m',
|
||||
|
@ -82,6 +96,7 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'response.include_headers': true,
|
||||
'response.include_body': 'never',
|
||||
'check.request.method': null,
|
||||
'check.request.headers': { sampleHeader: 'sampleHeaderValue' },
|
||||
'check.request.body': 'testValue',
|
||||
'check.response.status': ['200', '201'],
|
||||
'ssl.certificate': 't.string',
|
||||
|
@ -91,8 +106,18 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'ssl.verification_mode': 'certificate',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2'],
|
||||
processors: [
|
||||
{ add_observer_metadata: { geo: { name: 'Fleet managed' } } },
|
||||
{ add_fields: { target: '', fields: { 'monitor.fleet_managed': true } } },
|
||||
{
|
||||
add_observer_metadata: { geo: { name: locationName || 'Test private location 0' } },
|
||||
},
|
||||
{
|
||||
add_fields: {
|
||||
target: '',
|
||||
fields: {
|
||||
'monitor.fleet_managed': true,
|
||||
config_id: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -126,8 +151,13 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'ssl.key_passphrase': { type: 'text' },
|
||||
'ssl.verification_mode': { type: 'text' },
|
||||
'ssl.supported_protocols': { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
id: { type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { type: 'text' },
|
||||
},
|
||||
id: 'synthetics/tcp-tcp-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
id: 'synthetics/tcp-tcp-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -150,8 +180,13 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'service.name': { type: 'text' },
|
||||
timeout: { type: 'text' },
|
||||
tags: { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
id: { type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { type: 'text' },
|
||||
},
|
||||
id: 'synthetics/icmp-icmp-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
id: 'synthetics/icmp-icmp-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -177,7 +212,9 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'source.zip_url.folder': { type: 'text' },
|
||||
'source.zip_url.password': { type: 'password' },
|
||||
'source.inline.script': { type: 'yaml' },
|
||||
'source.project.content': { type: 'text' },
|
||||
params: { type: 'yaml' },
|
||||
playwright_options: { type: 'yaml' },
|
||||
screenshots: { type: 'text' },
|
||||
synthetics_args: { type: 'text' },
|
||||
ignore_https_errors: { type: 'bool' },
|
||||
|
@ -191,8 +228,15 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
'source.zip_url.ssl.verification_mode': { type: 'text' },
|
||||
'source.zip_url.ssl.supported_protocols': { type: 'yaml' },
|
||||
'source.zip_url.proxy_url': { type: 'text' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
id: { type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { type: 'text' },
|
||||
'monitor.project.id': { type: 'text' },
|
||||
'monitor.project.name': { type: 'text' },
|
||||
},
|
||||
id: 'synthetics/browser-browser-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
id: 'synthetics/browser-browser-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
compiled_stream: {
|
||||
__ui: null,
|
||||
type: 'browser',
|
||||
|
@ -210,7 +254,7 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'synthetics', dataset: 'browser.network' },
|
||||
id: 'synthetics/browser-browser.network-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
id: 'synthetics/browser-browser.network-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
compiled_stream: {
|
||||
processors: [
|
||||
{ add_observer_metadata: { geo: { name: 'Fleet managed' } } },
|
||||
|
@ -221,7 +265,7 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'synthetics', dataset: 'browser.screenshot' },
|
||||
id: 'synthetics/browser-browser.screenshot-5863efe0-0368-11ed-8df7-a7424c6f5167-5347cd10-0368-11ed-8df7-a7424c6f5167',
|
||||
id: 'synthetics/browser-browser.screenshot-2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default',
|
||||
compiled_stream: {
|
||||
processors: [
|
||||
{ add_observer_metadata: { geo: { name: 'Fleet managed' } } },
|
||||
|
@ -234,9 +278,9 @@ export const getTestSyntheticsPolicy = (name: string): PackagePolicy => ({
|
|||
],
|
||||
is_managed: true,
|
||||
revision: 1,
|
||||
created_at: '2022-07-14T11:30:23.034Z',
|
||||
created_at: '2022-08-23T14:09:17.176Z',
|
||||
created_by: 'system',
|
||||
updated_at: '2022-07-14T11:30:23.034Z',
|
||||
updated_at: '2022-08-23T14:09:17.176Z',
|
||||
updated_by: 'system',
|
||||
});
|
||||
|
||||
|
@ -244,21 +288,27 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
{
|
||||
name,
|
||||
inputs = {},
|
||||
configId,
|
||||
id,
|
||||
}: {
|
||||
name?: string;
|
||||
inputs: Record<string, { value: string | boolean; type: string }>;
|
||||
configId: string;
|
||||
id: string;
|
||||
} = {
|
||||
name: 'check if title is present-Test private location 0',
|
||||
inputs: {},
|
||||
configId: '',
|
||||
id: '',
|
||||
}
|
||||
): PackagePolicy => ({
|
||||
id: 'cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
version: 'WzMwNTMsMV0=',
|
||||
name: 'check if title is present-Test private location 0',
|
||||
id: '4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
version: 'WzEzMDksMV0=',
|
||||
name: '4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-Test private location 0',
|
||||
namespace: 'default',
|
||||
package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.9.4' },
|
||||
package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.10.2' },
|
||||
enabled: true,
|
||||
policy_id: '46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
policy_id: 'd70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
output_id: '',
|
||||
inputs: [
|
||||
{
|
||||
|
@ -298,8 +348,13 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
'ssl.key_passphrase': { type: 'text' },
|
||||
'ssl.verification_mode': { type: 'text' },
|
||||
'ssl.supported_protocols': { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
id: { type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { type: 'text' },
|
||||
},
|
||||
id: 'synthetics/http-http-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
id: 'synthetics/http-http-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -331,8 +386,13 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
'ssl.key_passphrase': { type: 'text' },
|
||||
'ssl.verification_mode': { type: 'text' },
|
||||
'ssl.supported_protocols': { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
id: { type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { type: 'text' },
|
||||
},
|
||||
id: 'synthetics/tcp-tcp-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
id: 'synthetics/tcp-tcp-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -355,8 +415,13 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
'service.name': { type: 'text' },
|
||||
timeout: { type: 'text' },
|
||||
tags: { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
id: { type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { type: 'text' },
|
||||
},
|
||||
id: 'synthetics/icmp-icmp-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
id: 'synthetics/icmp-icmp-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -386,7 +451,16 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
'source.zip_url.folder': { value: '', type: 'text' },
|
||||
'source.zip_url.password': { value: '', type: 'password' },
|
||||
'source.inline.script': { value: null, type: 'yaml' },
|
||||
'source.project.content': {
|
||||
value:
|
||||
'UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA',
|
||||
type: 'text',
|
||||
},
|
||||
params: { value: '', type: 'yaml' },
|
||||
playwright_options: {
|
||||
value: '{"headless":true,"chromiumSandbox":false}',
|
||||
type: 'yaml',
|
||||
},
|
||||
screenshots: { value: 'on', type: 'text' },
|
||||
synthetics_args: { value: null, type: 'text' },
|
||||
ignore_https_errors: { value: false, type: 'bool' },
|
||||
|
@ -400,9 +474,16 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
'source.zip_url.ssl.verification_mode': { value: null, type: 'text' },
|
||||
'source.zip_url.ssl.supported_protocols': { value: null, type: 'yaml' },
|
||||
'source.zip_url.proxy_url': { value: '', type: 'text' },
|
||||
location_name: { value: 'Test private location 0', type: 'text' },
|
||||
id: { value: id, type: 'text' },
|
||||
config_id: { value: configId, type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
origin: { value: 'project', type: 'text' },
|
||||
'monitor.project.id': { value: 'test-suite', type: 'text' },
|
||||
'monitor.project.name': { value: 'test-suite', type: 'text' },
|
||||
...inputs,
|
||||
},
|
||||
id: 'synthetics/browser-browser-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
id: 'synthetics/browser-browser-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
compiled_stream: {
|
||||
__ui: {
|
||||
script_source: { is_generated_script: false, file_name: '' },
|
||||
|
@ -410,15 +491,30 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
},
|
||||
type: 'browser',
|
||||
name: 'check if title is present',
|
||||
id,
|
||||
origin: 'project',
|
||||
enabled: true,
|
||||
schedule: '@every 10m',
|
||||
timeout: null,
|
||||
throttling: '5d/3u/20l',
|
||||
'source.project.content':
|
||||
'UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA',
|
||||
playwright_options: { headless: true, chromiumSandbox: false },
|
||||
screenshots: 'on',
|
||||
'filter_journeys.match': 'check if title is present',
|
||||
processors: [
|
||||
{ add_observer_metadata: { geo: { name: 'Fleet managed' } } },
|
||||
{ add_fields: { target: '', fields: { 'monitor.fleet_managed': true } } },
|
||||
{ add_observer_metadata: { geo: { name: 'Test private location 0' } } },
|
||||
{
|
||||
add_fields: {
|
||||
target: '',
|
||||
fields: {
|
||||
'monitor.fleet_managed': true,
|
||||
config_id: configId,
|
||||
'monitor.project.name': 'test-suite',
|
||||
'monitor.project.id': 'test-suite',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
...Object.keys(inputs).reduce((acc: Record<string, unknown>, key) => {
|
||||
acc[key] = inputs[key].value;
|
||||
|
@ -429,7 +525,7 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'synthetics', dataset: 'browser.network' },
|
||||
id: 'synthetics/browser-browser.network-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
id: 'synthetics/browser-browser.network-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
compiled_stream: {
|
||||
processors: [
|
||||
{ add_observer_metadata: { geo: { name: 'Fleet managed' } } },
|
||||
|
@ -440,7 +536,7 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'synthetics', dataset: 'browser.screenshot' },
|
||||
id: 'synthetics/browser-browser.screenshot-cccec568-e488-4049-a399-def8b6a31f34-test-suite-default-46034710-0ba6-11ed-ba04-5f123b9faa8b',
|
||||
id: 'synthetics/browser-browser.screenshot-4b6abc6c-118b-4d93-a489-1135500d09f1-test-suite-default-d70a46e0-22ea-11ed-8c6b-09a2d21dfbc3',
|
||||
compiled_stream: {
|
||||
processors: [
|
||||
{ add_observer_metadata: { geo: { name: 'Fleet managed' } } },
|
||||
|
@ -453,9 +549,9 @@ export const getTestProjectSyntheticsPolicy = (
|
|||
],
|
||||
is_managed: true,
|
||||
revision: 1,
|
||||
created_at: '2022-07-24T23:13:55.606Z',
|
||||
created_at: '2022-08-23T13:52:42.531Z',
|
||||
created_by: 'system',
|
||||
updated_at: '2022-07-24T23:13:55.606Z',
|
||||
updated_at: '2022-08-23T13:52:42.531Z',
|
||||
updated_by: 'system',
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue