[Uptime] Remove legacy monitor management (#154471)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2023-04-11 18:22:16 +02:00 committed by GitHub
parent c39031e3da
commit 3ebc372cc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
251 changed files with 200 additions and 20395 deletions

View file

@ -140,7 +140,7 @@ export const getCertsRequestBody = ({
field: 'tls.server.hash.sha256',
inner_hits: {
_source: {
includes: ['monitor.id', 'monitor.name', 'url.full'],
includes: ['monitor.id', 'monitor.name', 'url.full', 'config_id'],
},
collapse: {
field: 'monitor.id',
@ -180,6 +180,7 @@ export const processCertsResult = (result: CertificatesResults): CertResult => {
return {
name: monitorPing?.monitor.name,
id: monitorPing?.monitor.id,
configId: monitorPing?.config_id,
url: monitorPing?.url?.full,
};
});

View file

@ -29,6 +29,7 @@ export type GetCertsParams = t.TypeOf<typeof GetCertsParamsType>;
export const CertMonitorType = t.partial({
name: t.string,
id: t.string,
configId: t.string,
url: t.string,
});

View file

@ -6,14 +6,8 @@
*/
export * from './data_view_permissions';
export * from './read_only_user';
export * from './alerts';
export * from './uptime.journey';
export * from './step_duration.journey';
export * from './monitor_details.journey';
export * from './monitor_name.journey';
export * from './monitor_management.journey';
export * from './monitor_management_enablement.journey';
export * from './monitor_details';
export * from './locations';
export * from './private_locations';

View file

@ -1,54 +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 { v4 as uuidv4 } from 'uuid';
import { journey, step, expect, after, Page } from '@elastic/synthetics';
import { recordVideo } from '../../helpers/record_video';
import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management';
journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
const name = `Test monitor ${uuidv4()}`;
after(async () => {
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
step('create basic monitor', async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
await uptime.createBasicHTTPMonitorDetails({
name,
locations: ['US Central'],
apmServiceName: 'synthetics',
url: 'https://www.google.com',
});
await uptime.confirmAndSave();
});
step('navigate to monitor details page', async () => {
await uptime.assertText({ text: name });
await Promise.all([page.waitForNavigation(), page.click(`text=${name}`)]);
await uptime.assertText({ text: name });
const url = await page.textContent('[data-test-subj="monitor-page-url"]');
const type = await page.textContent('[data-test-subj="monitor-page-type"]');
expect(url).toEqual('https://www.google.com(opens in a new tab or window)');
expect(type).toEqual('HTTP');
});
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
const isSuccessful = await uptime.deleteMonitors();
expect(isSuccessful).toBeTruthy();
});
});

View file

@ -48,7 +48,7 @@ journey('MonitorPingRedirects', async ({ page, params }: { page: Page; params: a
await delay(5000);
});
step('go to monitor-management', async () => {
step('go to overview page', async () => {
await monitorDetails.navigateToOverviewPage({
dateRangeEnd: testMonitor.end,
dateRangeStart: testMonitor.start,

View file

@ -1,269 +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 { v4 as uuidv4 } from 'uuid';
import { journey, step, expect, after, Page } from '@elastic/synthetics';
import { byTestId } from '../../helpers/utils';
import { recordVideo } from '../../helpers/record_video';
import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management';
import { DataStream } from '../../../common/runtime_types/monitor_management';
const customLocation = process.env.SYNTHETICS_TEST_LOCATION;
const basicMonitorDetails = {
location: customLocation || 'US Central',
schedule: '3',
};
const httpName = `http monitor ${uuidv4()}`;
const icmpName = `icmp monitor ${uuidv4()}`;
const tcpName = `tcp monitor ${uuidv4()}`;
const browserName = `browser monitor ${uuidv4()}`;
const configuration = {
[DataStream.HTTP]: {
monitorConfig: {
...basicMonitorDetails,
name: httpName,
url: 'https://elastic.co',
locations: [basicMonitorDetails.location],
apmServiceName: 'Sample APM Service',
},
monitorDetails: {
...basicMonitorDetails,
name: httpName,
url: 'https://elastic.co',
},
},
[DataStream.TCP]: {
monitorConfig: {
...basicMonitorDetails,
name: tcpName,
host: 'smtp.gmail.com:587',
locations: [basicMonitorDetails.location],
apmServiceName: 'Sample APM Service',
},
monitorDetails: {
...basicMonitorDetails,
name: tcpName,
host: 'smtp.gmail.com:587',
},
},
[DataStream.ICMP]: {
monitorConfig: {
...basicMonitorDetails,
name: icmpName,
host: '1.1.1.1',
locations: [basicMonitorDetails.location],
apmServiceName: 'Sample APM Service',
},
monitorDetails: {
...basicMonitorDetails,
name: icmpName,
hosts: '1.1.1.1',
},
},
[DataStream.BROWSER]: {
monitorConfig: {
...basicMonitorDetails,
schedule: '10',
name: browserName,
inlineScript: 'step("test step", () => {})',
locations: [basicMonitorDetails.location],
apmServiceName: 'Sample APM Service',
},
monitorDetails: {
...basicMonitorDetails,
schedule: '10',
name: browserName,
},
},
};
const createMonitorJourney = ({
monitorName,
monitorType,
monitorConfig,
monitorDetails,
}: {
monitorName: string;
monitorType: DataStream;
monitorConfig: Record<string, string | string[]>;
monitorDetails: Record<string, string>;
}) => {
journey(
`MonitorManagement-monitor-${monitorType}`,
async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
const isRemote = process.env.SYNTHETICS_REMOTE_ENABLED;
after(async () => {
await uptime.navigateToMonitorManagement();
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
step(`create ${monitorType} monitor`, async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
await uptime.createMonitor({ monitorConfig, monitorType });
const isSuccessful = await uptime.confirmAndSave();
expect(isSuccessful).toBeTruthy();
});
step(`view ${monitorType} details in Monitor Management UI`, async () => {
await uptime.navigateToMonitorManagement();
const hasFailure = await uptime.findMonitorConfiguration(monitorDetails);
expect(hasFailure).toBeFalsy();
});
if (isRemote) {
step('view results in overview page', async () => {
await uptime.navigateToOverviewPage();
await page.waitForSelector(`text=${monitorName}`, { timeout: 160 * 1000 });
});
}
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
const isSuccessful = await uptime.deleteMonitors();
expect(isSuccessful).toBeTruthy();
});
}
);
};
Object.keys(configuration).forEach((type) => {
createMonitorJourney({
monitorType: type as DataStream,
monitorName: `${type} monitor`,
monitorConfig: configuration[type as DataStream].monitorConfig,
monitorDetails: configuration[type as DataStream].monitorDetails,
});
});
journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
const defaultMonitorDetails = {
name: `Sample monitor ${uuidv4()}`,
location: 'US Central',
schedule: '3',
apmServiceName: 'service',
};
after(async () => {
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
step('Check breadcrumb', async () => {
const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent();
expect(lastBreadcrumb).toEqual('Monitor Management');
});
step('check breadcrumbs', async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
const breadcrumbs = await page.$$('[data-test-subj="breadcrumb"]');
expect(await breadcrumbs[1].textContent()).toEqual('Monitor Management');
const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent();
expect(lastBreadcrumb).toEqual('Add monitor');
});
step('create monitor http monitor', async () => {
const monitorDetails = {
...defaultMonitorDetails,
url: 'https://elastic.co',
locations: [basicMonitorDetails.location],
};
await uptime.createBasicHTTPMonitorDetails(monitorDetails);
const isSuccessful = await uptime.confirmAndSave();
expect(isSuccessful).toBeTruthy();
});
step('edit http monitor and check breadcrumb', async () => {
await uptime.editMonitor();
// breadcrumb is available before edit page is loaded, make sure its edit view
await page.waitForSelector(byTestId('monitorManagementMonitorName'), { timeout: 60 * 1000 });
const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]');
expect(await breadcrumbs[1].textContent()).toEqual('Monitor Management');
const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent();
expect(lastBreadcrumb).toEqual('Edit monitor');
});
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
const isSuccessful = await uptime.deleteMonitors();
expect(isSuccessful).toBeTruthy();
});
});
journey(
'MonitorManagement-case-insensitive sort',
async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
const sortedMonitors = [
Object.assign({}, configuration[DataStream.ICMP].monitorConfig, {
name: `A ${uuidv4()}`,
}),
Object.assign({}, configuration[DataStream.ICMP].monitorConfig, {
name: `B ${uuidv4()}`,
}),
Object.assign({}, configuration[DataStream.ICMP].monitorConfig, {
name: `aa ${uuidv4()}`,
}),
];
after(async () => {
await uptime.navigateToMonitorManagement();
await uptime.deleteMonitors();
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
for (const monitorConfig of sortedMonitors) {
step(`create monitor ${monitorConfig.name}`, async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
await uptime.createMonitor({ monitorConfig, monitorType: DataStream.ICMP });
const isSuccessful = await uptime.confirmAndSave();
expect(isSuccessful).toBeTruthy();
});
}
step(`list monitors in Monitor Management UI`, async () => {
await uptime.navigateToMonitorManagement();
await Promise.all(
sortedMonitors.map((monitor) =>
page.waitForSelector(`text=${monitor.name}`, { timeout: 160 * 1000 })
)
);
// Get first cell value from monitor table -> monitor name
const rows = page.locator('tbody tr td:first-child div.euiTableCellContent');
expect(await rows.count()).toEqual(sortedMonitors.length);
const expectedSort = sortedMonitors
.map((mn) => mn.name)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
expect(await rows.allTextContents()).toEqual(expectedSort);
});
}
);

View file

@ -1,56 +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 { journey, step, expect, after, Page } from '@elastic/synthetics';
import { recordVideo } from '../../helpers/record_video';
import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management';
journey(
'Monitor Management-enablement-superuser',
async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
after(async () => {
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
step('check add monitor button', async () => {
expect(await uptime.checkIsEnabled()).toBe(false);
});
step('enable Monitor Management', async () => {
await uptime.enableMonitorManagement();
expect(await uptime.checkIsEnabled()).toBe(true);
});
}
);
journey(
'MonitorManagement-enablement-obs-admin',
async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
step('check add monitor button', async () => {
expect(await uptime.checkIsEnabled()).toBe(false);
});
step('check that enabled toggle does not appear', async () => {
expect(await page.$(`[data-test-subj=syntheticsEnableSwitch]`)).toBeFalsy();
});
}
);

View file

@ -1,68 +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 { v4 as uuidv4 } from 'uuid';
import { journey, step, expect, Page } from '@elastic/synthetics';
import { byTestId } from '../../helpers/utils';
import { recordVideo } from '../../helpers/record_video';
import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management';
journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const name = `Test monitor ${uuidv4()}`;
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
const createBasicMonitor = async () => {
await uptime.createBasicHTTPMonitorDetails({
name,
locations: ['US Central'],
apmServiceName: 'synthetics',
url: 'https://www.google.com',
});
};
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(true);
});
step('create basic monitor', async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
await createBasicMonitor();
await uptime.confirmAndSave();
});
step(`shows error if name already exists`, async () => {
await uptime.navigateToAddMonitor();
await uptime.createBasicHTTPMonitorDetails({
name,
locations: ['US Central'],
apmServiceName: 'synthetics',
url: 'https://www.google.com',
});
await uptime.assertText({ text: 'Monitor name already exists.' });
expect(await page.isEnabled(byTestId('monitorTestNowRunBtn'))).toBeFalsy();
});
step(`form becomes valid after change`, async () => {
await uptime.createBasicMonitorDetails({
name: 'Test monitor 2',
locations: ['US Central'],
apmServiceName: 'synthetics',
});
expect(await page.isEnabled(byTestId('monitorTestNowRunBtn'))).toBeTruthy();
});
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
await uptime.deleteMonitors();
await uptime.enableMonitorManagement(false);
});
});

View file

@ -1,98 +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 { v4 as uuidv4 } from 'uuid';
import { journey, step, expect, before } from '@elastic/synthetics';
import { TIMEOUT_60_SEC, byTestId } from '../../../helpers/utils';
import { recordVideo } from '../../../helpers/record_video';
import { cleanTestMonitors } from '../../synthetics/services/add_monitor';
import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management';
journey('AddPrivateLocationMonitor', async ({ page, params }) => {
recordVideo(page);
page.setDefaultTimeout(TIMEOUT_60_SEC.timeout);
const kibanaUrl = params.kibanaUrl;
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
const monitorName = `Private location monitor ${uuidv4()}`;
let monitorId: string;
before(async () => {
await cleanTestMonitors(params);
page.on('request', (evt) => {
if (
evt.resourceType() === 'fetch' &&
evt.url().includes('/internal/uptime/service/monitors?preserve_namespace=true')
) {
evt
.response()
?.then((res) => res?.json())
.then((res) => {
monitorId = res.id;
});
}
});
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
step('login to Kibana', async () => {
await uptime.loginToKibana();
const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`);
expect(await invalid.isVisible()).toBeFalsy();
});
step('enable management', async () => {
await uptime.enableMonitorManagement();
});
step('Click text=Add monitor', async () => {
await page.click('text=Add monitor');
expect(page.url()).toBe(`${kibanaUrl}/app/uptime/add-monitor`);
await uptime.waitForLoadingToFinish();
await page.click('input[name="name"]');
await page.fill('input[name="name"]', monitorName);
await page.click('label:has-text("Test private location Private")', TIMEOUT_60_SEC);
await page.selectOption('select', 'http');
await page.click(byTestId('syntheticsUrlField'));
await page.fill(byTestId('syntheticsUrlField'), 'https://www.google.com');
await page.click('text=Save monitor');
await page.click(`text=${monitorName}`);
await page.click('[data-test-subj="superDatePickerApplyTimeButton"]');
});
step('Integration cannot be edited in Fleet', async () => {
await page.goto(`${kibanaUrl}/app/integrations/detail/synthetics/policies`);
await page.waitForSelector('h1:has-text("Elastic Synthetics")');
await page.click(`text=${monitorName}`);
await page.waitForSelector('h1:has-text("Edit Elastic Synthetics integration")');
await page.waitForSelector('text="This package policy is managed by the Synthetics app."');
});
step('Integration edit button leads to correct Synthetics edit page', async () => {
const url = page.url();
const policyId = url.split('edit-integration/').pop();
const btn = await page.locator(byTestId('syntheticsEditMonitorButton'));
expect(await btn.getAttribute('href')).toBe(
`/app/synthetics/edit-monitor/${monitorId}?packagePolicyId=${policyId}`
);
await page.click('text="Edit in Synthetics"');
await page.waitForSelector('h1:has-text("Edit Monitor")');
await page.waitForSelector('h2:has-text("Monitor details")');
expect(await page.inputValue('[data-test-subj="syntheticsMonitorConfigName"]')).toBe(
monitorName
);
});
});

View file

@ -1,9 +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.
*/
export * from './manage_locations';
export * from './add_monitor_private_location';

View file

@ -1,80 +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 { journey, step, expect } from '@elastic/synthetics';
import { byTestId, TIMEOUT_60_SEC } from '../../../helpers/utils';
import { recordVideo } from '../../../helpers/record_video';
import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management';
journey('ManagePrivateLocation', async ({ page, params: { kibanaUrl } }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
step('login to Kibana', async () => {
await uptime.loginToKibana();
const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`);
expect(await invalid.isVisible()).toBeFalsy();
});
step('enable management', async () => {
await uptime.enableMonitorManagement();
});
step('Open manage location', async () => {
await page.click('button:has-text("Private locations")');
});
step('Add two agent policies', async () => {
await page.click('text=Create agent policy');
await addAgentPolicy('Fleet test policy');
await page.click('text=Create agent policy');
await addAgentPolicy('Fleet test policy 2');
await page.goBack({ waitUntil: 'networkidle' });
await page.goBack({ waitUntil: 'networkidle' });
await page.goBack({ waitUntil: 'networkidle' });
});
step('Add new private location', async () => {
await page.waitForTimeout(30 * 1000);
await page.click('button:has-text("Close")');
await page.click('button:has-text("Private locations")');
await page.click(byTestId('addPrivateLocationButton'));
await addPrivateLocation('Test private location', 'Fleet test policy');
});
step('Add another location', async () => {
await page.click(byTestId('addPrivateLocationButton'), TIMEOUT_60_SEC);
await page.click('[aria-label="Select agent policy"]');
await page.isDisabled(`button[role="option"]:has-text("Fleet test policyAgents: 0")`);
await addPrivateLocation('Test private location 2', 'Fleet test policy 2');
});
const addPrivateLocation = async (name: string, policy: string) => {
await page.click('[aria-label="Location name"]');
await page.fill('[aria-label="Location name"]', name);
await page.click('[aria-label="Select agent policy"]');
await page.click(`button[role="option"]:has-text("${policy}Agents: 0")`);
await page.click('button:has-text("Save")');
};
const addAgentPolicy = async (name: string) => {
await page.click('[placeholder="Choose a name"]');
await page.fill('[placeholder="Choose a name"]', name);
await page.click('text=Collect system logs and metrics');
await page.click('div[role="dialog"] button:has-text("Create agent policy")');
};
});

View file

@ -1,8 +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.
*/
export * from './monitor_management';

View file

@ -1,32 +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 { expect, journey, Page, step } from '@elastic/synthetics';
import { byTestId } from '../../../helpers/utils';
import { recordVideo } from '../../../helpers/record_video';
import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management';
journey(
'Monitor Management read only user',
async ({ page, params }: { page: Page; params: any }) => {
recordVideo(page);
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement(false);
});
step('login to Kibana', async () => {
await uptime.loginToKibana('viewer', 'changeme');
});
step('Adding monitor is disabled', async () => {
expect(await page.isEnabled(byTestId('syntheticsAddMonitorBtn'))).toBeFalsy();
});
}
);

View file

@ -32,7 +32,7 @@ journey('StepsDuration', async ({ page, params }) => {
});
step('Go to monitor details', async () => {
await page.click('button:has-text("test-monitor - inline")');
await page.click('text="test-monitor - inline"');
expect(page.url()).toBe(`${baseUrl}/monitor/dGVzdC1tb25pdG9yLWlubGluZQ==/?${queryParams}`);
});

View file

@ -6,8 +6,9 @@
*/
import { Page } from '@elastic/synthetics';
import { byTestId, delay } from '../../helpers/utils';
import { monitorManagementPageProvider } from './monitor_management';
import { utilsPageProvider } from '../utils';
import { byTestId, delay, getQuerystring } from '../../helpers/utils';
import { loginPageProvider } from '../login';
interface AlertType {
id: string;
@ -15,9 +16,21 @@ interface AlertType {
}
export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; kibanaUrl: string }) {
return {
...monitorManagementPageProvider({ page, kibanaUrl }),
const remoteKibanaUrl = process.env.SYNTHETICS_REMOTE_KIBANA_URL;
const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED);
const remoteUsername = process.env.SYNTHETICS_REMOTE_KIBANA_USERNAME;
const remotePassword = process.env.SYNTHETICS_REMOTE_KIBANA_PASSWORD;
const basePath = isRemote ? remoteKibanaUrl : kibanaUrl;
const overview = `${basePath}/app/uptime`;
return {
...loginPageProvider({
page,
isRemote,
username: isRemote ? remoteUsername : 'elastic',
password: isRemote ? remotePassword : 'changeme',
}),
...utilsPageProvider({ page }),
async navigateToMonitorDetails(monitorId: string) {
await page.click(byTestId(`monitor-page-link-${monitorId}`));
},
@ -104,8 +117,8 @@ export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; ki
},
async selectAlertThreshold(threshold: string) {
await this.clickByTestSubj('uptimeAnomalySeverity');
await this.clickByTestSubj('anomalySeveritySelect');
await page.click(byTestId('uptimeAnomalySeverity'));
await page.click(byTestId('anomalySeveritySelect'));
await page.click(`text=${threshold}`);
},
@ -125,5 +138,10 @@ export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; ki
await page.waitForSelector('text=Rule successfully disabled!');
await this.closeAnomalyDetectionMenu();
},
async navigateToOverviewPage(options?: object) {
await page.goto(`${overview}${options ? `?${getQuerystring(options)}` : ''}`, {
waitUntil: 'networkidle',
});
},
};
}

View file

@ -1,301 +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 { expect, Page } from '@elastic/synthetics';
import { getQuerystring, TIMEOUT_60_SEC } from '../../helpers/utils';
import { DataStream } from '../../../common/runtime_types/monitor_management';
import { loginPageProvider } from '../login';
import { utilsPageProvider } from '../utils';
export function monitorManagementPageProvider({
page,
kibanaUrl,
}: {
page: Page;
kibanaUrl: string;
}) {
const remoteKibanaUrl = process.env.SYNTHETICS_REMOTE_KIBANA_URL;
const remoteUsername = process.env.SYNTHETICS_REMOTE_KIBANA_USERNAME;
const remotePassword = process.env.SYNTHETICS_REMOTE_KIBANA_PASSWORD;
const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED);
const basePath = isRemote ? remoteKibanaUrl : kibanaUrl;
const monitorManagement = `${basePath}/app/uptime/manage-monitors/all`;
const addMonitor = `${basePath}/app/uptime/add-monitor`;
const overview = `${basePath}/app/uptime`;
return {
...loginPageProvider({
page,
isRemote,
username: isRemote ? remoteUsername : 'elastic',
password: isRemote ? remotePassword : 'changeme',
}),
...utilsPageProvider({ page }),
async navigateToMonitorManagement(doLogin = false) {
await page.goto(monitorManagement, {
waitUntil: 'networkidle',
});
if (doLogin) {
await this.loginToKibana();
}
await this.waitForMonitorManagementLoadingToFinish();
},
async waitForMonitorManagementLoadingToFinish() {
while (true) {
if ((await page.$(this.byTestId('uptimeLoader'))) === null) break;
await page.waitForTimeout(5 * 1000);
}
},
async enableMonitorManagement(shouldEnable: boolean = true) {
const isEnabled = await this.checkIsEnabled();
if (isEnabled === shouldEnable) {
return;
}
const [toggle, button] = await Promise.all([
page.$(this.byTestId('syntheticsEnableSwitch')),
page.$(this.byTestId('syntheticsEnableButton')),
]);
if (toggle === null && button === null) {
return null;
}
if (toggle) {
if (isEnabled !== shouldEnable) {
await toggle.click();
}
} else {
await button?.click();
}
if (shouldEnable) {
await this.findByText('Monitor Management enabled successfully.');
} else {
await this.findByText('Monitor Management disabled successfully.');
}
},
async getEnableToggle() {
return await this.findByTestSubj('syntheticsEnableSwitch');
},
async getEnableButton() {
return await this.findByTestSubj('syntheticsEnableSwitch');
},
async getAddMonitorButton() {
return await this.findByTestSubj('syntheticsAddMonitorBtn');
},
async checkIsEnabled() {
await page.waitForTimeout(5 * 1000);
const addMonitorBtn = await this.getAddMonitorButton();
const isDisabled = await addMonitorBtn.isDisabled();
return !isDisabled;
},
async navigateToAddMonitor() {
await page.goto(addMonitor, {
waitUntil: 'networkidle',
});
},
async navigateToOverviewPage(options?: object) {
await page.goto(`${overview}${options ? `?${getQuerystring(options)}` : ''}`, {
waitUntil: 'networkidle',
});
},
async clickAddMonitor() {
const isEnabled = await this.checkIsEnabled();
expect(isEnabled).toBe(true);
await page.click('text=Add monitor');
},
async deleteMonitors() {
let isSuccessful: boolean = false;
await page.waitForSelector('[data-test-subj="monitorManagementDeleteMonitor"]');
while (true) {
if ((await page.$(this.byTestId('monitorManagementDeleteMonitor'))) === null) break;
await page.click(this.byTestId('monitorManagementDeleteMonitor'), { delay: 800 });
await page.waitForSelector('[data-test-subj="confirmModalTitleText"]');
await this.clickByTestSubj('confirmModalConfirmButton');
isSuccessful = Boolean(await this.findByTestSubj('uptimeDeleteMonitorSuccess'));
await page.waitForTimeout(5 * 1000);
}
return isSuccessful;
},
async editMonitor() {
await page.click(this.byTestId('monitorManagementEditMonitor'), { delay: 800 });
},
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 selectMonitorType(monitorType: string) {
await this.selectByTestSubj('syntheticsMonitorTypeField', monitorType);
},
async ensureIsOnMonitorConfigPage() {
await page.isVisible('[data-test-subj=monitorSettingsSection]');
},
async confirmAndSave(isEditPage?: boolean) {
await this.ensureIsOnMonitorConfigPage();
if (isEditPage) {
await page.click('text=Update monitor');
} else {
await page.click('text=Save monitor');
}
return await this.findByText('Monitor added successfully.');
},
async fillCodeEditor(value: string) {
await page.fill('[data-test-subj=codeEditorContainer] textarea', value);
},
async selectLocations({ locations }: { locations: string[] }) {
for (let i = 0; i < locations.length; i++) {
await page.check(`text=${locations[i]}`, TIMEOUT_60_SEC);
}
},
async createBasicMonitorDetails({
name,
apmServiceName,
locations,
}: {
name: string;
apmServiceName: string;
locations: string[];
}) {
await this.fillByTestSubj('monitorManagementMonitorName', name);
await this.fillByTestSubj('syntheticsAPMServiceName', apmServiceName);
await this.selectLocations({ locations });
},
async createBasicHTTPMonitorDetails({
name,
url,
apmServiceName,
locations,
}: {
name: string;
url: string;
apmServiceName: string;
locations: string[];
}) {
await this.selectMonitorType('http');
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
await this.fillByTestSubj('syntheticsUrlField', url);
},
async createBasicTCPMonitorDetails({
name,
host,
apmServiceName,
locations,
}: {
name: string;
host: string;
apmServiceName: string;
locations: string[];
}) {
await this.selectMonitorType('tcp');
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
await this.fillByTestSubj('syntheticsTCPHostField', host);
},
async createBasicICMPMonitorDetails({
name,
host,
apmServiceName,
locations,
}: {
name: string;
host: string;
apmServiceName: string;
locations: string[];
}) {
await this.selectMonitorType('icmp');
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
await this.fillByTestSubj('syntheticsICMPHostField', host);
},
async createBasicBrowserMonitorDetails(
{
name,
inlineScript,
zipUrl,
folder,
params,
username,
password,
apmServiceName,
locations,
}: {
name: string;
inlineScript?: string;
zipUrl?: string;
folder?: string;
params?: string;
username?: string;
password?: string;
apmServiceName: string;
locations: string[];
},
isInline: boolean = false
) {
await this.selectMonitorType('browser');
await this.createBasicMonitorDetails({ name, apmServiceName, locations });
if (isInline && inlineScript) {
await this.clickByTestSubj('syntheticsSourceTab__inline');
await this.fillCodeEditor(inlineScript);
return;
}
await this.fillByTestSubj('syntheticsBrowserZipUrl', zipUrl || '');
await this.fillByTestSubj('syntheticsBrowserZipUrlFolder', folder || '');
await this.fillByTestSubj('syntheticsBrowserZipUrlUsername', username || '');
await this.fillByTestSubj('syntheticsBrowserZipUrlPassword', password || '');
await this.fillCodeEditor(params || '');
},
async createMonitor({
monitorConfig,
monitorType,
}: {
monitorConfig: Record<string, string | string[]>;
monitorType: DataStream;
}) {
switch (monitorType) {
case DataStream.HTTP:
// @ts-ignore
await this.createBasicHTTPMonitorDetails(monitorConfig);
break;
case DataStream.TCP:
// @ts-ignore
await this.createBasicTCPMonitorDetails(monitorConfig);
break;
case DataStream.ICMP:
// @ts-ignore
await this.createBasicICMPMonitorDetails(monitorConfig);
break;
case DataStream.BROWSER:
// @ts-ignore
await this.createBasicBrowserMonitorDetails(monitorConfig, true);
break;
default:
break;
}
},
};
}

View file

@ -0,0 +1,52 @@
/*
* 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 { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui';
import { InvalidApiKeyCalloutCallout } from './invalid_api_key_callout';
import * as labels from './labels';
import { useEnablement } from '../../../hooks';
export const DisabledCallout = ({ total }: { total: number }) => {
const { enablement, enableSynthetics, invalidApiKeyError, loading } = useEnablement();
const showDisableCallout = !enablement.isEnabled && total > 0;
if (invalidApiKeyError) {
return <InvalidApiKeyCalloutCallout />;
}
if (!showDisableCallout) {
return null;
}
return (
<EuiCallOut title={labels.CALLOUT_MANAGEMENT_DISABLED} color="warning" iconType="help">
<p>{labels.CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{enablement.canEnable || loading ? (
<EuiButton
data-test-subj="syntheticsMonitorManagementPageButton"
fill
color="primary"
onClick={() => {
enableSynthetics();
}}
isLoading={loading}
>
{labels.SYNTHETICS_ENABLE_LABEL}
</EuiButton>
) : (
<p>
{labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '}
<EuiLink data-test-subj="syntheticsMonitorManagementPageLink" href="#" target="_blank">
{labels.LEARN_MORE_LABEL}
</EuiLink>
</p>
)}
</EuiCallOut>
);
};

View file

@ -8,20 +8,16 @@
import React from 'react';
import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useEnablement } from '../../components/monitor_management/hooks/use_enablement';
import { useEnablement } from '../../../hooks';
export const InvalidApiKeyCalloutCallout = () => {
const { enablement, enableSynthetics, invalidApiKeyError } = useEnablement();
if (!invalidApiKeyError || !enablement.isEnabled) {
return null;
}
const { enablement, enableSynthetics, loading } = useEnablement();
return (
<>
<EuiCallOut title={API_KEY_MISSING} color="warning" iconType="help">
<p>{CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{enablement.canEnable ? (
{enablement.canEnable || loading ? (
<EuiButton
data-test-subj="syntheticsInvalidApiKeyCalloutCalloutButton"
fill
@ -29,6 +25,7 @@ export const InvalidApiKeyCalloutCallout = () => {
onClick={() => {
enableSynthetics();
}}
isLoading={loading}
>
{SYNTHETICS_ENABLE_LABEL}
</EuiButton>

View file

@ -7,9 +7,9 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { DisabledCallout } from './management/disabled_callout';
import { useOverviewStatus } from './hooks/use_overview_status';
import { GETTING_STARTED_ROUTE } from '../../../../../common/constants';
@ -33,9 +33,8 @@ const MonitorManagementPage: React.FC = () => {
const {
error: enablementError,
enablement: { isEnabled, canEnable },
enablement: { isEnabled },
loading: enablementLoading,
enableSynthetics,
} = useEnablement();
useOverviewStatus({ scopeStatusByLocation: false });
@ -59,37 +58,7 @@ const MonitorManagementPage: React.FC = () => {
errorTitle={labels.ERROR_HEADING_LABEL}
errorBody={labels.ERROR_HEADING_BODY}
>
{!isEnabled && syntheticsMonitors.length > 0 ? (
<>
<EuiCallOut title={labels.CALLOUT_MANAGEMENT_DISABLED} color="warning" iconType="help">
<p>{labels.CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{canEnable ? (
<EuiButton
data-test-subj="syntheticsMonitorManagementPageButton"
fill
color="primary"
onClick={() => {
enableSynthetics();
}}
>
{labels.SYNTHETICS_ENABLE_LABEL}
</EuiButton>
) : (
<p>
{labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '}
<EuiLink
data-test-subj="syntheticsMonitorManagementPageLink"
href="#"
target="_blank"
>
{labels.LEARN_MORE_LABEL}
</EuiLink>
</p>
)}
</EuiCallOut>
<EuiSpacer size="s" />
</>
) : null}
<DisabledCallout total={absoluteTotal} />
<MonitorListContainer isEnabled={isEnabled} monitorListProps={monitorListProps} />
</Loader>
{showEmptyState && <EnablementEmptyState />}

View file

@ -9,6 +9,7 @@ import { EuiFlexGroup, EuiSpacer, EuiFlexItem } from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { Redirect, useLocation } from 'react-router-dom';
import { DisabledCallout } from '../management/disabled_callout';
import { FilterGroup } from '../common/monitor_filters/filter_group';
import { OverviewAlerts } from './overview/overview_alerts';
import { useEnablement } from '../../../hooks';
@ -99,6 +100,7 @@ export const OverviewPage: React.FC = () => {
return (
<>
<DisabledCallout total={absoluteTotal} />
<EuiFlexGroup gutterSize="s" wrap={true}>
<EuiFlexItem>
<SearchField />

View file

@ -20,10 +20,10 @@ export function useEnablement() {
const { loading, error, enablement } = useSelector(selectSyntheticsEnablement);
useEffect(() => {
if (!enablement) {
if (!enablement && !loading) {
dispatch(getSyntheticsEnablement());
}
}, [dispatch, enablement]);
}, [dispatch, enablement, loading]);
return {
enablement: {

View file

@ -24,7 +24,9 @@ export const disableSyntheticsFailure = createAction<IHttpSerializedFetchError>(
);
export const enableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] ENABLE');
export const enableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS');
export const enableSyntheticsSuccess = createAction<MonitorManagementEnablementResult>(
'[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS'
);
export const enableSyntheticsFailure = createAction<IHttpSerializedFetchError>(
'[SYNTHETICS_ENABLEMENT] ENABLE FAILURE'
);

View file

@ -25,6 +25,6 @@ export const fetchDisableSynthetics = async (): Promise<{}> => {
return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT);
};
export const fetchEnableSynthetics = async (): Promise<{}> => {
export const fetchEnableSynthetics = async (): Promise<MonitorManagementEnablementResult> => {
return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT);
};

View file

@ -68,17 +68,12 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder)
.addCase(enableSynthetics, (state) => {
state.loading = true;
state.enablement = null;
})
.addCase(enableSyntheticsSuccess, (state, action) => {
state.loading = false;
state.error = null;
state.enablement = {
canEnable: state.enablement?.canEnable ?? false,
areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false,
canManageApiKeys: state.enablement?.canManageApiKeys ?? false,
isValidApiKey: state.enablement?.isValidApiKey ?? false,
isEnabled: true,
};
state.enablement = action.payload;
})
.addCase(enableSyntheticsFailure, (state, action) => {
state.loading = false;

View file

@ -6,18 +6,14 @@ exports[`CertMonitors renders expected elements for valid props 1`] = `
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="syntheticsMonitorPageLinkLink"
type="button"
data-test-subj="monitor-page-link-bad-ssl-dashboard"
href="/monitor/YmFkLXNzbC1kYXNoYm9hcmQ="
rel="noreferrer"
>
<a
data-test-subj="monitor-page-link-bad-ssl-dashboard"
href="/monitor/YmFkLXNzbC1kYXNoYm9hcmQ="
>
bad-ssl-dashboard
</a>
</button>
bad-ssl-dashboard
</a>
</span>
</span>
<span>
@ -25,18 +21,14 @@ exports[`CertMonitors renders expected elements for valid props 1`] = `
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="syntheticsMonitorPageLinkLink"
type="button"
data-test-subj="monitor-page-link-elastic-co"
href="/monitor/ZWxhc3RpYy1jbw=="
rel="noreferrer"
>
<a
data-test-subj="monitor-page-link-elastic-co"
href="/monitor/ZWxhc3RpYy1jbw=="
>
elastic
</a>
</button>
elastic
</a>
</span>
</span>
<span>
@ -44,18 +36,14 @@ exports[`CertMonitors renders expected elements for valid props 1`] = `
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
<a
class="euiLink emotion-euiLink-primary"
data-test-subj="syntheticsMonitorPageLinkLink"
type="button"
data-test-subj="monitor-page-link-extended-validation"
href="/monitor/ZXh0ZW5kZWQtdmFsaWRhdGlvbg=="
rel="noreferrer"
>
<a
data-test-subj="monitor-page-link-extended-validation"
href="/monitor/ZXh0ZW5kZWQtdmFsaWRhdGlvbg=="
>
extended-validation
</a>
</button>
extended-validation
</a>
</span>
</span>
</span>

View file

@ -21,7 +21,7 @@ export const CertMonitors: React.FC<Props> = ({ monitors }) => {
<span key={mon.id}>
{ind > 0 && ', '}
<EuiToolTip content={mon.url}>
<MonitorPageLink monitorId={mon.id!} linkParameters={''}>
<MonitorPageLink monitorId={mon.id!} configId={mon.configId} linkParameters={''}>
{mon.name || mon.id}
</MonitorPageLink>
</EuiToolTip>

View file

@ -29,9 +29,7 @@ describe('CertificateList', () => {
/>
);
expect(
getByText('No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+')
).toBeInTheDocument();
expect(getByText('No Certificates found.')).toBeInTheDocument();
});
it('renders certificates list', () => {

View file

@ -67,8 +67,8 @@ export const COPY_FINGERPRINT = i18n.translate('xpack.synthetics.certs.list.copy
defaultMessage: 'Click to copy fingerprint value',
});
export const NO_CERTS_AVAILABLE = i18n.translate('xpack.synthetics.certs.list.empty', {
defaultMessage: 'No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+',
export const NO_CERTS_AVAILABLE = i18n.translate('xpack.synthetics.certs.list.noCerts', {
defaultMessage: 'No Certificates found.',
});
export const LOADING_CERTIFICATES = i18n.translate('xpack.synthetics.certificates.loading', {

View file

@ -1,23 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MonitorPageLink component renders a help link when link parameters present 1`] = `
<EuiLink
data-test-subj="syntheticsMonitorPageLinkLink"
>
<Link
data-test-subj="monitor-page-link-bad-ssl"
to="/monitor/YmFkLXNzbA==/selectedPingStatus=down"
/>
</EuiLink>
<ReactRouterEuiLink
data-test-subj="monitor-page-link-bad-ssl"
to="/monitor/YmFkLXNzbA==/selectedPingStatus=down"
/>
`;
exports[`MonitorPageLink component renders the link properly 1`] = `
<EuiLink
data-test-subj="syntheticsMonitorPageLinkLink"
>
<Link
data-test-subj="monitor-page-link-bad-ssl"
to="/monitor/YmFkLXNzbA=="
/>
</EuiLink>
<ReactRouterEuiLink
data-test-subj="monitor-page-link-bad-ssl"
to="/monitor/YmFkLXNzbA=="
/>
`;

View file

@ -5,34 +5,29 @@
* 2.0.
*/
import { EuiHeaderLink } from '@elastic/eui';
import { EuiHeaderLink, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { MONITOR_MANAGEMENT_ROUTE } from '../../../../../common/constants';
export const ManageMonitorsBtn = () => {
const history = useHistory();
return (
<EuiHeaderLink
aria-label={NAVIGATE_LABEL}
color="text"
data-test-subj="syntheticsManagementPageLink"
href={history.createHref({
pathname: MONITOR_MANAGEMENT_ROUTE,
})}
>
<FormattedMessage
id="xpack.synthetics.page_header.manageMonitors"
defaultMessage="Monitor Management"
/>
</EuiHeaderLink>
<EuiToolTip content={NAVIGATE_LABEL}>
<EuiHeaderLink
aria-label={NAVIGATE_LABEL}
color="text"
data-test-subj="syntheticsManagementPageLink"
>
<FormattedMessage
id="xpack.synthetics.page_header.manageMonitors"
defaultMessage="Monitor Management"
/>
</EuiHeaderLink>
</EuiToolTip>
);
};
const NAVIGATE_LABEL = i18n.translate('xpack.synthetics.page_header.manageLink.label', {
defaultMessage: 'Navigate to the Uptime Monitor Management page',
const NAVIGATE_LABEL = i18n.translate('xpack.synthetics.page_header.manageLink.not', {
defaultMessage:
'Monitor Management is no longer available in Uptime, use the Synthetics app instead.',
});

View file

@ -7,13 +7,15 @@
import React, { FC } from 'react';
import { EuiLink } from '@elastic/eui';
import { Link } from 'react-router-dom';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ReactRouterEuiLink } from './react_router_helpers';
interface DetailPageLinkProps {
/**
* MonitorId to be used to redirect to detail page
*/
monitorId: string;
configId?: string;
/**
* Link parameters usually filter states
*/
@ -23,19 +25,31 @@ interface DetailPageLinkProps {
export const MonitorPageLink: FC<DetailPageLinkProps> = ({
children,
monitorId,
configId,
linkParameters,
}) => {
const basePath = useKibana().services.http?.basePath.get();
if (configId) {
return (
<EuiLink
data-test-subj="syntheticsMonitorPageLinkLink"
href={`${basePath}/app/synthetics/monitor/${configId}`}
>
{children}
</EuiLink>
);
}
const getLocationTo = () => {
// encode monitorId param as 64 base string to make it a valid URL, since it can be a url
return linkParameters
? `/monitor/${btoa(monitorId)}/${linkParameters}`
: `/monitor/${btoa(monitorId)}`;
};
return (
<EuiLink data-test-subj="syntheticsMonitorPageLinkLink">
<Link data-test-subj={`monitor-page-link-${monitorId}`} to={getLocationTo()}>
{children}
</Link>
</EuiLink>
<ReactRouterEuiLink data-test-subj={`monitor-page-link-${monitorId}`} to={getLocationTo()}>
{children}
</ReactRouterEuiLink>
);
};

View file

@ -1,109 +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 { fireEvent } from '@testing-library/react';
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import { BrowserAdvancedFields } from './advanced_fields';
import {
ConfigKey,
BrowserAdvancedFields as BrowserAdvancedFieldsType,
BrowserSimpleFields,
Validation,
DataStream,
} from '../types';
import {
BrowserAdvancedFieldsContextProvider,
BrowserSimpleFieldsContextProvider,
defaultBrowserAdvancedFields as defaultConfig,
defaultBrowserSimpleFields,
} from '../contexts';
import { validate as centralValidation } from '../../monitor_management/validation';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
const defaultValidation = centralValidation[DataStream.BROWSER];
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
describe('<BrowserAdvancedFields />', () => {
const WrappedComponent = ({
defaultValues = defaultConfig,
defaultSimpleFields = defaultBrowserSimpleFields,
validate = defaultValidation,
children,
onFieldBlur,
}: {
defaultValues?: BrowserAdvancedFieldsType;
defaultSimpleFields?: BrowserSimpleFields;
validate?: Validation;
children?: React.ReactNode;
onFieldBlur?: (field: ConfigKey) => void;
}) => {
return (
<IntlProvider locale="en">
<BrowserSimpleFieldsContextProvider defaultValues={defaultSimpleFields}>
<BrowserAdvancedFieldsContextProvider defaultValues={defaultValues}>
<BrowserAdvancedFields validate={validate} onFieldBlur={onFieldBlur}>
{children}
</BrowserAdvancedFields>
</BrowserAdvancedFieldsContextProvider>
</BrowserSimpleFieldsContextProvider>
</IntlProvider>
);
};
it('renders BrowserAdvancedFields', () => {
const { getByLabelText } = render(<WrappedComponent />);
const syntheticsArgs = getByLabelText('Synthetics args');
const screenshots = getByLabelText('Screenshot options') as HTMLInputElement;
expect(screenshots.value).toEqual(defaultConfig[ConfigKey.SCREENSHOTS]);
expect(syntheticsArgs).toBeInTheDocument();
});
describe('handles changing fields', () => {
it('for screenshot options', () => {
const { getByLabelText } = render(<WrappedComponent />);
const screenshots = getByLabelText('Screenshot options') as HTMLInputElement;
userEvent.selectOptions(screenshots, ['off']);
expect(screenshots.value).toEqual('off');
});
it('calls onFieldBlur after change', () => {
const onFieldBlur = jest.fn();
const { getByLabelText } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const screenshots = getByLabelText('Screenshot options') as HTMLInputElement;
userEvent.selectOptions(screenshots, ['off']);
fireEvent.blur(screenshots);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.SCREENSHOTS);
});
});
it('does not display filter options (zip url has been deprecated)', () => {
const { queryByText } = render(<WrappedComponent />);
expect(
queryByText(
/Use these options to apply the selected monitor settings to a subset of the tests in your suite./
)
).not.toBeInTheDocument();
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
expect(upstream).toBeInTheDocument();
});
});

View file

@ -1,162 +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, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiAccordion, EuiSelect, EuiCheckbox, EuiFormRow, EuiSpacer } from '@elastic/eui';
import { ComboBox } from '../combo_box';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { useBrowserAdvancedFieldsContext } from '../contexts';
import { ConfigKey, Validation, ScreenshotOption } from '../types';
import { OptionalLabel } from '../optional_label';
import { ThrottlingFields } from './throttling_fields';
interface Props {
validate: Validation;
children?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const BrowserAdvancedFields = memo<Props>(
({ validate, children, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<EuiAccordion
id="syntheticsIntegrationBrowserAdvancedOptions"
buttonContent="Advanced Browser options"
data-test-subj="syntheticsBrowserAdvancedFieldsAccordion"
>
<EuiSpacer size="m" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.title"
defaultMessage="Synthetics agent options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.description"
defaultMessage="Provide fine-tuned configuration for the synthetics agent."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.ignoreHttpsErrors.helpText"
defaultMessage="Set this option to true to disable TLS/SSL validation in the synthetics browser. This is useful for testing sites that use self-signed certs."
/>
</>
}
data-test-subj="syntheticsBrowserIgnoreHttpsErrors"
>
<EuiCheckbox
id="syntheticsBrowserIgnoreHttpsErrorsCheckbox"
checked={fields[ConfigKey.IGNORE_HTTPS_ERRORS]}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.ignoreHttpsErrors.label"
defaultMessage="Ignore HTTPS errors"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.IGNORE_HTTPS_ERRORS,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.IGNORE_HTTPS_ERRORS)}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.screenshots.label"
defaultMessage="Screenshot options"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.screenshots.helpText"
defaultMessage="Set this option to manage the screenshots captured by the synthetics agent."
/>
}
>
<EuiSelect
options={requestMethodOptions}
value={fields[ConfigKey.SCREENSHOTS]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.SCREENSHOTS,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.SCREENSHOTS)}
data-test-subj="syntheticsBrowserScreenshots"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.syntheticsArgs.label"
defaultMessage="Synthetics args"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.syntheticsArgs.helpText"
defaultMessage="Extra arguments to pass to the synthetics agent package. Takes a list of strings. This is useful in rare scenarios, and should not ordinarily need to be set."
/>
}
>
<ComboBox
selectedOptions={fields[ConfigKey.SYNTHETICS_ARGS]}
onChange={(value) =>
handleInputChange({ value, configKey: ConfigKey.SYNTHETICS_ARGS })
}
onBlur={() => onFieldBlur?.(ConfigKey.SYNTHETICS_ARGS)}
data-test-subj="syntheticsBrowserSyntheticsArgs"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<ThrottlingFields
validate={validate}
minColumnWidth={minColumnWidth}
onFieldBlur={onFieldBlur}
/>
{children}
</EuiAccordion>
);
}
);
const requestMethodOptions = Object.values(ScreenshotOption).map((option) => ({
value: option,
text: option.replace(/-/g, ' '),
}));

View file

@ -1,125 +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 { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../../lib/helper/rtl_helpers';
import { ScriptRecorderFields } from './script_recorder_fields';
import { PolicyConfigContextProvider } from '../contexts';
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 (
<PolicyConfigContextProvider isEditable={isEditable}>
<ScriptRecorderFields script={script} fileName={fileName} onChange={onChange} />
</PolicyConfigContextProvider>
);
};
beforeEach(() => {
jest.clearAllMocks();
file = new File([testScript], 'samplescript.js', { type: 'text/javascript' });
});
it('renders ScriptRecorderFields', () => {
const { getByText, queryByText } = render(<WrappedComponent />);
const downloadLink = getByText('Download the Elastic Synthetics Recorder');
expect(downloadLink).toBeInTheDocument();
expect(downloadLink).toHaveAttribute('target', '_blank');
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: '' });
});
});
});

View file

@ -1,140 +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, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutHeader,
EuiFormRow,
EuiCodeBlock,
EuiTitle,
EuiButton,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { usePolicyConfigContext } from '../contexts/policy_config_context';
import { Uploader } from './uploader';
interface Props {
onChange: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void;
script: string;
fileName?: string;
}
export function ScriptRecorderFields({ onChange, script, fileName }: Props) {
const [showScript, setShowScript] = useState(false);
const { isEditable } = usePolicyConfigContext();
const handleUpload = useCallback(
({ scriptText, fileName: fileNameT }: { scriptText: string; fileName: string }) => {
onChange({ scriptText, fileName: fileNameT });
},
[onChange]
);
return (
<>
<EuiSpacer size="m" />
<EuiLink
data-test-subj="syntheticsScriptRecorderFieldsDownloadTheElasticSyntheticsRecorderLink"
href="https://github.com/elastic/synthetics-recorder/releases/"
target="_blank"
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.recorderLink"
defaultMessage="Download the Elastic Synthetics Recorder"
/>
</EuiLink>
<EuiSpacer size="m" />
{isEditable && script ? (
<EuiFormRow label="Testing script">
<EuiText size="s">
<strong>{fileName}</strong>
</EuiText>
</EuiFormRow>
) : (
<Uploader onUpload={handleUpload} />
)}
{script && (
<>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="syntheticsScriptRecorderFieldsShowScriptButton"
onClick={() => setShowScript(true)}
iconType="editorCodeBlock"
iconSide="right"
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.showScriptLabel"
defaultMessage="Show script"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{isEditable && (
<EuiButton
data-test-subj="syntheticsScriptRecorderFieldsRemoveScriptButton"
onClick={() => onChange({ scriptText: '', fileName: '' })}
iconType="trash"
iconSide="right"
color="danger"
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.removeScriptLabel"
defaultMessage="Remove script"
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
{showScript && (
<EuiFlyout
ownFocus
onClose={() => setShowScript(false)}
aria-labelledby="syntheticsBrowserScriptBlockHeader"
closeButtonProps={{ 'aria-label': 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',
}
);

View file

@ -1,110 +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, { memo, useMemo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow } from '@elastic/eui';
import { Validation } from '../types';
import { ConfigKey, MonitorFields } from '../types';
import { useBrowserSimpleFieldsContext } from '../contexts';
import { ScheduleField } from '../schedule_field';
import { SourceField } from './source_field';
import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const BrowserSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields, defaultValues } = useBrowserSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
const onChangeSourceField = useCallback(
({ inlineScript, params, isGeneratedScript, fileName }) => {
setFields((prevFields) => ({
...prevFields,
[ConfigKey.SOURCE_INLINE]: inlineScript,
[ConfigKey.PARAMS]: params,
[ConfigKey.METADATA]: {
...prevFields[ConfigKey.METADATA],
script_source: {
is_generated_script: isGeneratedScript,
file_name: fileName,
},
},
}));
},
[setFields]
);
return (
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
id="syntheticsFleetScheduleField--number syntheticsFleetScheduleField--unit"
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval"
defaultMessage="Frequency"
/>
}
isInvalid={!!validate[ConfigKey.SCHEDULE]?.(fields as Partial<MonitorFields>)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval.error"
defaultMessage="Monitor frequency is required"
/>
}
>
<ScheduleField
onChange={(schedule) =>
handleInputChange({
value: schedule,
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.sourceType.label"
defaultMessage="Source Type"
/>
}
>
<SourceField
validate={validate}
onChange={onChangeSourceField}
onFieldBlur={onFieldBlur}
defaultConfig={useMemo(
() => ({
inlineScript: defaultValues[ConfigKey.SOURCE_INLINE],
params: defaultValues[ConfigKey.PARAMS],
isGeneratedScript:
defaultValues[ConfigKey.METADATA].script_source?.is_generated_script,
fileName: defaultValues[ConfigKey.METADATA].script_source?.file_name,
}),
[defaultValues]
)}
/>
</EuiFormRow>
</SimpleFieldsWrapper>
);
});

View file

@ -1,92 +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 'jest-canvas-mock';
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { render } from '../../../lib/helper/rtl_helpers';
import { IPolicyConfigContextProvider } from '../contexts/policy_config_context';
import { SourceField, Props } from './source_field';
import { BrowserSimpleFieldsContextProvider, PolicyConfigContextProvider } from '../contexts';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility'),
useGeneratedHtmlId: () => `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}
id={props.id}
onChange={props.onChange}
/>
</>
),
};
});
const onChange = jest.fn();
const onBlur = jest.fn();
describe('<SourceField />', () => {
const WrappedComponent = ({
defaultConfig,
}: Omit<IPolicyConfigContextProvider, 'children'> & Partial<Props>) => {
return (
<PolicyConfigContextProvider>
<BrowserSimpleFieldsContextProvider>
<SourceField onChange={onChange} onFieldBlur={onBlur} defaultConfig={defaultConfig} />
</BrowserSimpleFieldsContextProvider>
</PolicyConfigContextProvider>
);
};
beforeEach(() => {
jest.clearAllMocks();
});
it('selects inline script by default', () => {
render(<WrappedComponent />);
expect(
screen.getByText('Runs Synthetic test scripts that are defined inline.')
).toBeInTheDocument();
});
it('does not show ZipUrl source type', async () => {
render(<WrappedComponent />);
expect(screen.queryByTestId('syntheticsSourceTab__zipUrl')).not.toBeInTheDocument();
});
it('shows params for all source types', async () => {
const { getByText, getByTestId } = render(<WrappedComponent />);
const inlineTab = getByTestId('syntheticsSourceTab__inline');
fireEvent.click(inlineTab);
expect(getByText('Parameters')).toBeInTheDocument();
const recorder = getByTestId('syntheticsSourceTab__scriptRecorder');
fireEvent.click(recorder);
expect(getByText('Parameters')).toBeInTheDocument();
});
});

View file

@ -1,231 +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, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
EuiCode,
EuiTabbedContent,
EuiFormRow,
EuiSpacer,
EuiBetaBadge,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { OptionalLabel } from '../optional_label';
import { CodeEditor } from '../code_editor';
import { ScriptRecorderFields } from './script_recorder_fields';
import { ConfigKey, MonacoEditorLangId, Validation } from '../types';
enum SourceType {
INLINE = 'syntheticsBrowserInlineConfig',
SCRIPT_RECORDER = 'syntheticsBrowserScriptRecorderConfig',
}
interface SourceConfig {
inlineScript: string;
params: string;
isGeneratedScript?: boolean;
fileName?: string;
}
export interface Props {
onChange: (sourceConfig: SourceConfig) => void;
onFieldBlur: (field: ConfigKey) => void;
defaultConfig?: SourceConfig;
validate?: Validation;
}
export const defaultValues = {
inlineScript: '',
params: '',
isGeneratedScript: false,
fileName: '',
};
const getDefaultTab = (defaultConfig: SourceConfig) => {
if (defaultConfig.inlineScript && defaultConfig.isGeneratedScript) {
return SourceType.SCRIPT_RECORDER;
} else {
return SourceType.INLINE;
}
};
export const SourceField = ({
onChange,
onFieldBlur,
defaultConfig = defaultValues,
validate,
}: Props) => {
const [sourceType, setSourceType] = useState<SourceType>(getDefaultTab(defaultConfig));
const [config, setConfig] = useState<SourceConfig>(defaultConfig);
useEffect(() => {
onChange(config);
}, [config, onChange]);
const isSourceInlineInvalid =
validate?.[ConfigKey.SOURCE_INLINE]?.({
[ConfigKey.SOURCE_INLINE]: config.inlineScript,
}) ?? false;
const params = (
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.params.label"
defaultMessage="Parameters"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.params.helpText"
defaultMessage="Use JSON to define parameters that can be referenced in your script with {code}"
values={{ code: <EuiCode>params.value</EuiCode> }}
/>
}
>
<CodeEditor
ariaLabel={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBody.codeEditor.json.ariaLabel',
{
defaultMessage: 'JSON code editor',
}
)}
id="jsonParamsEditor"
languageId={MonacoEditorLangId.JSON}
onChange={(code) => {
setConfig((prevConfig) => ({ ...prevConfig, params: code }));
onFieldBlur(ConfigKey.PARAMS);
}}
value={config.params}
data-test-subj="syntheticsBrowserParams"
/>
</EuiFormRow>
);
const tabs = [
{
id: 'syntheticsBrowserInlineConfig',
name: (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.inlineScript.label"
defaultMessage="Inline script"
/>
),
'data-test-subj': `syntheticsSourceTab__inline`,
content: (
<>
<EuiFormRow
isInvalid={isSourceInlineInvalid}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.inlineScript.error"
defaultMessage="Script is required"
/>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.inlineScript.helpText"
defaultMessage="Runs Synthetic test scripts that are defined inline."
/>
}
>
<CodeEditor
ariaLabel={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.requestBody.codeEditor.javascript.ariaLabel',
{
defaultMessage: 'JavaScript code editor',
}
)}
id="javascript"
languageId={MonacoEditorLangId.JAVASCRIPT}
onChange={(code) => {
setConfig((prevConfig) => ({ ...prevConfig, inlineScript: code }));
onFieldBlur(ConfigKey.SOURCE_INLINE);
}}
value={config.inlineScript}
/>
</EuiFormRow>
{params}
</>
),
},
{
id: 'syntheticsBrowserScriptRecorderConfig',
name: (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.label"
defaultMessage="Script recorder"
/>
</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,
inlineScript: scriptText,
isGeneratedScript: true,
fileName,
}))
}
script={config.inlineScript}
fileName={config.fileName}
/>
<EuiSpacer size="s" />
{params}
</>
),
},
];
return (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs.find((tab) => tab.id === sourceType)}
autoFocus="selected"
onTabClick={(tab) => {
if (tab.id !== sourceType) {
setConfig(defaultValues);
}
setSourceType(tab.id as SourceType);
}}
/>
);
};
const StyledBetaBadgeWrapper = styled(EuiFlexItem)`
.euiToolTipAnchor {
display: flex;
}
`;

View file

@ -1,361 +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 { fireEvent } from '@testing-library/react';
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import { ThrottlingFields } from './throttling_fields';
import {
DataStream,
BrowserAdvancedFields,
BrowserSimpleFields,
Validation,
ConfigKey,
BandwidthLimitKey,
} from '../types';
import {
BrowserAdvancedFieldsContextProvider,
BrowserSimpleFieldsContextProvider,
PolicyConfigContextProvider,
IPolicyConfigContextProvider,
defaultPolicyConfigValues,
defaultBrowserAdvancedFields as defaultConfig,
defaultBrowserSimpleFields,
} from '../contexts';
import { validate as centralValidation } from '../../monitor_management/validation';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
const defaultValidation = centralValidation[DataStream.BROWSER];
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
describe('<ThrottlingFields />', () => {
const defaultLocation = {
id: 'test',
label: 'Test',
geo: { lat: 1, lon: 2 },
url: 'https://example.com',
isServiceManaged: true,
};
const WrappedComponent = ({
defaultValues = defaultConfig,
defaultSimpleFields = defaultBrowserSimpleFields,
policyConfigOverrides = {},
validate = defaultValidation,
onFieldBlur,
}: {
defaultValues?: BrowserAdvancedFields;
defaultSimpleFields?: BrowserSimpleFields;
policyConfigOverrides?: Partial<IPolicyConfigContextProvider>;
validate?: Validation;
onFieldBlur?: (field: ConfigKey) => void;
}) => {
const policyConfigValues = { ...defaultPolicyConfigValues, ...policyConfigOverrides };
return (
<IntlProvider locale="en">
<BrowserSimpleFieldsContextProvider defaultValues={defaultSimpleFields}>
<BrowserAdvancedFieldsContextProvider defaultValues={defaultValues}>
<PolicyConfigContextProvider {...policyConfigValues}>
<ThrottlingFields validate={validate} onFieldBlur={onFieldBlur} />
</PolicyConfigContextProvider>
</BrowserAdvancedFieldsContextProvider>
</BrowserSimpleFieldsContextProvider>
</IntlProvider>
);
};
it('renders ThrottlingFields', () => {
const { getByLabelText, getByTestId } = render(<WrappedComponent />);
const enableSwitch = getByTestId('syntheticsBrowserIsThrottlingEnabled');
const downloadSpeed = getByLabelText('Download Speed');
const uploadSpeed = getByLabelText('Upload Speed');
const latency = getByLabelText('Latency');
expect(enableSwitch).toBeChecked();
expect(downloadSpeed).toBeInTheDocument();
expect(uploadSpeed).toBeInTheDocument();
expect(latency).toBeInTheDocument();
});
describe('handles changing fields', () => {
it('for the enable switch', () => {
const { getByTestId } = render(<WrappedComponent />);
const enableSwitch = getByTestId('syntheticsBrowserIsThrottlingEnabled');
userEvent.click(enableSwitch);
expect(enableSwitch).not.toBeChecked();
});
it('for the download option', () => {
const { getByLabelText } = render(<WrappedComponent />);
const downloadSpeed = getByLabelText('Download Speed') as HTMLInputElement;
userEvent.clear(downloadSpeed);
userEvent.type(downloadSpeed, '1337');
expect(downloadSpeed.value).toEqual('1337');
});
it('for the upload option', () => {
const { getByLabelText } = render(<WrappedComponent />);
const uploadSpeed = getByLabelText('Upload Speed') as HTMLInputElement;
userEvent.clear(uploadSpeed);
userEvent.type(uploadSpeed, '1338');
expect(uploadSpeed.value).toEqual('1338');
});
it('for the latency option', () => {
const { getByLabelText } = render(<WrappedComponent />);
const latency = getByLabelText('Latency') as HTMLInputElement;
userEvent.clear(latency);
userEvent.type(latency, '1339');
expect(latency.value).toEqual('1339');
});
});
describe('calls onBlur on fields', () => {
const onFieldBlur = jest.fn();
afterEach(() => {
jest.resetAllMocks();
});
it('for the enable switch', () => {
const { getByTestId } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const enableSwitch = getByTestId('syntheticsBrowserIsThrottlingEnabled');
fireEvent.focus(enableSwitch);
fireEvent.blur(enableSwitch);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.IS_THROTTLING_ENABLED);
});
it('for throttling inputs', () => {
const { getByLabelText } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const downloadSpeed = getByLabelText('Download Speed') as HTMLInputElement;
const uploadSpeed = getByLabelText('Upload Speed') as HTMLInputElement;
const latency = getByLabelText('Latency') as HTMLInputElement;
fireEvent.blur(downloadSpeed);
fireEvent.blur(uploadSpeed);
fireEvent.blur(latency);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.DOWNLOAD_SPEED);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.UPLOAD_SPEED);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.LATENCY);
});
});
describe('validates changing fields', () => {
it('disallows negative/zero download speeds', () => {
const { getByLabelText, queryByText } = render(<WrappedComponent />);
const downloadSpeed = getByLabelText('Download Speed') as HTMLInputElement;
userEvent.clear(downloadSpeed);
userEvent.type(downloadSpeed, '-1337');
expect(queryByText('Download speed must be greater than zero.')).toBeInTheDocument();
userEvent.clear(downloadSpeed);
userEvent.type(downloadSpeed, '0');
expect(queryByText('Download speed must be greater than zero.')).toBeInTheDocument();
userEvent.clear(downloadSpeed);
userEvent.type(downloadSpeed, '1');
expect(queryByText('Download speed must be greater than zero.')).not.toBeInTheDocument();
});
it('disallows negative/zero upload speeds', () => {
const { getByLabelText, queryByText } = render(<WrappedComponent />);
const uploadSpeed = getByLabelText('Upload Speed') as HTMLInputElement;
userEvent.clear(uploadSpeed);
userEvent.type(uploadSpeed, '-1337');
expect(queryByText('Upload speed must be greater than zero.')).toBeInTheDocument();
userEvent.clear(uploadSpeed);
userEvent.type(uploadSpeed, '0');
expect(queryByText('Upload speed must be greater than zero.')).toBeInTheDocument();
userEvent.clear(uploadSpeed);
userEvent.type(uploadSpeed, '1');
expect(queryByText('Upload speed must be greater than zero.')).not.toBeInTheDocument();
});
it('disallows negative latency values', () => {
const { getByLabelText, queryByText } = render(<WrappedComponent />);
const latency = getByLabelText('Latency') as HTMLInputElement;
userEvent.clear(latency);
userEvent.type(latency, '-1337');
expect(queryByText('Latency must not be negative.')).toBeInTheDocument();
userEvent.clear(latency);
userEvent.type(latency, '0');
expect(queryByText('Latency must not be negative.')).not.toBeInTheDocument();
userEvent.clear(latency);
userEvent.type(latency, '1');
expect(queryByText('Latency must not be negative.')).not.toBeInTheDocument();
});
});
describe('throttling warnings', () => {
const throttling = {
[BandwidthLimitKey.DOWNLOAD]: 100,
[BandwidthLimitKey.UPLOAD]: 50,
};
const defaultLocations = [defaultLocation];
it('shows automatic throttling warnings only when throttling is disabled', () => {
const { getByTestId, queryByText } = render(
<WrappedComponent
policyConfigOverrides={{ throttling, defaultLocations, runsOnService: true }}
/>
);
expect(queryByText('Automatic cap')).not.toBeInTheDocument();
expect(
queryByText(
"When disabling throttling, your monitor will still have its bandwidth capped by the configurations of the Synthetics Nodes in which it's running."
)
).not.toBeInTheDocument();
const enableSwitch = getByTestId('syntheticsBrowserIsThrottlingEnabled');
userEvent.click(enableSwitch);
expect(queryByText('Automatic cap')).toBeInTheDocument();
expect(
queryByText(
"When disabling throttling, your monitor will still have its bandwidth capped by the configurations of the Synthetics Nodes in which it's running."
)
).toBeInTheDocument();
});
it("shows throttling warnings when exceeding the node's download limits", () => {
const { getByLabelText, queryByText } = render(
<WrappedComponent
policyConfigOverrides={{ throttling, defaultLocations, runsOnService: true }}
/>
);
const downloadLimit = throttling[BandwidthLimitKey.DOWNLOAD];
const download = getByLabelText('Download Speed') as HTMLInputElement;
userEvent.clear(download);
userEvent.type(download, String(downloadLimit + 1));
expect(
queryByText(
`You have exceeded the download limit for Synthetic Nodes. The download value can't be larger than ${downloadLimit}Mbps.`
)
).toBeInTheDocument();
expect(
queryByText("You've exceeded the Synthetics Node bandwidth limits")
).toBeInTheDocument();
expect(
queryByText(
'When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped.'
)
).toBeInTheDocument();
userEvent.clear(download);
userEvent.type(download, String(downloadLimit - 1));
expect(
queryByText(
`You have exceeded the download limit for Synthetic Nodes. The download value can't be larger than ${downloadLimit}Mbps.`
)
).not.toBeInTheDocument();
expect(
queryByText("You've exceeded the Synthetics Node bandwidth limits")
).not.toBeInTheDocument();
expect(
queryByText(
'When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped.'
)
).not.toBeInTheDocument();
});
it("shows throttling warnings when exceeding the node's upload limits", () => {
const { getByLabelText, queryByText } = render(
<WrappedComponent
policyConfigOverrides={{ throttling, defaultLocations, runsOnService: true }}
/>
);
const uploadLimit = throttling[BandwidthLimitKey.UPLOAD];
const upload = getByLabelText('Upload Speed') as HTMLInputElement;
userEvent.clear(upload);
userEvent.type(upload, String(uploadLimit + 1));
expect(
queryByText(
`You have exceeded the upload limit for Synthetic Nodes. The upload value can't be larger than ${uploadLimit}Mbps.`
)
).toBeInTheDocument();
expect(
queryByText("You've exceeded the Synthetics Node bandwidth limits")
).toBeInTheDocument();
expect(
queryByText(
'When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped.'
)
).toBeInTheDocument();
userEvent.clear(upload);
userEvent.type(upload, String(uploadLimit - 1));
expect(
queryByText(
`You have exceeded the upload limit for Synthetic Nodes. The upload value can't be larger than ${uploadLimit}Mbps.`
)
).not.toBeInTheDocument();
expect(
queryByText("You've exceeded the Synthetics Node bandwidth limits")
).not.toBeInTheDocument();
expect(
queryByText(
'When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped.'
)
).not.toBeInTheDocument();
});
});
it('only displays download, upload, and latency fields with throttling is on', () => {
const { getByLabelText, getByTestId } = render(<WrappedComponent />);
const enableSwitch = getByTestId('syntheticsBrowserIsThrottlingEnabled');
const downloadSpeed = getByLabelText('Download Speed');
const uploadSpeed = getByLabelText('Upload Speed');
const latency = getByLabelText('Latency');
expect(downloadSpeed).toBeInTheDocument();
expect(uploadSpeed).toBeInTheDocument();
expect(latency).toBeInTheDocument();
userEvent.click(enableSwitch);
expect(downloadSpeed).not.toBeInTheDocument();
expect(uploadSpeed).not.toBeInTheDocument();
expect(latency).not.toBeInTheDocument();
});
});

View file

@ -1,293 +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, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiSwitch,
EuiSpacer,
EuiFormRow,
EuiFieldNumber,
EuiText,
EuiCallOut,
} from '@elastic/eui';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { OptionalLabel } from '../optional_label';
import { useBrowserAdvancedFieldsContext, usePolicyConfigContext } from '../contexts';
import { Validation, ConfigKey, BandwidthLimitKey } from '../types';
interface Props {
validate?: Validation;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
readOnly?: boolean;
}
type ThrottlingConfigs =
| ConfigKey.IS_THROTTLING_ENABLED
| ConfigKey.DOWNLOAD_SPEED
| ConfigKey.UPLOAD_SPEED
| ConfigKey.LATENCY;
export const ThrottlingDisabledCallout = () => {
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.automatic_node_cap.title"
defaultMessage="Automatic cap"
/>
}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.automatic_node_cap.message"
defaultMessage="When disabling throttling, your monitor will still have its bandwidth capped by the configurations of the Synthetics Nodes in which it's running."
/>
</EuiCallOut>
);
};
export const ThrottlingExceededCallout = () => {
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.exceeded_throttling.title"
defaultMessage="You've exceeded the Synthetics Node bandwidth limits"
/>
}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.exceeded_throttling.message"
defaultMessage="When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped."
/>
</EuiCallOut>
);
};
export const ThrottlingExceededMessage = ({
throttlingField,
limit,
}: {
throttlingField: string;
limit: number;
}) => {
return (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.throttling_exceeded.message"
defaultMessage="You have exceeded the { throttlingField } limit for Synthetic Nodes. The { throttlingField } value can't be larger than { limit }Mbps."
values={{ throttlingField, limit }}
/>
);
};
export const ThrottlingFields = memo<Props>(
({ validate, minColumnWidth, onFieldBlur, readOnly = false }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const { runsOnService, throttling } = usePolicyConfigContext();
const maxDownload = throttling[BandwidthLimitKey.DOWNLOAD];
const maxUpload = throttling[BandwidthLimitKey.UPLOAD];
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ThrottlingConfigs }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
const exceedsDownloadLimits =
runsOnService && parseFloat(fields[ConfigKey.DOWNLOAD_SPEED]) > maxDownload;
const exceedsUploadLimits =
runsOnService && parseFloat(fields[ConfigKey.UPLOAD_SPEED]) > maxUpload;
const isThrottlingEnabled = fields[ConfigKey.IS_THROTTLING_ENABLED];
const throttlingInputs = isThrottlingEnabled ? (
<>
<EuiSpacer size="m" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.download.label"
defaultMessage="Download Speed"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={
(validate ? !!validate[ConfigKey.DOWNLOAD_SPEED]?.(fields) : false) ||
exceedsDownloadLimits
}
error={
exceedsDownloadLimits ? (
<ThrottlingExceededMessage throttlingField="download" limit={maxDownload} />
) : (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.download.error"
defaultMessage="Download speed must be greater than zero."
/>
)
}
>
<EuiFieldNumber
min={0}
step={0.001}
value={fields[ConfigKey.DOWNLOAD_SPEED]}
onChange={(event) => {
handleInputChange({
value: event.target.value,
configKey: ConfigKey.DOWNLOAD_SPEED,
});
}}
onBlur={() => onFieldBlur?.(ConfigKey.DOWNLOAD_SPEED)}
data-test-subj="syntheticsBrowserDownloadSpeed"
append={
<EuiText size="xs">
<strong>Mbps</strong>
</EuiText>
}
readOnly={readOnly}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.upload.label"
defaultMessage="Upload Speed"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={
(validate ? !!validate[ConfigKey.UPLOAD_SPEED]?.(fields) : false) || exceedsUploadLimits
}
error={
exceedsUploadLimits ? (
<ThrottlingExceededMessage throttlingField="upload" limit={maxUpload} />
) : (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.upload.error"
defaultMessage="Upload speed must be greater than zero."
/>
)
}
>
<EuiFieldNumber
min={0}
step={0.001}
value={fields[ConfigKey.UPLOAD_SPEED]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.UPLOAD_SPEED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.UPLOAD_SPEED)}
data-test-subj="syntheticsBrowserUploadSpeed"
append={
<EuiText size="xs">
<strong>Mbps</strong>
</EuiText>
}
readOnly={readOnly}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.latency.label"
defaultMessage="Latency"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={validate ? !!validate[ConfigKey.LATENCY]?.(fields) : false}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.latency.error"
defaultMessage="Latency must not be negative."
/>
}
>
<EuiFieldNumber
min={0}
value={fields[ConfigKey.LATENCY]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.LATENCY,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.LATENCY)}
data-test-subj="syntheticsBrowserLatency"
append={
<EuiText size="xs">
<strong>ms</strong>
</EuiText>
}
readOnly={readOnly}
/>
</EuiFormRow>
</>
) : (
<>
<EuiSpacer />
<ThrottlingDisabledCallout />
</>
);
return (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.title"
defaultMessage="Throttling options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.description"
defaultMessage="Control the monitor's download and upload speeds, and its latency to simulate your application's behaviour on slower or laggier networks."
/>
}
>
<EuiSwitch
id={'uptimeFleetIsThrottlingEnabled'}
aria-label="enable throttling configuration"
data-test-subj="syntheticsBrowserIsThrottlingEnabled"
checked={fields[ConfigKey.IS_THROTTLING_ENABLED]}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.browserAdvancedSettings.throttling.switch.description"
defaultMessage="Enable throttling"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.IS_THROTTLING_ENABLED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)}
disabled={readOnly}
/>
{isThrottlingEnabled && (exceedsDownloadLimits || exceedsUploadLimits) ? (
<>
<EuiSpacer />
<ThrottlingExceededCallout />
</>
) : null}
{throttlingInputs}
</DescribedFormGroupWithWrap>
);
}
);

View file

@ -1,93 +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, { 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} label={TESTING_SCRIPT_LABEL}>
<EuiFilePicker
id="syntheticsFleetScriptRecorderUploader"
data-test-subj="syntheticsFleetScriptRecorderUploader"
ref={filePickerRef}
initialPromptText={PROMPT_TEXT}
onChange={handleFileChosen}
display={'large'}
/>
</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.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.label',
{
defaultMessage: 'Select recorder-generated .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.',
}
);

View file

@ -1,67 +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 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;
readOnly?: boolean;
}
export const CodeEditor = ({
ariaLabel,
id,
languageId,
onChange,
value,
readOnly = false,
}: 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',
readOnly,
}}
isCopyable={true}
allowFullScreen={true}
/>
</MonacoCodeContainer>
</CodeEditorContainer>
);
};
const MonacoCodeContainer = euiStyled.div`
& > .kibanaCodeEditor {
z-index: 0;
}
`;

View file

@ -1,37 +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 { fireEvent } from '@testing-library/react';
import React from 'react';
import { render } from '../../lib/helper/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);
});
});

View file

@ -1,87 +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, { useState, useCallback } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
export interface Props {
onChange: (value: string[]) => void;
onBlur?: () => void;
selectedOptions: string[];
readOnly?: boolean;
}
export const ComboBox = ({
onChange,
onBlur,
selectedOptions,
readOnly = false,
...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}
isDisabled={readOnly}
{...props}
/>
);
};
const isValid = (value: string) => {
// Ensure that the tag is more than whitespace
return value.match(/\S+/) !== null;
};

View file

@ -1,140 +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 { EuiFieldNumber, EuiFieldText, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect } from 'react';
import { ComboBox } from '../combo_box';
import { usePolicyConfigContext } from '../contexts';
import { OptionalLabel } from '../optional_label';
import { CommonFields as CommonFieldsType, ConfigKey, DataStream, Validation } from '../types';
interface Props {
validate: Validation;
fields: CommonFieldsType;
onChange: ({
value,
configKey,
}: {
value: string | string[] | null;
configKey: ConfigKey;
}) => void;
onFieldBlur?: (field: ConfigKey) => void;
}
export function CommonFields({ fields, onChange, onFieldBlur, validate }: Props) {
const { monitorType } = usePolicyConfigContext();
const isBrowser = monitorType === DataStream.BROWSER;
useEffect(() => {
if (monitorType === DataStream.BROWSER) {
onChange({
value: null,
configKey: ConfigKey.TIMEOUT,
});
}
}, [onChange, monitorType]);
return (
<>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.APMServiceName.label"
defaultMessage="APM service name"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.APMServiceName.helpText"
defaultMessage="APM service name for this monitor. Corresponds to the service.name ECS field. Set this when monitoring an app that is also using APM to enable integrations between Uptime and APM data in Kibana."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.APM_SERVICE_NAME]}
onChange={(event) =>
onChange({
value: event.target.value,
configKey: ConfigKey.APM_SERVICE_NAME,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.APM_SERVICE_NAME)}
data-test-subj="syntheticsAPMServiceName"
/>
</EuiFormRow>
{!isBrowser && (
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.label"
defaultMessage="Timeout in seconds"
/>
}
isInvalid={!!validate[ConfigKey.TIMEOUT]?.(fields)}
error={
parseInt(fields[ConfigKey.TIMEOUT] || '', 10) < 0 ? (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.moreThanZeroError"
defaultMessage="Timeout must be greater than or equal to 0"
/>
) : (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.lessThanIntervalError"
defaultMessage="Timeout must be less than the monitor frequency"
/>
)
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.helpText"
defaultMessage="The total time allowed for testing the connection and exchanging data."
/>
}
>
<EuiFieldNumber
data-test-subj="syntheticsCommonFieldsFieldNumber"
min={0}
value={fields[ConfigKey.TIMEOUT] || ''}
onChange={(event) =>
onChange({
value: event.target.value,
configKey: ConfigKey.TIMEOUT,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.TIMEOUT)}
step={'any'}
/>
</EuiFormRow>
)}
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tags.label"
defaultMessage="Tags"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tags.helpText"
defaultMessage="A list of tags that will be sent with the monitor event. Press enter to add a new tag. Displayed in Uptime and enables searching by tag."
/>
}
>
<ComboBox
selectedOptions={fields[ConfigKey.TAGS]}
onChange={(value) => onChange({ value, configKey: ConfigKey.TAGS })}
onBlur={() => onFieldBlur?.(ConfigKey.TAGS)}
data-test-subj="syntheticsTags"
/>
</EuiFormRow>
</>
);
}

View file

@ -1,23 +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 { EuiDescribedFormGroup } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
/**
* EuiForm group doesn't expose props to control the flex wrapping on flex groups defining form rows.
* This override allows to define a minimum column width to which the Described Form's flex rows should wrap.
*/
export const DescribedFormGroupWithWrap = euiStyled(EuiDescribedFormGroup)<{
minColumnWidth?: string;
}>`
> .euiFlexGroup {
${({ minColumnWidth }) => (minColumnWidth ? `flex-wrap: wrap;` : '')}
> .euiFlexItem {
${({ minColumnWidth }) => (minColumnWidth ? `min-width: ${minColumnWidth};` : '')}
}
}
`;

View file

@ -1,52 +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 { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { ConfigKey, CommonFields } from '../types';
interface Props {
fields: CommonFields;
onChange: ({ value, configKey }: { value: boolean; configKey: ConfigKey }) => void;
onBlur?: () => void;
readOnly?: boolean;
}
export function Enabled({ fields, onChange, onBlur, readOnly }: Props) {
return (
<>
<EuiFormRow
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.enabled.helpText"
defaultMessage="Switch this configuration off to disable the monitor."
/>
}
>
<EuiSwitch
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.enabled.label"
defaultMessage="Enabled"
/>
}
data-test-subj="syntheticsEnabled"
checked={fields[ConfigKey.ENABLED]}
onChange={(event) =>
onChange({
value: event.target.checked,
configKey: ConfigKey.ENABLED,
})
}
onBlur={() => onBlur?.()}
disabled={readOnly}
/>
</EuiFormRow>
</>
);
}

View file

@ -1,28 +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 { cronToSecondsNormalizer, jsonToJavascriptNormalizer } from './normalizers';
describe('normalizers', () => {
describe('cronToSecondsNormalizer', () => {
it('returns number of seconds from cron formatted seconds', () => {
expect(cronToSecondsNormalizer('3s')).toEqual('3');
});
});
describe('jsonToJavascriptNormalizer', () => {
it('takes a json object string and returns an object', () => {
expect(jsonToJavascriptNormalizer('{\n "key": "value"\n}')).toEqual({
key: 'value',
});
});
it('takes a json array string and returns an array', () => {
expect(jsonToJavascriptNormalizer('["tag1","tag2"]')).toEqual(['tag1', 'tag2']);
});
});
});

View file

@ -1,102 +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 { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { parseJsonIfString } from '../helpers/parsers';
import { CommonFields, ConfigKey, DataStream } from '../types';
import {
DEFAULT_COMMON_FIELDS,
DEFAULT_NAMESPACE_STRING,
DEFAULT_FIELDS,
} from '../../../../../common/constants/monitor_defaults';
// TO DO: create a standard input format that all fields resolve to
export type Normalizer = (fields: NewPackagePolicyInput['vars']) => unknown;
// create a type of all the common policy fields, as well as the fleet managed 'name' field
export type CommonNormalizerMap = Record<keyof CommonFields | ConfigKey.NAME, Normalizer>;
/**
* Takes a cron formatted seconds and returns just the number of seconds. Assumes that cron is already in seconds format.
* @params {string} value (Ex '3s')
* @return {string} (Ex '3')
*/
export const cronToSecondsNormalizer = (value: string) =>
value ? value.slice(0, value.length - 1) : null;
export const jsonToJavascriptNormalizer = (value: string) =>
value ? parseJsonIfString(value) : null;
export function getNormalizer<Fields>(key: string, defaultValues: Fields): Normalizer {
return (fields: NewPackagePolicyInput['vars']) =>
fields?.[key]?.value ?? defaultValues[key as keyof Fields];
}
export function getJsonToJavascriptNormalizer<Fields>(
key: string,
defaultValues: Fields
): Normalizer {
return (fields: NewPackagePolicyInput['vars']) =>
jsonToJavascriptNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields];
}
export function getCronNormalizer<Fields>(key: string, defaultValues: Fields): Normalizer {
return (fields: NewPackagePolicyInput['vars']) =>
cronToSecondsNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields];
}
export const getCommonNormalizer = (key: ConfigKey) => {
return getNormalizer(key, DEFAULT_COMMON_FIELDS);
};
export const getCommonjsonToJavascriptNormalizer = (key: ConfigKey) => {
return getJsonToJavascriptNormalizer(key, DEFAULT_COMMON_FIELDS);
};
export const getCommonCronToSecondsNormalizer = (key: ConfigKey) => {
return getCronNormalizer(key, DEFAULT_COMMON_FIELDS);
};
export const commonNormalizers: CommonNormalizerMap = {
[ConfigKey.NAME]: (fields) => fields?.[ConfigKey.NAME]?.value ?? '',
[ConfigKey.LOCATIONS]: getCommonNormalizer(ConfigKey.LOCATIONS),
[ConfigKey.ENABLED]: getCommonNormalizer(ConfigKey.ENABLED),
[ConfigKey.ALERT_CONFIG]: getCommonNormalizer(ConfigKey.ENABLED),
[ConfigKey.MONITOR_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_TYPE),
[ConfigKey.LOCATIONS]: getCommonNormalizer(ConfigKey.LOCATIONS),
[ConfigKey.SCHEDULE]: (fields) => {
const value = fields?.[ConfigKey.SCHEDULE]?.value;
const type = fields?.[ConfigKey.MONITOR_TYPE]?.value as DataStream;
if (value) {
const fullString = JSON.parse(fields?.[ConfigKey.SCHEDULE]?.value);
const fullSchedule = fullString.replace('@every ', '');
const unit = fullSchedule.slice(-1);
const number = fullSchedule.slice(0, fullSchedule.length - 1);
return {
unit,
number,
};
} else {
return DEFAULT_FIELDS[type][ConfigKey.SCHEDULE];
}
},
[ConfigKey.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKey.APM_SERVICE_NAME),
[ConfigKey.CONFIG_ID]: getCommonNormalizer(ConfigKey.CONFIG_ID),
[ConfigKey.TAGS]: getCommonjsonToJavascriptNormalizer(ConfigKey.TAGS),
[ConfigKey.TIMEOUT]: getCommonCronToSecondsNormalizer(ConfigKey.TIMEOUT),
[ConfigKey.NAMESPACE]: (fields) =>
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),
[ConfigKey.JOURNEY_ID]: getCommonNormalizer(ConfigKey.JOURNEY_ID),
[ConfigKey.PROJECT_ID]: getCommonNormalizer(ConfigKey.PROJECT_ID),
[ConfigKey.CUSTOM_HEARTBEAT_ID]: getCommonNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID),
[ConfigKey.ORIGINAL_SPACE]: getCommonNormalizer(ConfigKey.ORIGINAL_SPACE),
[ConfigKey.CONFIG_HASH]: getCommonNormalizer(ConfigKey.CONFIG_HASH),
[ConfigKey.MONITOR_QUERY_ID]: getCommonNormalizer(ConfigKey.MONITOR_QUERY_ID),
};

View file

@ -1,44 +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 { CommonFields } from './common_fields';
import { Enabled } from './enabled';
import { CommonFields as CommonFieldsType, ConfigKey, Validation } from '../types';
interface Props {
validate: Validation;
onInputChange: ({ value, configKey }: { value: unknown; configKey: ConfigKey }) => void;
onFieldBlur?: (field: ConfigKey) => void;
children: React.ReactNode;
fields: CommonFieldsType;
}
export const SimpleFieldsWrapper = ({
validate,
onInputChange,
onFieldBlur,
children,
fields,
}: Props) => {
return (
<>
<Enabled
fields={fields}
onChange={onInputChange}
onBlur={() => onFieldBlur?.(ConfigKey.ENABLED)}
/>
{children}
<CommonFields
fields={fields}
validate={validate}
onChange={onInputChange}
onFieldBlur={onFieldBlur}
/>
</>
);
};

View file

@ -1,411 +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, { useEffect, useState, memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiCallOut,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
EuiTextArea,
EuiFormFieldset,
EuiSelect,
EuiScreenReaderOnly,
EuiSpacer,
EuiFieldPassword,
} from '@elastic/eui';
import { VerificationMode, TLSVersion } from '../types';
import { OptionalLabel } from '../optional_label';
type TLSRole = 'client' | 'server';
export interface TLSConfig {
certificateAuthorities?: string;
certificate?: string;
key?: string;
keyPassphrase?: string;
verificationMode?: VerificationMode;
version?: TLSVersion[];
}
const defaultConfig = {
certificateAuthorities: '',
certificate: '',
key: '',
keyPassphrase: '',
verificationMode: VerificationMode.STRICT,
version: [],
};
interface Props {
onChange: (defaultConfig: TLSConfig) => void;
defaultValues: TLSConfig;
tlsRole: TLSRole;
}
export const TLSOptions: React.FunctionComponent<Props> = memo(
({ onChange, defaultValues = defaultConfig, tlsRole }) => {
const [verificationVersionInputRef, setVerificationVersionInputRef] =
useState<HTMLInputElement | null>(null);
const [hasVerificationVersionError, setHasVerificationVersionError] = useState<
string | undefined
>(undefined);
const [config, setConfig] = useState<TLSConfig>(defaultValues);
useEffect(() => {
onChange(config);
}, [config, onChange]);
const onVerificationVersionChange = (
selectedVersionOptions: Array<EuiComboBoxOptionOption<TLSVersion>>
) => {
setConfig((prevConfig) => ({
...prevConfig,
version: selectedVersionOptions.map((option) => option.label as TLSVersion),
}));
setHasVerificationVersionError(undefined);
};
const onSearchChange = (value: string, hasMatchingOptions?: boolean) => {
setHasVerificationVersionError(
value.length === 0 || hasMatchingOptions ? undefined : `"${value}" is not a valid option`
);
};
const onBlur = () => {
if (verificationVersionInputRef) {
const { value } = verificationVersionInputRef;
setHasVerificationVersionError(
value.length === 0 ? undefined : `"${value}" is not a valid option`
);
}
};
return (
<EuiFormFieldset
legend={{
children: (
<EuiScreenReaderOnly>
<span>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.legend"
defaultMessage="Certificate settings"
/>
</span>
</EuiScreenReaderOnly>
),
}}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.label"
defaultMessage="Verification mode"
/>
}
helpText={
config.verificationMode ? verificationModeHelpText[config.verificationMode] : ''
}
>
<EuiSelect
options={verificationModeOptions}
value={config.verificationMode}
onChange={(event) => {
const verificationMode = event.target.value as VerificationMode;
setConfig((prevConfig) => ({
...prevConfig,
verificationMode,
}));
}}
data-test-subj="syntheticsTLSVerificationMode"
/>
</EuiFormRow>
{config.verificationMode === VerificationMode.NONE && (
<>
<EuiSpacer size="s" />
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.warning.title"
defaultMessage="Disabling TLS"
/>
}
color="warning"
size="s"
>
<p>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.warning.description"
defaultMessage="This mode disables many of the security benefits of SSL/TLS and should only be used
after cautious consideration."
/>
</p>
</EuiCallOut>
<EuiSpacer size="s" />
</>
)}
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.version.label"
defaultMessage="Supported TLS protocols"
/>
}
error={hasVerificationVersionError}
isInvalid={hasVerificationVersionError !== undefined}
>
<EuiComboBox
placeholder={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.version.placeholder',
{
defaultMessage: 'Select one or more TLS protocols.',
}
)}
options={tlsVersionOptions}
selectedOptions={(config.version || []).map((version: TLSVersion) => ({
label: version,
}))}
inputRef={setVerificationVersionInputRef}
onChange={onVerificationVersionChange}
onSearchChange={onSearchChange}
onBlur={onBlur}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificateAuthorities.label"
defaultMessage="Certificate authorities"
/>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificateAuthorities.helpText"
defaultMessage="PEM formatted custom certificate authorities."
/>
}
labelAppend={<OptionalLabel />}
>
<EuiTextArea
value={config.certificateAuthorities}
onChange={(event) => {
const certificateAuthorities = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
certificateAuthorities,
}));
}}
onBlur={(event) => {
const certificateAuthorities = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
certificateAuthorities: certificateAuthorities.trim(),
}));
}}
data-test-subj="syntheticsTLSCA"
/>
</EuiFormRow>
<EuiFormRow
label={
<>
{tlsRoleLabels[tlsRole]}{' '}
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificate.label"
defaultMessage="certificate"
/>
</>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificate.helpText"
defaultMessage="PEM formatted certificate for TLS client authentication."
/>
}
labelAppend={<OptionalLabel />}
>
<EuiTextArea
value={config.certificate}
onChange={(event) => {
const certificate = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
certificate,
}));
}}
onBlur={(event) => {
const certificate = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
certificate: certificate.trim(),
}));
}}
data-test-subj="syntheticsTLSCert"
/>
</EuiFormRow>
<EuiFormRow
label={
<>
{tlsRoleLabels[tlsRole]}{' '}
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificateKey.label"
defaultMessage="key"
/>
</>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificateKey.helpText"
defaultMessage="PEM formatted certificate key for TLS client authentication."
/>
}
labelAppend={<OptionalLabel />}
>
<EuiTextArea
value={config.key}
onChange={(event) => {
const key = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
key,
}));
}}
onBlur={(event) => {
const key = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
key: key.trim(),
}));
}}
data-test-subj="syntheticsTLSCertKey"
/>
</EuiFormRow>
<EuiFormRow
label={
<>
{tlsRoleLabels[tlsRole]}{' '}
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificateKeyPassphrase.label"
defaultMessage="key passphrase"
/>
</>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.certificateKeyPassphrase.helpText"
defaultMessage="Certificate key passphrase for TLS client authentication."
/>
}
labelAppend={<OptionalLabel />}
>
<EuiFieldPassword
value={config.keyPassphrase}
onChange={(event) => {
const keyPassphrase = event.target.value;
setConfig((prevConfig) => ({
...prevConfig,
keyPassphrase,
}));
}}
data-test-subj="syntheticsTLSCertKeyPassphrase"
/>
</EuiFormRow>
</EuiFormFieldset>
);
}
);
const tlsRoleLabels = {
client: (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.tlsRole.client"
defaultMessage="Client"
/>
),
server: (
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certsField.tlsRole.server"
defaultMessage="Server"
/>
),
};
const verificationModeHelpText = {
[VerificationMode.CERTIFICATE]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.certificate.description',
{
defaultMessage:
'Verifies that the provided certificate is signed by a trusted authority (CA), but does not perform any hostname verification.',
}
),
[VerificationMode.FULL]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.full.description',
{
defaultMessage:
'Verifies that the provided certificate is signed by a trusted authority (CA) and also verifies that the servers hostname (or IP address) matches the names identified within the certificate.',
}
),
[VerificationMode.NONE]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.none.description',
{
defaultMessage:
'Performs no verification of the servers certificate. It is primarily intended as a temporary diagnostic mechanism when attempting to resolve TLS errors; its use in production environments is strongly discouraged.',
}
),
[VerificationMode.STRICT]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.strict.description',
{
defaultMessage:
'Verifies that the provided certificate is signed by a trusted authority (CA) and also verifies that the servers hostname (or IP address) matches the names identified within the certificate. If the Subject Alternative Name is empty, it returns an error.',
}
),
};
const verificationModeLabels = {
[VerificationMode.CERTIFICATE]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.certificate.label',
{
defaultMessage: 'Certificate',
}
),
[VerificationMode.FULL]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.full.label',
{
defaultMessage: 'Full',
}
),
[VerificationMode.NONE]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.none.label',
{
defaultMessage: 'None',
}
),
[VerificationMode.STRICT]: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.certsField.verificationMode.strict.label',
{
defaultMessage: 'Strict',
}
),
};
const verificationModeOptions = [
{
value: VerificationMode.CERTIFICATE,
text: verificationModeLabels[VerificationMode.CERTIFICATE],
},
{ value: VerificationMode.FULL, text: verificationModeLabels[VerificationMode.FULL] },
{ value: VerificationMode.NONE, text: verificationModeLabels[VerificationMode.NONE] },
{ value: VerificationMode.STRICT, text: verificationModeLabels[VerificationMode.STRICT] },
];
const tlsVersionOptions = Object.values(TLSVersion).map((method) => ({
label: method,
}));

View file

@ -1,50 +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, { createContext, useContext, useMemo, useState } from 'react';
import { BrowserSimpleFields } from '../types';
import { DEFAULT_BROWSER_SIMPLE_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface BrowserSimpleFieldsContext {
setFields: React.Dispatch<React.SetStateAction<BrowserSimpleFields>>;
fields: BrowserSimpleFields;
defaultValues: BrowserSimpleFields;
}
interface BrowserSimpleFieldsContextProvider {
children: React.ReactNode;
defaultValues?: BrowserSimpleFields;
}
export const initialValues: BrowserSimpleFields = DEFAULT_BROWSER_SIMPLE_FIELDS;
const defaultContext: BrowserSimpleFieldsContext = {
setFields: (_fields: React.SetStateAction<BrowserSimpleFields>) => {
throw new Error(
'setFields was not initialized for Browser Simple Fields, set it when you invoke the context'
);
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const BrowserSimpleFieldsContext = createContext(defaultContext);
export const BrowserSimpleFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: BrowserSimpleFieldsContextProvider) => {
const [fields, setFields] = useState<BrowserSimpleFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <BrowserSimpleFieldsContext.Provider value={value} children={children} />;
};
export const useBrowserSimpleFieldsContext = () => useContext(BrowserSimpleFieldsContext);

View file

@ -1,50 +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, { createContext, useContext, useMemo, useState } from 'react';
import { BrowserAdvancedFields } from '../types';
import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface BrowserAdvancedFieldsContext {
setFields: React.Dispatch<React.SetStateAction<BrowserAdvancedFields>>;
fields: BrowserAdvancedFields;
defaultValues: BrowserAdvancedFields;
}
interface BrowserAdvancedFieldsContextProvider {
children: React.ReactNode;
defaultValues?: BrowserAdvancedFields;
}
export const initialValues: BrowserAdvancedFields = DEFAULT_BROWSER_ADVANCED_FIELDS;
const defaultContext: BrowserAdvancedFieldsContext = {
setFields: (_fields: React.SetStateAction<BrowserAdvancedFields>) => {
throw new Error(
'setFields was not initialized for Browser Advanced Fields, set it when you invoke the context'
);
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const BrowserAdvancedFieldsContext = createContext(defaultContext);
export const BrowserAdvancedFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: BrowserAdvancedFieldsContextProvider) => {
const [fields, setFields] = useState<BrowserAdvancedFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <BrowserAdvancedFieldsContext.Provider value={value} children={children} />;
};
export const useBrowserAdvancedFieldsContext = () => useContext(BrowserAdvancedFieldsContext);

View file

@ -1,52 +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, { ReactNode } from 'react';
import { BrowserFields, BrowserSimpleFields, BrowserAdvancedFields } from '../types';
import {
BrowserSimpleFieldsContextProvider,
BrowserAdvancedFieldsContextProvider,
defaultBrowserSimpleFields,
defaultBrowserAdvancedFields,
} from '.';
import { formatDefaultValues } from '../helpers/context_helpers';
interface BrowserContextProviderProps {
defaultValues?: BrowserFields;
children: ReactNode;
}
export const BrowserContextProvider = ({
defaultValues,
children,
}: BrowserContextProviderProps) => {
const simpleKeys = Object.keys(defaultBrowserSimpleFields) as Array<keyof BrowserSimpleFields>;
const advancedKeys = Object.keys(defaultBrowserAdvancedFields) as Array<
keyof BrowserAdvancedFields
>;
const formattedDefaultSimpleFields = formatDefaultValues<BrowserSimpleFields>(
simpleKeys,
defaultValues || {}
);
const formattedDefaultAdvancedFields = formatDefaultValues<BrowserAdvancedFields>(
advancedKeys,
defaultValues || {}
);
const simpleFields: BrowserSimpleFields | undefined = defaultValues
? formattedDefaultSimpleFields
: undefined;
const advancedFields: BrowserAdvancedFields | undefined = defaultValues
? formattedDefaultAdvancedFields
: undefined;
return (
<BrowserAdvancedFieldsContextProvider defaultValues={advancedFields}>
<BrowserSimpleFieldsContextProvider defaultValues={simpleFields}>
{children}
</BrowserSimpleFieldsContextProvider>
</BrowserAdvancedFieldsContextProvider>
);
};

View file

@ -1,50 +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, { createContext, useContext, useMemo, useState } from 'react';
import { HTTPSimpleFields } from '../types';
import { DEFAULT_HTTP_SIMPLE_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface HTTPSimpleFieldsContext {
setFields: React.Dispatch<React.SetStateAction<HTTPSimpleFields>>;
fields: HTTPSimpleFields;
defaultValues: HTTPSimpleFields;
}
interface HTTPSimpleFieldsContextProvider {
children: React.ReactNode;
defaultValues?: HTTPSimpleFields;
}
export const initialValues: HTTPSimpleFields = DEFAULT_HTTP_SIMPLE_FIELDS;
const defaultContext: HTTPSimpleFieldsContext = {
setFields: (_fields: React.SetStateAction<HTTPSimpleFields>) => {
throw new Error(
'setFields was not initialized for HTTP Simple Fields, set it when you invoke the context'
);
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const HTTPSimpleFieldsContext = createContext(defaultContext);
export const HTTPSimpleFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: HTTPSimpleFieldsContextProvider) => {
const [fields, setFields] = useState<HTTPSimpleFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <HTTPSimpleFieldsContext.Provider value={value} children={children} />;
};
export const useHTTPSimpleFieldsContext = () => useContext(HTTPSimpleFieldsContext);

View file

@ -1,48 +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, { createContext, useContext, useMemo, useState } from 'react';
import { HTTPAdvancedFields } from '../types';
import { DEFAULT_HTTP_ADVANCED_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface HTTPAdvancedFieldsContext {
setFields: React.Dispatch<React.SetStateAction<HTTPAdvancedFields>>;
fields: HTTPAdvancedFields;
defaultValues: HTTPAdvancedFields;
}
interface HTTPAdvancedFieldsContextProvider {
children: React.ReactNode;
defaultValues?: HTTPAdvancedFields;
}
export const initialValues: HTTPAdvancedFields = DEFAULT_HTTP_ADVANCED_FIELDS;
export const defaultContext: HTTPAdvancedFieldsContext = {
setFields: (_fields: React.SetStateAction<HTTPAdvancedFields>) => {
throw new Error('setFields was not initialized, set it when you invoke the context');
},
fields: initialValues,
defaultValues: initialValues,
};
export const HTTPAdvancedFieldsContext = createContext(defaultContext);
export const HTTPAdvancedFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: HTTPAdvancedFieldsContextProvider) => {
const [fields, setFields] = useState<HTTPAdvancedFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <HTTPAdvancedFieldsContext.Provider value={value} children={children} />;
};
export const useHTTPAdvancedFieldsContext = () => useContext(HTTPAdvancedFieldsContext);

View file

@ -1,45 +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, { ReactNode } from 'react';
import { HTTPFields, HTTPSimpleFields, HTTPAdvancedFields } from '../types';
import {
HTTPSimpleFieldsContextProvider,
HTTPAdvancedFieldsContextProvider,
defaultHTTPSimpleFields,
defaultHTTPAdvancedFields,
} from '.';
import { formatDefaultValues } from '../helpers/context_helpers';
interface HTTPContextProviderProps {
defaultValues?: HTTPFields;
children: ReactNode;
}
export const HTTPContextProvider = ({ defaultValues, children }: HTTPContextProviderProps) => {
const simpleKeys = Object.keys(defaultHTTPSimpleFields) as Array<keyof HTTPSimpleFields>;
const advancedKeys = Object.keys(defaultHTTPAdvancedFields) as Array<keyof HTTPAdvancedFields>;
const formattedDefaultHTTPSimpleFields = formatDefaultValues<HTTPSimpleFields>(
simpleKeys,
defaultValues || {}
);
const formattedDefaultHTTPAdvancedFields = formatDefaultValues<HTTPAdvancedFields>(
advancedKeys,
defaultValues || {}
);
const httpAdvancedFields = defaultValues ? formattedDefaultHTTPAdvancedFields : undefined;
const httpSimpleFields: HTTPSimpleFields | undefined = defaultValues
? formattedDefaultHTTPSimpleFields
: undefined;
return (
<HTTPAdvancedFieldsContextProvider defaultValues={httpAdvancedFields}>
<HTTPSimpleFieldsContextProvider defaultValues={httpSimpleFields}>
{children}
</HTTPSimpleFieldsContextProvider>
</HTTPAdvancedFieldsContextProvider>
);
};

View file

@ -1,50 +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, { createContext, useContext, useMemo, useState } from 'react';
import { ICMPSimpleFields } from '../types';
import { DEFAULT_ICMP_SIMPLE_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface ICMPSimpleFieldsContext {
setFields: React.Dispatch<React.SetStateAction<ICMPSimpleFields>>;
fields: ICMPSimpleFields;
defaultValues: ICMPSimpleFields;
}
interface ICMPSimpleFieldsContextProvider {
children: React.ReactNode;
defaultValues?: ICMPSimpleFields;
}
export const initialValues: ICMPSimpleFields = DEFAULT_ICMP_SIMPLE_FIELDS;
const defaultContext: ICMPSimpleFieldsContext = {
setFields: (_fields: React.SetStateAction<ICMPSimpleFields>) => {
throw new Error(
'setFields was not initialized for ICMP Simple Fields, set it when you invoke the context'
);
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const ICMPSimpleFieldsContext = createContext(defaultContext);
export const ICMPSimpleFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: ICMPSimpleFieldsContextProvider) => {
const [fields, setFields] = useState<ICMPSimpleFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <ICMPSimpleFieldsContext.Provider value={value} children={children} />;
};
export const useICMPSimpleFieldsContext = () => useContext(ICMPSimpleFieldsContext);

View file

@ -1,66 +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.
*/
export type { IPolicyConfigContextProvider } from './policy_config_context';
export {
PolicyConfigContext,
PolicyConfigContextProvider,
initialMonitorTypeValue as defaultPolicyConfig,
defaultContext as defaultPolicyConfigValues,
usePolicyConfigContext,
} from './policy_config_context';
export {
HTTPSimpleFieldsContext,
HTTPSimpleFieldsContextProvider,
initialValues as defaultHTTPSimpleFields,
useHTTPSimpleFieldsContext,
} from './http_context';
export {
HTTPAdvancedFieldsContext,
HTTPAdvancedFieldsContextProvider,
initialValues as defaultHTTPAdvancedFields,
useHTTPAdvancedFieldsContext,
} from './http_context_advanced';
export {
TCPSimpleFieldsContext,
TCPSimpleFieldsContextProvider,
initialValues as defaultTCPSimpleFields,
useTCPSimpleFieldsContext,
} from './tcp_context';
export {
ICMPSimpleFieldsContext,
ICMPSimpleFieldsContextProvider,
initialValues as defaultICMPSimpleFields,
useICMPSimpleFieldsContext,
} from './icmp_context';
export {
TCPAdvancedFieldsContext,
TCPAdvancedFieldsContextProvider,
initialValues as defaultTCPAdvancedFields,
useTCPAdvancedFieldsContext,
} from './tcp_context_advanced';
export {
BrowserSimpleFieldsContext,
BrowserSimpleFieldsContextProvider,
initialValues as defaultBrowserSimpleFields,
useBrowserSimpleFieldsContext,
} from './browser_context';
export {
BrowserAdvancedFieldsContext,
BrowserAdvancedFieldsContextProvider,
initialValues as defaultBrowserAdvancedFields,
useBrowserAdvancedFieldsContext,
} from './browser_context_advanced';
export {
TLSFieldsContext,
TLSFieldsContextProvider,
initialValues as defaultTLSFields,
useTLSFieldsContext,
} from './tls_fields_context';
export { HTTPContextProvider } from './http_provider';
export { TCPContextProvider } from './tcp_provider';
export { BrowserContextProvider } from './browser_provider';
export { SyntheticsProviders } from './synthetics_context_providers';

View file

@ -1,162 +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, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useRouteMatch } from 'react-router-dom';
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants';
import { DEFAULT_NAMESPACE_STRING } from '../../../../../common/constants/monitor_defaults';
import {
ScheduleUnit,
SourceType,
MonitorServiceLocations,
ThrottlingOptions,
DEFAULT_THROTTLING,
} from '../../../../../common/runtime_types';
import { DataStream } from '../types';
interface IPolicyConfigContext {
setMonitorType: React.Dispatch<React.SetStateAction<DataStream>>;
setName: React.Dispatch<React.SetStateAction<string>>;
setLocations: React.Dispatch<React.SetStateAction<MonitorServiceLocations>>;
setIsTLSEnabled: React.Dispatch<React.SetStateAction<boolean>>;
setNamespace: React.Dispatch<React.SetStateAction<string>>;
monitorType: DataStream;
defaultMonitorType: DataStream;
isTLSEnabled?: boolean;
runsOnService?: boolean;
defaultIsTLSEnabled?: boolean;
isEditable?: boolean;
defaultName?: string;
name?: string;
defaultLocations?: MonitorServiceLocations;
locations?: MonitorServiceLocations;
allowedScheduleUnits?: ScheduleUnit[];
defaultNamespace?: string;
namespace?: string;
throttling: ThrottlingOptions;
sourceType?: SourceType;
}
export interface IPolicyConfigContextProvider {
children: React.ReactNode;
defaultMonitorType?: DataStream;
runsOnService?: boolean;
defaultIsTLSEnabled?: boolean;
defaultName?: string;
defaultLocations?: MonitorServiceLocations;
defaultNamespace?: string;
isEditable?: boolean;
allowedScheduleUnits?: ScheduleUnit[];
throttling?: ThrottlingOptions;
sourceType?: SourceType;
}
export const initialMonitorTypeValue = DataStream.HTTP;
export const defaultContext: IPolicyConfigContext = {
setMonitorType: (_monitorType: React.SetStateAction<DataStream>) => {
throw new Error('setMonitorType was not initialized, set it when you invoke the context');
},
setName: (_name: React.SetStateAction<string>) => {
throw new Error('setName was not initialized, set it when you invoke the context');
},
setLocations: (_locations: React.SetStateAction<MonitorServiceLocations>) => {
throw new Error('setLocations was not initialized, set it when you invoke the context');
},
setIsTLSEnabled: (_isTLSEnabled: React.SetStateAction<boolean>) => {
throw new Error('setIsTLSEnabled was not initialized, set it when you invoke the context');
},
setNamespace: (_namespace: React.SetStateAction<string>) => {
throw new Error('setNamespace was not initialized, set it when you invoke the context');
},
monitorType: initialMonitorTypeValue, // mutable
defaultMonitorType: initialMonitorTypeValue, // immutable,
runsOnService: false,
defaultIsTLSEnabled: false,
defaultName: '',
defaultLocations: [],
isEditable: false,
allowedScheduleUnits: [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS],
defaultNamespace: DEFAULT_NAMESPACE_STRING,
throttling: DEFAULT_THROTTLING,
sourceType: SourceType.UI,
};
export const PolicyConfigContext = createContext(defaultContext);
export function PolicyConfigContextProvider<ExtraFields = unknown>({
children,
throttling = DEFAULT_THROTTLING,
defaultMonitorType = initialMonitorTypeValue,
defaultIsTLSEnabled = false,
defaultName = '',
defaultLocations = [],
defaultNamespace = DEFAULT_NAMESPACE_STRING,
isEditable = false,
runsOnService = false,
allowedScheduleUnits = [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS],
sourceType,
}: IPolicyConfigContextProvider) {
const [monitorType, setMonitorType] = useState<DataStream>(defaultMonitorType);
const [name, setName] = useState<string>(defaultName);
const [locations, setLocations] = useState<MonitorServiceLocations>(defaultLocations);
const [isTLSEnabled, setIsTLSEnabled] = useState<boolean>(defaultIsTLSEnabled);
const [namespace, setNamespace] = useState<string>(defaultNamespace);
const isAddMonitorRoute = useRouteMatch(MONITOR_ADD_ROUTE);
useEffect(() => {
if (isAddMonitorRoute?.isExact) {
setMonitorType(DataStream.BROWSER);
}
}, [isAddMonitorRoute?.isExact]);
const value = useMemo(() => {
return {
monitorType,
setMonitorType,
defaultMonitorType,
runsOnService,
isTLSEnabled,
setIsTLSEnabled,
defaultIsTLSEnabled,
isEditable,
defaultName,
name,
setName,
defaultLocations,
locations,
setLocations,
allowedScheduleUnits,
namespace,
setNamespace,
throttling,
sourceType,
} as IPolicyConfigContext;
}, [
monitorType,
defaultMonitorType,
runsOnService,
isTLSEnabled,
defaultIsTLSEnabled,
isEditable,
name,
defaultName,
locations,
defaultLocations,
allowedScheduleUnits,
namespace,
throttling,
sourceType,
]);
return <PolicyConfigContext.Provider value={value} children={children} />;
}
export function usePolicyConfigContext() {
return useContext(PolicyConfigContext);
}

View file

@ -1,53 +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 { HTTPFields, TCPFields, ICMPFields, BrowserFields, TLSFields } from '../types';
import {
PolicyConfigContextProvider,
TCPContextProvider,
ICMPSimpleFieldsContextProvider,
HTTPContextProvider,
BrowserContextProvider,
TLSFieldsContextProvider,
} from '.';
import { IPolicyConfigContextProvider } from './policy_config_context';
interface Props {
children: React.ReactNode;
httpDefaultValues?: HTTPFields;
tcpDefaultValues?: TCPFields;
icmpDefaultValues?: ICMPFields;
browserDefaultValues?: BrowserFields;
tlsDefaultValues?: TLSFields;
policyDefaultValues?: Omit<IPolicyConfigContextProvider, 'children'>;
}
export const SyntheticsProviders = ({
children,
httpDefaultValues,
tcpDefaultValues,
icmpDefaultValues,
browserDefaultValues,
tlsDefaultValues,
policyDefaultValues,
}: Props) => {
return (
<PolicyConfigContextProvider {...policyDefaultValues}>
<HTTPContextProvider defaultValues={httpDefaultValues}>
<TCPContextProvider defaultValues={tcpDefaultValues}>
<TLSFieldsContextProvider defaultValues={tlsDefaultValues}>
<ICMPSimpleFieldsContextProvider defaultValues={icmpDefaultValues}>
<BrowserContextProvider defaultValues={browserDefaultValues}>
{children}
</BrowserContextProvider>
</ICMPSimpleFieldsContextProvider>
</TLSFieldsContextProvider>
</TCPContextProvider>
</HTTPContextProvider>
</PolicyConfigContextProvider>
);
};

View file

@ -1,50 +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, { createContext, useContext, useMemo, useState } from 'react';
import { TCPSimpleFields } from '../types';
import { DEFAULT_TCP_SIMPLE_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface TCPSimpleFieldsContext {
setFields: React.Dispatch<React.SetStateAction<TCPSimpleFields>>;
fields: TCPSimpleFields;
defaultValues: TCPSimpleFields;
}
interface TCPSimpleFieldsContextProvider {
children: React.ReactNode;
defaultValues?: TCPSimpleFields;
}
export const initialValues: TCPSimpleFields = DEFAULT_TCP_SIMPLE_FIELDS;
const defaultContext: TCPSimpleFieldsContext = {
setFields: (_fields: React.SetStateAction<TCPSimpleFields>) => {
throw new Error(
'setFields was not initialized for TCP Simple Fields, set it when you invoke the context'
);
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const TCPSimpleFieldsContext = createContext(defaultContext);
export const TCPSimpleFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: TCPSimpleFieldsContextProvider) => {
const [fields, setFields] = useState<TCPSimpleFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <TCPSimpleFieldsContext.Provider value={value} children={children} />;
};
export const useTCPSimpleFieldsContext = () => useContext(TCPSimpleFieldsContext);

View file

@ -1,48 +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, { createContext, useContext, useMemo, useState } from 'react';
import { TCPAdvancedFields } from '../types';
import { DEFAULT_TCP_ADVANCED_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface TCPAdvancedFieldsContext {
setFields: React.Dispatch<React.SetStateAction<TCPAdvancedFields>>;
fields: TCPAdvancedFields;
defaultValues: TCPAdvancedFields;
}
interface TCPAdvancedFieldsContextProvider {
children: React.ReactNode;
defaultValues?: TCPAdvancedFields;
}
export const initialValues: TCPAdvancedFields = DEFAULT_TCP_ADVANCED_FIELDS;
const defaultContext: TCPAdvancedFieldsContext = {
setFields: (_fields: React.SetStateAction<TCPAdvancedFields>) => {
throw new Error('setFields was not initialized, set it when you invoke the context');
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const TCPAdvancedFieldsContext = createContext(defaultContext);
export const TCPAdvancedFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: TCPAdvancedFieldsContextProvider) => {
const [fields, setFields] = useState<TCPAdvancedFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <TCPAdvancedFieldsContext.Provider value={value} children={children} />;
};
export const useTCPAdvancedFieldsContext = () => useContext(TCPAdvancedFieldsContext);

View file

@ -1,47 +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, { ReactNode } from 'react';
import { TCPFields, TCPSimpleFields, TCPAdvancedFields } from '../types';
import {
TCPSimpleFieldsContextProvider,
TCPAdvancedFieldsContextProvider,
defaultTCPSimpleFields,
defaultTCPAdvancedFields,
} from '.';
import { formatDefaultValues } from '../helpers/context_helpers';
interface TCPContextProviderProps {
defaultValues?: TCPFields;
children: ReactNode;
}
export const TCPContextProvider = ({ defaultValues, children }: TCPContextProviderProps) => {
const simpleKeys = Object.keys(defaultTCPSimpleFields) as Array<keyof TCPSimpleFields>;
const advancedKeys = Object.keys(defaultTCPAdvancedFields) as Array<keyof TCPAdvancedFields>;
const formattedDefaultSimpleFields = formatDefaultValues<TCPSimpleFields>(
simpleKeys,
defaultValues || {}
);
const formattedDefaultAdvancedFields = formatDefaultValues<TCPAdvancedFields>(
advancedKeys,
defaultValues || {}
);
const simpleFields: TCPSimpleFields | undefined = defaultValues
? formattedDefaultSimpleFields
: undefined;
const advancedFields: TCPAdvancedFields | undefined = defaultValues
? formattedDefaultAdvancedFields
: undefined;
return (
<TCPAdvancedFieldsContextProvider defaultValues={advancedFields}>
<TCPSimpleFieldsContextProvider defaultValues={simpleFields}>
{children}
</TCPSimpleFieldsContextProvider>
</TCPAdvancedFieldsContextProvider>
);
};

View file

@ -1,48 +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, { createContext, useContext, useMemo, useState } from 'react';
import { TLSFields } from '../types';
import { DEFAULT_TLS_FIELDS } from '../../../../../common/constants/monitor_defaults';
interface TLSFieldsContext {
setFields: React.Dispatch<React.SetStateAction<TLSFields>>;
fields: TLSFields;
defaultValues: TLSFields;
}
interface TLSFieldsContextProvider {
children: React.ReactNode;
defaultValues?: TLSFields;
}
export const initialValues: TLSFields = DEFAULT_TLS_FIELDS;
const defaultContext: TLSFieldsContext = {
setFields: (_fields: React.SetStateAction<TLSFields>) => {
throw new Error('setFields was not initialized, set it when you invoke the context');
},
fields: initialValues, // mutable
defaultValues: initialValues, // immutable
};
export const TLSFieldsContext = createContext(defaultContext);
export const TLSFieldsContextProvider = ({
children,
defaultValues = initialValues,
}: TLSFieldsContextProvider) => {
const [fields, setFields] = useState<TLSFields>(defaultValues);
const value = useMemo(() => {
return { fields, setFields, defaultValues };
}, [fields, defaultValues]);
return <TLSFieldsContext.Provider value={value} children={children} />;
};
export const useTLSFieldsContext = () => useContext(TLSFieldsContext);

View file

@ -1,393 +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 'jest-canvas-mock';
import React from 'react';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/rtl_helpers';
import {
TCPContextProvider,
HTTPContextProvider,
BrowserContextProvider,
ICMPSimpleFieldsContextProvider,
PolicyConfigContextProvider,
TLSFieldsContextProvider,
} from './contexts';
import { CustomFields } from './custom_fields';
import { ConfigKey, DataStream, ScheduleUnit } from './types';
import { validate as centralValidation } from '../monitor_management/validation';
import { defaultConfig } from './synthetics_policy_create_extension';
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'),
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility', () => ({
...jest.requireActual('@elastic/eui/lib/services/accessibility'),
useGeneratedHtmlId: () => `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 defaultValidation = centralValidation[DataStream.HTTP];
const defaultHTTPConfig = defaultConfig[DataStream.HTTP];
const defaultTCPConfig = defaultConfig[DataStream.TCP];
describe('<CustomFields />', () => {
let onFieldBlurMock: jest.Mock | undefined;
const WrappedComponent = ({
validate = defaultValidation,
isEditable = false,
dataStreams = [DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER],
onFieldBlur = onFieldBlurMock,
}) => {
return (
<HTTPContextProvider>
<PolicyConfigContextProvider isEditable={isEditable}>
<TCPContextProvider>
<BrowserContextProvider>
<ICMPSimpleFieldsContextProvider>
<TLSFieldsContextProvider>
<CustomFields
validate={validate}
dataStreams={dataStreams}
onFieldBlur={onFieldBlur}
/>
</TLSFieldsContextProvider>
</ICMPSimpleFieldsContextProvider>
</BrowserContextProvider>
</TCPContextProvider>
</PolicyConfigContextProvider>
</HTTPContextProvider>
);
};
beforeEach(() => {
onFieldBlurMock = undefined;
jest.resetAllMocks();
});
it('renders CustomFields', async () => {
const { getByText, getByLabelText, queryByLabelText } = render(
<WrappedComponent onFieldBlur={undefined} />
);
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
const url = getByLabelText('URL') as HTMLInputElement;
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement;
const monitorIntervalUnit = getByLabelText('Unit') as HTMLInputElement;
const apmServiceName = getByLabelText('APM service name') as HTMLInputElement;
const maxRedirects = getByLabelText('Max redirects') as HTMLInputElement;
const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement;
expect(monitorType).toBeInTheDocument();
expect(url).toBeInTheDocument();
expect(url.value).toEqual(defaultHTTPConfig[ConfigKey.URLS]);
expect(proxyUrl).toBeInTheDocument();
expect(proxyUrl.value).toEqual(defaultHTTPConfig[ConfigKey.PROXY_URL]);
expect(monitorIntervalNumber).toBeInTheDocument();
expect(monitorIntervalNumber.value).toEqual(defaultHTTPConfig[ConfigKey.SCHEDULE].number);
expect(monitorIntervalUnit).toBeInTheDocument();
expect(monitorIntervalUnit.value).toEqual(defaultHTTPConfig[ConfigKey.SCHEDULE].unit);
// expect(tags).toBeInTheDocument();
expect(apmServiceName).toBeInTheDocument();
expect(apmServiceName.value).toEqual(defaultHTTPConfig[ConfigKey.APM_SERVICE_NAME]);
expect(maxRedirects).toBeInTheDocument();
expect(maxRedirects.value).toEqual(`${defaultHTTPConfig[ConfigKey.MAX_REDIRECTS]}`);
expect(timeout).toBeInTheDocument();
expect(timeout.value).toEqual(`${defaultHTTPConfig[ConfigKey.TIMEOUT]}`);
// ensure other monitor type options are not in the DOM
expect(queryByLabelText('Host')).not.toBeInTheDocument();
expect(queryByLabelText('Wait in seconds')).not.toBeInTheDocument();
// ensure at least one http advanced option is present
const advancedOptionsButton = getByText('Advanced HTTP options');
fireEvent.click(advancedOptionsButton);
await waitFor(() => {
expect(getByLabelText('Request method')).toBeInTheDocument();
});
});
it('does not show monitor type dropdown when isEditable is true', async () => {
const { queryByLabelText } = render(<WrappedComponent isEditable />);
const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement;
expect(monitorType).not.toBeInTheDocument();
});
it('shows SSL fields when Enable SSL Fields is checked', async () => {
const { findByLabelText, queryByLabelText } = render(<WrappedComponent />);
const enableSSL = queryByLabelText('Enable TLS configuration') as HTMLInputElement;
expect(queryByLabelText('Certificate authorities')).not.toBeInTheDocument();
expect(queryByLabelText('Client key')).not.toBeInTheDocument();
expect(queryByLabelText('Client certificate')).not.toBeInTheDocument();
expect(queryByLabelText('Client key passphrase')).not.toBeInTheDocument();
expect(queryByLabelText('Verification mode')).not.toBeInTheDocument();
// ensure at least one http advanced option is present
fireEvent.click(enableSSL);
const ca = (await findByLabelText('Certificate authorities')) as HTMLInputElement;
const clientKey = (await findByLabelText('Client key')) as HTMLInputElement;
const clientKeyPassphrase = (await findByLabelText(
'Client key passphrase'
)) as HTMLInputElement;
const clientCertificate = (await findByLabelText('Client certificate')) as HTMLInputElement;
const verificationMode = (await findByLabelText('Verification mode')) as HTMLInputElement;
expect(ca).toBeInTheDocument();
expect(clientKey).toBeInTheDocument();
expect(clientKeyPassphrase).toBeInTheDocument();
expect(clientCertificate).toBeInTheDocument();
expect(verificationMode).toBeInTheDocument();
await waitFor(() => {
expect(ca.value).toEqual(defaultHTTPConfig[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]);
expect(clientKey.value).toEqual(defaultHTTPConfig[ConfigKey.TLS_KEY]);
expect(clientKeyPassphrase.value).toEqual(defaultHTTPConfig[ConfigKey.TLS_KEY_PASSPHRASE]);
expect(clientCertificate.value).toEqual(defaultHTTPConfig[ConfigKey.TLS_CERTIFICATE]);
expect(verificationMode.value).toEqual(defaultHTTPConfig[ConfigKey.TLS_VERIFICATION_MODE]);
});
});
it('handles updating each field (besides TLS)', async () => {
const { getByLabelText } = render(<WrappedComponent />);
const url = getByLabelText('URL') as HTMLInputElement;
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement;
const monitorIntervalUnit = getByLabelText('Unit') as HTMLInputElement;
const apmServiceName = getByLabelText('APM service name') as HTMLInputElement;
const maxRedirects = getByLabelText('Max redirects') as HTMLInputElement;
const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement;
fireEvent.change(url, { target: { value: 'http://elastic.co' } });
fireEvent.change(proxyUrl, { target: { value: 'http://proxy.co' } });
fireEvent.change(monitorIntervalNumber, { target: { value: '1' } });
fireEvent.change(monitorIntervalUnit, { target: { value: ScheduleUnit.MINUTES } });
fireEvent.change(apmServiceName, { target: { value: 'APM Service' } });
fireEvent.change(maxRedirects, { target: { value: '2' } });
fireEvent.change(timeout, { target: { value: '3' } });
expect(url.value).toEqual('http://elastic.co');
expect(proxyUrl.value).toEqual('http://proxy.co');
expect(monitorIntervalNumber.value).toEqual('1');
expect(monitorIntervalUnit.value).toEqual(ScheduleUnit.MINUTES);
expect(apmServiceName.value).toEqual('APM Service');
expect(maxRedirects.value).toEqual('2');
expect(timeout.value).toEqual('3');
});
it('handles switching monitor type', () => {
const { getByText, queryByText, getByLabelText, queryByLabelText } = render(
<WrappedComponent />
);
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
expect(monitorType).toBeInTheDocument();
expect(monitorType.value).toEqual(defaultHTTPConfig[ConfigKey.MONITOR_TYPE]);
fireEvent.change(monitorType, { target: { value: DataStream.TCP } });
// expect tcp fields to be in the DOM
const host = getByLabelText('Host:Port') as HTMLInputElement;
expect(host).toBeInTheDocument();
expect(host.value).toEqual(defaultTCPConfig[ConfigKey.HOSTS]);
// expect HTTP fields not to be in the DOM
expect(queryByLabelText('URL')).not.toBeInTheDocument();
expect(queryByLabelText('Max redirects')).not.toBeInTheDocument();
// expect tls options to be available for TCP
// here we must getByText because EUI will generate duplicate aria-labelledby
// values within the test-env generator used, and that will conflict with other
// automatically generated labels. See:
// https://github.com/elastic/eui/blob/91b416dcd51e116edb2cb4a2cac4c306512e28c7/src/services/accessibility/html_id_generator.testenv.ts#L12
expect(queryByText(/Enable TLS configuration/)).toBeInTheDocument();
// ensure at least one tcp advanced option is present
let advancedOptionsButton = getByText('Advanced TCP options');
fireEvent.click(advancedOptionsButton);
expect(queryByLabelText('Request method')).not.toBeInTheDocument();
expect(getByLabelText('Request payload')).toBeInTheDocument();
fireEvent.change(monitorType, { target: { value: DataStream.ICMP } });
// expect ICMP fields to be in the DOM
expect(getByLabelText('Wait in seconds')).toBeInTheDocument();
// expect tls options not to be available for ICMP
expect(queryByText(/Enable TLS configuration/)).not.toBeInTheDocument();
// expect TCP fields not to be in the DOM
expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument();
fireEvent.change(monitorType, { target: { value: DataStream.BROWSER } });
// expect browser fields to be in the DOM
expect(
screen.getByText('Runs Synthetic test scripts that are defined inline.')
).toBeInTheDocument();
expect(
getByText(/To create a "Browser" monitor, please ensure you are using the/)
).toBeInTheDocument();
// ensure at least one browser advanced option is present
advancedOptionsButton = getByText('Advanced Browser options');
fireEvent.click(advancedOptionsButton);
expect(getByLabelText('Screenshot options')).toBeInTheDocument();
// expect ICMP fields not to be in the DOM
expect(queryByLabelText('Wait in seconds')).not.toBeInTheDocument();
});
it('does not show timeout for browser monitors', () => {
const { getByLabelText, queryByLabelText } = render(<WrappedComponent />);
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
let timeout = getByLabelText('Timeout in seconds') as HTMLInputElement;
expect(monitorType).toBeInTheDocument();
expect(monitorType.value).toEqual(defaultHTTPConfig[ConfigKey.MONITOR_TYPE]);
expect(timeout.value).toEqual(defaultHTTPConfig[ConfigKey.TIMEOUT]);
// change to browser monitor
fireEvent.change(monitorType, { target: { value: DataStream.BROWSER } });
// expect timeout not to be in the DOM
expect(queryByLabelText('Timeout in seconds')).not.toBeInTheDocument();
// change back to HTTP
fireEvent.change(monitorType, { target: { value: DataStream.HTTP } });
// expect timeout value to be present with the correct value
timeout = getByLabelText('Timeout in seconds') as HTMLInputElement;
expect(timeout.value).toEqual(defaultHTTPConfig[ConfigKey.TIMEOUT]);
});
it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => {
const { getByLabelText, queryByLabelText } = render(<WrappedComponent />);
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
fireEvent.change(monitorType, { target: { value: DataStream.TCP } });
expect(queryByLabelText('Resolve hostnames locally')).not.toBeInTheDocument();
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
fireEvent.change(proxyUrl, { target: { value: 'sampleProxyUrl' } });
expect(getByLabelText('Resolve hostnames locally')).toBeInTheDocument();
});
it('handles validation', () => {
const { getByText, getByLabelText, queryByText } = render(<WrappedComponent />);
const url = getByLabelText('URL') as HTMLInputElement;
const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement;
const maxRedirects = getByLabelText('Max redirects') as HTMLInputElement;
const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement;
// create errors
fireEvent.change(monitorIntervalNumber, { target: { value: '-1' } });
fireEvent.change(maxRedirects, { target: { value: '-1' } });
fireEvent.change(timeout, { target: { value: '-1' } });
const urlError = getByText('URL is required');
const monitorIntervalError = getByText('Monitor frequency is required');
const maxRedirectsError = getByText('Max redirects must be 0 or greater');
const timeoutError = getByText('Timeout must be greater than or equal to 0');
expect(urlError).toBeInTheDocument();
expect(monitorIntervalError).toBeInTheDocument();
expect(maxRedirectsError).toBeInTheDocument();
expect(timeoutError).toBeInTheDocument();
// resolve errors
fireEvent.change(url, { target: { value: 'http://elastic.co' } });
fireEvent.change(monitorIntervalNumber, { target: { value: '1' } });
fireEvent.change(maxRedirects, { target: { value: '1' } });
fireEvent.change(timeout, { target: { value: '1' } });
expect(queryByText('URL is required')).not.toBeInTheDocument();
expect(queryByText('Monitor frequency is required')).not.toBeInTheDocument();
expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument();
expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument();
// create more errors
fireEvent.change(monitorIntervalNumber, { target: { value: '1' } }); // 1 minute
fireEvent.change(timeout, { target: { value: '611' } }); // timeout cannot be more than monitor interval
const timeoutError2 = getByText('Timeout must be less than the monitor frequency');
expect(timeoutError2).toBeInTheDocument();
});
it('does not show monitor options that are not contained in datastreams', async () => {
const { getByText, queryByText, queryByLabelText } = render(
<WrappedComponent dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP]} />
);
const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement;
// resolve errors
fireEvent.click(monitorType);
await waitFor(() => {
expect(getByText('HTTP')).toBeInTheDocument();
expect(getByText('TCP')).toBeInTheDocument();
expect(getByText('ICMP')).toBeInTheDocument();
expect(queryByText('Browser (Beta)')).not.toBeInTheDocument();
});
});
it('allows monitors to be disabled', async () => {
const { queryByLabelText } = render(
<WrappedComponent dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP]} />
);
const enabled = queryByLabelText('Enabled') as HTMLInputElement;
expect(enabled).toBeChecked();
fireEvent.click(enabled);
await waitFor(() => {
expect(enabled).not.toBeChecked();
});
});
it('calls onFieldBlur on fields', () => {
onFieldBlurMock = jest.fn();
const { queryByLabelText } = render(
<WrappedComponent
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP]}
onFieldBlur={onFieldBlurMock}
/>
);
const monitorTypeSelect = queryByLabelText('Monitor Type') as HTMLInputElement;
fireEvent.click(monitorTypeSelect);
fireEvent.blur(monitorTypeSelect);
expect(onFieldBlurMock).toHaveBeenCalledWith(ConfigKey.MONITOR_TYPE);
});
});

View file

@ -1,253 +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, { useMemo, memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiSelect,
EuiSpacer,
EuiSwitch,
EuiCallOut,
EuiCode,
EuiLink,
} from '@elastic/eui';
import { DescribedFormGroupWithWrap } from './common/described_form_group_with_wrap';
import { ConfigKey, DataStream, Validation } from './types';
import { usePolicyConfigContext } from './contexts';
import { TLSFields } from './tls_fields';
import { HTTPSimpleFields } from './http/simple_fields';
import { HTTPAdvancedFields } from './http/advanced_fields';
import { TCPSimpleFields } from './tcp/simple_fields';
import { TCPAdvancedFields } from './tcp/advanced_fields';
import { ICMPSimpleFields } from './icmp/simple_fields';
import { BrowserSimpleFields } from './browser/simple_fields';
import { BrowserAdvancedFields } from './browser/advanced_fields';
import { ICMPAdvancedFields } from './icmp/advanced_fields';
interface Props {
validate: Validation;
dataStreams?: DataStream[];
children?: React.ReactNode;
appendAdvancedFields?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
const dataStreamToString = [
{
value: DataStream.BROWSER,
text: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browserLabel',
{
defaultMessage: 'Browser (Beta)',
}
),
},
{ value: DataStream.HTTP, text: 'HTTP' },
{ value: DataStream.TCP, text: 'TCP' },
{ value: DataStream.ICMP, text: 'ICMP' },
];
export const CustomFields = memo<Props>(
({ validate, dataStreams = [], children, appendAdvancedFields, minColumnWidth, onFieldBlur }) => {
const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } =
usePolicyConfigContext();
const isHTTP = monitorType === DataStream.HTTP;
const isTCP = monitorType === DataStream.TCP;
const isBrowser = monitorType === DataStream.BROWSER;
const isICMP = monitorType === DataStream.ICMP;
const dataStreamOptions = useMemo(() => {
return dataStreamToString.filter((dataStream) => dataStreams.includes(dataStream.value));
}, [dataStreams]);
const renderSimpleFields = (type: DataStream) => {
switch (type) {
case DataStream.HTTP:
return (
<HTTPSimpleFields validate={validate} onFieldBlur={(field) => onFieldBlur?.(field)} />
);
case DataStream.ICMP:
return (
<ICMPSimpleFields validate={validate} onFieldBlur={(field) => onFieldBlur?.(field)} />
);
case DataStream.TCP:
return (
<TCPSimpleFields validate={validate} onFieldBlur={(field) => onFieldBlur?.(field)} />
);
case DataStream.BROWSER:
return (
<BrowserSimpleFields
validate={validate}
onFieldBlur={(field) => onFieldBlur?.(field)}
/>
);
default:
return null;
}
};
const isWithInUptime = window.location.pathname.includes('/app/uptime');
return (
<EuiForm component="form">
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSectionTitle"
defaultMessage="Monitor settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSectionDescription"
defaultMessage="Configure your monitor with the following options."
/>
}
data-test-subj="monitorSettingsSection"
>
<EuiFlexGroup>
<EuiFlexItem>
{children}
{!isEditable && (
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType"
defaultMessage="Monitor Type"
/>
}
isInvalid={
!!validate[ConfigKey.MONITOR_TYPE]?.({
[ConfigKey.MONITOR_TYPE]: monitorType as DataStream,
})
}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.error"
defaultMessage="Monitor type is required"
/>
}
>
<EuiSelect
options={dataStreamOptions}
value={monitorType}
onChange={(event) => setMonitorType(event.target.value as DataStream)}
onBlur={() => onFieldBlur?.(ConfigKey.MONITOR_TYPE)}
data-test-subj="syntheticsMonitorTypeField"
/>
</EuiFormRow>
)}
<EuiSpacer size="s" />
{isBrowser && !isWithInUptime && (
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.title"
defaultMessage="Requirement"
/>
}
size="s"
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.content"
defaultMessage='To create a "Browser" monitor, please ensure you are using the {agent} Docker container, which contains the dependencies to run these monitors. For more information, please visit our {link}.'
values={{
agent: <EuiCode>elastic-agent-complete</EuiCode>,
link: (
<EuiLink
data-test-subj="syntheticsCustomFieldsSyntheticsDocumentationLink"
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/synthetics-quickstart-fleet.html"
external
>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.link"
defaultMessage="synthetics documentation"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
)}
<EuiSpacer size="s" />
{renderSimpleFields(monitorType)}
</EuiFlexItem>
</EuiFlexGroup>
</DescribedFormGroupWithWrap>
{(isHTTP || isTCP) && (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tlsSettings.label"
defaultMessage="TLS settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tlsSettings.description"
defaultMessage="Configure TLS options, including verification mode, certificate authorities, and client certificates."
/>
}
id="uptimeFleetIsTLSEnabled"
>
<EuiSwitch
id="uptimeFleetIsTLSEnabled"
data-test-subj="syntheticsIsTLSEnabled"
checked={!!isTLSEnabled}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.certificateSettings.enableSSLSettings.label"
defaultMessage="Enable TLS configuration"
/>
}
onChange={(event) => setIsTLSEnabled(event.target.checked)}
/>
<TLSFields />
</DescribedFormGroupWithWrap>
)}
<EuiSpacer size="m" />
{isHTTP && (
<HTTPAdvancedFields
validate={validate}
minColumnWidth={minColumnWidth}
onFieldBlur={onFieldBlur}
>
{appendAdvancedFields}
</HTTPAdvancedFields>
)}
{isTCP && (
<TCPAdvancedFields minColumnWidth={minColumnWidth} onFieldBlur={onFieldBlur}>
{appendAdvancedFields}
</TCPAdvancedFields>
)}
{isBrowser && (
<BrowserAdvancedFields
validate={validate}
minColumnWidth={minColumnWidth}
onFieldBlur={onFieldBlur}
>
{appendAdvancedFields}
</BrowserAdvancedFields>
)}
{isICMP && <ICMPAdvancedFields>{appendAdvancedFields}</ICMPAdvancedFields>}
</EuiForm>
);
}
);

View file

@ -1,109 +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 { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/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],
});
});
});
});

View file

@ -1,77 +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, { 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,
};

View file

@ -1,17 +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.
*/
export function formatDefaultValues<Fields>(
keys: Array<keyof Fields>,
defaultValues: Partial<Fields>
) {
return keys.reduce((acc: any, currentValue) => {
const key = currentValue as keyof Fields;
acc[key] = defaultValues?.[key];
return acc;
}, {}) as Fields;
}

View file

@ -1,14 +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.
*/
export function parseJsonIfString<T>(value: T) {
if (typeof value === 'string') {
return JSON.parse(value);
}
return value;
}

View file

@ -1,119 +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 { useMemo } from 'react';
import {
PolicyConfig,
DataStream,
ConfigKey,
HTTPFields,
TCPFields,
ICMPFields,
BrowserFields,
} from '../types';
import {
usePolicyConfigContext,
useTCPSimpleFieldsContext,
useTCPAdvancedFieldsContext,
useICMPSimpleFieldsContext,
useHTTPSimpleFieldsContext,
useHTTPAdvancedFieldsContext,
useTLSFieldsContext,
useBrowserSimpleFieldsContext,
useBrowserAdvancedFieldsContext,
} from '../contexts';
import { DEFAULT_FIELDS } from '../../../../../common/constants/monitor_defaults';
export const defaultConfig: PolicyConfig = DEFAULT_FIELDS;
export const usePolicy = (fleetPolicyName: string = '') => {
const {
isTLSEnabled,
name: monitorName, // the monitor name can come from two different places, either from fleet or from uptime
locations,
namespace,
} = usePolicyConfigContext();
const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext();
const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext();
const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext();
const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext();
const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext();
const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext();
const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext();
const { fields: tlsFields } = useTLSFieldsContext();
const metadata = useMemo(
() => ({
is_tls_enabled: isTLSEnabled,
}),
[isTLSEnabled]
);
/* TODO add locations to policy config for synthetics service */
const policyConfig: PolicyConfig = useMemo(
() => ({
[DataStream.HTTP]: {
...httpSimpleFields,
...httpAdvancedFields,
...tlsFields,
[ConfigKey.METADATA]: {
...httpSimpleFields[ConfigKey.METADATA],
...metadata,
},
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as HTTPFields,
[DataStream.TCP]: {
...tcpSimpleFields,
...tcpAdvancedFields,
...tlsFields,
[ConfigKey.METADATA]: {
...tcpSimpleFields[ConfigKey.METADATA],
...metadata,
},
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as TCPFields,
[DataStream.ICMP]: {
...icmpSimpleFields,
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as ICMPFields,
[DataStream.BROWSER]: {
...browserSimpleFields,
...browserAdvancedFields,
[ConfigKey.METADATA]: {
...browserSimpleFields[ConfigKey.METADATA],
...metadata,
},
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as BrowserFields,
}),
[
metadata,
httpSimpleFields,
httpAdvancedFields,
tcpSimpleFields,
tcpAdvancedFields,
icmpSimpleFields,
browserSimpleFields,
browserAdvancedFields,
tlsFields,
fleetPolicyName,
monitorName,
locations,
namespace,
]
);
return policyConfig;
};

View file

@ -1,157 +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 { fireEvent } from '@testing-library/react';
import React from 'react';
import { render } from '../../../lib/helper/rtl_helpers';
import {
defaultHTTPAdvancedFields as defaultConfig,
HTTPAdvancedFieldsContextProvider,
} from '../contexts';
import {
ConfigKey,
DataStream,
HTTPAdvancedFields as HTTPAdvancedFieldsType,
HTTPMethod,
Validation,
} from '../types';
import { validate as centralValidation } from '../../monitor_management/validation';
import { HTTPAdvancedFields } from './advanced_fields';
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);
}}
/>
),
};
});
const defaultValidation = centralValidation[DataStream.HTTP];
describe('<HTTPAdvancedFields />', () => {
const onFieldBlur = jest.fn();
const WrappedComponent = ({
defaultValues,
validate = defaultValidation,
children,
}: {
defaultValues?: HTTPAdvancedFieldsType;
validate?: Validation;
children?: React.ReactNode;
}) => {
return (
<HTTPAdvancedFieldsContextProvider defaultValues={defaultValues}>
<HTTPAdvancedFields validate={validate} onFieldBlur={onFieldBlur}>
{children}
</HTTPAdvancedFields>
</HTTPAdvancedFieldsContextProvider>
);
};
it('renders HTTPAdvancedFields', () => {
const { getByText, getByLabelText } = render(<WrappedComponent />);
const requestMethod = getByLabelText('Request method') as HTMLInputElement;
const requestHeaders = getByText('Request headers');
const requestBody = getByText('Request body');
const indexResponseBody = getByLabelText('Index response body') as HTMLInputElement;
const indexResponseBodySelect = getByLabelText(
'Response body index policy'
) as HTMLInputElement;
const indexResponseHeaders = getByLabelText('Index response headers') as HTMLInputElement;
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
const responseHeadersContain = getByText('Check response headers contain');
const responseStatusEquals = getByText('Check response status equals');
const responseBodyContains = getByText('Check response body contains');
const responseBodyDoesNotContain = getByText('Check response body does not contain');
const username = getByLabelText('Username') as HTMLInputElement;
const password = getByLabelText('Password') as HTMLInputElement;
expect(requestMethod).toBeInTheDocument();
expect(requestMethod.value).toEqual(defaultConfig[ConfigKey.REQUEST_METHOD_CHECK]);
expect(requestHeaders).toBeInTheDocument();
expect(requestBody).toBeInTheDocument();
expect(indexResponseBody).toBeInTheDocument();
expect(indexResponseBody.checked).toBe(true);
expect(indexResponseBodySelect).toBeInTheDocument();
expect(indexResponseBodySelect.value).toEqual(defaultConfig[ConfigKey.RESPONSE_BODY_INDEX]);
expect(indexResponseHeaders).toBeInTheDocument();
expect(indexResponseHeaders.checked).toBe(true);
expect(proxyUrl).toBeInTheDocument();
expect(proxyUrl.value).toEqual(defaultConfig[ConfigKey.PROXY_URL]);
expect(responseStatusEquals).toBeInTheDocument();
expect(responseBodyContains).toBeInTheDocument();
expect(responseBodyDoesNotContain).toBeInTheDocument();
expect(responseHeadersContain).toBeInTheDocument();
expect(username).toBeInTheDocument();
expect(username.value).toBe(defaultConfig[ConfigKey.USERNAME]);
expect(password).toBeInTheDocument();
expect(password.value).toBe(defaultConfig[ConfigKey.PASSWORD]);
});
it('handles changing fields', () => {
const { getByText, getByLabelText } = render(<WrappedComponent />);
const username = getByLabelText('Username') as HTMLInputElement;
const password = getByLabelText('Password') as HTMLInputElement;
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
const requestMethod = getByLabelText('Request method') as HTMLInputElement;
const requestHeaders = getByText('Request headers');
const indexResponseBody = getByLabelText('Index response body') as HTMLInputElement;
const indexResponseHeaders = getByLabelText('Index response headers') as HTMLInputElement;
fireEvent.change(username, { target: { value: 'username' } });
fireEvent.change(password, { target: { value: 'password' } });
fireEvent.change(proxyUrl, { target: { value: 'proxyUrl' } });
fireEvent.change(requestMethod, { target: { value: HTTPMethod.POST } });
fireEvent.click(indexResponseBody);
fireEvent.click(indexResponseHeaders);
expect(username.value).toEqual('username');
expect(password.value).toEqual('password');
expect(proxyUrl.value).toEqual('proxyUrl');
expect(requestMethod.value).toEqual(HTTPMethod.POST);
expect(requestHeaders).toBeInTheDocument();
expect(indexResponseBody.checked).toBe(false);
expect(indexResponseHeaders.checked).toBe(false);
});
it('calls onBlur', () => {
const { getByLabelText } = render(<WrappedComponent />);
const username = getByLabelText('Username') as HTMLInputElement;
const requestMethod = getByLabelText('Request method') as HTMLInputElement;
const indexResponseBody = getByLabelText('Index response body') as HTMLInputElement;
[username, requestMethod, indexResponseBody].forEach((field) => fireEvent.blur(field));
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.USERNAME);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.REQUEST_METHOD_CHECK);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.RESPONSE_BODY_INDEX);
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
expect(upstream).toBeInTheDocument();
});
});

View file

@ -1,492 +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, { useCallback, memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiAccordion,
EuiCode,
EuiFieldText,
EuiFormRow,
EuiSelect,
EuiCheckbox,
EuiSpacer,
EuiFieldPassword,
} from '@elastic/eui';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { useHTTPAdvancedFieldsContext } from '../contexts';
import { ConfigKey, HTTPMethod, Validation } from '../types';
import { OptionalLabel } from '../optional_label';
import { HeaderField } from '../header_field';
import { RequestBodyField } from '../request_body_field';
import { ResponseBodyIndexField } from '../index_response_body_field';
import { ComboBox } from '../combo_box';
interface Props {
validate: Validation;
children?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const HTTPAdvancedFields = memo<Props>(
({ validate, children, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useHTTPAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<EuiAccordion
id="uptimeFleetHttpAdvancedOptions"
buttonContent={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions"
defaultMessage="Advanced HTTP options"
/>
}
data-test-subj="syntheticsHTTPAdvancedFieldsAccordion"
>
<EuiSpacer size="xl" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.title"
defaultMessage="Request configuration"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.description"
defaultMessage="Configure an optional request to send to the remote host including method, body, and headers."
/>
}
data-test-subj="httpAdvancedFieldsSection"
>
<EuiSpacer size="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.username.label"
defaultMessage="Username"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.username.helpText"
defaultMessage="Username for authenticating with the server."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.USERNAME]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.USERNAME,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.USERNAME)}
data-test-subj="syntheticsUsername"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.password.label"
defaultMessage="Password"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.password.helpText"
defaultMessage="Password for authenticating with the server."
/>
}
>
<EuiFieldPassword
value={fields[ConfigKey.PASSWORD]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PASSWORD,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.PASSWORD)}
data-test-subj="syntheticsPassword"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyURL.http.label"
defaultMessage="Proxy URL"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyUrl.http.helpText"
defaultMessage="HTTP proxy URL."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.PROXY_URL]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PROXY_URL,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.PROXY_URL)}
data-test-subj="syntheticsProxyUrl"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestMethod.label"
defaultMessage="Request method"
/>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestMethod.helpText"
defaultMessage="The HTTP method to use."
/>
}
>
<EuiSelect
options={requestMethodOptions}
value={fields[ConfigKey.REQUEST_METHOD_CHECK]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.REQUEST_METHOD_CHECK,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_METHOD_CHECK)}
data-test-subj="syntheticsRequestMethod"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestHeaders"
defaultMessage="Request headers"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.REQUEST_HEADERS_CHECK]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestHeadersField.error"
defaultMessage="Header key must be a valid HTTP token."
/>
}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestHeadersField.helpText"
defaultMessage="A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself."
/>
}
>
<HeaderField
contentMode={
fields[ConfigKey.REQUEST_BODY_CHECK].value
? fields[ConfigKey.REQUEST_BODY_CHECK].type
: undefined
} // only pass contentMode if the request body is truthy
defaultValue={fields[ConfigKey.REQUEST_HEADERS_CHECK]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.REQUEST_HEADERS_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_HEADERS_CHECK)}
data-test-subj="syntheticsRequestHeaders"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestBody"
defaultMessage="Request body"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestBody.helpText"
defaultMessage="Request body content."
/>
}
fullWidth
>
<RequestBodyField
value={fields[ConfigKey.REQUEST_BODY_CHECK].value}
type={fields[ConfigKey.REQUEST_BODY_CHECK].type}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.REQUEST_BODY_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_BODY_CHECK)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<EuiSpacer size="xl" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfiguration.title"
defaultMessage="Response configuration"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfiguration.description"
defaultMessage="Control the indexing of the HTTP response contents."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.indexResponseHeaders.helpText"
defaultMessage="Controls the indexing of the HTTP response headers to "
/>
<EuiCode>http.response.body.headers</EuiCode>
</>
}
data-test-subj="syntheticsIndexResponseHeaders"
>
<EuiCheckbox
id={'uptimeFleetIndexResponseHeaders'}
checked={fields[ConfigKey.RESPONSE_HEADERS_INDEX]}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfig.indexResponseHeaders"
defaultMessage="Index response headers"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.RESPONSE_HEADERS_INDEX,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_HEADERS_INDEX)}
/>
</EuiFormRow>
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.indexResponseBody.helpText"
defaultMessage="Controls the indexing of the HTTP response body contents to "
/>
<EuiCode>http.response.body.contents</EuiCode>
</>
}
>
<ResponseBodyIndexField
defaultValue={fields[ConfigKey.RESPONSE_BODY_INDEX]}
onChange={useCallback(
(policy) =>
handleInputChange({ value: policy, configKey: ConfigKey.RESPONSE_BODY_INDEX }),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_BODY_INDEX)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.title"
defaultMessage="Response checks"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.description"
defaultMessage="Configure the expected HTTP response."
/>
}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.label"
defaultMessage="Check response status equals"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.RESPONSE_STATUS_CHECK]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.error"
defaultMessage="Status code must contain digits only."
/>
}
helpText={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.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.',
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_STATUS_CHECK]}
onChange={(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_STATUS_CHECK,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_STATUS_CHECK)}
data-test-subj="syntheticsResponseStatusCheck"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.checkResponseHeadersContain"
defaultMessage="Check response headers contain"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.RESPONSE_HEADERS_CHECK]?.(fields)}
error={[
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseHeadersField.error"
defaultMessage="Header key must be a valid HTTP token."
/>,
]}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseHeadersField.helpText"
defaultMessage="A list of expected response headers."
/>
}
>
<HeaderField
defaultValue={fields[ConfigKey.RESPONSE_HEADERS_CHECK]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_HEADERS_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_HEADERS_CHECK)}
data-test-subj="syntheticsResponseHeaders"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseCheckPositive.label"
defaultMessage="Check response body contains"
/>
}
labelAppend={<OptionalLabel />}
helpText={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseBodyCheckPositive.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.',
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_BODY_CHECK_POSITIVE,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_BODY_CHECK_POSITIVE)}
data-test-subj="syntheticsResponseBodyCheckPositive"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseCheckNegative.label"
defaultMessage="Check response body does not contain"
/>
}
labelAppend={<OptionalLabel />}
helpText={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.httpAdvancedOptions.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.',
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE)}
data-test-subj="syntheticsResponseBodyCheckNegative"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
{children}
</EuiAccordion>
);
}
);
const requestMethodOptions = Object.values(HTTPMethod).map((method) => ({
value: method,
text: method,
}));

View file

@ -1,127 +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 { EuiFieldNumber, EuiFieldText, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { memo, useCallback } from 'react';
import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
import { useHTTPSimpleFieldsContext } from '../contexts';
import { OptionalLabel } from '../optional_label';
import { ScheduleField } from '../schedule_field';
import { ConfigKey, Validation } from '../types';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const HTTPSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields } = useHTTPSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.URL"
defaultMessage="URL"
/>
}
isInvalid={!!validate[ConfigKey.URLS]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.URL.error"
defaultMessage="URL is required"
/>
}
>
<EuiFieldText
value={fields[ConfigKey.URLS]}
onChange={(event) =>
handleInputChange({ value: event.target.value, configKey: ConfigKey.URLS })
}
onBlur={() => onFieldBlur(ConfigKey.URLS)}
data-test-subj="syntheticsUrlField"
/>
</EuiFormRow>
<EuiFormRow
id="syntheticsFleetScheduleField"
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval"
defaultMessage="Frequency"
/>
}
isInvalid={!!validate[ConfigKey.SCHEDULE]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval.error"
defaultMessage="Monitor frequency is required"
/>
}
>
<ScheduleField
onChange={(schedule) =>
handleInputChange({
value: schedule,
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.maxRedirects"
defaultMessage="Max redirects"
/>
}
isInvalid={!!validate[ConfigKey.MAX_REDIRECTS]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.maxRedirects.error"
defaultMessage="Max redirects must be 0 or greater"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.maxRedirects.helpText"
defaultMessage="The total number of redirections to follow."
/>
}
>
<EuiFieldNumber
data-test-subj="syntheticsHTTPSimpleFieldsFieldNumber"
min={0}
value={fields[ConfigKey.MAX_REDIRECTS]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.MAX_REDIRECTS,
})
}
onBlur={() => onFieldBlur(ConfigKey.MAX_REDIRECTS)}
/>
</EuiFormRow>
</SimpleFieldsWrapper>
);
});

View file

@ -1,33 +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 { render } from '../../../lib/helper/rtl_helpers';
import { ICMPAdvancedFields } from './advanced_fields';
// ensures fields and labels map appropriately
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
describe('<ICMPAdvancedFields />', () => {
const WrappedComponent = ({ children }: { children?: React.ReactNode }) => (
<ICMPAdvancedFields>{children}</ICMPAdvancedFields>
);
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText, getByTestId } = render(
<WrappedComponent>{upstreamFieldsText}</WrappedComponent>
);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
const accordion = getByTestId('syntheticsICMPAdvancedFieldsAccordion') as HTMLInputElement;
expect(upstream).toBeInTheDocument();
expect(accordion).toBeInTheDocument();
});
});

View file

@ -1,32 +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 { EuiAccordion, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
export const ICMPAdvancedFields = ({ children }: { children?: React.ReactNode }) => {
if (!!children) {
return (
<EuiAccordion
id="uptimeFleetIcmpAdvancedOptions"
buttonContent={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.icmpAdvancedOptions"
defaultMessage="Advanced ICMP options"
/>
}
data-test-subj="syntheticsICMPAdvancedFieldsAccordion"
>
<EuiSpacer size="xl" />
{children}
</EuiAccordion>
);
}
return <></>;
};

View file

@ -1,131 +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, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui';
import { ConfigKey, Validation } from '../types';
import { useICMPSimpleFieldsContext } from '../contexts';
import { OptionalLabel } from '../optional_label';
import { ScheduleField } from '../schedule_field';
import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const ICMPSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields } = useICMPSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.icmp.hosts"
defaultMessage="Host"
/>
}
isInvalid={!!validate[ConfigKey.HOSTS]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.icmp.hosts.error"
defaultMessage="Host is required"
/>
}
>
<EuiFieldText
value={fields[ConfigKey.HOSTS]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.HOSTS,
})
}
onBlur={() => onFieldBlur(ConfigKey.HOSTS)}
data-test-subj="syntheticsICMPHostField"
/>
</EuiFormRow>
<EuiFormRow
id="syntheticsFleetScheduleField--number syntheticsFleetScheduleField--unit"
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval"
defaultMessage="Frequency"
/>
}
isInvalid={!!validate[ConfigKey.SCHEDULE]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval.error"
defaultMessage="Monitor frequency is required"
/>
}
>
<ScheduleField
onChange={(schedule) =>
handleInputChange({
value: schedule,
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.wait.label"
defaultMessage="Wait in seconds"
/>
}
isInvalid={!!validate[ConfigKey.WAIT]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.wait.error"
defaultMessage="Wait must be 0 or greater"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.wait.helpText"
defaultMessage="The duration to wait before emitting another ICMP Echo Request if no response is received."
/>
}
>
<EuiFieldNumber
data-test-subj="syntheticsICMPSimpleFieldsFieldNumber"
min={0}
value={fields[ConfigKey.WAIT]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.WAIT,
})
}
onBlur={() => onFieldBlur(ConfigKey.WAIT)}
step={'any'}
/>
</EuiFormRow>
</SimpleFieldsWrapper>
);
});

View file

@ -1,115 +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 { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/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);
});
});
});

View file

@ -1,101 +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, { 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',
}
),
},
];

View file

@ -1,89 +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 userEvent from '@testing-library/user-event';
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/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]]);
});
});
});

View file

@ -1,201 +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, { 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>
);
};

View file

@ -1,20 +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 { 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>
);
};

View file

@ -1,86 +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 'jest-canvas-mock';
import React, { useState, useCallback } from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/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
type={config.type}
value={config.value}
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();
});
});
});

View file

@ -1,206 +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, { 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;
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, 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',
}
),
};

View file

@ -1,136 +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 { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { useState } from 'react';
import { render } from '../../lib/helper/rtl_helpers';
import { PolicyConfigContextProvider } from './contexts';
import { IPolicyConfigContextProvider } from './contexts/policy_config_context';
import { ScheduleField } from './schedule_field';
import { ScheduleUnit } from './types';
describe('<ScheduleField/>', () => {
const number = '1';
const unit = ScheduleUnit.MINUTES;
const onBlur = jest.fn();
const WrappedComponent = ({
allowedScheduleUnits,
}: Omit<IPolicyConfigContextProvider, 'children'>) => {
const [config, setConfig] = useState({
number,
unit,
});
return (
<PolicyConfigContextProvider allowedScheduleUnits={allowedScheduleUnits}>
<ScheduleField
number={config.number}
unit={config.unit}
onChange={(value) => setConfig(value)}
onBlur={onBlur}
/>
</PolicyConfigContextProvider>
);
};
afterEach(() => {
jest.resetAllMocks();
});
it('shows all options by default (allowedScheduleUnits is not provided)', () => {
const { getByText } = render(<WrappedComponent />);
expect(getByText('Minutes')).toBeInTheDocument();
expect(getByText('Seconds')).toBeInTheDocument();
});
it('shows only Minutes when allowedScheduleUnits = [ScheduleUnit.Minutes])', () => {
const { queryByText } = render(
<WrappedComponent allowedScheduleUnits={[ScheduleUnit.MINUTES]} />
);
expect(queryByText('Minutes')).toBeInTheDocument();
expect(queryByText('Seconds')).not.toBeInTheDocument();
});
it('shows only Seconds when allowedScheduleUnits = [ScheduleUnit.Seconds])', () => {
const { queryByText } = render(
<WrappedComponent allowedScheduleUnits={[ScheduleUnit.SECONDS]} />
);
expect(queryByText('Minutes')).not.toBeInTheDocument();
expect(queryByText('Seconds')).toBeInTheDocument();
});
it('only accepts whole number when allowedScheduleUnits = [ScheduleUnit.Minutes])', async () => {
const { getByTestId } = render(
<WrappedComponent allowedScheduleUnits={[ScheduleUnit.MINUTES]} />
);
const input = getByTestId('scheduleFieldInput') as HTMLInputElement;
const select = getByTestId('scheduleFieldSelect') as HTMLInputElement;
expect(input.value).toBe(number);
expect(select.value).toBe(ScheduleUnit.MINUTES);
userEvent.clear(input);
userEvent.type(input, '1.5');
// Click away to cause blur on input
userEvent.click(select);
await waitFor(() => {
expect(input.value).toBe('2');
});
});
it('handles schedule', () => {
const { getByText, getByTestId } = render(<WrappedComponent />);
const input = getByTestId('scheduleFieldInput') as HTMLInputElement;
const select = getByTestId('scheduleFieldSelect') as HTMLInputElement;
expect(input.value).toBe(number);
expect(select.value).toBe(unit);
expect(getByText('Minutes')).toBeInTheDocument();
});
it('handles on change', async () => {
const { getByText, getByTestId } = render(<WrappedComponent />);
const input = getByTestId('scheduleFieldInput') as HTMLInputElement;
const select = getByTestId('scheduleFieldSelect') as HTMLInputElement;
const newNumber = '2';
const newUnit = ScheduleUnit.SECONDS;
expect(input.value).toBe(number);
expect(select.value).toBe(unit);
userEvent.clear(input);
userEvent.type(input, newNumber);
await waitFor(() => {
expect(input.value).toBe(newNumber);
});
userEvent.selectOptions(select, newUnit);
await waitFor(() => {
expect(select.value).toBe(newUnit);
expect(getByText('Seconds')).toBeInTheDocument();
});
});
it('calls onBlur when changed', () => {
const { getByTestId } = render(
<WrappedComponent allowedScheduleUnits={[ScheduleUnit.SECONDS, ScheduleUnit.MINUTES]} />
);
const input = getByTestId('scheduleFieldInput') as HTMLInputElement;
const select = getByTestId('scheduleFieldSelect') as HTMLInputElement;
userEvent.clear(input);
userEvent.type(input, '2');
userEvent.selectOptions(select, ScheduleUnit.MINUTES);
userEvent.click(input);
expect(onBlur).toHaveBeenCalledTimes(2);
});
});

View file

@ -1,105 +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 { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { usePolicyConfigContext } from './contexts';
import { ConfigKey, MonitorFields, ScheduleUnit } from './types';
interface Props {
number: string;
onChange: (schedule: MonitorFields[ConfigKey.SCHEDULE]) => void;
onBlur: () => void;
unit: ScheduleUnit;
readOnly?: boolean;
}
export const ScheduleField = ({ number, onChange, onBlur, unit, readOnly = false }: Props) => {
const { allowedScheduleUnits } = usePolicyConfigContext();
const options = !allowedScheduleUnits?.length
? allOptions
: allOptions.filter((opt) => allowedScheduleUnits.includes(opt.value));
// When only minutes are allowed, don't allow user to input fractional value
const allowedStep = options.length === 1 && options[0].value === ScheduleUnit.MINUTES ? 1 : 'any';
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldNumber
aria-label={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.scheduleField.number',
{
defaultMessage: 'Number',
}
)}
id="syntheticsFleetScheduleField--number"
data-test-subj="scheduleFieldInput"
step={allowedStep}
min={1}
value={number}
onChange={(event) => {
const updatedNumber = event.target.value;
onChange({ number: updatedNumber, unit });
}}
onBlur={(event) => {
// Enforce whole number
if (allowedStep === 1) {
const updatedNumber = `${Math.ceil(+event.target.value)}`;
onChange({ number: updatedNumber, unit });
}
onBlur();
}}
readOnly={readOnly}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSelect
aria-label={i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.scheduleField.unit',
{
defaultMessage: 'Unit',
}
)}
id="syntheticsFleetScheduleField--unit"
data-test-subj="scheduleFieldSelect"
options={options}
value={unit}
onChange={(event) => {
const updatedUnit = event.target.value;
onChange({ number, unit: updatedUnit as ScheduleUnit });
}}
onBlur={() => onBlur()}
disabled={readOnly}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const allOptions = [
{
text: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.scheduleField.seconds',
{
defaultMessage: 'Seconds',
}
),
value: ScheduleUnit.SECONDS,
},
{
text: i18n.translate(
'xpack.synthetics.createPackagePolicy.stepConfigure.scheduleField.minutes',
{
defaultMessage: 'Minutes',
}
),
value: ScheduleUnit.MINUTES,
},
];

View file

@ -10,10 +10,6 @@ import { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/pu
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DeprecateNoticeModal } from './deprecate_notice_modal';
import { PolicyConfig } from './types';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
export const defaultConfig: PolicyConfig = DEFAULT_FIELDS;
/**
* Exports Synthetics-specific package policy instructions

View file

@ -11,8 +11,8 @@ import type { FleetStartServices } from '@kbn/fleet-plugin/public';
import { EuiButton, EuiCallOut } from '@elastic/eui';
import type { PackagePolicyEditExtensionComponentProps } from '@kbn/fleet-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ConfigKey, DataStream } from '../../../../common/runtime_types';
import { DeprecateNoticeModal } from './deprecate_notice_modal';
import { ConfigKey, DataStream } from './types';
import { useEditMonitorLocator } from '../../../apps/synthetics/hooks';
/**

View file

@ -1,94 +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 { fireEvent } from '@testing-library/react';
import { render } from '../../../lib/helper/rtl_helpers';
import { TCPAdvancedFields } from './advanced_fields';
import {
TCPAdvancedFieldsContextProvider,
defaultTCPAdvancedFields as defaultConfig,
} from '../contexts';
import { ConfigKey, TCPAdvancedFields as TCPAdvancedFieldsType } from '../types';
// ensures fields and labels map appropriately
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
describe('<TCPAdvancedFields />', () => {
const WrappedComponent = ({
defaultValues = defaultConfig,
children,
onFieldBlur,
}: {
defaultValues?: TCPAdvancedFieldsType;
children?: React.ReactNode;
onFieldBlur?: (field: ConfigKey) => void;
}) => {
return (
<TCPAdvancedFieldsContextProvider defaultValues={defaultValues}>
<TCPAdvancedFields onFieldBlur={onFieldBlur}>{children}</TCPAdvancedFields>
</TCPAdvancedFieldsContextProvider>
);
};
it('renders TCPAdvancedFields', () => {
const { getByLabelText } = render(<WrappedComponent />);
const requestPayload = getByLabelText('Request payload') as HTMLInputElement;
const proxyURL = getByLabelText('Proxy URL') as HTMLInputElement;
// ComboBox has an issue with associating labels with the field
const responseContains = getByLabelText('Check response contains') as HTMLInputElement;
expect(requestPayload).toBeInTheDocument();
expect(requestPayload.value).toEqual(defaultConfig[ConfigKey.REQUEST_SEND_CHECK]);
expect(proxyURL).toBeInTheDocument();
expect(proxyURL.value).toEqual(defaultConfig[ConfigKey.PROXY_URL]);
expect(responseContains).toBeInTheDocument();
expect(responseContains.value).toEqual(defaultConfig[ConfigKey.RESPONSE_RECEIVE_CHECK]);
});
it('handles changing fields', () => {
const { getByLabelText } = render(<WrappedComponent />);
const requestPayload = getByLabelText('Request payload') as HTMLInputElement;
fireEvent.change(requestPayload, { target: { value: 'success' } });
expect(requestPayload.value).toEqual('success');
});
it('calls onBlur on fields', () => {
const onFieldBlur = jest.fn();
const { getByLabelText } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const requestPayload = getByLabelText('Request payload') as HTMLInputElement;
fireEvent.change(requestPayload, { target: { value: 'success' } });
fireEvent.blur(requestPayload);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.REQUEST_SEND_CHECK);
});
it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => {
const { getByLabelText, queryByLabelText } = render(<WrappedComponent />);
expect(queryByLabelText('Resolve hostnames locally')).not.toBeInTheDocument();
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
fireEvent.change(proxyUrl, { target: { value: 'sampleProxyUrl' } });
expect(getByLabelText('Resolve hostnames locally')).toBeInTheDocument();
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
expect(upstream).toBeInTheDocument();
});
});

View file

@ -1,187 +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, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiAccordion, EuiCheckbox, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { useTCPAdvancedFieldsContext } from '../contexts';
import { ConfigKey } from '../types';
import { OptionalLabel } from '../optional_label';
interface Props {
children?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useTCPAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<EuiAccordion
id="uptimeFleetTCPAdvancedOptions"
buttonContent="Advanced TCP options"
data-test-subj="syntheticsTCPAdvancedFieldsAccordion"
>
<EuiSpacer size="m" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvacnedSettings.requestConfiguration.title"
defaultMessage="Request configuration"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvacnedSettings.requestConfiguration.description"
defaultMessage="Configure the payload sent to the remote host."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyURL.label"
defaultMessage="Proxy URL"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyUrl.tcp.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://."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.PROXY_URL]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PROXY_URL,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.PROXY_URL)}
data-test-subj="syntheticsProxyUrl"
/>
</EuiFormRow>
{!!fields[ConfigKey.PROXY_URL] && (
<EuiFormRow data-test-subj="syntheticsUseLocalResolver">
<EuiCheckbox
id={'uptimeFleetUseLocalResolverCheckbox'}
checked={fields[ConfigKey.PROXY_USE_LOCAL_RESOLVER]}
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.resolveHostnamesLocally"
defaultMessage="Resolve hostnames locally"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.PROXY_USE_LOCAL_RESOLVER,
})
}
/>
</EuiFormRow>
)}
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvacnedSettings.requestConfiguration.requestPayload.label"
defaultMessage="Request payload"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvacnedSettings.requestConfiguration.requestPayload.helpText"
defaultMessage="A payload string to send to the remote host."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.REQUEST_SEND_CHECK]}
onChange={useCallback(
(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.REQUEST_SEND_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_SEND_CHECK)}
data-test-subj="syntheticsTCPRequestSendCheck"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvancedOptions.responseConfiguration.title"
defaultMessage="Response checks"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvancedOptions.responseConfiguration.description"
defaultMessage="Configure the expected response from the remote host."
/>
}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvacnedSettings.responseConfiguration.responseContains.label"
defaultMessage="Check response contains"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.tcpAdvacnedSettings.responseConfiguration.responseContains.helpText"
defaultMessage="The expected remote host response."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.RESPONSE_RECEIVE_CHECK]}
onChange={useCallback(
(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.RESPONSE_RECEIVE_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_RECEIVE_CHECK)}
data-test-subj="syntheticsTCPResponseReceiveCheck"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
{children}
</EuiAccordion>
);
});

View file

@ -1,95 +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, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
import { ConfigKey, Validation } from '../types';
import { useTCPSimpleFieldsContext } from '../contexts';
import { ScheduleField } from '../schedule_field';
import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const TCPSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields } = useTCPSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts"
defaultMessage="Host:Port"
/>
}
isInvalid={!!validate[ConfigKey.HOSTS]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts.error"
defaultMessage="Host and port are required"
/>
}
>
<EuiFieldText
value={fields[ConfigKey.HOSTS]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.HOSTS,
})
}
onBlur={() => onFieldBlur(ConfigKey.HOSTS)}
data-test-subj="syntheticsTCPHostField"
/>
</EuiFormRow>
<EuiFormRow
id="syntheticsFleetScheduleField--number syntheticsFleetScheduleField--unit"
label={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval"
defaultMessage="Frequency"
/>
}
isInvalid={!!validate[ConfigKey.SCHEDULE]?.(fields)}
error={
<FormattedMessage
id="xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorInterval.error"
defaultMessage="Monitor frequency is required"
/>
}
>
<ScheduleField
onChange={(schedule) =>
handleInputChange({
value: schedule,
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
</EuiFormRow>
</SimpleFieldsWrapper>
);
});

View file

@ -1,45 +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 { parseJsonIfString } from '../helpers/parsers';
import { TLSFields, ConfigKey } from '../types';
import { Normalizer } from '../common/normalizers';
import { defaultTLSFields } from '../contexts';
type TLSNormalizerMap = Record<keyof TLSFields, Normalizer>;
export const tlsNormalizers: TLSNormalizerMap = {
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: (fields) =>
tlsJsonToObjectNormalizer(
fields?.[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]?.value,
ConfigKey.TLS_CERTIFICATE_AUTHORITIES
),
[ConfigKey.TLS_CERTIFICATE]: (fields) =>
tlsJsonToObjectNormalizer(
fields?.[ConfigKey.TLS_CERTIFICATE]?.value,
ConfigKey.TLS_CERTIFICATE
),
[ConfigKey.TLS_KEY]: (fields) =>
tlsJsonToObjectNormalizer(fields?.[ConfigKey.TLS_KEY]?.value, ConfigKey.TLS_KEY),
[ConfigKey.TLS_KEY_PASSPHRASE]: (fields) =>
tlsStringToObjectNormalizer(
fields?.[ConfigKey.TLS_KEY_PASSPHRASE]?.value,
ConfigKey.TLS_KEY_PASSPHRASE
),
[ConfigKey.TLS_VERIFICATION_MODE]: (fields) =>
tlsStringToObjectNormalizer(
fields?.[ConfigKey.TLS_VERIFICATION_MODE]?.value,
ConfigKey.TLS_VERIFICATION_MODE
),
[ConfigKey.TLS_VERSION]: (fields) =>
tlsJsonToObjectNormalizer(fields?.[ConfigKey.TLS_VERSION]?.value, ConfigKey.TLS_VERSION),
};
export const tlsStringToObjectNormalizer = (value: string = '', key: keyof TLSFields) =>
value ?? defaultTLSFields[key];
export const tlsJsonToObjectNormalizer = (value: string = '', key: keyof TLSFields) =>
value ? parseJsonIfString(value) : defaultTLSFields[key];

View file

@ -1,103 +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 { fireEvent } from '@testing-library/react';
import { render } from '../../lib/helper/rtl_helpers';
import { TLSFields } from './tls_fields';
import { ConfigKey, VerificationMode } from './types';
import {
TLSFieldsContextProvider,
PolicyConfigContextProvider,
defaultTLSFields as defaultValues,
} from './contexts';
// ensures that fields appropriately match to their label
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
describe('<TLSFields />', () => {
const WrappedComponent = ({ isEnabled = true }: { isEnabled?: boolean }) => {
return (
<PolicyConfigContextProvider defaultIsTLSEnabled={isEnabled}>
<TLSFieldsContextProvider defaultValues={defaultValues}>
<TLSFields />
</TLSFieldsContextProvider>
</PolicyConfigContextProvider>
);
};
it('renders TLSFields', () => {
const { getByLabelText, getByText } = render(<WrappedComponent />);
expect(getByText('Certificate settings')).toBeInTheDocument();
expect(getByText('Supported TLS protocols')).toBeInTheDocument();
expect(getByLabelText('Client certificate')).toBeInTheDocument();
expect(getByLabelText('Client key')).toBeInTheDocument();
expect(getByLabelText('Certificate authorities')).toBeInTheDocument();
expect(getByLabelText('Verification mode')).toBeInTheDocument();
});
it('updates fields and calls onChange', async () => {
const { getByLabelText } = render(<WrappedComponent />);
const clientCertificate = getByLabelText('Client certificate') as HTMLInputElement;
const clientKey = getByLabelText('Client key') as HTMLInputElement;
const clientKeyPassphrase = getByLabelText('Client key passphrase') as HTMLInputElement;
const certificateAuthorities = getByLabelText('Certificate authorities') as HTMLInputElement;
const verificationMode = getByLabelText('Verification mode') as HTMLInputElement;
const newValues = {
[ConfigKey.TLS_CERTIFICATE]: 'sampleClientCertificate',
[ConfigKey.TLS_KEY]: 'sampleClientKey',
[ConfigKey.TLS_KEY_PASSPHRASE]: 'sampleClientKeyPassphrase',
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: 'sampleCertificateAuthorities',
[ConfigKey.TLS_VERIFICATION_MODE]: VerificationMode.NONE,
};
fireEvent.change(clientCertificate, {
target: { value: newValues[ConfigKey.TLS_CERTIFICATE] },
});
fireEvent.change(clientKey, { target: { value: newValues[ConfigKey.TLS_KEY] } });
fireEvent.change(clientKeyPassphrase, {
target: { value: newValues[ConfigKey.TLS_KEY_PASSPHRASE] },
});
fireEvent.change(certificateAuthorities, {
target: { value: newValues[ConfigKey.TLS_CERTIFICATE_AUTHORITIES] },
});
fireEvent.change(verificationMode, {
target: { value: newValues[ConfigKey.TLS_VERIFICATION_MODE] },
});
expect(clientCertificate.value).toEqual(newValues[ConfigKey.TLS_CERTIFICATE]);
expect(clientKey.value).toEqual(newValues[ConfigKey.TLS_KEY]);
expect(certificateAuthorities.value).toEqual(newValues[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]);
expect(verificationMode.value).toEqual(newValues[ConfigKey.TLS_VERIFICATION_MODE]);
});
it('shows warning when verification mode is set to none', () => {
const { getByLabelText, getByText } = render(<WrappedComponent />);
const verificationMode = getByLabelText('Verification mode') as HTMLInputElement;
fireEvent.change(verificationMode, {
target: { value: VerificationMode.NONE },
});
expect(getByText('Disabling TLS')).toBeInTheDocument();
});
it('does not show fields when isEnabled is false', async () => {
const { queryByLabelText } = render(<WrappedComponent isEnabled={false} />);
expect(queryByLabelText('Client certificate')).not.toBeInTheDocument();
expect(queryByLabelText('Client key')).not.toBeInTheDocument();
expect(queryByLabelText('Client key passphrase')).not.toBeInTheDocument();
expect(queryByLabelText('Certificate authorities')).not.toBeInTheDocument();
expect(queryByLabelText('verification mode')).not.toBeInTheDocument();
});
});

View file

@ -1,60 +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, { useCallback, useEffect } from 'react';
import { TLSOptions, TLSConfig } from './common/tls_options';
import { useTLSFieldsContext, usePolicyConfigContext } from './contexts';
import { ConfigKey } from './types';
export const TLSFields = () => {
const { defaultValues, setFields } = useTLSFieldsContext();
const { isTLSEnabled } = usePolicyConfigContext();
const handleOnChange = useCallback(
(tlsConfig: TLSConfig) => {
setFields({
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: tlsConfig.certificateAuthorities,
[ConfigKey.TLS_CERTIFICATE]: tlsConfig.certificate,
[ConfigKey.TLS_KEY]: tlsConfig.key,
[ConfigKey.TLS_KEY_PASSPHRASE]: tlsConfig.keyPassphrase,
[ConfigKey.TLS_VERIFICATION_MODE]: tlsConfig.verificationMode,
[ConfigKey.TLS_VERSION]: tlsConfig.version,
});
},
[setFields]
);
useEffect(() => {
if (!isTLSEnabled) {
setFields({
[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: undefined,
[ConfigKey.TLS_CERTIFICATE]: undefined,
[ConfigKey.TLS_KEY]: undefined,
[ConfigKey.TLS_KEY_PASSPHRASE]: undefined,
[ConfigKey.TLS_VERIFICATION_MODE]: undefined,
[ConfigKey.TLS_VERSION]: undefined,
});
}
}, [setFields, isTLSEnabled]);
return isTLSEnabled ? (
<TLSOptions
defaultValues={{
certificateAuthorities: defaultValues[ConfigKey.TLS_CERTIFICATE_AUTHORITIES],
certificate: defaultValues[ConfigKey.TLS_CERTIFICATE],
key: defaultValues[ConfigKey.TLS_KEY],
keyPassphrase: defaultValues[ConfigKey.TLS_KEY_PASSPHRASE],
verificationMode: defaultValues[ConfigKey.TLS_VERIFICATION_MODE],
version: defaultValues[ConfigKey.TLS_VERSION],
}}
onChange={handleOnChange}
tlsRole="client"
/>
) : null;
};

View file

@ -1,41 +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 {
HTTPFields,
TCPFields,
ICMPFields,
BrowserFields,
ConfigKey,
ContentType,
DataStream,
Mode,
ThrottlingConfigKey,
ThrottlingSuffix,
ThrottlingSuffixType,
} from '../../../../common/runtime_types';
export * from '../../../../common/runtime_types/monitor_management';
export * from '../../../../common/types/monitor_validation';
export interface PolicyConfig {
[DataStream.HTTP]: HTTPFields;
[DataStream.TCP]: TCPFields;
[DataStream.ICMP]: ICMPFields;
[DataStream.BROWSER]: BrowserFields;
}
export const contentTypesToMode = {
[ContentType.FORM]: Mode.FORM,
[ContentType.JSON]: Mode.JSON,
[ContentType.TEXT]: Mode.PLAINTEXT,
[ContentType.XML]: Mode.XML,
};
export const configKeyToThrottlingSuffix: Record<ThrottlingConfigKey, ThrottlingSuffixType> = {
[ConfigKey.DOWNLOAD_SPEED]: ThrottlingSuffix.DOWNLOAD,
[ConfigKey.UPLOAD_SPEED]: ThrottlingSuffix.UPLOAD,
[ConfigKey.LATENCY]: ThrottlingSuffix.LATENCY,
};

View file

@ -1,103 +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 { screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import * as fetchers from '../../../state/api/monitor_management';
import {
DataStream,
HTTPFields,
ScheduleUnit,
SyntheticsMonitor,
} from '../../../../../common/runtime_types';
import { ActionBar } from './action_bar';
describe('<ActionBar />', () => {
const setMonitor = jest.spyOn(fetchers, 'setMonitor');
const monitor: SyntheticsMonitor = {
name: 'test-monitor',
schedule: {
unit: ScheduleUnit.MINUTES,
number: '2',
},
urls: 'https://elastic.co',
type: DataStream.HTTP,
} as unknown as HTTPFields;
beforeEach(() => {
jest.clearAllMocks();
});
it('only calls setMonitor when valid and after submission', () => {
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={true} />);
act(() => {
userEvent.click(screen.getByText('Save monitor'));
});
expect(setMonitor).toBeCalledWith({ monitor, id: undefined });
});
it('does not call setMonitor until submission', () => {
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={true} />);
expect(setMonitor).not.toBeCalled();
act(() => {
userEvent.click(screen.getByText('Save monitor'));
});
expect(setMonitor).toBeCalledWith({ monitor, id: undefined });
});
it('does not call setMonitor if invalid', () => {
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={false} />);
expect(setMonitor).not.toBeCalled();
act(() => {
userEvent.click(screen.getByText('Save monitor'));
});
expect(setMonitor).not.toBeCalled();
});
it('disables button and displays help text when form is invalid after first submission', async () => {
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={false} />);
expect(
screen.queryByText('Your monitor has errors. Please fix them before saving.')
).not.toBeInTheDocument();
expect(screen.getByText('Save monitor')).not.toBeDisabled();
act(() => {
userEvent.click(screen.getByText('Save monitor'));
});
await waitFor(() => {
expect(
screen.getByText('Your monitor has errors. Please fix them before saving.')
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Save monitor' })).toBeDisabled();
});
});
it('calls option onSave when saving monitor', () => {
const onSave = jest.fn();
render(
<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={false} onSave={onSave} />
);
act(() => {
userEvent.click(screen.getByText('Save monitor'));
});
expect(onSave).toBeCalled();
});
});

View file

@ -1,294 +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, { useCallback, useContext, useState, useEffect, useRef } from 'react';
import { useParams, Redirect } from 'react-router-dom';
import {
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
EuiText,
EuiPopover,
EuiOutsideClickDetector,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useSelector } from 'react-redux';
import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { showSyncErrors } from '../../../../apps/synthetics/components/monitors_page/management/show_sync_errors';
import { MONITOR_MANAGEMENT_ROUTE } from '../../../../../common/constants';
import { UptimeSettingsContext } from '../../../contexts';
import { setMonitor } from '../../../state/api';
import {
ConfigKey,
SyntheticsMonitor,
SourceType,
ServiceLocationErrors,
} from '../../../../../common/runtime_types';
import { TestRun } from '../test_now_mode/test_now_mode';
import { monitorManagementListSelector } from '../../../state/selectors';
import { kibanaService } from '../../../state/kibana_service';
import {
PRIVATE_AVAILABLE_LABEL,
TEST_SCHEDULED_LABEL,
} from '../../overview/monitor_list/translations';
export interface ActionBarProps {
monitor: SyntheticsMonitor;
isValid: boolean;
testRun?: TestRun;
isTestRunInProgress: boolean;
onSave?: () => void;
onTestNow?: () => void;
}
export const ActionBar = ({
monitor,
isValid,
onSave,
onTestNow,
testRun,
isTestRunInProgress,
}: ActionBarProps) => {
const { monitorId } = useParams<{ monitorId: string }>();
const { basePath } = useContext(UptimeSettingsContext);
const { locations } = useSelector(monitorManagementListSelector);
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isSuccessful, setIsSuccessful] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean | undefined>(undefined);
const mouseMoveTimeoutIds = useRef<[number, number]>([0, 0]);
const isReadOnly = monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
const isAnyPublicLocationSelected = monitor.locations?.some((loc) => loc.isServiceManaged);
const isOnlyPrivateLocations =
!locations.some((loc) => loc.isServiceManaged) ||
((monitor.locations?.length ?? 0) > 0 && !isAnyPublicLocationSelected);
const { data, status } = useFetcher(() => {
if (!isSaving || !isValid) {
return;
}
return setMonitor({
monitor,
id: monitorId,
});
}, [monitor, monitorId, isValid, isSaving]);
const hasErrors = data && 'attributes' in data && data.attributes.errors?.length > 0;
const loading = status === FETCH_STATUS.LOADING;
const handleOnSave = useCallback(() => {
if (onSave) {
onSave();
}
setIsSaving(true);
setHasBeenSubmitted(true);
}, [onSave]);
useEffect(() => {
if (!isSaving) {
return;
}
if (!isValid) {
setIsSaving(false);
return;
}
if (status === FETCH_STATUS.FAILURE || status === FETCH_STATUS.SUCCESS) {
setIsSaving(false);
}
if (status === FETCH_STATUS.FAILURE) {
kibanaService.toasts.addDanger({
title: MONITOR_FAILURE_LABEL,
toastLifeTimeMs: 3000,
});
} else if (status === FETCH_STATUS.SUCCESS && !hasErrors && !loading) {
kibanaService.toasts.addSuccess({
title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL,
toastLifeTimeMs: 3000,
});
setIsSuccessful(true);
} else if (hasErrors && !loading) {
showSyncErrors(
(data as { attributes: { errors: ServiceLocationErrors } })?.attributes.errors ?? [],
locations,
kibanaService.toasts
);
setIsSuccessful(true);
}
}, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]);
return isSuccessful ? (
<Redirect to={MONITOR_MANAGEMENT_ROUTE + '/all'} />
) : (
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="syntheticsActionBarButton"
color="ghost"
size="s"
href={`${basePath}/app/uptime/${MONITOR_MANAGEMENT_ROUTE}/all`}
>
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
{!isReadOnly ? (
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexItem grow={false}>
<WarningText>{!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL}</WarningText>
</EuiFlexItem>
{onTestNow && (
<EuiFlexItem grow={false}>
{/* Popover is used instead of EuiTooltip until the resolution of https://github.com/elastic/eui/issues/5604 */}
<EuiOutsideClickDetector
onOutsideClick={() => {
setIsPopoverOpen(false);
}}
>
<EuiPopover
repositionOnScroll={true}
ownFocus={false}
initialFocus={''}
button={
<EuiButton
css={{ width: '100%' }}
fill
size="s"
color="success"
iconType="play"
disabled={!isValid || isTestRunInProgress || !isAnyPublicLocationSelected}
data-test-subj={'monitorTestNowRunBtn'}
onClick={() => onTestNow()}
onMouseOver={() => {
// We need this custom logic to display a popover even when button is disabled.
clearTimeout(mouseMoveTimeoutIds.current[1]);
if (mouseMoveTimeoutIds.current[0] === 0) {
mouseMoveTimeoutIds.current[0] = setTimeout(() => {
clearTimeout(mouseMoveTimeoutIds.current[1]);
setIsPopoverOpen(true);
}, 250) as unknown as number;
}
}}
onMouseOut={() => {
// We need this custom logic to display a popover even when button is disabled.
clearTimeout(mouseMoveTimeoutIds.current[1]);
mouseMoveTimeoutIds.current[1] = setTimeout(() => {
clearTimeout(mouseMoveTimeoutIds.current[0]);
setIsPopoverOpen(false);
mouseMoveTimeoutIds.current = [0, 0];
}, 100) as unknown as number;
}}
>
{testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL}
</EuiButton>
}
isOpen={isPopoverOpen}
>
<EuiText style={{ width: 260, outline: 'none' }}>
<p>
{isTestRunInProgress
? TEST_SCHEDULED_LABEL
: isOnlyPrivateLocations || (isValid && !isAnyPublicLocationSelected)
? PRIVATE_AVAILABLE_LABEL
: TEST_NOW_DESCRIPTION}
</p>
</EuiText>
</EuiPopover>
</EuiOutsideClickDetector>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="syntheticsActionBarButton"
color="primary"
fill
size="s"
iconType="check"
onClick={handleOnSave}
isLoading={isSaving}
disabled={hasBeenSubmitted && !isValid}
>
{monitorId ? UPDATE_MONITOR_LABEL : SAVE_MONITOR_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
};
const WarningText = euiStyled(EuiText)`
box-shadow: -4px 0 ${(props) => props.theme.eui.euiColorWarning};
padding-left: 8px;
`;
const DISCARD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.discardLabel', {
defaultMessage: 'Cancel',
});
const SAVE_MONITOR_LABEL = i18n.translate('xpack.synthetics.monitorManagement.saveMonitorLabel', {
defaultMessage: 'Save monitor',
});
const UPDATE_MONITOR_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.updateMonitorLabel',
{
defaultMessage: 'Update monitor',
}
);
const RUN_TEST_LABEL = i18n.translate('xpack.synthetics.monitorManagement.runTest', {
defaultMessage: 'Run test',
});
const RE_RUN_TEST_LABEL = i18n.translate('xpack.synthetics.monitorManagement.reRunTest', {
defaultMessage: 'Re-run test',
});
const VALIDATION_ERROR_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.validationError',
{
defaultMessage: 'Your monitor has errors. Please fix them before saving.',
}
);
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.',
}
);
const TEST_NOW_DESCRIPTION = i18n.translate('xpack.synthetics.testRun.description', {
defaultMessage: 'Test your monitor and verify the results before saving',
});

View file

@ -1,81 +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 { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
import {
DataStream,
HTTPFields,
ScheduleUnit,
SyntheticsMonitor,
} from '../../../../../common/runtime_types';
import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher';
import * as kibana from '../../../state/kibana_service';
import { ActionBar } from './action_bar';
import { mockLocationsState } from '../mocks';
jest.mock('../../../state/kibana_service', () => ({
...jest.requireActual('../../../state/kibana_service'),
kibanaService: {
toasts: {
addWarning: jest.fn(),
},
},
}));
const monitor: SyntheticsMonitor = {
name: 'test-monitor',
schedule: {
unit: ScheduleUnit.MINUTES,
number: '2',
},
urls: 'https://elastic.co',
type: DataStream.HTTP,
} as unknown as HTTPFields;
describe('<ActionBar /> Service Errors', () => {
let useFetcher: jest.SpyInstance;
const toast = jest.fn();
beforeEach(() => {
useFetcher?.mockClear();
useFetcher = spyOnUseFetcher({});
});
it('Handles service errors', async () => {
jest.spyOn(kibana.kibanaService.toasts, 'addWarning').mockImplementation(toast);
useFetcher.mockReturnValue({
data: {
attributes: {
errors: [
{ locationId: 'us_central', error: { reason: 'Invalid config', status: 400 } },
{ locationId: 'us_central', error: { reason: 'Cannot schedule', status: 500 } },
],
},
},
status: FETCH_STATUS.SUCCESS,
refetch: () => {},
});
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={true} />, {
state: mockLocationsState,
});
userEvent.click(screen.getByText('Save monitor'));
await waitFor(() => {
expect(toast).toBeCalledTimes(2);
expect(toast).toBeCalledWith(
expect.objectContaining({
title: 'Unable to sync monitor config',
toastLifeTimeMs: 30000,
})
);
});
});
});

View file

@ -1,20 +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 { InPortal } from 'react-reverse-portal';
import { ActionBarPortalNode } from '../../../pages/monitor_management/portals';
import { ActionBar, ActionBarProps } from './action_bar';
export const ActionBarPortal = (props: ActionBarProps) => {
return (
<InPortal node={ActionBarPortalNode}>
<ActionBar {...props} />
</InPortal>
);
};

Some files were not shown because too many files have changed in this diff Show more