mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics] Enable private locations via elastic agent (#135782)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co>
This commit is contained in:
parent
c3ac0a163a
commit
0958e55c17
111 changed files with 3622 additions and 331 deletions
|
@ -99,6 +99,8 @@ const previouslyRegisteredTypes = [
|
|||
'siem-ui-timeline-pinned-event',
|
||||
'space',
|
||||
'spaces-usage-stats',
|
||||
'synthetics-monitor',
|
||||
'synthetics-privates-locations',
|
||||
'tag',
|
||||
'task',
|
||||
'telemetry',
|
||||
|
@ -110,6 +112,7 @@ const previouslyRegisteredTypes = [
|
|||
'upgrade-assistant-reindex-operation',
|
||||
'upgrade-assistant-telemetry',
|
||||
'uptime-dynamic-settings',
|
||||
'uptime-synthetics-api-key',
|
||||
'url',
|
||||
'usage-counters',
|
||||
'visualization',
|
||||
|
|
|
@ -160,7 +160,6 @@ describe('Fleet preconfiguration reset', () => {
|
|||
perPage: 10000,
|
||||
});
|
||||
|
||||
expect(packagePolicies.total).toBe(3);
|
||||
expect(
|
||||
packagePolicies.saved_objects.find((so) => so.id === 'elastic-cloud-fleet-server')
|
||||
).toBeDefined();
|
||||
|
|
|
@ -298,7 +298,7 @@ describe('Fleet setup preconfiguration with multiple instances Kibana', () => {
|
|||
type: 'epm-packages',
|
||||
perPage: 10000,
|
||||
});
|
||||
expect(packages.saved_objects).toHaveLength(2);
|
||||
expect(packages.saved_objects.length).toBeGreaterThanOrEqual(2);
|
||||
expect(packages.saved_objects.map((p) => p.attributes.name)).toEqual(
|
||||
expect.arrayContaining(['fleet_server', 'apm'])
|
||||
);
|
||||
|
|
|
@ -180,7 +180,7 @@ describe('Uprade package install version', () => {
|
|||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
expect(res.saved_objects).toHaveLength(4);
|
||||
|
||||
res.saved_objects.forEach((so) => {
|
||||
expect(so.attributes.install_format_schema_version).toBe(FLEET_INSTALL_FORMAT_VERSION);
|
||||
if (!OUTDATED_PACKAGES.includes(so.attributes.name)) {
|
||||
|
|
|
@ -37,6 +37,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
|
|||
unit: ScheduleUnit.MINUTES,
|
||||
},
|
||||
[ConfigKey.APM_SERVICE_NAME]: '',
|
||||
[ConfigKey.CONFIG_ID]: '',
|
||||
[ConfigKey.TAGS]: [],
|
||||
[ConfigKey.TIMEOUT]: '16',
|
||||
[ConfigKey.NAME]: '',
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export enum ConfigKey {
|
||||
APM_SERVICE_NAME = 'service.name',
|
||||
CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id',
|
||||
CONFIG_ID = 'config_id',
|
||||
ENABLED = 'enabled',
|
||||
HOSTS = 'hosts',
|
||||
IGNORE_HTTPS_ERRORS = 'ignore_https_errors',
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { BrowserFields, ConfigKey } from '../../runtime_types/monitor_management';
|
||||
|
||||
import { BrowserFields, ConfigKey } from '../types';
|
||||
import {
|
||||
Formatter,
|
||||
commonFormatters,
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CommonFields, MonitorFields, ConfigKey } from '../types';
|
||||
import { CommonFields, ConfigKey, MonitorFields } from '../../runtime_types/monitor_management';
|
||||
|
||||
export type Formatter = null | ((fields: Partial<MonitorFields>) => string | null);
|
||||
|
||||
|
@ -16,6 +16,7 @@ export const commonFormatters: CommonFormatMap = {
|
|||
[ConfigKey.LOCATIONS]: null,
|
||||
[ConfigKey.MONITOR_TYPE]: null,
|
||||
[ConfigKey.ENABLED]: null,
|
||||
[ConfigKey.CONFIG_ID]: null,
|
||||
[ConfigKey.SCHEDULE]: (fields) =>
|
||||
JSON.stringify(
|
||||
`@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}`
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { formatters } from './formatters';
|
||||
import { ConfigKey, DataStream, MonitorFields } from '../runtime_types';
|
||||
|
||||
export const formatSyntheticsPolicy = (
|
||||
newPolicy: NewPackagePolicy,
|
||||
monitorType: DataStream,
|
||||
config: Partial<MonitorFields & { location_name: string }>
|
||||
) => {
|
||||
const configKeys = Object.keys(config) as ConfigKey[];
|
||||
|
||||
const formattedPolicy = { ...newPolicy };
|
||||
const currentInput = formattedPolicy.inputs.find(
|
||||
(input) => input.type === `synthetics/${monitorType}`
|
||||
);
|
||||
const dataStream = currentInput?.streams.find(
|
||||
(stream) => stream.data_stream.dataset === monitorType
|
||||
);
|
||||
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
|
||||
|
||||
if (currentInput && dataStream) {
|
||||
// reset all data streams to enabled false
|
||||
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
|
||||
// enable only the input type and data stream that matches the monitor type.
|
||||
currentInput.enabled = true;
|
||||
dataStream.enabled = true;
|
||||
}
|
||||
|
||||
configKeys.forEach((key) => {
|
||||
const configItem = dataStream?.vars?.[key];
|
||||
if (configItem) {
|
||||
if (formatters[key]) {
|
||||
configItem.value = formatters[key]?.(config);
|
||||
} else {
|
||||
configItem.value = config[key] === undefined || config[key] === null ? null : config[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { formattedPolicy, dataStream, currentInput };
|
||||
};
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataStream } from '../types';
|
||||
|
||||
import { httpFormatters, HTTPFormatMap } from '../http/formatters';
|
||||
import { tcpFormatters, TCPFormatMap } from '../tcp/formatters';
|
||||
import { icmpFormatters, ICMPFormatMap } from '../icmp/formatters';
|
||||
import { browserFormatters, BrowserFormatMap } from '../browser/formatters';
|
||||
import { commonFormatters, CommonFormatMap } from '../common/formatters';
|
||||
import { DataStream } from '../runtime_types';
|
||||
import { httpFormatters, HTTPFormatMap } from './http/formatters';
|
||||
import { tcpFormatters, TCPFormatMap } from './tcp/formatters';
|
||||
import { icmpFormatters, ICMPFormatMap } from './icmp/formatters';
|
||||
import { browserFormatters, BrowserFormatMap } from './browser/formatters';
|
||||
import { commonFormatters, CommonFormatMap } from './common/formatters';
|
||||
|
||||
type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap;
|
||||
|
|
@ -5,14 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HTTPFields, ConfigKey } from '../types';
|
||||
import { tlsFormatters } from '../tls/formatters';
|
||||
import { HTTPFields, ConfigKey } from '../../runtime_types/monitor_management';
|
||||
|
||||
import {
|
||||
Formatter,
|
||||
commonFormatters,
|
||||
objectToJsonFormatter,
|
||||
arrayToJsonFormatter,
|
||||
} from '../common/formatters';
|
||||
import { tlsFormatters } from '../tls/formatters';
|
||||
|
||||
export type HTTPFormatMap = Record<keyof HTTPFields, Formatter>;
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ICMPFields, ConfigKey } from '../types';
|
||||
import { ICMPFields, ConfigKey } from '../../runtime_types/monitor_management';
|
||||
|
||||
import { Formatter, commonFormatters, secondsToCronFormatter } from '../common/formatters';
|
||||
|
||||
export type ICMPFormatMap = Record<keyof ICMPFields, Formatter>;
|
8
x-pack/plugins/synthetics/common/formatters/index.ts
Normal file
8
x-pack/plugins/synthetics/common/formatters/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './formatters';
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TCPFields, ConfigKey } from '../types';
|
||||
import { TCPFields, ConfigKey } from '../../runtime_types/monitor_management';
|
||||
|
||||
import { Formatter, commonFormatters, objectToJsonFormatter } from '../common/formatters';
|
||||
import { tlsFormatters } from '../tls/formatters';
|
||||
|
|
@ -4,8 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TLSFields, ConfigKey } from '../types';
|
||||
import { TLSFields, ConfigKey } from '../../runtime_types/monitor_management';
|
||||
import { Formatter } from '../common/formatters';
|
||||
|
||||
type TLSFormatMap = Record<keyof TLSFields, Formatter>;
|
|
@ -14,3 +14,4 @@ export * from './ping';
|
|||
export * from './snapshot';
|
||||
export * from './network_events';
|
||||
export * from './monitor_management';
|
||||
export * from './monitor_management/synthetics_private_locations';
|
||||
|
|
|
@ -53,14 +53,19 @@ export const ManifestLocationCodec = t.interface({
|
|||
status: LocationStatusCodec,
|
||||
});
|
||||
|
||||
export const ServiceLocationCodec = t.interface({
|
||||
id: t.string,
|
||||
label: t.string,
|
||||
geo: LocationGeoCodec,
|
||||
url: t.string,
|
||||
isServiceManaged: t.boolean,
|
||||
status: LocationStatusCodec,
|
||||
});
|
||||
export const ServiceLocationCodec = t.intersection([
|
||||
t.interface({
|
||||
id: t.string,
|
||||
label: t.string,
|
||||
geo: LocationGeoCodec,
|
||||
url: t.string,
|
||||
isServiceManaged: t.boolean,
|
||||
status: LocationStatusCodec,
|
||||
}),
|
||||
t.partial({
|
||||
isInvalid: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const MonitorServiceLocationCodec = t.intersection([
|
||||
t.interface({
|
||||
|
@ -71,6 +76,7 @@ export const MonitorServiceLocationCodec = t.intersection([
|
|||
label: t.string,
|
||||
geo: LocationGeoCodec,
|
||||
url: t.string,
|
||||
isInvalid: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ export const CommonFieldsCodec = t.intersection([
|
|||
[ConfigKey.TIMEOUT]: t.union([t.string, t.null]),
|
||||
[ConfigKey.REVISION]: t.number,
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec,
|
||||
[ConfigKey.CONFIG_ID]: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const PrivateLocationType = t.type({
|
||||
name: t.string,
|
||||
id: t.string,
|
||||
policyHostId: t.string,
|
||||
concurrentMonitors: t.number,
|
||||
latLon: t.string,
|
||||
});
|
||||
|
||||
export const SyntheticsPrivateLocationsType = t.type({
|
||||
locations: t.array(PrivateLocationType),
|
||||
});
|
||||
export type PrivateLocation = t.TypeOf<typeof PrivateLocationType>;
|
||||
export type SyntheticsPrivateLocations = t.TypeOf<typeof SyntheticsPrivateLocationsType>;
|
|
@ -105,6 +105,7 @@ export const MonitorType = t.intersection([
|
|||
gte: t.string,
|
||||
lt: t.string,
|
||||
}),
|
||||
fleet_managed: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton';
|
||||
export const privateLocationsSavedObjectName = 'synthetics-privates-locations';
|
|
@ -16,3 +16,4 @@ export * from './monitor_management.journey';
|
|||
export * from './monitor_management_enablement.journey';
|
||||
export * from './monitor_details';
|
||||
export * from './locations';
|
||||
export * from './private_locations';
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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, before } from '@elastic/synthetics';
|
||||
import { assertText, byTestId, TIMEOUT_60_SEC } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { monitorManagementPageProvider } from '../../page_objects/monitor_management';
|
||||
|
||||
journey('AddPrivateLocationMonitor', async ({ page, params: { kibanaUrl } }) => {
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
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"]', 'Private location monitor');
|
||||
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=Private location monitor');
|
||||
|
||||
await page.click('text=Private location monitorLast 15 minutes1 mRefresh >> span');
|
||||
});
|
||||
step('Click [placeholder="Find apps, content, and more. Ex: Discover"]', async () => {
|
||||
await page.click('[placeholder="Find apps, content, and more. Ex: Discover"]');
|
||||
await page.fill('[placeholder="Find apps, content, and more. Ex: Discover"]', 'integ');
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/* { url: '${kibanaUrl}/app/integrations/browse' }*/),
|
||||
page.click('text=Integrations'),
|
||||
]);
|
||||
await page.click('text=Installed integrations');
|
||||
expect(page.url()).toBe(`${kibanaUrl}/app/integrations/installed`);
|
||||
await page.click(`text=Elastic Synthetics`);
|
||||
expect(page.url()).toBe(`${kibanaUrl}/app/integrations/detail/synthetics-0.9.5/overview`);
|
||||
await page.click('text=Integration policies');
|
||||
expect(page.url()).toBe(`${kibanaUrl}/app/integrations/detail/synthetics-0.9.5/policies`);
|
||||
});
|
||||
step('Click text=Edit Elastic Synthetics integration', async () => {
|
||||
await assertText({ page, text: 'This table contains 1 rows out of 1 rows; Page 1 of 1.' });
|
||||
await page.click('[data-test-subj="integrationNameLink"]');
|
||||
await page.click('text=Edit in uptime');
|
||||
await page.click('text=Private location monitor');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './manage_locations';
|
||||
export * from './add_monitor_private_location';
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { journey, step, expect, before } from '@elastic/synthetics';
|
||||
import { monitorManagementPageProvider } from '../../page_objects/monitor_management';
|
||||
|
||||
journey('ManagePrivateLocation', async ({ page, params: { kibanaUrl } }) => {
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
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("Manage private locations")');
|
||||
await page.click('button:has-text("Add location")');
|
||||
});
|
||||
|
||||
step('Click text=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("Manage private locations")');
|
||||
await page.click('button:has-text("Add location")');
|
||||
|
||||
await addPrivateLocation('Test private location', 'Fleet test policy');
|
||||
});
|
||||
|
||||
step('Add another location', async () => {
|
||||
await page.click('button:has-text("Add location")');
|
||||
|
||||
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("Create location")');
|
||||
};
|
||||
|
||||
const addAgentPolicy = async (name: string) => {
|
||||
await page.click('[placeholder="Choose a name"]');
|
||||
await page.fill('[placeholder="Choose a name"]', name);
|
||||
await page.click('div[role="dialog"] button:has-text("Create agent policy")');
|
||||
};
|
||||
});
|
|
@ -112,6 +112,7 @@ const Application = (props: UptimeAppProps) => {
|
|||
...plugins,
|
||||
storage,
|
||||
data: startPlugins.data,
|
||||
fleet: startPlugins.fleet,
|
||||
inspector: startPlugins.inspector,
|
||||
triggersActionsUi: startPlugins.triggersActionsUi,
|
||||
observability: startPlugins.observability,
|
||||
|
|
|
@ -84,6 +84,7 @@ export const commonNormalizers: CommonNormalizerMap = {
|
|||
}
|
||||
},
|
||||
[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) =>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { NewPackagePolicy } from '@kbn/fleet-plugin/public';
|
||||
import { formatSyntheticsPolicy } from '../../../../../common/formatters/format_synthetics_policy';
|
||||
import { ConfigKey, DataStream, Validation, MonitorFields } from '../types';
|
||||
import { formatters } from '../helpers/formatters';
|
||||
|
||||
interface Props {
|
||||
monitorType: DataStream;
|
||||
|
@ -41,32 +41,15 @@ export const useUpdatePolicy = ({
|
|||
const configDidUpdate = configKeys.some((key) => config[key] !== currentConfig.current[key]);
|
||||
const isValid =
|
||||
!!newPolicy.name && !validationKeys.find((key) => validate[monitorType]?.[key]?.(config));
|
||||
const formattedPolicy = { ...newPolicy };
|
||||
const currentInput = formattedPolicy.inputs.find(
|
||||
(input) => input.type === `synthetics/${monitorType}`
|
||||
|
||||
const { formattedPolicy, dataStream, currentInput } = formatSyntheticsPolicy(
|
||||
newPolicy,
|
||||
monitorType,
|
||||
config
|
||||
);
|
||||
const dataStream = currentInput?.streams.find(
|
||||
(stream) => stream.data_stream.dataset === monitorType
|
||||
);
|
||||
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
|
||||
if (currentInput && dataStream) {
|
||||
// reset all data streams to enabled false
|
||||
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
|
||||
// enable only the input type and data stream that matches the monitor type.
|
||||
currentInput.enabled = true;
|
||||
dataStream.enabled = true;
|
||||
}
|
||||
|
||||
// prevent an infinite loop of updating the policy
|
||||
if (currentInput && dataStream && configDidUpdate) {
|
||||
configKeys.forEach((key) => {
|
||||
const configItem = dataStream.vars?.[key];
|
||||
if (configItem && formatters[key]) {
|
||||
configItem.value = formatters[key]?.(config);
|
||||
} else if (configItem) {
|
||||
configItem.value = config[key] === undefined || config[key] === null ? null : config[key];
|
||||
}
|
||||
});
|
||||
currentConfig.current = config;
|
||||
setUpdatedPolicy(formattedPolicy);
|
||||
onChange({
|
||||
|
|
|
@ -65,6 +65,8 @@ export const ActionBar = ({
|
|||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean | undefined>(undefined);
|
||||
const isReadOnly = monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
|
||||
const hasServiceManagedLocation = monitor.locations?.some((loc) => loc.isServiceManaged);
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (!isSaving || !isValid) {
|
||||
return;
|
||||
|
@ -150,7 +152,7 @@ export const ActionBar = ({
|
|||
size="s"
|
||||
color="success"
|
||||
iconType="play"
|
||||
disabled={!isValid || isTestRunInProgress}
|
||||
disabled={!isValid || isTestRunInProgress || !hasServiceManagedLocation}
|
||||
data-test-subj={'monitorTestNowRunBtn'}
|
||||
onClick={() => onTestNow()}
|
||||
onMouseEnter={() => {
|
||||
|
|
|
@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiSwitch } from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useLocations } from './hooks/use_locations';
|
||||
import { ClientPluginsSetup, ClientPluginsStart } from '../../../plugin';
|
||||
import { kibanaService } from '../../state/kibana_service';
|
||||
import { MONITOR_ADD_ROUTE } from '../../../../common/constants';
|
||||
import { useEnablement } from './hooks/use_enablement';
|
||||
|
@ -29,6 +31,8 @@ export const AddMonitorBtn = () => {
|
|||
} = useEnablement();
|
||||
const { isEnabled, canEnable, areApiKeysEnabled } = enablement || {};
|
||||
|
||||
const { locations } = useLocations();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEnabling && isEnabled) {
|
||||
setIsEnabling(false);
|
||||
|
@ -90,7 +94,16 @@ export const AddMonitorBtn = () => {
|
|||
|
||||
const loading = allowedLoading || enablementLoading;
|
||||
|
||||
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
|
||||
const kServices = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const canSave: boolean = !!kServices?.application?.capabilities.uptime.save;
|
||||
|
||||
const canSaveIntegrations: boolean =
|
||||
!!kServices?.fleet?.authz.integrations.writeIntegrationPolicies;
|
||||
|
||||
const isCloud = useKibana<ClientPluginsSetup>().services?.cloud?.isCloudEnabled;
|
||||
|
||||
const canSavePrivate: boolean = Boolean(isCloud) || canSaveIntegrations;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
|
@ -120,7 +133,9 @@ export const AddMonitorBtn = () => {
|
|||
<EuiButton
|
||||
isLoading={loading}
|
||||
fill
|
||||
isDisabled={!canSave || !isEnabled || !isAllowed}
|
||||
isDisabled={
|
||||
!canSave || !isEnabled || !isAllowed || !canSavePrivate || locations.length === 0
|
||||
}
|
||||
iconType="plus"
|
||||
data-test-subj="syntheticsAddMonitorBtn"
|
||||
href={history.createHref({
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
import { BrowserFields, ConfigKey } from '../../../../../common/runtime_types';
|
||||
|
||||
export function usePrivateLocationPermissions(monitor?: BrowserFields) {
|
||||
const kServices = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const canSaveIntegrations: boolean =
|
||||
!!kServices?.fleet?.authz.integrations.writeIntegrationPolicies;
|
||||
|
||||
const locations = (monitor as BrowserFields)?.[ConfigKey.LOCATIONS];
|
||||
|
||||
const hasPrivateLocation = locations?.some((location) => !location.isServiceManaged);
|
||||
|
||||
const canUpdatePrivateMonitor = !(hasPrivateLocation && !canSaveIntegrations);
|
||||
|
||||
return { canUpdatePrivateMonitor };
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setManageFlyoutOpen } from '../../../state/private_locations';
|
||||
|
||||
export const EmptyLocations = ({
|
||||
setIsAddingNew,
|
||||
disabled,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
setIsAddingNew?: (val: boolean) => void;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="visMapCoordinate"
|
||||
title={<h2>{START_ADDING_LOCATIONS}</h2>}
|
||||
body={<p>{START_ADDING_LOCATIONS_DESCRIPTION}</p>}
|
||||
actions={
|
||||
<EuiButton
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
fill
|
||||
onClick={() => {
|
||||
setIsAddingNew?.(true);
|
||||
dispatch(setManageFlyoutOpen(true));
|
||||
}}
|
||||
>
|
||||
{ADD_LOCATION}
|
||||
</EuiButton>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>{LEARN_MORE}</h3>
|
||||
</EuiTitle>
|
||||
<EuiLink href="#" target="_blank">
|
||||
{READ_DOCS}
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const START_ADDING_LOCATIONS = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.startAddingLocations',
|
||||
{
|
||||
defaultMessage: 'Start adding private locations',
|
||||
}
|
||||
);
|
||||
|
||||
const START_ADDING_LOCATIONS_DESCRIPTION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.startAddingLocationsDescription',
|
||||
{
|
||||
defaultMessage: 'Add your first private location to run monitors on premiss via Elastic agent.',
|
||||
}
|
||||
);
|
||||
|
||||
const ADD_LOCATION = i18n.translate('xpack.synthetics.monitorManagement.addLocation', {
|
||||
defaultMessage: 'Add location',
|
||||
});
|
||||
|
||||
const READ_DOCS = i18n.translate('xpack.synthetics.monitorManagement.readDocs', {
|
||||
defaultMessage: 'Read the docs',
|
||||
});
|
||||
|
||||
const LEARN_MORE = i18n.translate('xpack.synthetics.monitorManagement.learnMore', {
|
||||
defaultMessage: 'Want to learn more?',
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useLocationMonitors } from './use_location_monitors';
|
||||
import { defaultCore, WrappedHelper } from '../../../../../apps/synthetics/utils/testing';
|
||||
|
||||
describe('useLocationMonitors', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useLocationMonitors(), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ locations: [] });
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
locations: {
|
||||
terms: { field: 'synthetics-monitor.attributes.locations.id', size: 10000 },
|
||||
},
|
||||
},
|
||||
perPage: 0,
|
||||
type: 'synthetics-monitor',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns expected results after data', async () => {
|
||||
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
|
||||
aggregations: {
|
||||
locations: {
|
||||
buckets: [
|
||||
{ key: 'Test', doc_count: 5 },
|
||||
{ key: 'Test 1', doc_count: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationMonitors(), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ locations: [] });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
locations: [
|
||||
{
|
||||
id: 'Test',
|
||||
count: 5,
|
||||
},
|
||||
{
|
||||
id: 'Test 1',
|
||||
count: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
monitorAttributes,
|
||||
syntheticsMonitorType,
|
||||
} from '../../../../../../common/types/saved_objects';
|
||||
|
||||
interface AggsResponse {
|
||||
locations: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const useLocationMonitors = () => {
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data } = useFetcher(() => {
|
||||
const aggs = {
|
||||
locations: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.locations.id`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
return savedObjects?.client.find<unknown, typeof aggs>({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 0,
|
||||
aggs,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
if (data?.aggregations) {
|
||||
const newValues = (data.aggregations as AggsResponse)?.locations.buckets.map(
|
||||
({ key, doc_count: count }) => ({ id: key, count })
|
||||
);
|
||||
|
||||
return { locations: newValues };
|
||||
}
|
||||
return { locations: [] };
|
||||
}, [data]);
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { defaultCore, WrappedHelper } from '../../../../../apps/synthetics/utils/testing';
|
||||
import { useLocationsAPI } from './use_locations_api';
|
||||
|
||||
describe('useLocationsAPI', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useLocationsAPI({ isOpen: false }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
fetchLoading: true,
|
||||
deleteLoading: true,
|
||||
privateLocations: [],
|
||||
})
|
||||
);
|
||||
expect(defaultCore.savedObjects.client.get).toHaveBeenCalledWith(
|
||||
'synthetics-privates-locations',
|
||||
'synthetics-privates-locations-singleton'
|
||||
);
|
||||
});
|
||||
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
|
||||
attributes: {
|
||||
locations: [
|
||||
{
|
||||
id: 'Test',
|
||||
policyHostId: 'testPolicy',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
it('returns expected results after data', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI({ isOpen: true }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
deleteLoading: true,
|
||||
fetchLoading: true,
|
||||
privateLocations: [],
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
deleteLoading: false,
|
||||
fetchLoading: false,
|
||||
privateLocations: [
|
||||
{
|
||||
id: 'Test',
|
||||
policyHostId: 'testPolicy',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds location on submit', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI({ isOpen: true }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.onSubmit({
|
||||
id: 'new',
|
||||
policyHostId: 'newPolicy',
|
||||
name: 'new',
|
||||
concurrentMonitors: 1,
|
||||
latLon: '',
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultCore.savedObjects.client.create).toHaveBeenCalledWith(
|
||||
'synthetics-privates-locations',
|
||||
{
|
||||
locations: [
|
||||
{ id: 'Test', policyHostId: 'testPolicy' },
|
||||
{
|
||||
concurrentMonitors: 1,
|
||||
id: 'newPolicy',
|
||||
latLon: '',
|
||||
name: 'new',
|
||||
policyHostId: 'newPolicy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes location on delete', async () => {
|
||||
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
|
||||
attributes: {
|
||||
locations: [
|
||||
{
|
||||
id: 'Test',
|
||||
policyHostId: 'testPolicy',
|
||||
},
|
||||
{
|
||||
id: 'Test1',
|
||||
policyHostId: 'testPolicy1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI({ isOpen: true }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.onDelete('Test');
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultCore.savedObjects.client.create).toHaveBeenLastCalledWith(
|
||||
'synthetics-privates-locations',
|
||||
{
|
||||
locations: [
|
||||
{
|
||||
id: 'Test1',
|
||||
policyHostId: 'testPolicy1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useState } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import {
|
||||
setSyntheticsPrivateLocations,
|
||||
getSyntheticsPrivateLocations,
|
||||
} from '../../../../state/private_locations/api';
|
||||
|
||||
export const useLocationsAPI = ({ isOpen }: { isOpen: boolean }) => {
|
||||
const [formData, setFormData] = useState<PrivateLocation>();
|
||||
const [deleteId, setDeleteId] = useState<string>();
|
||||
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data: currentPrivateLocations, loading: fetchLoading } = useFetcher(() => {
|
||||
if (!formData) return getSyntheticsPrivateLocations(savedObjects?.client!);
|
||||
return Promise.resolve(null);
|
||||
}, [formData, deleteId, isOpen]);
|
||||
|
||||
const { loading: saveLoading } = useFetcher(async () => {
|
||||
if (currentPrivateLocations && formData) {
|
||||
const existingLocations = currentPrivateLocations.filter((loc) => loc.id !== formData.id);
|
||||
|
||||
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
|
||||
locations: [...(existingLocations ?? []), { ...formData, id: formData.policyHostId }],
|
||||
});
|
||||
setFormData(undefined);
|
||||
return result;
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}, [formData, currentPrivateLocations]);
|
||||
|
||||
const onSubmit = (data: PrivateLocation) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
setDeleteId(id);
|
||||
};
|
||||
|
||||
const { loading: deleteLoading } = useFetcher(async () => {
|
||||
if (deleteId) {
|
||||
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
|
||||
locations: (currentPrivateLocations ?? []).filter((loc) => loc.id !== deleteId),
|
||||
});
|
||||
setDeleteId(undefined);
|
||||
return result;
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}, [deleteId, currentPrivateLocations]);
|
||||
|
||||
return {
|
||||
onSubmit,
|
||||
onDelete,
|
||||
fetchLoading: Boolean(fetchLoading || Boolean(formData)),
|
||||
saveLoading: Boolean(saveLoading),
|
||||
deleteLoading: Boolean(deleteLoading),
|
||||
privateLocations: currentPrivateLocations ?? [],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PolicyHostNeeded } from './policy_host_needed';
|
||||
import { PrivateLocation } from '../../../../../common/runtime_types';
|
||||
import { PolicyHostsField } from './policy_hosts';
|
||||
import { useFormWrapped } from '../../../../hooks/use_form_wrapped';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const LocationForm = ({
|
||||
onSubmit,
|
||||
loading,
|
||||
location,
|
||||
onDiscard,
|
||||
privateLocations,
|
||||
}: {
|
||||
onSubmit: (val: PrivateLocation) => void;
|
||||
onDiscard?: () => void;
|
||||
loading?: boolean;
|
||||
location?: PrivateLocation;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isValid, isSubmitted, isDirty },
|
||||
} = useFormWrapped({
|
||||
mode: 'onTouched',
|
||||
reValidateMode: 'onChange',
|
||||
shouldFocusError: true,
|
||||
defaultValues: location || {
|
||||
name: '',
|
||||
policyHostId: '',
|
||||
id: '',
|
||||
latLon: '',
|
||||
concurrentMonitors: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = useSelector(selectAgentPolicies);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.items.length === 0 && <PolicyHostNeeded />}
|
||||
<EuiForm
|
||||
component="form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
isInvalid={isSubmitted && !isValid && !isEmpty(errors)}
|
||||
noValidate
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={LOCATION_NAME_LABEL}
|
||||
isInvalid={Boolean(errors?.name)}
|
||||
error={errors?.name?.message}
|
||||
>
|
||||
<EuiFieldText
|
||||
disabled={Boolean(location)}
|
||||
aria-label={LOCATION_NAME_LABEL}
|
||||
{...register('name', {
|
||||
required: {
|
||||
value: true,
|
||||
message: NAME_REQUIRED,
|
||||
},
|
||||
validate: (val: string) => {
|
||||
return privateLocations.some((loc) => loc.name === val)
|
||||
? NAME_ALREADY_EXISTS
|
||||
: undefined;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PolicyHostsField
|
||||
errors={errors}
|
||||
control={control}
|
||||
isDisabled={Boolean(location)}
|
||||
privateLocations={privateLocations}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
isDisabled={!isDirty && Boolean(location)}
|
||||
onClick={() => {
|
||||
reset();
|
||||
onDiscard?.();
|
||||
}}
|
||||
>
|
||||
{DISCARD_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton size="s" type="submit" fill isLoading={loading} isDisabled={!isDirty}>
|
||||
{location ? UPDATE_LOCATION_LABEL : CREATE_LOCATION_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LOCATION_NAME_LABEL = i18n.translate('xpack.synthetics.monitorManagement.locationName', {
|
||||
defaultMessage: 'Location name',
|
||||
});
|
||||
|
||||
const DISCARD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.discard', {
|
||||
defaultMessage: 'Discard',
|
||||
});
|
||||
|
||||
const UPDATE_LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.updateLocation', {
|
||||
defaultMessage: 'Update location',
|
||||
});
|
||||
|
||||
const CREATE_LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
|
||||
defaultMessage: 'Create location',
|
||||
});
|
||||
|
||||
const NAME_ALREADY_EXISTS = i18n.translate('xpack.synthetics.monitorManagement.alreadyExists', {
|
||||
defaultMessage: 'Location name already exists.',
|
||||
});
|
||||
|
||||
const NAME_REQUIRED = i18n.translate('xpack.synthetics.monitorManagement.nameRequired', {
|
||||
defaultMessage: 'Location name is required',
|
||||
});
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiButtonIcon,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiLoadingSpinner,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useLocationMonitors } from './hooks/use_location_monitors';
|
||||
import { PrivateLocation } from '../../../../../common/runtime_types';
|
||||
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
|
||||
import { LocationForm } from './location_form';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const PrivateLocationsList = ({
|
||||
privateLocations,
|
||||
onSubmit,
|
||||
loading,
|
||||
onDelete,
|
||||
hasFleetPermissions,
|
||||
}: {
|
||||
loading: boolean;
|
||||
privateLocations: PrivateLocation[];
|
||||
hasFleetPermissions: boolean;
|
||||
onSubmit: (location: PrivateLocation) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}) => {
|
||||
const { basePath } = useUptimeSettingsContext();
|
||||
|
||||
const { data: policies } = useSelector(selectAgentPolicies);
|
||||
|
||||
const { locations } = useLocationMonitors();
|
||||
|
||||
const [openLocationMap, setOpenLocationMap] = useState<Record<string, boolean>>({});
|
||||
|
||||
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
|
||||
|
||||
if (loading) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{privateLocations.map((location, index) => {
|
||||
const monCount = locations?.find((l) => l.id === location.id)?.count ?? 0;
|
||||
const canDelete = monCount === 0 || !hasFleetPermissions;
|
||||
const policy = policies?.items.find((policyT) => policyT.id === location.policyHostId);
|
||||
return (
|
||||
<div key={location.id}>
|
||||
<EuiAccordion
|
||||
data-test-subj={`location-accordion-` + index}
|
||||
id={location.id}
|
||||
element="fieldset"
|
||||
className="euiAccordionForm"
|
||||
buttonClassName="euiAccordionForm__button"
|
||||
onToggle={(val) => {
|
||||
setOpenLocationMap((prevState) => ({ [location.id]: val, ...prevState }));
|
||||
}}
|
||||
buttonContent={
|
||||
<div>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="visMapCoordinate" size="m" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" className="eui-textNoWrap">
|
||||
<h3>{location.name}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">
|
||||
<EuiTextColor color="subdued">
|
||||
{RUNNING_MONITORS}: {monCount}
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{hasFleetPermissions && (
|
||||
<EuiTextColor color="subdued">
|
||||
{AGENT_POLICY_LABEL}:{' '}
|
||||
{policy ? (
|
||||
<EuiLink
|
||||
href={`${basePath}/app/fleet/policies/${location.policyHostId}`}
|
||||
>
|
||||
{policy?.name}
|
||||
</EuiLink>
|
||||
) : (
|
||||
<EuiText color="danger" size="s" className="eui-displayInline">
|
||||
{POLICY_IS_DELETED}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiTextColor>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
}
|
||||
extraAction={
|
||||
<EuiToolTip
|
||||
content={
|
||||
canDelete
|
||||
? DELETE_LABEL
|
||||
: i18n.translate('xpack.synthetics.monitorManagement.cannotDelete', {
|
||||
defaultMessage: `This location cannot be deleted, because it has {monCount} monitors running. Please remove this location from your monitors before deleting this location.`,
|
||||
values: { monCount },
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={DELETE_LABEL}
|
||||
onClick={() => onDelete(location.id)}
|
||||
isDisabled={!canDelete || !canSave}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
paddingSize="l"
|
||||
>
|
||||
{openLocationMap[location.id] && (
|
||||
<LocationForm
|
||||
onSubmit={onSubmit}
|
||||
loading={loading}
|
||||
location={location}
|
||||
privateLocations={privateLocations}
|
||||
/>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
<EuiSpacer />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
&&& {
|
||||
.euiAccordion__button {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DELETE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.delete', {
|
||||
defaultMessage: 'Delete location',
|
||||
});
|
||||
|
||||
const RUNNING_MONITORS = i18n.translate('xpack.synthetics.monitorManagement.runningMonitors', {
|
||||
defaultMessage: 'Running monitors',
|
||||
});
|
||||
|
||||
const POLICY_IS_DELETED = i18n.translate('xpack.synthetics.monitorManagement.deletedPolicy', {
|
||||
defaultMessage: 'Policy is deleted',
|
||||
});
|
||||
|
||||
const AGENT_POLICY_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agentPolicy', {
|
||||
defaultMessage: 'Agent Policy',
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { ManageLocationsFlyout } from './manage_locations_flyout';
|
||||
import { ManageLocationsPortalNode } from '../../../pages/monitor_management/portals';
|
||||
|
||||
export const ManageLocationsPortal = () => {
|
||||
return (
|
||||
<InPortal node={ManageLocationsPortalNode}>
|
||||
<ManageLocationsFlyout />
|
||||
</InPortal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
import { EmptyLocations } from './empty_locations';
|
||||
import { getServiceLocations } from '../../../state/actions';
|
||||
import { LocationForm } from './location_form';
|
||||
import { PrivateLocationsList } from './locations_list';
|
||||
import { useLocationsAPI } from './hooks/use_locations_api';
|
||||
import {
|
||||
getAgentPoliciesAction,
|
||||
selectManageFlyoutOpen,
|
||||
setManageFlyoutOpen,
|
||||
} from '../../../state/private_locations';
|
||||
|
||||
export const ManageLocationsFlyout = () => {
|
||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setIsOpen = (val: boolean) => dispatch(setManageFlyoutOpen(val));
|
||||
|
||||
const isOpen = useSelector(selectManageFlyoutOpen);
|
||||
|
||||
const { onSubmit, saveLoading, fetchLoading, deleteLoading, privateLocations, onDelete } =
|
||||
useLocationsAPI({
|
||||
isOpen,
|
||||
});
|
||||
|
||||
const { fleet } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const hasFleetPermissions = Boolean(fleet?.authz.fleet.readAgentPolicies);
|
||||
|
||||
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
dispatch(getAgentPoliciesAction.get());
|
||||
}
|
||||
}, [dispatch, isOpen]);
|
||||
|
||||
const closeFlyout = () => {
|
||||
setIsOpen(false);
|
||||
dispatch(getServiceLocations());
|
||||
};
|
||||
|
||||
const flyout = (
|
||||
<EuiFlyout onClose={closeFlyout}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{MANAGE_PRIVATE_LOCATIONS}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{!hasFleetPermissions && (
|
||||
<EuiCallOut title={NEED_PERMISSIONS} color="warning" iconType="help">
|
||||
<p>{NEED_FLEET_READ_AGENT_POLICIES_PERMISSION}</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
{privateLocations.length === 0 && !(saveLoading || fetchLoading) && !isAddingNew ? (
|
||||
<EmptyLocations setIsAddingNew={setIsAddingNew} disabled={!hasFleetPermissions} />
|
||||
) : (
|
||||
<PrivateLocationsList
|
||||
privateLocations={privateLocations}
|
||||
loading={fetchLoading}
|
||||
onDelete={onDelete}
|
||||
onSubmit={onSubmit}
|
||||
hasFleetPermissions={hasFleetPermissions}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
{isAddingNew && (
|
||||
<LocationForm
|
||||
privateLocations={privateLocations}
|
||||
onSubmit={(val) => {
|
||||
onSubmit(val);
|
||||
setIsAddingNew(false);
|
||||
}}
|
||||
onDiscard={() => setIsAddingNew(false)}
|
||||
/>
|
||||
)}
|
||||
{!isAddingNew && privateLocations.length > 0 && (
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
isLoading={saveLoading || fetchLoading || deleteLoading}
|
||||
disabled={!hasFleetPermissions || !canSave}
|
||||
onClick={() => setIsAddingNew(true)}
|
||||
>
|
||||
{ADD_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
|
||||
{CLOSE_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiButtonEmpty onClick={() => setIsOpen(true)} iconType="visMapCoordinate">
|
||||
{MANAGE_PRIVATE_LOCATIONS}
|
||||
</EuiButtonEmpty>
|
||||
{isOpen ? flyout : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MANAGE_PRIVATE_LOCATIONS = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.managePrivateLocations',
|
||||
{
|
||||
defaultMessage: 'Manage private locations',
|
||||
}
|
||||
);
|
||||
|
||||
const CLOSE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.closeLabel', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
||||
|
||||
const ADD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.addLocation', {
|
||||
defaultMessage: 'Add location',
|
||||
});
|
||||
|
||||
const NEED_PERMISSIONS = i18n.translate('xpack.synthetics.monitorManagement.needPermissions', {
|
||||
defaultMessage: 'Need permissions',
|
||||
});
|
||||
|
||||
const NEED_FLEET_READ_AGENT_POLICIES_PERMISSION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission',
|
||||
{
|
||||
defaultMessage:
|
||||
'You are not authorized to access Fleet. Fleet permissions are required to create new private locations.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCallOut, EuiLink, EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
|
||||
|
||||
export const PolicyHostNeeded = () => {
|
||||
const { basePath } = useUptimeSettingsContext();
|
||||
return (
|
||||
<EuiCallOut title={AGENT_POLICY_NEEDED} color="warning" iconType="help">
|
||||
<p>
|
||||
{ADD_AGENT_POLICY_DESCRIPTION}
|
||||
<EuiLink href="#">{READ_THE_DOCS}</EuiLink>.
|
||||
</p>
|
||||
<EuiButton href={`${basePath}/app/fleet/policies?create`} color="primary">
|
||||
{CREATE_AGENT_POLICY}
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
const CREATE_AGENT_POLICY = i18n.translate('xpack.synthetics.monitorManagement.createAgentPolicy', {
|
||||
defaultMessage: 'Create agent policy',
|
||||
});
|
||||
|
||||
const AGENT_POLICY_NEEDED = i18n.translate('xpack.synthetics.monitorManagement.agentPolicyNeeded', {
|
||||
defaultMessage: 'No agent policy found. Please create one.',
|
||||
});
|
||||
const ADD_AGENT_POLICY_DESCRIPTION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.addAgentPolicyDesc',
|
||||
{
|
||||
defaultMessage:
|
||||
'To add a synthetics private location, a Fleet policy with active elastic Agent is needed.',
|
||||
}
|
||||
);
|
||||
|
||||
const READ_THE_DOCS = i18n.translate('xpack.synthetics.monitorManagement.readDocs', {
|
||||
defaultMessage: 'Read the docs',
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 { Controller, FieldErrors, Control } from 'react-hook-form';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiHealth,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { PrivateLocation } from '../../../../../common/runtime_types';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const PolicyHostsField = ({
|
||||
isDisabled,
|
||||
errors,
|
||||
control,
|
||||
privateLocations,
|
||||
}: {
|
||||
isDisabled: boolean;
|
||||
errors: FieldErrors;
|
||||
control: Control<PrivateLocation, any>;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const { data } = useSelector(selectAgentPolicies);
|
||||
|
||||
const policyHostsOptions = data?.items.map((item) => {
|
||||
const hasLocation = privateLocations.find((location) => location.policyHostId === item.id);
|
||||
return {
|
||||
disabled: Boolean(hasLocation),
|
||||
value: item.id,
|
||||
inputDisplay: (
|
||||
<EuiHealth
|
||||
color={item.status === 'active' ? 'success' : 'warning'}
|
||||
style={{ lineHeight: 'inherit' }}
|
||||
>
|
||||
{item.name}
|
||||
</EuiHealth>
|
||||
),
|
||||
'data-test-subj': item.name,
|
||||
dropdownDisplay: (
|
||||
<EuiToolTip
|
||||
content={
|
||||
hasLocation?.name
|
||||
? i18n.translate('xpack.synthetics.monitorManagement.anotherPrivateLocation', {
|
||||
defaultMessage:
|
||||
'This agent policy is already attached to location: {locationName}.',
|
||||
values: { locationName: hasLocation?.name },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<>
|
||||
<EuiHealth
|
||||
color={item.status === 'active' ? 'success' : 'warning'}
|
||||
style={{ lineHeight: 'inherit' }}
|
||||
>
|
||||
<strong>{item.name}</strong>
|
||||
</EuiHealth>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
{AGENTS_LABEL} {item.agents}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>{item.description}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={POLICY_HOST_LABEL}
|
||||
helpText={!errors?.policyHostId ? SELECT_POLICY_HOSTS : undefined}
|
||||
isInvalid={!!errors?.policyHostId}
|
||||
error={SELECT_POLICY_HOSTS}
|
||||
>
|
||||
<Controller
|
||||
name="policyHostId"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<EuiSuperSelect
|
||||
disabled={isDisabled}
|
||||
fullWidth
|
||||
aria-label={SELECT_POLICY_HOSTS}
|
||||
placeholder={SELECT_POLICY_HOSTS}
|
||||
valueOfSelected={field.value}
|
||||
itemLayoutAlign="top"
|
||||
popoverProps={{ repositionOnScroll: true }}
|
||||
hasDividers
|
||||
isInvalid={!!errors?.policyHostId}
|
||||
options={policyHostsOptions ?? []}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const AGENTS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agentsLabel', {
|
||||
defaultMessage: 'Agents: ',
|
||||
});
|
||||
|
||||
const SELECT_POLICY_HOSTS = i18n.translate('xpack.synthetics.monitorManagement.selectPolicyHost', {
|
||||
defaultMessage: 'Select agent policy',
|
||||
});
|
||||
|
||||
const POLICY_HOST_LABEL = i18n.translate('xpack.synthetics.monitorManagement.policyHost', {
|
||||
defaultMessage: 'Agent policy',
|
||||
});
|
|
@ -22,6 +22,7 @@ describe('<ServiceLocations />', () => {
|
|||
lon: 1,
|
||||
},
|
||||
url: 'url',
|
||||
isServiceManaged: true,
|
||||
};
|
||||
const locationTestSubId = `syntheticsServiceLocation--${location.id}`;
|
||||
const state = {
|
||||
|
|
|
@ -8,9 +8,13 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiCheckboxGroup, EuiFormRow, EuiText, EuiBadge } from '@elastic/eui';
|
||||
import { EuiCheckboxGroup, EuiFormRow, EuiText, EuiBadge, EuiIconTip } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { monitorManagementListSelector } from '../../../state/selectors';
|
||||
import { MonitorServiceLocations, LocationStatus } from '../../../../../common/runtime_types';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
import { MONITOR_EDIT_ROUTE } from '../../../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
selectedLocations: MonitorServiceLocations;
|
||||
|
@ -33,6 +37,8 @@ export const ServiceLocations = ({
|
|||
);
|
||||
const { locations } = useSelector(monitorManagementListSelector);
|
||||
|
||||
const isEditMonitor = useRouteMatch(MONITOR_EDIT_ROUTE);
|
||||
|
||||
const onLocationChange = (optionId: string) => {
|
||||
const isSelected = !checkboxIdToSelectedMap[optionId];
|
||||
const location = locations.find((loc) => loc.id === optionId);
|
||||
|
@ -50,6 +56,11 @@ export const ServiceLocations = ({
|
|||
|
||||
const errorMessage = error ?? (isInvalid ? VALIDATION_ERROR : null);
|
||||
|
||||
const kServices = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const canSaveIntegrations: boolean =
|
||||
!!kServices?.fleet?.authz.integrations.writeIntegrationPolicies;
|
||||
|
||||
useEffect(() => {
|
||||
const newCheckboxIdToSelectedMap = selectedLocations.reduce<Record<string, boolean>>(
|
||||
(acc, location) => {
|
||||
|
@ -65,18 +76,35 @@ export const ServiceLocations = ({
|
|||
<EuiFormRow label={LOCATIONS_LABEL} error={errorMessage} isInvalid={errorMessage !== null}>
|
||||
<EuiCheckboxGroup
|
||||
options={locations.map((location) => {
|
||||
const badge =
|
||||
let badge =
|
||||
location.status !== LocationStatus.GA ? (
|
||||
<EuiBadge color="warning">Tech Preview</EuiBadge>
|
||||
<EuiBadge color="warning">{TECH_PREVIEW_LABEL}</EuiBadge>
|
||||
) : null;
|
||||
if (!location.isServiceManaged) {
|
||||
badge = <EuiBadge color="primary">{PRIVATE_LABEL}</EuiBadge>;
|
||||
}
|
||||
const invalidBadge = location.isInvalid ? (
|
||||
<EuiBadge color="danger">{INVALID_LABEL}</EuiBadge>
|
||||
) : null;
|
||||
|
||||
const isPrivateDisabled =
|
||||
!location.isServiceManaged && (Boolean(location.isInvalid) || !canSaveIntegrations);
|
||||
|
||||
const iconTip =
|
||||
isPrivateDisabled && !canSaveIntegrations ? (
|
||||
<EuiIconTip content={CANNOT_SAVE_INTEGRATION_LABEL} position="right" />
|
||||
) : null;
|
||||
|
||||
const label = (
|
||||
<EuiText size="s" data-test-subj={`syntheticsServiceLocationText--${location.id}`}>
|
||||
{location.label} {badge}
|
||||
{location.label} {badge} {invalidBadge}
|
||||
{iconTip}
|
||||
</EuiText>
|
||||
);
|
||||
return {
|
||||
...location,
|
||||
label,
|
||||
disabled: isPrivateDisabled && !isEditMonitor?.isExact,
|
||||
'data-test-subj': `syntheticsServiceLocation--${location.id}`,
|
||||
};
|
||||
})}
|
||||
|
@ -102,3 +130,26 @@ export const LOCATIONS_LABEL = i18n.translate(
|
|||
defaultMessage: 'Monitor locations',
|
||||
}
|
||||
);
|
||||
|
||||
export const TECH_PREVIEW_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.techPreviewLabel',
|
||||
{
|
||||
defaultMessage: 'Tech Preview',
|
||||
}
|
||||
);
|
||||
|
||||
export const PRIVATE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.privateLabel', {
|
||||
defaultMessage: 'Private',
|
||||
});
|
||||
|
||||
export const INVALID_LABEL = i18n.translate('xpack.synthetics.monitorManagement.invalidLabel', {
|
||||
defaultMessage: 'Invalid',
|
||||
});
|
||||
|
||||
export const CANNOT_SAVE_INTEGRATION_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.cannotSaveIntegration',
|
||||
{
|
||||
defaultMessage:
|
||||
'You are not authorized to update integrations. Integrations write permissions are required.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -9,6 +9,8 @@ import React, { useContext } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { usePrivateLocationPermissions } from '../hooks/use_private_location_permission';
|
||||
import { CANNOT_SAVE_INTEGRATION_LABEL } from '../monitor_config/locations';
|
||||
import { UptimeSettingsContext } from '../../../contexts';
|
||||
import { DeleteMonitor } from './delete_monitor';
|
||||
import { InlineError } from './inline_error';
|
||||
|
@ -47,16 +49,22 @@ export const Actions = ({ id, name, onUpdate, isDisabled, errorSummaries, monito
|
|||
}
|
||||
}
|
||||
|
||||
const { canUpdatePrivateMonitor } = usePrivateLocationPermissions(
|
||||
monitor?.attributes as BrowserFields
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
isDisabled={isDisabled}
|
||||
iconType="pencil"
|
||||
href={`${basePath}/app/uptime/edit-monitor/${id}`}
|
||||
aria-label={EDIT_MONITOR_LABEL}
|
||||
data-test-subj="monitorManagementEditMonitor"
|
||||
/>
|
||||
<EuiToolTip content={!canUpdatePrivateMonitor ? CANNOT_SAVE_INTEGRATION_LABEL : ''}>
|
||||
<EuiButtonIcon
|
||||
isDisabled={isDisabled || !canUpdatePrivateMonitor}
|
||||
iconType="pencil"
|
||||
href={`${basePath}/app/uptime/edit-monitor/${id}`}
|
||||
aria-label={EDIT_MONITOR_LABEL}
|
||||
data-test-subj="monitorManagementEditMonitor"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
|
@ -73,7 +81,7 @@ export const Actions = ({ id, name, onUpdate, isDisabled, errorSummaries, monito
|
|||
onUpdate={onUpdate}
|
||||
name={name}
|
||||
id={id}
|
||||
isDisabled={isDisabled || isProjectMonitor}
|
||||
isDisabled={isDisabled || isProjectMonitor || !canUpdatePrivateMonitor}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -7,7 +7,16 @@
|
|||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiButton,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useEnablement } from '../hooks/use_enablement';
|
||||
import { kibanaService } from '../../../state/kibana_service';
|
||||
import { SYNTHETICS_ENABLE_SUCCESS, SYNTHETICS_DISABLE_SUCCESS } from '../content';
|
||||
|
@ -46,44 +55,81 @@ export const EnablementEmptyState = ({ focusButton }: { focusButton: boolean })
|
|||
}, [focusButton]);
|
||||
|
||||
return !isEnabled && !loading ? (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_LABEL : MONITOR_MANAGEMENT_DISABLED_LABEL}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE : MONITOR_MANAGEMENT_DISABLED_MESSAGE}
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
canEnable ? (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={handleEnableSynthetics}
|
||||
data-test-subj="syntheticsEnableButton"
|
||||
buttonRef={buttonRef}
|
||||
>
|
||||
{MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL}
|
||||
</EuiButton>
|
||||
) : null
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>{LEARN_MORE_LABEL}</h3>
|
||||
</EuiTitle>
|
||||
<EuiLink
|
||||
href="https://docs.google.com/document/d/1hkzFibu9LggPWXQqfbAd0mMlV75wCME7_BebXlEH-oI"
|
||||
target="_blank"
|
||||
>
|
||||
{DOCS_LABEL}
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_LABEL : MONITOR_MANAGEMENT_DISABLED_LABEL}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{canEnable
|
||||
? MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE
|
||||
: MONITOR_MANAGEMENT_DISABLED_MESSAGE}
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
canEnable ? (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={handleEnableSynthetics}
|
||||
data-test-subj="syntheticsEnableButton"
|
||||
buttonRef={buttonRef}
|
||||
>
|
||||
{MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL}
|
||||
</EuiButton>
|
||||
) : null
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>{LEARN_MORE_LABEL}</h3>
|
||||
</EuiTitle>
|
||||
<EuiLink
|
||||
href="https://docs.google.com/document/d/1hkzFibu9LggPWXQqfbAd0mMlV75wCME7_BebXlEH-oI"
|
||||
target="_blank"
|
||||
>
|
||||
{DOCS_LABEL}
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiEmptyPrompt
|
||||
paddingSize="none"
|
||||
body={
|
||||
<EuiCallOut title="Please note" className="eui-textLeft">
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorManagement.disclaimer"
|
||||
defaultMessage="By using this feature, Customer acknowledges that it has read and agrees to {link}. "
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/agreements/beta-release-terms"
|
||||
target="_blank"
|
||||
>
|
||||
{"Elastic's Beta Release Terms"}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorManagement.disclaimerLinkLabel"
|
||||
defaultMessage="There is no cost for the use of the service to execute your tests during the beta period. A fair usage policy will apply. Normal data storage costs apply for test results stored in your Elastic cluster.', "
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitor } from '../../../../../common/runtime_types';
|
||||
import { usePrivateLocationPermissions } from '../hooks/use_private_location_permission';
|
||||
import {
|
||||
BrowserFields,
|
||||
ConfigKey,
|
||||
EncryptedSyntheticsMonitor,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { setMonitor } from '../../../state/api';
|
||||
|
||||
interface Props {
|
||||
|
@ -25,6 +30,8 @@ export const MonitorEnabled = ({ id, monitor, onUpdate, isDisabled }: Props) =>
|
|||
|
||||
const { notifications } = useKibana();
|
||||
|
||||
const { canUpdatePrivateMonitor } = usePrivateLocationPermissions(monitor as BrowserFields);
|
||||
|
||||
const { status } = useFetcher(() => {
|
||||
if (isEnabled !== null) {
|
||||
return setMonitor({ id, monitor: { ...monitor, [ConfigKey.ENABLED]: isEnabled } });
|
||||
|
@ -69,7 +76,7 @@ export const MonitorEnabled = ({ id, monitor, onUpdate, isDisabled }: Props) =>
|
|||
<div css={{ position: 'relative' }} aria-busy={isLoading}>
|
||||
<EuiSwitch
|
||||
checked={enabled}
|
||||
disabled={isLoading || isDisabled}
|
||||
disabled={isLoading || isDisabled || !canUpdatePrivateMonitor}
|
||||
showLabel={false}
|
||||
label={enabled ? DISABLE_MONITOR_LABEL : ENABLE_MONITOR_LABEL}
|
||||
title={enabled ? DISABLE_MONITOR_LABEL : ENABLE_MONITOR_LABEL}
|
||||
|
|
|
@ -9,6 +9,8 @@ import React, { useCallback, Dispatch } from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { useLocations } from '../hooks/use_locations';
|
||||
import { EmptyLocations } from '../manage_locations/empty_locations';
|
||||
import { monitorManagementListSelector } from '../../../state/selectors';
|
||||
import { MonitorAsyncError } from './monitor_async_error';
|
||||
import { useInlineErrors } from '../hooks/use_inline_errors';
|
||||
|
@ -53,10 +55,16 @@ export const MonitorListContainer = ({
|
|||
|
||||
const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries);
|
||||
|
||||
const { locations } = useLocations();
|
||||
|
||||
if (!isEnabled && monitorList.list.total === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEnabled && monitorList.list.total === 0 && locations.length === 0) {
|
||||
return <EmptyLocations />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MonitorAsyncError />
|
||||
|
|
|
@ -9,20 +9,31 @@ import { EuiButtonIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Ping } from '../../../../../../common/runtime_types';
|
||||
import { testNowMonitorAction } from '../../../../state/actions';
|
||||
import { testNowRunSelector, TestRunStats } from '../../../../state/reducers/test_now_runs';
|
||||
|
||||
export const TestNowColumn = ({
|
||||
monitorId,
|
||||
configId,
|
||||
selectedMonitor,
|
||||
}: {
|
||||
monitorId: string;
|
||||
configId?: string;
|
||||
selectedMonitor: Ping;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const testNowRun = useSelector(testNowRunSelector(configId));
|
||||
|
||||
if (selectedMonitor.monitor.fleet_managed) {
|
||||
return (
|
||||
<EuiToolTip content={PRIVATE_AVAILABLE_LABEL}>
|
||||
<>--</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
if (!configId) {
|
||||
return (
|
||||
<EuiToolTip content={TEST_NOW_AVAILABLE_LABEL}>
|
||||
|
@ -68,6 +79,13 @@ export const TEST_NOW_AVAILABLE_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PRIVATE_AVAILABLE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorList.testNow.available.private',
|
||||
{
|
||||
defaultMessage: 'For now, Test now is disabled for private locations monitors.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TEST_NOW_LABEL = i18n.translate('xpack.synthetics.monitorList.testNow.label', {
|
||||
defaultMessage: 'Test now',
|
||||
});
|
||||
|
|
|
@ -208,7 +208,11 @@ export const MonitorListComponent: ({
|
|||
name: TEST_NOW_COLUMN,
|
||||
width: '100px',
|
||||
render: (item: MonitorSummary) => (
|
||||
<TestNowColumn monitorId={item.monitor_id} configId={item.configId} />
|
||||
<TestNowColumn
|
||||
monitorId={item.monitor_id}
|
||||
configId={item.configId}
|
||||
selectedMonitor={item.state.summaryPings[0]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(!hideExtraColumns
|
||||
|
|
|
@ -128,4 +128,5 @@ export const mockState: AppState = {
|
|||
hitCount: [],
|
||||
},
|
||||
testNowRuns: {},
|
||||
agentPolicies: { loading: false, data: null, error: null },
|
||||
};
|
||||
|
|
|
@ -5,15 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ScheduleUnit } from '../../../../common/runtime_types';
|
||||
import { SyntheticsProviders } from '../../components/fleet_package/contexts';
|
||||
import { Loader } from '../../components/monitor_management/loader/loader';
|
||||
import { MonitorConfig } from '../../components/monitor_management/monitor_config/monitor_config';
|
||||
import { useLocations } from '../../components/monitor_management/hooks/use_locations';
|
||||
import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs';
|
||||
import { getAgentPoliciesAction } from '../../state/private_locations';
|
||||
|
||||
export const AddMonitorPage: React.FC = () => {
|
||||
useTrackPageview({ app: 'uptime', path: 'add-monitor' });
|
||||
|
@ -21,8 +23,14 @@ export const AddMonitorPage: React.FC = () => {
|
|||
|
||||
const { error, loading, locations, throttling } = useLocations();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useMonitorManagementBreadcrumbs({ isAddMonitor: true });
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAgentPoliciesAction.get());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Loader
|
||||
error={Boolean(error) || (locations && locations.length === 0)}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { OutPortal } from 'react-reverse-portal';
|
||||
import { ManageLocationsPortalNode } from './portals';
|
||||
|
||||
export const ManageLocations = () => {
|
||||
return (
|
||||
<div>
|
||||
<OutPortal node={ManageLocationsPortalNode} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { ManageLocationsPortal } from '../../components/monitor_management/manage_locations/manage_locations';
|
||||
import { monitorManagementListSelector } from '../../state/selectors';
|
||||
import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs';
|
||||
import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container';
|
||||
|
@ -91,6 +92,7 @@ export const MonitorManagementPage: React.FC = () => {
|
|||
pageState={pageState}
|
||||
dispatchPageAction={dispatchPageAction}
|
||||
/>
|
||||
<ManageLocationsPortal />
|
||||
</Loader>
|
||||
{showEmptyState && <EnablementEmptyState focusButton={shouldFocusEnablementButton} />}
|
||||
</>
|
||||
|
|
|
@ -10,3 +10,5 @@ import { createPortalNode } from 'react-reverse-portal';
|
|||
export const ActionBarPortalNode = createPortalNode();
|
||||
|
||||
export const APIKeysPortalNode = createPortalNode();
|
||||
|
||||
export const ManageLocationsPortalNode = createPortalNode();
|
||||
|
|
|
@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
|
||||
import { useInspectorContext } from '@kbn/observability-plugin/public';
|
||||
import { ManageLocations } from './pages/monitor_management/manage_locations';
|
||||
import {
|
||||
CERTIFICATES_ROUTE,
|
||||
MAPPING_ERROR_ROUTE,
|
||||
|
@ -209,6 +210,7 @@ const getRoutes = (): RouteProps[] => {
|
|||
defaultMessage="Add Monitor"
|
||||
/>
|
||||
),
|
||||
rightSideItems: [<APIKeysButton />, <ManageLocations />],
|
||||
},
|
||||
bottomBar: <MonitorManagementBottomBar />,
|
||||
bottomBarProps: { paddingSize: 'm' as const },
|
||||
|
@ -233,6 +235,7 @@ const getRoutes = (): RouteProps[] => {
|
|||
defaultMessage="Edit Monitor"
|
||||
/>
|
||||
),
|
||||
rightSideItems: [<APIKeysButton />, <ManageLocations />],
|
||||
},
|
||||
bottomBar: <MonitorManagementBottomBar />,
|
||||
bottomBarProps: { paddingSize: 'm' as const },
|
||||
|
@ -273,7 +276,7 @@ const getRoutes = (): RouteProps[] => {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
rightSideItems: [<AddMonitorBtn />, <APIKeysButton />],
|
||||
rightSideItems: [<AddMonitorBtn />, <APIKeysButton />, <ManageLocations />],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { fork } from 'redux-saga/effects';
|
||||
import { fetchAgentPoliciesEffect } from '../private_locations';
|
||||
import { fetchMonitorDetailsEffect } from './monitor';
|
||||
import { fetchMonitorListEffect, fetchUpdatedMonitorEffect } from './monitor_list';
|
||||
import {
|
||||
|
@ -49,4 +50,5 @@ export function* rootEffect() {
|
|||
yield fork(generateBlockStatsOnPut);
|
||||
yield fork(pruneBlockCache);
|
||||
yield fork(fetchSyntheticsServiceAllowedEffect);
|
||||
yield fork(fetchAgentPoliciesEffect);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { createAction } from '@reduxjs/toolkit';
|
||||
import { AgentPoliciesList } from '.';
|
||||
import { createAsyncAction } from '../../../apps/synthetics/state/utils/actions';
|
||||
|
||||
export const getAgentPoliciesAction = createAsyncAction<void, AgentPoliciesList>(
|
||||
'[AGENT POLICIES] GET'
|
||||
);
|
||||
|
||||
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract } from '@kbn/core/public';
|
||||
import { AgentPoliciesList } from '.';
|
||||
import {
|
||||
privateLocationsSavedObjectId,
|
||||
privateLocationsSavedObjectName,
|
||||
} from '../../../../common/saved_objects/private_locations';
|
||||
import { apiService } from '../api/utils';
|
||||
import { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
|
||||
|
||||
const FLEET_URLS = {
|
||||
AGENT_POLICIES: '/api/fleet/agent_policies',
|
||||
};
|
||||
|
||||
export const fetchAgentPolicies = async (): Promise<AgentPoliciesList> => {
|
||||
return await apiService.get(
|
||||
FLEET_URLS.AGENT_POLICIES,
|
||||
{ page: 1, perPage: 10000, sortField: 'name', sortOrder: 'asc', full: true },
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
export const setSyntheticsPrivateLocations = async (
|
||||
client: SavedObjectsClientContract,
|
||||
privateLocations: SyntheticsPrivateLocations
|
||||
) => {
|
||||
await client.create(privateLocationsSavedObjectName, privateLocations, {
|
||||
id: privateLocationsSavedObjectId,
|
||||
overwrite: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const getSyntheticsPrivateLocations = async (client: SavedObjectsClientContract) => {
|
||||
try {
|
||||
const obj = await client.get<SyntheticsPrivateLocations>(
|
||||
privateLocationsSavedObjectName,
|
||||
privateLocationsSavedObjectId
|
||||
);
|
||||
return obj?.attributes.locations ?? [];
|
||||
} catch (getErr) {
|
||||
return [];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { takeLeading } from 'redux-saga/effects';
|
||||
import { fetchAgentPolicies } from './api';
|
||||
import { getAgentPoliciesAction } from './actions';
|
||||
import { fetchEffectFactory } from '../../../apps/synthetics/state/utils/fetch_effect';
|
||||
|
||||
export function* fetchAgentPoliciesEffect() {
|
||||
yield takeLeading(
|
||||
getAgentPoliciesAction.get,
|
||||
fetchEffectFactory(
|
||||
fetchAgentPolicies,
|
||||
getAgentPoliciesAction.success,
|
||||
getAgentPoliciesAction.fail
|
||||
)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { AgentPolicy } from '@kbn/fleet-plugin/common';
|
||||
import { getAgentPoliciesAction, setManageFlyoutOpen } from './actions';
|
||||
|
||||
export interface AgentPoliciesList {
|
||||
items: AgentPolicy[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export interface AgentPoliciesState {
|
||||
data: AgentPoliciesList | null;
|
||||
loading: boolean;
|
||||
error: IHttpFetchError<ResponseErrorBody> | null;
|
||||
isManageFlyoutOpen?: boolean;
|
||||
}
|
||||
|
||||
const initialState: AgentPoliciesState = {
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
isManageFlyoutOpen: false,
|
||||
};
|
||||
|
||||
export const agentPoliciesReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(getAgentPoliciesAction.get, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(getAgentPoliciesAction.success, (state, action) => {
|
||||
state.data = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(getAgentPoliciesAction.fail, (state, action) => {
|
||||
state.error = action.payload as IHttpFetchError<ResponseErrorBody>;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(setManageFlyoutOpen, (state, action) => {
|
||||
state.isManageFlyoutOpen = action.payload;
|
||||
state.loading = false;
|
||||
});
|
||||
});
|
||||
|
||||
export * from './actions';
|
||||
export * from './effects';
|
||||
export * from './selectors';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { createSelector } from 'reselect';
|
||||
import { AppState } from '..';
|
||||
|
||||
const getState = (appState: AppState) => appState.agentPolicies;
|
||||
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
||||
export const selectManageFlyoutOpen = createSelector(getState, (state) =>
|
||||
Boolean(state.isManageFlyoutOpen)
|
||||
);
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { combineReducers } from 'redux';
|
||||
import { agentPoliciesReducer } from '../private_locations';
|
||||
import { monitorReducer } from './monitor';
|
||||
import { uiReducer } from './ui';
|
||||
import { monitorStatusReducer } from './monitor_status';
|
||||
|
@ -44,4 +45,5 @@ export const rootReducer = combineReducers({
|
|||
networkEvents: networkEventsReducer,
|
||||
synthetics: syntheticsReducer,
|
||||
testNowRuns: testNowRunsReducer,
|
||||
agentPolicies: agentPoliciesReducer,
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
|
||||
import { CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { PLUGIN } from '../common/constants/plugin';
|
||||
import { MONITORS_ROUTE } from '../common/constants/ui';
|
||||
|
@ -74,6 +74,7 @@ export interface ClientPluginsStart {
|
|||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
cases: CasesUiStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
cloud?: CloudStart;
|
||||
}
|
||||
|
||||
export interface UptimePluginServices extends Partial<CoreStart> {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations';
|
||||
import { PLUGIN } from '../common/constants/plugin';
|
||||
import { UPTIME_RULE_TYPES } from '../common/constants/alerts';
|
||||
import { umDynamicSettings } from './legacy_uptime/lib/saved_objects/uptime_settings';
|
||||
|
@ -29,7 +30,12 @@ export const uptimeFeature = {
|
|||
catalogue: ['uptime'],
|
||||
api: ['uptime-read', 'uptime-write', 'lists-all'],
|
||||
savedObject: {
|
||||
all: [umDynamicSettings.name, syntheticsMonitorType, syntheticsApiKeyObjectType],
|
||||
all: [
|
||||
umDynamicSettings.name,
|
||||
syntheticsMonitorType,
|
||||
syntheticsApiKeyObjectType,
|
||||
privateLocationsSavedObjectName,
|
||||
],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
|
@ -51,7 +57,12 @@ export const uptimeFeature = {
|
|||
api: ['uptime-read', 'lists-read'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [umDynamicSettings.name, syntheticsMonitorType, syntheticsApiKeyObjectType],
|
||||
read: [
|
||||
umDynamicSettings.name,
|
||||
syntheticsMonitorType,
|
||||
syntheticsApiKeyObjectType,
|
||||
privateLocationsSavedObjectName,
|
||||
],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
|
|
|
@ -33,7 +33,6 @@ import { UptimeESClient } from '../../lib';
|
|||
import type { TelemetryEventsSender } from '../../telemetry/sender';
|
||||
import type { UptimeRouter } from '../../../../types';
|
||||
import { UptimeConfig } from '../../../../../common/config';
|
||||
import { SyntheticsService } from '../../../../synthetics_service/synthetics_service';
|
||||
|
||||
export type UMElasticsearchQueryFn<P, R = any> = (
|
||||
params: {
|
||||
|
@ -57,7 +56,6 @@ export interface UptimeServerSetup {
|
|||
savedObjectsClient?: SavedObjectsClientContract;
|
||||
authSavedObjectsClient?: SavedObjectsClientContract;
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
||||
syntheticsService: SyntheticsService;
|
||||
kibanaVersion: string;
|
||||
logger: Logger;
|
||||
telemetry: TelemetryEventsSender;
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsErrorHelpers,
|
||||
SavedObjectsType,
|
||||
} from '@kbn/core/server';
|
||||
import { privateLocationsSavedObjectName } from '../../../../common/saved_objects/private_locations';
|
||||
import { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
|
||||
export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton';
|
||||
|
||||
export const privateLocationsSavedObject: SavedObjectsType = {
|
||||
name: privateLocationsSavedObjectName,
|
||||
hidden: false,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
/* Leaving these commented to make it clear that these fields exist, even though we don't want them indexed.
|
||||
When adding new fields please add them here. If they need to be searchable put them in the uncommented
|
||||
part of properties.
|
||||
*/
|
||||
},
|
||||
},
|
||||
management: {
|
||||
importableAndExportable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const getSyntheticsPrivateLocations = async (client: SavedObjectsClientContract) => {
|
||||
try {
|
||||
const obj = await client.get<SyntheticsPrivateLocations>(
|
||||
privateLocationsSavedObject.name,
|
||||
privateLocationsSavedObjectId
|
||||
);
|
||||
return obj?.attributes.locations ?? [];
|
||||
} catch (getErr) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
|
||||
return [];
|
||||
}
|
||||
throw getErr;
|
||||
}
|
||||
};
|
||||
export const setSyntheticsPrivateLocations = async (
|
||||
client: SavedObjectsClientContract,
|
||||
privateLocations: SyntheticsPrivateLocations
|
||||
) => {
|
||||
await client.create(privateLocationsSavedObject.name, privateLocations, {
|
||||
id: privateLocationsSavedObjectId,
|
||||
overwrite: true,
|
||||
});
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
import { SavedObjectsErrorHelpers, SavedObjectsServiceSetup } from '@kbn/core/server';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
|
||||
import { privateLocationsSavedObject } from './private_locations';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
|
||||
import { secretKeys } from '../../../../common/constants/monitor_management';
|
||||
import { DynamicSettings } from '../../../../common/runtime_types';
|
||||
|
@ -19,34 +20,32 @@ import { syntheticsServiceApiKey } from './service_api_key';
|
|||
|
||||
export const registerUptimeSavedObjects = (
|
||||
savedObjectsService: SavedObjectsServiceSetup,
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
|
||||
isServiceEnabled: boolean
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
) => {
|
||||
savedObjectsService.registerType(umDynamicSettings);
|
||||
savedObjectsService.registerType(privateLocationsSavedObject);
|
||||
|
||||
if (isServiceEnabled) {
|
||||
savedObjectsService.registerType(syntheticsMonitor);
|
||||
savedObjectsService.registerType(syntheticsServiceApiKey);
|
||||
savedObjectsService.registerType(syntheticsMonitor);
|
||||
savedObjectsService.registerType(syntheticsServiceApiKey);
|
||||
|
||||
encryptedSavedObjects.registerType({
|
||||
type: syntheticsServiceApiKey.name,
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
});
|
||||
encryptedSavedObjects.registerType({
|
||||
type: syntheticsServiceApiKey.name,
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
});
|
||||
|
||||
encryptedSavedObjects.registerType({
|
||||
type: syntheticsMonitor.name,
|
||||
attributesToEncrypt: new Set([
|
||||
'secrets',
|
||||
/* adding secretKeys to the list of attributes to encrypt ensures
|
||||
* that secrets are never stored on the resulting saved object,
|
||||
* even in the presence of developer error.
|
||||
*
|
||||
* In practice, all secrets should be stored as a single JSON
|
||||
* payload on the `secrets` key. This ensures performant decryption. */
|
||||
...secretKeys,
|
||||
]),
|
||||
});
|
||||
}
|
||||
encryptedSavedObjects.registerType({
|
||||
type: syntheticsMonitor.name,
|
||||
attributesToEncrypt: new Set([
|
||||
'secrets',
|
||||
/* adding secretKeys to the list of attributes to encrypt ensures
|
||||
* that secrets are never stored on the resulting saved object,
|
||||
* even in the presence of developer error.
|
||||
*
|
||||
* In practice, all secrets should be stored as a single JSON
|
||||
* payload on the `secrets` key. This ensures performant decryption. */
|
||||
...secretKeys,
|
||||
]),
|
||||
});
|
||||
};
|
||||
|
||||
export interface UMSavedObjectsAdapter {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
KibanaResponseFactory,
|
||||
IKibanaResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { UMServerLibs, UptimeESClient } from '../lib/lib';
|
||||
import type { UptimeRequestHandlerContext } from '../../types';
|
||||
import { UptimeServerSetup } from '../lib/adapters';
|
||||
|
@ -54,6 +55,7 @@ export type UptimeRoute = UMRouteDefinition<UMRouteHandler>;
|
|||
* Functions of this type accept custom lib functions and outputs a route object.
|
||||
*/
|
||||
export type UMRestApiRouteFactory = (libs: UMServerLibs) => UptimeRoute;
|
||||
export type SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => SyntheticsRoute;
|
||||
|
||||
/**
|
||||
* Functions of this type accept our internal route format and output a route
|
||||
|
@ -64,6 +66,14 @@ export type UMKibanaRouteWrapper = (
|
|||
server: UptimeServerSetup
|
||||
) => UMKibanaRoute;
|
||||
|
||||
export type SyntheticsRoute = UMRouteDefinition<SyntheticsRouteHandler>;
|
||||
|
||||
export type SyntheticsRouteWrapper = (
|
||||
uptimeRoute: SyntheticsRoute,
|
||||
server: UptimeServerSetup,
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient
|
||||
) => UMKibanaRoute;
|
||||
|
||||
/**
|
||||
* This is the contract we specify internally for route handling.
|
||||
*/
|
||||
|
@ -82,3 +92,20 @@ export type UMRouteHandler = ({
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
server: UptimeServerSetup;
|
||||
}) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
|
||||
export type SyntheticsRouteHandler = ({
|
||||
uptimeEsClient,
|
||||
context,
|
||||
request,
|
||||
response,
|
||||
server,
|
||||
savedObjectsClient,
|
||||
}: {
|
||||
uptimeEsClient: UptimeESClient;
|
||||
context: UptimeRequestHandlerContext;
|
||||
request: KibanaRequest<Record<string, any>, Record<string, any>, Record<string, any>>;
|
||||
response: KibanaResponseFactory;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
server: UptimeServerSetup;
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
}) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { mappingFromFieldMap } from '@kbn/rule-registry-plugin/common/mapping_from_field_map';
|
||||
import { experimentalRuleFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/experimental_rule_field_map';
|
||||
import { Dataset } from '@kbn/rule-registry-plugin/server';
|
||||
import { SyntheticsMonitorClient } from './synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { initSyntheticsServer } from './server';
|
||||
import { initUptimeServer } from './legacy_uptime/uptime_server';
|
||||
import { uptimeFeature } from './feature';
|
||||
|
@ -42,7 +43,8 @@ export class Plugin implements PluginType {
|
|||
private initContext: PluginInitializerContext;
|
||||
private logger: Logger;
|
||||
private server?: UptimeServerSetup;
|
||||
private syntheticService?: SyntheticsService;
|
||||
private syntheticsService?: SyntheticsService;
|
||||
private syntheticsMonitorClient?: SyntheticsMonitorClient;
|
||||
private readonly telemetryEventsSender: TelemetryEventsSender;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext<UptimeConfig>) {
|
||||
|
@ -87,28 +89,21 @@ export class Plugin implements PluginType {
|
|||
spaces: plugins.spaces,
|
||||
} as UptimeServerSetup;
|
||||
|
||||
if (this.server.config.service) {
|
||||
this.syntheticService = new SyntheticsService(
|
||||
this.logger,
|
||||
this.server,
|
||||
this.server.config.service
|
||||
);
|
||||
this.syntheticsService = new SyntheticsService(this.server);
|
||||
|
||||
this.syntheticService.registerSyncTask(plugins.taskManager);
|
||||
this.telemetryEventsSender.setup(plugins.telemetry);
|
||||
}
|
||||
this.syntheticsService.setup(plugins.taskManager);
|
||||
|
||||
this.syntheticsMonitorClient = new SyntheticsMonitorClient(this.syntheticsService, this.server);
|
||||
|
||||
this.telemetryEventsSender.setup(plugins.telemetry);
|
||||
|
||||
plugins.features.registerKibanaFeature(uptimeFeature);
|
||||
|
||||
initUptimeServer(this.server, plugins, ruleDataClient, this.logger);
|
||||
|
||||
initSyntheticsServer(this.server);
|
||||
initSyntheticsServer(this.server, this.syntheticsMonitorClient);
|
||||
|
||||
registerUptimeSavedObjects(
|
||||
core.savedObjects,
|
||||
plugins.encryptedSavedObjects,
|
||||
Boolean(this.server.config.service)
|
||||
);
|
||||
registerUptimeSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
|
||||
|
||||
KibanaTelemetryAdapter.registerUsageCollector(
|
||||
plugins.usageCollection,
|
||||
|
@ -120,32 +115,21 @@ export class Plugin implements PluginType {
|
|||
};
|
||||
}
|
||||
|
||||
public start(coreStart: CoreStart, plugins: UptimeCorePluginsStart) {
|
||||
if (this.server?.config.service) {
|
||||
this.savedObjectsClient = new SavedObjectsClient(
|
||||
coreStart.savedObjects.createInternalRepository([syntheticsServiceApiKey.name])
|
||||
);
|
||||
} else {
|
||||
this.savedObjectsClient = new SavedObjectsClient(
|
||||
coreStart.savedObjects.createInternalRepository()
|
||||
);
|
||||
}
|
||||
public start(coreStart: CoreStart, pluginsStart: UptimeCorePluginsStart) {
|
||||
this.savedObjectsClient = new SavedObjectsClient(
|
||||
coreStart.savedObjects.createInternalRepository([syntheticsServiceApiKey.name])
|
||||
);
|
||||
|
||||
if (this.server) {
|
||||
this.server.security = plugins.security;
|
||||
this.server.fleet = plugins.fleet;
|
||||
this.server.encryptedSavedObjects = plugins.encryptedSavedObjects;
|
||||
this.server.security = pluginsStart.security;
|
||||
this.server.fleet = pluginsStart.fleet;
|
||||
this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects;
|
||||
this.server.savedObjectsClient = this.savedObjectsClient;
|
||||
}
|
||||
|
||||
if (this.server?.config.service) {
|
||||
this.syntheticService?.init();
|
||||
this.syntheticService?.scheduleSyncTask(plugins.taskManager);
|
||||
if (this.server && this.syntheticService) {
|
||||
this.server.syntheticsService = this.syntheticService;
|
||||
}
|
||||
this.telemetryEventsSender.start(plugins.telemetry, coreStart);
|
||||
}
|
||||
this.syntheticsService?.start(pluginsStart.taskManager);
|
||||
|
||||
this.telemetryEventsSender.start(pluginsStart.telemetry, coreStart);
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { UMServerLibs } from '../legacy_uptime/lib/lib';
|
||||
import {
|
||||
SyntheticsRestApiRouteFactory,
|
||||
SyntheticsRoute,
|
||||
SyntheticsRouteHandler,
|
||||
} from '../legacy_uptime/routes';
|
||||
|
||||
export const createSyntheticsRouteWithAuth = (
|
||||
libs: UMServerLibs,
|
||||
routeCreator: SyntheticsRestApiRouteFactory
|
||||
): SyntheticsRoute => {
|
||||
const restRoute = routeCreator(libs);
|
||||
const { handler, method, path, options, ...rest } = restRoute;
|
||||
const licenseCheckHandler: SyntheticsRouteHandler = async ({
|
||||
context,
|
||||
response,
|
||||
...restProps
|
||||
}) => {
|
||||
const { statusCode, message } = libs.license((await context.licensing).license);
|
||||
if (statusCode === 200) {
|
||||
return handler({
|
||||
context,
|
||||
response,
|
||||
...restProps,
|
||||
});
|
||||
}
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return response.badRequest({ body: { message } });
|
||||
case 401:
|
||||
return response.unauthorized({ body: { message } });
|
||||
case 403:
|
||||
return response.forbidden({ body: { message } });
|
||||
default:
|
||||
throw new Error('Failed to validate the license');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
method,
|
||||
path,
|
||||
options,
|
||||
handler: licenseCheckHandler,
|
||||
...rest,
|
||||
};
|
||||
};
|
|
@ -26,9 +26,9 @@ import { installIndexTemplatesRoute } from './synthetics_service/install_index_t
|
|||
import { editSyntheticsMonitorRoute } from './monitor_cruds/edit_monitor';
|
||||
import { addSyntheticsMonitorRoute } from './monitor_cruds/add_monitor';
|
||||
import { addSyntheticsProjectMonitorRoute } from './monitor_cruds/add_monitor_project';
|
||||
import { UMRestApiRouteFactory } from '../legacy_uptime/routes';
|
||||
import { SyntheticsRestApiRouteFactory } from '../legacy_uptime/routes';
|
||||
|
||||
export const syntheticsAppRestApiRoutes: UMRestApiRouteFactory[] = [
|
||||
export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
|
||||
addSyntheticsProjectMonitorRoute,
|
||||
addSyntheticsMonitorRoute,
|
||||
getSyntheticsEnablementRoute,
|
||||
|
|
|
@ -6,23 +6,23 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import {
|
||||
ConfigKey,
|
||||
MonitorFields,
|
||||
SyntheticsMonitor,
|
||||
EncryptedSyntheticsMonitor,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults';
|
||||
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
|
||||
import { validateMonitor } from './monitor_validation';
|
||||
import { sendTelemetryEvents, formatTelemetryEvent } from '../telemetry/monitor_upgrade_sender';
|
||||
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
|
||||
import { formatSecrets } from '../../synthetics_service/utils/secrets';
|
||||
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
|
||||
|
||||
export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
||||
export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'POST',
|
||||
path: API_URLS.SYNTHETICS_MONITORS,
|
||||
validate: {
|
||||
|
@ -31,7 +31,13 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
id: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({
|
||||
request,
|
||||
response,
|
||||
savedObjectsClient,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
// usually id is auto generated, but this is useful for testing
|
||||
const { id } = request.query;
|
||||
|
||||
|
@ -78,7 +84,12 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
});
|
||||
}
|
||||
|
||||
const errors = await syncNewMonitor({ monitor, monitorSavedObject: newMonitor, server });
|
||||
const errors = await syncNewMonitor({
|
||||
monitor,
|
||||
monitorSavedObject: newMonitor,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
return response.ok({
|
||||
|
@ -98,17 +109,16 @@ export const syncNewMonitor = async ({
|
|||
monitor,
|
||||
monitorSavedObject,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}: {
|
||||
monitor: SyntheticsMonitor;
|
||||
monitorSavedObject: SavedObject<EncryptedSyntheticsMonitor>;
|
||||
server: UptimeServerSetup;
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
}) => {
|
||||
const errors = await server.syntheticsService.addConfig(
|
||||
formatHeartbeatRequest({
|
||||
monitor,
|
||||
monitorId: monitorSavedObject.id,
|
||||
customHeartbeatId: (monitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID],
|
||||
})
|
||||
const errors = await syntheticsMonitorClient.addMonitor(
|
||||
monitor as MonitorFields,
|
||||
monitorSavedObject.id
|
||||
);
|
||||
|
||||
sendTelemetryEvents(
|
||||
|
|
|
@ -8,12 +8,14 @@ import { schema } from '@kbn/config-schema';
|
|||
import { UMServerLibs } from '../../legacy_uptime/lib/lib';
|
||||
import { ProjectBrowserMonitor, Locations } from '../../../common/runtime_types';
|
||||
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { getServiceLocations } from '../../synthetics_service/get_service_locations';
|
||||
import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor_formatter';
|
||||
|
||||
export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
|
||||
libs: UMServerLibs
|
||||
) => ({
|
||||
method: 'PUT',
|
||||
path: API_URLS.SYNTHETICS_MONITORS_PROJECT,
|
||||
validate: {
|
||||
|
@ -23,7 +25,13 @@ export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UM
|
|||
monitors: schema.arrayOf(schema.any()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({
|
||||
request,
|
||||
response,
|
||||
savedObjectsClient,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
const monitors = (request.body?.monitors as ProjectBrowserMonitor[]) || [];
|
||||
const spaceId = server.spaces.spacesService.getSpaceId(request);
|
||||
const { keep_stale: keepStale, project: projectId } = request.body || {};
|
||||
|
@ -39,6 +47,7 @@ export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UM
|
|||
savedObjectsClient,
|
||||
monitors,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
});
|
||||
|
||||
await pushMonitorFormatter.configureAllProjectMonitors();
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import {
|
||||
ConfigKey,
|
||||
MonitorFields,
|
||||
EncryptedSyntheticsMonitor,
|
||||
SyntheticsMonitorWithSecrets,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import {
|
||||
syntheticsMonitorType,
|
||||
|
@ -26,7 +27,7 @@ import {
|
|||
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
|
||||
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
|
||||
|
||||
export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
||||
export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'DELETE',
|
||||
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
|
||||
validate: {
|
||||
|
@ -34,11 +35,22 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({
|
||||
request,
|
||||
response,
|
||||
savedObjectsClient,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
const { monitorId } = request.params;
|
||||
|
||||
try {
|
||||
const errors = await deleteMonitor({ savedObjectsClient, server, monitorId });
|
||||
const errors = await deleteMonitor({
|
||||
savedObjectsClient,
|
||||
server,
|
||||
monitorId,
|
||||
syntheticsMonitorClient,
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
return response.ok({
|
||||
|
@ -61,12 +73,14 @@ export const deleteMonitor = async ({
|
|||
savedObjectsClient,
|
||||
server,
|
||||
monitorId,
|
||||
syntheticsMonitorClient,
|
||||
}: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
server: UptimeServerSetup;
|
||||
monitorId: string;
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
}) => {
|
||||
const { syntheticsService, logger, telemetry, kibanaVersion, encryptedSavedObjects } = server;
|
||||
const { logger, telemetry, kibanaVersion, encryptedSavedObjects } = server;
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
|
||||
try {
|
||||
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitor>(
|
||||
|
@ -86,14 +100,11 @@ export const deleteMonitor = async ({
|
|||
const normalizedMonitor = normalizeSecrets(monitor);
|
||||
|
||||
await savedObjectsClient.delete(syntheticsMonitorType, monitorId);
|
||||
const errors = await syntheticsService.deleteConfigs([
|
||||
{
|
||||
...normalizedMonitor.attributes,
|
||||
id:
|
||||
(normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] ||
|
||||
monitorId,
|
||||
},
|
||||
]);
|
||||
const errors = await syntheticsMonitorClient.deleteMonitor({
|
||||
...normalizedMonitor.attributes,
|
||||
id:
|
||||
(normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || monitorId,
|
||||
});
|
||||
|
||||
sendTelemetryEvents(
|
||||
logger,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
|||
import { EncryptedSyntheticsMonitor, SyntheticsMonitor } from '../../../common/runtime_types';
|
||||
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
|
||||
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
|
||||
jest.mock('../telemetry/monitor_upgrade_sender', () => ({
|
||||
sendTelemetryEvents: jest.fn(),
|
||||
|
@ -25,18 +26,19 @@ describe('syncEditedMonitor', () => {
|
|||
kibanaVersion: null,
|
||||
authSavedObjectsClient: { bulkUpdate: jest.fn() },
|
||||
logger,
|
||||
config: {
|
||||
service: {
|
||||
username: 'dev',
|
||||
password: '12345',
|
||||
},
|
||||
},
|
||||
} as unknown as UptimeServerSetup;
|
||||
|
||||
const syntheticsService = new SyntheticsService(logger, serverMock, {
|
||||
username: 'dev',
|
||||
password: '12345',
|
||||
});
|
||||
const syntheticsService = new SyntheticsService(serverMock);
|
||||
|
||||
const fakePush = jest.fn();
|
||||
|
||||
jest.spyOn(syntheticsService, 'pushConfigs').mockImplementationOnce(fakePush);
|
||||
|
||||
serverMock.syntheticsService = syntheticsService;
|
||||
jest.spyOn(syntheticsService, 'editConfig').mockImplementationOnce(fakePush);
|
||||
|
||||
const editedMonitor = {
|
||||
type: 'http',
|
||||
|
@ -61,21 +63,21 @@ describe('syncEditedMonitor', () => {
|
|||
id: 'saved-obj-id',
|
||||
} as SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
|
||||
|
||||
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
|
||||
it('includes the isEdit flag', () => {
|
||||
syncEditedMonitor({
|
||||
editedMonitor,
|
||||
editedMonitorSavedObject,
|
||||
previousMonitor,
|
||||
syntheticsMonitorClient,
|
||||
server: serverMock,
|
||||
});
|
||||
|
||||
expect(fakePush).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'saved-obj-id',
|
||||
}),
|
||||
]),
|
||||
true
|
||||
expect.objectContaining({
|
||||
id: 'saved-obj-id',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import {
|
||||
MonitorFields,
|
||||
EncryptedSyntheticsMonitor,
|
||||
|
@ -15,7 +16,7 @@ import {
|
|||
SyntheticsMonitor,
|
||||
ConfigKey,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import {
|
||||
syntheticsMonitorType,
|
||||
|
@ -27,12 +28,11 @@ import {
|
|||
sendTelemetryEvents,
|
||||
formatTelemetryUpdateEvent,
|
||||
} from '../telemetry/monitor_upgrade_sender';
|
||||
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
|
||||
import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets';
|
||||
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
|
||||
|
||||
// Simplify return promise type and type it with runtime_types
|
||||
export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
||||
export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'PUT',
|
||||
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
|
||||
validate: {
|
||||
|
@ -41,7 +41,13 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
}),
|
||||
body: schema.any(),
|
||||
},
|
||||
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({
|
||||
request,
|
||||
response,
|
||||
savedObjectsClient,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
const { encryptedSavedObjects, logger } = server;
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
|
||||
const monitor = request.body as SyntheticsMonitor;
|
||||
|
@ -95,6 +101,7 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
editedMonitor,
|
||||
editedMonitorSavedObject,
|
||||
previousMonitor,
|
||||
syntheticsMonitorClient,
|
||||
});
|
||||
|
||||
// Return service sync errors in OK response
|
||||
|
@ -121,21 +128,17 @@ export const syncEditedMonitor = async ({
|
|||
editedMonitorSavedObject,
|
||||
previousMonitor,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}: {
|
||||
editedMonitor: SyntheticsMonitor;
|
||||
editedMonitorSavedObject: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
|
||||
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
|
||||
server: UptimeServerSetup;
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
}) => {
|
||||
const errors = await server.syntheticsService.pushConfigs(
|
||||
[
|
||||
formatHeartbeatRequest({
|
||||
monitor: editedMonitor,
|
||||
monitorId: editedMonitorSavedObject.id,
|
||||
customHeartbeatId: (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID],
|
||||
}),
|
||||
],
|
||||
true
|
||||
const errors = await syntheticsMonitorClient.editMonitor(
|
||||
editedMonitor as MonitorFields,
|
||||
editedMonitorSavedObject.id
|
||||
);
|
||||
|
||||
sendTelemetryEvents(
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
|
||||
import { generateAPIKey } from '../../synthetics_service/get_api_key';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
|
||||
export const getAPIKeySyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
|
||||
export const getAPIKeySyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.SYNTHETICS_APIKEY,
|
||||
validate: {},
|
||||
handler: async ({ request, response, server }): Promise<any> => {
|
||||
handler: async ({ request, server }): Promise<any> => {
|
||||
const { security } = server;
|
||||
|
||||
const apiKey = await generateAPIKey({
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
SavedObjectsErrorHelpers,
|
||||
SavedObjectsFindResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
|
||||
import {
|
||||
ConfigKey,
|
||||
EncryptedSyntheticsMonitor,
|
||||
|
@ -17,11 +18,10 @@ import {
|
|||
} from '../../../common/runtime_types';
|
||||
import { monitorAttributes } from '../../../common/types/saved_objects';
|
||||
import { UMServerLibs } from '../../legacy_uptime/lib/lib';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS, SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
|
||||
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
|
||||
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
|
||||
|
||||
const querySchema = schema.object({
|
||||
page: schema.maybe(schema.number()),
|
||||
|
@ -40,7 +40,7 @@ type MonitorsQuery = TypeOf<typeof querySchema>;
|
|||
|
||||
const getMonitors = (
|
||||
request: MonitorsQuery,
|
||||
server: UptimeServerSetup,
|
||||
syntheticsService: SyntheticsService,
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): Promise<SavedObjectsFindResponse<EncryptedSyntheticsMonitor>> => {
|
||||
const {
|
||||
|
@ -55,7 +55,7 @@ const getMonitors = (
|
|||
filter = '',
|
||||
} = request as MonitorsQuery;
|
||||
|
||||
const locationFilter = parseLocationFilter(server.syntheticsService.locations, locations);
|
||||
const locationFilter = parseLocationFilter(syntheticsService.locations, locations);
|
||||
|
||||
const filters =
|
||||
getFilter('tags', tags) +
|
||||
|
@ -74,7 +74,7 @@ const getMonitors = (
|
|||
});
|
||||
};
|
||||
|
||||
export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
|
||||
validate: {
|
||||
|
@ -106,15 +106,19 @@ export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerL
|
|||
},
|
||||
});
|
||||
|
||||
export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
||||
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.SYNTHETICS_MONITORS,
|
||||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({ request, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
|
||||
const { filters, query } = request.query;
|
||||
const monitorsPromise = getMonitors(request.query, server, savedObjectsClient);
|
||||
const monitorsPromise = getMonitors(
|
||||
request.query,
|
||||
syntheticsMonitorClient.syntheticsService,
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
if (filters || query) {
|
||||
const totalMonitorsPromise = savedObjectsClient.find({
|
||||
|
@ -132,7 +136,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
monitors,
|
||||
perPage: perPageT,
|
||||
absoluteTotal: total,
|
||||
syncErrors: server.syntheticsService.syncErrors,
|
||||
syncErrors: syntheticsMonitorClient.syntheticsService.syncErrors,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -143,7 +147,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
monitors,
|
||||
perPage: perPageT,
|
||||
absoluteTotal: rest.total,
|
||||
syncErrors: server.syntheticsService.syncErrors,
|
||||
syncErrors: syntheticsMonitorClient.syntheticsService.syncErrors,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -180,13 +184,13 @@ export const findLocationItem = (query: string, locations: ServiceLocations) =>
|
|||
return locations.find(({ id, label }) => query === id || label === query);
|
||||
};
|
||||
|
||||
export const getSyntheticsMonitorOverviewRoute: UMRestApiRouteFactory = () => ({
|
||||
export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'GET',
|
||||
path: SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW,
|
||||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({ request, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
|
||||
const { perPage = 5 } = request.query;
|
||||
const { saved_objects: monitors } = await getMonitors(
|
||||
{
|
||||
|
@ -195,7 +199,7 @@ export const getSyntheticsMonitorOverviewRoute: UMRestApiRouteFactory = () => ({
|
|||
sortOrder: 'asc',
|
||||
page: 1,
|
||||
},
|
||||
server,
|
||||
syntheticsMonitorClient.syntheticsService,
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ describe('validateMonitor', () => {
|
|||
testCommonFields = {
|
||||
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
|
||||
[ConfigKey.NAME]: 'test-monitor-name',
|
||||
[ConfigKey.CONFIG_ID]: 'test-monitor-id',
|
||||
[ConfigKey.ENABLED]: true,
|
||||
[ConfigKey.TAGS]: testTags,
|
||||
[ConfigKey.SCHEDULE]: testSchedule,
|
||||
|
@ -434,6 +435,7 @@ function getJsonPayload() {
|
|||
' "TLSv1.2"' +
|
||||
' ],' +
|
||||
' "name": "test-monitor-name",' +
|
||||
' "config_id": "test-monitor-id",' +
|
||||
' "namespace": "testnamespace",' +
|
||||
' "locations": [{' +
|
||||
' "id": "eu-west-01",' +
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { UMServerLibs } from '../../legacy_uptime/uptime_server';
|
||||
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { ConfigKey, MonitorFields } from '../../../common/runtime_types';
|
||||
|
||||
|
@ -20,7 +20,7 @@ const queryParams = schema.object({
|
|||
|
||||
type QueryParams = TypeOf<typeof queryParams>;
|
||||
|
||||
export const createGetMonitorStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
export const createGetMonitorStatusRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
path: SYNTHETICS_API_URLS.MONITOR_STATUS,
|
||||
validate: {
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import {
|
||||
SyntheticsRestApiRouteFactory,
|
||||
UMRestApiRouteFactory,
|
||||
} from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { SyntheticsForbiddenError } from '../../synthetics_service/get_api_key';
|
||||
|
||||
|
@ -27,12 +30,19 @@ export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const disableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
|
||||
export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ({
|
||||
method: 'DELETE',
|
||||
path: API_URLS.SYNTHETICS_ENABLEMENT,
|
||||
validate: {},
|
||||
handler: async ({ response, request, server, savedObjectsClient }): Promise<any> => {
|
||||
const { syntheticsService, security } = server;
|
||||
handler: async ({
|
||||
response,
|
||||
request,
|
||||
server,
|
||||
savedObjectsClient,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
const { security } = server;
|
||||
const { syntheticsService } = syntheticsMonitorClient;
|
||||
try {
|
||||
const { canEnable } = await libs.requests.getSyntheticsEnablement({ request, server });
|
||||
if (!canEnable) {
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
|
||||
export const getServiceAllowedRoute: UMRestApiRouteFactory = () => ({
|
||||
export const getServiceAllowedRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.SERVICE_ALLOWED,
|
||||
validate: {},
|
||||
handler: async ({ server }): Promise<any> => {
|
||||
handler: async ({ syntheticsMonitorClient }): Promise<any> => {
|
||||
return {
|
||||
serviceAllowed: server.syntheticsService.isAllowed,
|
||||
signupUrl: server.syntheticsService.signupUrl,
|
||||
serviceAllowed: true,
|
||||
signupUrl: syntheticsMonitorClient.syntheticsService.signupUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,19 +6,41 @@
|
|||
*/
|
||||
|
||||
import { getServiceLocations } from '../../synthetics_service/get_service_locations';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
|
||||
|
||||
export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({
|
||||
export const getServiceLocationsRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.SERVICE_LOCATIONS,
|
||||
validate: {},
|
||||
handler: async ({ server }): Promise<any> => {
|
||||
if (server.syntheticsService.locations.length > 0) {
|
||||
const { throttling, locations } = server.syntheticsService;
|
||||
return { throttling, locations };
|
||||
handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
|
||||
const { syntheticsService, privateLocationAPI } = syntheticsMonitorClient;
|
||||
const privateLocations = await getSyntheticsPrivateLocations(savedObjectsClient);
|
||||
const agentPolicies = await privateLocationAPI.getAgentPolicies();
|
||||
|
||||
const privateLocs =
|
||||
privateLocations?.map((loc) => ({
|
||||
label: loc.name,
|
||||
isServiceManaged: false,
|
||||
isInvalid: agentPolicies.find((policy) => policy.id === loc.policyHostId) === undefined,
|
||||
...loc,
|
||||
})) ?? [];
|
||||
|
||||
if (syntheticsService.locations.length > 0) {
|
||||
const { throttling, locations } = syntheticsService;
|
||||
|
||||
return {
|
||||
throttling,
|
||||
locations: [...locations, ...privateLocs],
|
||||
};
|
||||
}
|
||||
|
||||
return getServiceLocations(server);
|
||||
const { locations, throttling } = await getServiceLocations(server);
|
||||
|
||||
return {
|
||||
locations: [...locations, ...privateLocs],
|
||||
throttling,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
|
||||
|
||||
export const installIndexTemplatesRoute: UMRestApiRouteFactory = () => ({
|
||||
export const installIndexTemplatesRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.INDEX_TEMPLATES,
|
||||
validate: {},
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { MonitorFields } from '../../../common/runtime_types';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
|
||||
import { validateMonitor } from '../monitor_cruds/monitor_validation';
|
||||
|
||||
export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
||||
export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'POST',
|
||||
path: API_URLS.RUN_ONCE_MONITOR + '/{monitorId}',
|
||||
validate: {
|
||||
|
@ -20,7 +20,7 @@ export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, response, server }): Promise<any> => {
|
||||
handler: async ({ request, response, server, syntheticsMonitorClient }): Promise<any> => {
|
||||
const monitor = request.body as MonitorFields;
|
||||
const { monitorId } = request.params;
|
||||
|
||||
|
@ -31,7 +31,7 @@ export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
|
||||
}
|
||||
|
||||
const { syntheticsService } = server;
|
||||
const { syntheticsService } = syntheticsMonitorClient;
|
||||
|
||||
const errors = await syntheticsService.runOnceConfigs([
|
||||
formatHeartbeatRequest({
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
SyntheticsMonitor,
|
||||
SyntheticsMonitorWithSecrets,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import {
|
||||
syntheticsMonitor,
|
||||
|
@ -21,7 +21,7 @@ import {
|
|||
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
|
||||
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
|
||||
|
||||
export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
|
||||
export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'GET',
|
||||
path: API_URLS.TRIGGER_MONITOR + '/{monitorId}',
|
||||
validate: {
|
||||
|
@ -29,7 +29,12 @@ export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
|
||||
handler: async ({
|
||||
request,
|
||||
savedObjectsClient,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
const { monitorId } = request.params;
|
||||
const monitor = await savedObjectsClient.get<SyntheticsMonitor>(
|
||||
syntheticsMonitorType,
|
||||
|
@ -47,7 +52,7 @@ export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
|
||||
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } = monitor.attributes;
|
||||
|
||||
const { syntheticsService } = server;
|
||||
const { syntheticsService } = syntheticsMonitorClient;
|
||||
|
||||
const testRunId = uuidv4();
|
||||
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSyntheticsRouteWithAuth } from './routes/create_route_with_auth';
|
||||
import { SyntheticsMonitorClient } from './synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { syntheticsRouteWrapper } from './synthetics_route_wrapper';
|
||||
import { uptimeRequests } from './legacy_uptime/lib/requests';
|
||||
import { syntheticsAppRestApiRoutes } from './routes';
|
||||
import { createRouteWithAuth } from './legacy_uptime/routes';
|
||||
import { UptimeServerSetup } from './legacy_uptime/lib/adapters';
|
||||
import { licenseCheck } from './legacy_uptime/lib/domains';
|
||||
|
||||
export const initSyntheticsServer = (server: UptimeServerSetup) => {
|
||||
export const initSyntheticsServer = (
|
||||
server: UptimeServerSetup,
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient
|
||||
) => {
|
||||
const libs = {
|
||||
requests: uptimeRequests,
|
||||
license: licenseCheck,
|
||||
|
@ -20,8 +24,9 @@ export const initSyntheticsServer = (server: UptimeServerSetup) => {
|
|||
|
||||
syntheticsAppRestApiRoutes.forEach((route) => {
|
||||
const { method, options, handler, validate, path } = syntheticsRouteWrapper(
|
||||
createRouteWithAuth(libs, route),
|
||||
server
|
||||
createSyntheticsRouteWithAuth(libs, route),
|
||||
server,
|
||||
syntheticsMonitorClient
|
||||
);
|
||||
|
||||
const routeDefinition = {
|
||||
|
|
|
@ -9,10 +9,14 @@ import { KibanaResponse } from '@kbn/core-http-router-server-internal';
|
|||
import { enableInspectEsQueries } from '@kbn/observability-plugin/common';
|
||||
import { createUptimeESClient, inspectableEsQueriesMap } from './legacy_uptime/lib/lib';
|
||||
import { syntheticsServiceApiKey } from './legacy_uptime/lib/saved_objects/service_api_key';
|
||||
import { UMKibanaRouteWrapper } from './legacy_uptime/routes';
|
||||
import { SyntheticsRouteWrapper } from './legacy_uptime/routes';
|
||||
import { API_URLS } from '../common/constants';
|
||||
|
||||
export const syntheticsRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => ({
|
||||
export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
|
||||
uptimeRoute,
|
||||
server,
|
||||
syntheticsMonitorClient
|
||||
) => ({
|
||||
...uptimeRoute,
|
||||
options: {
|
||||
tags: ['access:uptime-read', ...(uptimeRoute?.writeAccess ? ['access:uptime-write'] : [])],
|
||||
|
@ -54,6 +58,7 @@ export const syntheticsRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server
|
|||
request,
|
||||
response,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
});
|
||||
|
||||
if (res instanceof KibanaResponse) {
|
||||
|
|
|
@ -18,6 +18,7 @@ export const commonFormatters: CommonFormatMap = {
|
|||
[ConfigKey.LOCATIONS]: null,
|
||||
[ConfigKey.ENABLED]: null,
|
||||
[ConfigKey.MONITOR_TYPE]: null,
|
||||
[ConfigKey.CONFIG_ID]: null,
|
||||
[ConfigKey.LOCATIONS]: null,
|
||||
[ConfigKey.SCHEDULE]: (fields) =>
|
||||
`@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}`,
|
||||
|
|
|
@ -69,6 +69,7 @@ describe('getServiceLocations', function () {
|
|||
lon: -95.86,
|
||||
},
|
||||
id: 'us_central',
|
||||
isInvalid: false,
|
||||
label: 'US Central',
|
||||
url: 'https://local.dev',
|
||||
isServiceManaged: true,
|
||||
|
@ -80,6 +81,7 @@ describe('getServiceLocations', function () {
|
|||
lon: -95.86,
|
||||
},
|
||||
id: 'us_east',
|
||||
isInvalid: false,
|
||||
label: 'US East',
|
||||
url: 'https://local.dev',
|
||||
isServiceManaged: true,
|
||||
|
@ -118,6 +120,7 @@ describe('getServiceLocations', function () {
|
|||
lon: -95.86,
|
||||
},
|
||||
id: 'us_central',
|
||||
isInvalid: false,
|
||||
label: 'US Central',
|
||||
url: 'https://local.dev',
|
||||
isServiceManaged: true,
|
||||
|
@ -154,6 +157,7 @@ describe('getServiceLocations', function () {
|
|||
lon: -95.86,
|
||||
},
|
||||
id: 'us_central',
|
||||
isInvalid: false,
|
||||
label: 'US Central',
|
||||
url: 'https://local.dev',
|
||||
isServiceManaged: true,
|
||||
|
@ -165,6 +169,7 @@ describe('getServiceLocations', function () {
|
|||
lon: -95.86,
|
||||
},
|
||||
id: 'us_east',
|
||||
isInvalid: false,
|
||||
label: 'US East',
|
||||
url: 'https://local.dev',
|
||||
isServiceManaged: true,
|
||||
|
|
|
@ -24,6 +24,7 @@ export const getDevLocation = (devUrl: string): ServiceLocation => ({
|
|||
url: devUrl,
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
});
|
||||
|
||||
export async function getServiceLocations(server: UptimeServerSetup) {
|
||||
|
@ -58,6 +59,7 @@ export async function getServiceLocations(server: UptimeServerSetup) {
|
|||
url: location.url,
|
||||
isServiceManaged: true,
|
||||
status: location.status,
|
||||
isInvalid: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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 { testMonitorPolicy } from './test_policy';
|
||||
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
|
||||
import { DataStream, MonitorFields, ScheduleUnit, SourceType } from '../../../common/runtime_types';
|
||||
|
||||
describe('SyntheticsPrivateLocation', () => {
|
||||
it('formats monitors stream properly', () => {
|
||||
const test = formatSyntheticsPolicy(testMonitorPolicy, DataStream.BROWSER, dummyBrowserConfig);
|
||||
|
||||
expect(test.formattedPolicy.inputs[3].streams[1]).toStrictEqual({
|
||||
data_stream: {
|
||||
dataset: 'browser',
|
||||
type: 'synthetics',
|
||||
},
|
||||
enabled: true,
|
||||
vars: {
|
||||
__ui: {
|
||||
type: 'yaml',
|
||||
value:
|
||||
'{"script_source":{"is_generated_script":false,"file_name":""},"is_zip_url_tls_enabled":false,"is_tls_enabled":true}',
|
||||
},
|
||||
config_id: {
|
||||
type: 'text',
|
||||
value: '75cdd125-5b62-4459-870c-46f59bf37e89',
|
||||
},
|
||||
enabled: {
|
||||
type: 'bool',
|
||||
value: true,
|
||||
},
|
||||
'filter_journeys.match': {
|
||||
type: 'text',
|
||||
value: null,
|
||||
},
|
||||
'filter_journeys.tags': {
|
||||
type: 'yaml',
|
||||
value: null,
|
||||
},
|
||||
ignore_https_errors: {
|
||||
type: 'bool',
|
||||
value: false,
|
||||
},
|
||||
location_name: {
|
||||
type: 'text',
|
||||
value: 'Fleet managed',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
value: 'Browser monitor',
|
||||
},
|
||||
params: {
|
||||
type: 'yaml',
|
||||
value: '',
|
||||
},
|
||||
run_once: {
|
||||
type: 'bool',
|
||||
value: false,
|
||||
},
|
||||
schedule: {
|
||||
type: 'text',
|
||||
value: '"@every 10m"',
|
||||
},
|
||||
screenshots: {
|
||||
type: 'text',
|
||||
value: 'on',
|
||||
},
|
||||
'service.name': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
'source.inline.script': {
|
||||
type: 'yaml',
|
||||
value:
|
||||
"\"step('Go to https://www.elastic.co/', async () => {\\n await page.goto('https://www.elastic.co/');\\n});\"",
|
||||
},
|
||||
'source.zip_url.folder': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
'source.zip_url.password': {
|
||||
type: 'password',
|
||||
value: '',
|
||||
},
|
||||
'source.zip_url.proxy_url': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
'source.zip_url.ssl.certificate': {
|
||||
type: 'yaml',
|
||||
},
|
||||
'source.zip_url.ssl.certificate_authorities': {
|
||||
type: 'yaml',
|
||||
},
|
||||
'source.zip_url.ssl.key': {
|
||||
type: 'yaml',
|
||||
},
|
||||
'source.zip_url.ssl.key_passphrase': {
|
||||
type: 'text',
|
||||
},
|
||||
'source.zip_url.ssl.supported_protocols': {
|
||||
type: 'yaml',
|
||||
},
|
||||
'source.zip_url.ssl.verification_mode': {
|
||||
type: 'text',
|
||||
},
|
||||
'source.zip_url.url': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
'source.zip_url.username': {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
synthetics_args: {
|
||||
type: 'text',
|
||||
value: null,
|
||||
},
|
||||
tags: {
|
||||
type: 'yaml',
|
||||
value: null,
|
||||
},
|
||||
'throttling.config': {
|
||||
type: 'text',
|
||||
value: '5d/3u/20l',
|
||||
},
|
||||
timeout: {
|
||||
type: 'text',
|
||||
value: null,
|
||||
},
|
||||
type: {
|
||||
type: 'text',
|
||||
value: 'browser',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const dummyBrowserConfig: Partial<MonitorFields> & {
|
||||
id: string;
|
||||
fields: Record<string, string | boolean>;
|
||||
fields_under_root: boolean;
|
||||
} = {
|
||||
type: DataStream.BROWSER,
|
||||
enabled: true,
|
||||
schedule: { unit: ScheduleUnit.MINUTES, number: '10' },
|
||||
'service.name': '',
|
||||
tags: [],
|
||||
timeout: null,
|
||||
name: 'Browser monitor',
|
||||
locations: [{ isServiceManaged: false, id: '1' }],
|
||||
namespace: 'default',
|
||||
origin: SourceType.UI,
|
||||
journey_id: '',
|
||||
project_id: '',
|
||||
playwright_options: '',
|
||||
__ui: {
|
||||
script_source: { is_generated_script: false, file_name: '' },
|
||||
is_zip_url_tls_enabled: false,
|
||||
is_tls_enabled: true,
|
||||
},
|
||||
params: '',
|
||||
'url.port': 443,
|
||||
'source.inline.script':
|
||||
"step('Go to https://www.elastic.co/', async () => {\n await page.goto('https://www.elastic.co/');\n});",
|
||||
'source.project.content': '',
|
||||
'source.zip_url.url': '',
|
||||
'source.zip_url.username': '',
|
||||
'source.zip_url.password': '',
|
||||
'source.zip_url.folder': '',
|
||||
'source.zip_url.proxy_url': '',
|
||||
urls: 'https://www.elastic.co/',
|
||||
screenshots: 'on',
|
||||
synthetics_args: [],
|
||||
'filter_journeys.match': '',
|
||||
'filter_journeys.tags': [],
|
||||
ignore_https_errors: false,
|
||||
'throttling.is_enabled': true,
|
||||
'throttling.download_speed': '5',
|
||||
'throttling.upload_speed': '3',
|
||||
'throttling.latency': '20',
|
||||
'throttling.config': '5d/3u/20l',
|
||||
id: '75cdd125-5b62-4459-870c-46f59bf37e89',
|
||||
config_id: '75cdd125-5b62-4459-870c-46f59bf37e89',
|
||||
fields: { config_id: '75cdd125-5b62-4459-870c-46f59bf37e89', run_once: true },
|
||||
fields_under_root: true,
|
||||
max_redirects: '0',
|
||||
};
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { NewPackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
|
||||
import { SyntheticsConfig } from '../formatters/format_configs';
|
||||
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
|
||||
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
|
||||
import {
|
||||
ConfigKey,
|
||||
MonitorFields,
|
||||
PrivateLocation,
|
||||
SyntheticsMonitorWithId,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
|
||||
|
||||
const getPolicyId = (config: SyntheticsMonitorWithId, privateLocation: PrivateLocation) =>
|
||||
config.id + '-' + privateLocation.id;
|
||||
|
||||
export class SyntheticsPrivateLocation {
|
||||
private readonly server: UptimeServerSetup;
|
||||
|
||||
constructor(_server: UptimeServerSetup) {
|
||||
this.server = _server;
|
||||
}
|
||||
|
||||
async generateNewPolicy(
|
||||
config: SyntheticsMonitorWithId,
|
||||
privateLocation: PrivateLocation
|
||||
): Promise<NewPackagePolicy> {
|
||||
if (!this.server.authSavedObjectsClient) {
|
||||
throw new Error('Could not find authSavedObjectsClient');
|
||||
}
|
||||
|
||||
const newPolicy = await this.server.fleet.packagePolicyService.buildPackagePolicyFromPackage(
|
||||
this.server.authSavedObjectsClient,
|
||||
'synthetics'
|
||||
);
|
||||
|
||||
if (!newPolicy) {
|
||||
throw new Error('Could not create new synthetics policy');
|
||||
}
|
||||
|
||||
newPolicy.is_managed = true;
|
||||
newPolicy.policy_id = privateLocation.policyHostId;
|
||||
newPolicy.name = config[ConfigKey.NAME] + '-' + privateLocation.name;
|
||||
newPolicy.output_id = '';
|
||||
newPolicy.namespace = 'default';
|
||||
|
||||
const { formattedPolicy } = formatSyntheticsPolicy(newPolicy, config.type, {
|
||||
...(config as Partial<MonitorFields>),
|
||||
config_id: config.id,
|
||||
location_name: privateLocation.name,
|
||||
});
|
||||
|
||||
return formattedPolicy;
|
||||
}
|
||||
|
||||
async createMonitor(config: SyntheticsMonitorWithId) {
|
||||
try {
|
||||
const { locations } = config;
|
||||
|
||||
const privateLocations = await getSyntheticsPrivateLocations(
|
||||
this.server.authSavedObjectsClient!
|
||||
);
|
||||
|
||||
const fleetManagedLocations = locations.filter((loc) => !loc.isServiceManaged);
|
||||
|
||||
for (const privateLocation of fleetManagedLocations) {
|
||||
const location = privateLocations?.find((loc) => loc.id === privateLocation.id)!;
|
||||
const newPolicy = await this.generateNewPolicy(config, location);
|
||||
|
||||
await this.createPolicy(newPolicy, getPolicyId(config, location));
|
||||
}
|
||||
} catch (e) {
|
||||
this.server.logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async editMonitor(config: SyntheticsConfig) {
|
||||
const { locations } = config;
|
||||
|
||||
const allPrivateLocations = await getSyntheticsPrivateLocations(
|
||||
this.server.authSavedObjectsClient!
|
||||
);
|
||||
|
||||
const monitorPrivateLocations = locations.filter((loc) => !loc.isServiceManaged);
|
||||
|
||||
for (const privateLocation of allPrivateLocations) {
|
||||
const hasLocation = monitorPrivateLocations?.some((loc) => loc.id === privateLocation.id);
|
||||
const currId = getPolicyId(config, privateLocation);
|
||||
const hasPolicy = await this.getMonitor(currId);
|
||||
|
||||
if (hasLocation) {
|
||||
const newPolicy = await this.generateNewPolicy(config, privateLocation);
|
||||
|
||||
if (hasPolicy) {
|
||||
await this.updatePolicy(newPolicy, currId);
|
||||
} else {
|
||||
await this.createPolicy(newPolicy, currId);
|
||||
}
|
||||
} else if (hasPolicy) {
|
||||
const soClient = this.server.authSavedObjectsClient!;
|
||||
const esClient = this.server.uptimeEsClient.baseESClient;
|
||||
await this.server.fleet.packagePolicyService.delete(soClient, esClient, [currId], {
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createPolicy(newPolicy: NewPackagePolicy, id: string) {
|
||||
const soClient = this.server.authSavedObjectsClient;
|
||||
const esClient = this.server.uptimeEsClient.baseESClient;
|
||||
if (soClient && esClient) {
|
||||
return await this.server.fleet.packagePolicyService.create(soClient, esClient, newPolicy, {
|
||||
id,
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updatePolicy(updatedPolicy: NewPackagePolicy, id: string) {
|
||||
const soClient = this.server.authSavedObjectsClient;
|
||||
const esClient = this.server.uptimeEsClient.baseESClient;
|
||||
if (soClient && esClient) {
|
||||
return await this.server.fleet.packagePolicyService.update(
|
||||
soClient,
|
||||
esClient,
|
||||
id,
|
||||
updatedPolicy,
|
||||
{
|
||||
force: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMonitor(id: string) {
|
||||
try {
|
||||
const soClient = this.server.authSavedObjectsClient;
|
||||
return await this.server.fleet.packagePolicyService.get(soClient!, id);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async findMonitor(config: SyntheticsMonitorWithId) {
|
||||
const soClient = this.server.authSavedObjectsClient;
|
||||
const list = await this.server.fleet.packagePolicyService.list(soClient!, {
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:synthetics`,
|
||||
});
|
||||
|
||||
const { locations } = config;
|
||||
|
||||
const fleetManagedLocationIds = locations
|
||||
.filter((loc) => !loc.isServiceManaged)
|
||||
.map((loc) => config.id + '-' + loc.id);
|
||||
|
||||
return list.items.filter((policy) => {
|
||||
return fleetManagedLocationIds.includes(policy.name);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMonitor(config: SyntheticsMonitorWithId) {
|
||||
const soClient = this.server.authSavedObjectsClient;
|
||||
const esClient = this.server.uptimeEsClient.baseESClient;
|
||||
if (soClient && esClient) {
|
||||
const { locations } = config;
|
||||
|
||||
const allPrivateLocations = await getSyntheticsPrivateLocations(soClient);
|
||||
|
||||
const monitorPrivateLocations = locations.filter((loc) => !loc.isServiceManaged);
|
||||
|
||||
for (const privateLocation of monitorPrivateLocations) {
|
||||
const location = allPrivateLocations?.find((loc) => loc.id === privateLocation.id);
|
||||
if (location) {
|
||||
await this.server.fleet.packagePolicyService.delete(
|
||||
soClient,
|
||||
esClient,
|
||||
[getPolicyId(config, location)],
|
||||
{
|
||||
force: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAgentPolicies() {
|
||||
const agentPolicies = await this.server.fleet.agentPolicyService.list(
|
||||
this.server.savedObjectsClient!,
|
||||
{
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
}
|
||||
);
|
||||
|
||||
return agentPolicies.items;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 const testMonitorPolicy = {
|
||||
name: 'synthetics-1',
|
||||
namespace: '',
|
||||
package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.9.10' },
|
||||
enabled: true,
|
||||
policy_id: '',
|
||||
output_id: 'fleet-default-output',
|
||||
inputs: [
|
||||
{
|
||||
type: 'synthetics/http',
|
||||
policy_template: 'synthetics',
|
||||
enabled: false,
|
||||
streams: [
|
||||
{
|
||||
enabled: false,
|
||||
data_stream: { type: 'synthetics', dataset: 'http' },
|
||||
vars: {
|
||||
__ui: { type: 'yaml' },
|
||||
enabled: { value: true, type: 'bool' },
|
||||
type: { value: 'http', type: 'text' },
|
||||
name: { type: 'text' },
|
||||
schedule: { value: '"@every 3m"', type: 'text' },
|
||||
urls: { type: 'text' },
|
||||
'service.name': { type: 'text' },
|
||||
timeout: { type: 'text' },
|
||||
max_redirects: { type: 'integer' },
|
||||
proxy_url: { type: 'text' },
|
||||
tags: { type: 'yaml' },
|
||||
username: { type: 'text' },
|
||||
password: { type: 'password' },
|
||||
'response.include_headers': { type: 'bool' },
|
||||
'response.include_body': { type: 'text' },
|
||||
'check.request.method': { type: 'text' },
|
||||
'check.request.headers': { type: 'yaml' },
|
||||
'check.request.body': { type: 'yaml' },
|
||||
'check.response.status': { type: 'yaml' },
|
||||
'check.response.headers': { type: 'yaml' },
|
||||
'check.response.body.positive': { type: 'yaml' },
|
||||
'check.response.body.negative': { type: 'yaml' },
|
||||
'ssl.certificate_authorities': { type: 'yaml' },
|
||||
'ssl.certificate': { type: 'yaml' },
|
||||
'ssl.key': { type: 'yaml' },
|
||||
'ssl.key_passphrase': { type: 'text' },
|
||||
'ssl.verification_mode': { type: 'text' },
|
||||
'ssl.supported_protocols': { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'synthetics/tcp',
|
||||
policy_template: 'synthetics',
|
||||
enabled: false,
|
||||
streams: [
|
||||
{
|
||||
enabled: false,
|
||||
data_stream: { type: 'synthetics', dataset: 'tcp' },
|
||||
vars: {
|
||||
__ui: { type: 'yaml' },
|
||||
enabled: { value: true, type: 'bool' },
|
||||
type: { value: 'tcp', type: 'text' },
|
||||
name: { type: 'text' },
|
||||
schedule: { value: '"@every 3m"', type: 'text' },
|
||||
hosts: { type: 'text' },
|
||||
'service.name': { type: 'text' },
|
||||
timeout: { type: 'text' },
|
||||
proxy_url: { type: 'text' },
|
||||
proxy_use_local_resolver: { value: false, type: 'bool' },
|
||||
tags: { type: 'yaml' },
|
||||
'check.send': { type: 'text' },
|
||||
'check.receive': { type: 'text' },
|
||||
'ssl.certificate_authorities': { type: 'yaml' },
|
||||
'ssl.certificate': { type: 'yaml' },
|
||||
'ssl.key': { type: 'yaml' },
|
||||
'ssl.key_passphrase': { type: 'text' },
|
||||
'ssl.verification_mode': { type: 'text' },
|
||||
'ssl.supported_protocols': { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'synthetics/icmp',
|
||||
policy_template: 'synthetics',
|
||||
enabled: false,
|
||||
streams: [
|
||||
{
|
||||
enabled: false,
|
||||
data_stream: { type: 'synthetics', dataset: 'icmp' },
|
||||
vars: {
|
||||
__ui: { type: 'yaml' },
|
||||
enabled: { value: true, type: 'bool' },
|
||||
type: { value: 'icmp', type: 'text' },
|
||||
name: { type: 'text' },
|
||||
schedule: { value: '"@every 3m"', type: 'text' },
|
||||
wait: { value: '1s', type: 'text' },
|
||||
hosts: { type: 'text' },
|
||||
'service.name': { type: 'text' },
|
||||
timeout: { type: 'text' },
|
||||
tags: { type: 'yaml' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'synthetics/browser',
|
||||
policy_template: 'synthetics',
|
||||
enabled: true,
|
||||
streams: [
|
||||
{ enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.network' } },
|
||||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'synthetics', dataset: 'browser' },
|
||||
vars: {
|
||||
__ui: { type: 'yaml' },
|
||||
enabled: { value: true, type: 'bool' },
|
||||
type: { value: 'browser', type: 'text' },
|
||||
name: { type: 'text' },
|
||||
schedule: { value: '"@every 3m"', type: 'text' },
|
||||
'service.name': { type: 'text' },
|
||||
timeout: { type: 'text' },
|
||||
tags: { type: 'yaml' },
|
||||
'source.zip_url.url': { type: 'text' },
|
||||
'source.zip_url.username': { type: 'text' },
|
||||
'source.zip_url.folder': { type: 'text' },
|
||||
'source.zip_url.password': { type: 'password' },
|
||||
'source.inline.script': { type: 'yaml' },
|
||||
params: { type: 'yaml' },
|
||||
screenshots: { type: 'text' },
|
||||
synthetics_args: { type: 'text' },
|
||||
ignore_https_errors: { type: 'bool' },
|
||||
'throttling.config': { type: 'text' },
|
||||
'filter_journeys.tags': { type: 'yaml' },
|
||||
'filter_journeys.match': { type: 'text' },
|
||||
'source.zip_url.ssl.certificate_authorities': { type: 'yaml' },
|
||||
'source.zip_url.ssl.certificate': { type: 'yaml' },
|
||||
'source.zip_url.ssl.key': { type: 'yaml' },
|
||||
'source.zip_url.ssl.key_passphrase': { type: 'text' },
|
||||
'source.zip_url.ssl.verification_mode': { type: 'text' },
|
||||
'source.zip_url.ssl.supported_protocols': { type: 'yaml' },
|
||||
'source.zip_url.proxy_url': { type: 'text' },
|
||||
location_name: { value: 'Fleet managed', type: 'text' },
|
||||
config_id: { type: 'text' },
|
||||
run_once: { value: false, type: 'bool' },
|
||||
},
|
||||
},
|
||||
{ enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.screenshot' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -11,6 +11,7 @@ import {
|
|||
SavedObjectsFindResult,
|
||||
} from '@kbn/core/server';
|
||||
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import { SyntheticsMonitorClient } from './synthetics_monitor/synthetics_monitor_client';
|
||||
import {
|
||||
BrowserFields,
|
||||
ConfigKey,
|
||||
|
@ -58,6 +59,7 @@ export class ProjectMonitorFormatter {
|
|||
public failedStaleMonitors: FailedMonitors = [];
|
||||
private server: UptimeServerSetup;
|
||||
private projectFilter: string;
|
||||
private syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
|
||||
constructor({
|
||||
locations,
|
||||
|
@ -68,6 +70,7 @@ export class ProjectMonitorFormatter {
|
|||
spaceId,
|
||||
monitors,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
}: {
|
||||
locations: Locations;
|
||||
keepStale: boolean;
|
||||
|
@ -77,6 +80,7 @@ export class ProjectMonitorFormatter {
|
|||
spaceId: string;
|
||||
monitors: ProjectBrowserMonitor[];
|
||||
server: UptimeServerSetup;
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
}) {
|
||||
this.projectId = projectId;
|
||||
this.spaceId = spaceId;
|
||||
|
@ -84,6 +88,7 @@ export class ProjectMonitorFormatter {
|
|||
this.keepStale = keepStale;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
|
||||
this.syntheticsMonitorClient = syntheticsMonitorClient;
|
||||
this.monitors = monitors;
|
||||
this.server = server;
|
||||
this.projectFilter = `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${this.projectId}"`;
|
||||
|
@ -148,6 +153,7 @@ export class ProjectMonitorFormatter {
|
|||
server: this.server,
|
||||
monitor: normalizedMonitor,
|
||||
monitorSavedObject: newMonitor,
|
||||
syntheticsMonitorClient: this.syntheticsMonitorClient,
|
||||
});
|
||||
this.createdMonitors.push(monitor.id);
|
||||
}
|
||||
|
@ -249,6 +255,7 @@ export class ProjectMonitorFormatter {
|
|||
editedMonitorSavedObject: editedMonitor,
|
||||
previousMonitor,
|
||||
server: this.server,
|
||||
syntheticsMonitorClient: this.syntheticsMonitorClient,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -290,6 +297,7 @@ export class ProjectMonitorFormatter {
|
|||
savedObjectsClient: this.savedObjectsClient,
|
||||
server: this.server,
|
||||
monitorId,
|
||||
syntheticsMonitorClient: this.syntheticsMonitorClient,
|
||||
});
|
||||
this.deletedMonitors.push(journeyId);
|
||||
} catch (e) {
|
||||
|
|
|
@ -34,13 +34,13 @@ export class ServiceAPIClient {
|
|||
private readonly authorization: string;
|
||||
public locations: ServiceLocations;
|
||||
private logger: Logger;
|
||||
private readonly config: ServiceConfig;
|
||||
private readonly config?: ServiceConfig;
|
||||
private readonly kibanaVersion: string;
|
||||
private readonly server: UptimeServerSetup;
|
||||
|
||||
constructor(logger: Logger, config: ServiceConfig, server: UptimeServerSetup) {
|
||||
this.config = config;
|
||||
const { username, password } = config;
|
||||
const { username, password } = config ?? {};
|
||||
this.username = username;
|
||||
this.kibanaVersion = server.kibanaVersion;
|
||||
|
||||
|
@ -61,7 +61,7 @@ export class ServiceAPIClient {
|
|||
const rejectUnauthorized = parsedTargetUrl.hostname !== 'localhost' || !this.server.isDev;
|
||||
const baseHttpsAgent = new https.Agent({ rejectUnauthorized });
|
||||
|
||||
const config = this.config;
|
||||
const config = this.config ?? {};
|
||||
|
||||
// If using basic-auth, ignore certificate configs
|
||||
if (this.authorization) return baseHttpsAgent;
|
||||
|
@ -171,9 +171,8 @@ export class ServiceAPIClient {
|
|||
const promises: Array<Observable<unknown>> = [];
|
||||
|
||||
this.locations.forEach(({ id, url }) => {
|
||||
const locMonitors = allMonitors.filter(
|
||||
({ locations }) =>
|
||||
!locations || locations.length === 0 || locations?.find((loc) => loc.id === id)
|
||||
const locMonitors = allMonitors.filter(({ locations }) =>
|
||||
locations?.find((loc) => loc.id === id && loc.isServiceManaged)
|
||||
);
|
||||
if (locMonitors.length > 0) {
|
||||
promises.push(
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { SyntheticsMonitorClient } from './synthetics_monitor_client';
|
||||
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
|
||||
import { SyntheticsService } from '../synthetics_service';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import times from 'lodash/times';
|
||||
import {
|
||||
LocationStatus,
|
||||
MonitorFields,
|
||||
SyntheticsMonitorWithId,
|
||||
} from '../../../common/runtime_types';
|
||||
|
||||
describe('SyntheticsMonitorClient', () => {
|
||||
const mockEsClient = {
|
||||
search: jest.fn(),
|
||||
};
|
||||
|
||||
const logger = loggerMock.create();
|
||||
|
||||
const serverMock: UptimeServerSetup = {
|
||||
logger,
|
||||
uptimeEsClient: mockEsClient,
|
||||
authSavedObjectsClient: {
|
||||
bulkUpdate: jest.fn(),
|
||||
get: jest.fn(),
|
||||
},
|
||||
config: {
|
||||
service: {
|
||||
username: 'dev',
|
||||
password: '12345',
|
||||
manifestUrl: 'http://localhost:8080/api/manifest',
|
||||
},
|
||||
},
|
||||
} as unknown as UptimeServerSetup;
|
||||
|
||||
const syntheticsService = new SyntheticsService(serverMock);
|
||||
|
||||
syntheticsService.addConfig = jest.fn();
|
||||
syntheticsService.editConfig = jest.fn();
|
||||
syntheticsService.deleteConfigs = jest.fn();
|
||||
|
||||
const locations = times(3).map((n) => {
|
||||
return {
|
||||
id: `loc-${n}`,
|
||||
label: `Location ${n}`,
|
||||
url: `https://example.com/${n}`,
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.GA,
|
||||
};
|
||||
});
|
||||
|
||||
const monitor = {
|
||||
type: 'http',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
number: '3',
|
||||
unit: 'm',
|
||||
},
|
||||
name: 'my mon',
|
||||
locations,
|
||||
urls: 'http://google.com',
|
||||
max_redirects: '0',
|
||||
password: '',
|
||||
proxy_url: '',
|
||||
id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d',
|
||||
fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' },
|
||||
fields_under_root: true,
|
||||
} as unknown as MonitorFields;
|
||||
|
||||
it('should add a monitor', async () => {
|
||||
locations[1].isServiceManaged = false;
|
||||
|
||||
const id = 'test-id-1';
|
||||
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
client.privateLocationAPI.createMonitor = jest.fn();
|
||||
|
||||
await client.addMonitor(monitor, id);
|
||||
|
||||
expect(syntheticsService.addConfig).toHaveBeenCalledTimes(1);
|
||||
expect(client.privateLocationAPI.createMonitor).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should edit a monitor', async () => {
|
||||
locations[1].isServiceManaged = false;
|
||||
|
||||
const id = 'test-id-1';
|
||||
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
client.privateLocationAPI.editMonitor = jest.fn();
|
||||
|
||||
await client.editMonitor(monitor, id);
|
||||
|
||||
expect(syntheticsService.editConfig).toHaveBeenCalledTimes(1);
|
||||
expect(client.privateLocationAPI.editMonitor).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should delete a monitor', async () => {
|
||||
locations[1].isServiceManaged = false;
|
||||
|
||||
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
client.privateLocationAPI.deleteMonitor = jest.fn();
|
||||
|
||||
await client.deleteMonitor(monitor as unknown as SyntheticsMonitorWithId);
|
||||
|
||||
expect(syntheticsService.deleteConfigs).toHaveBeenCalledTimes(1);
|
||||
expect(client.privateLocationAPI.deleteMonitor).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
|
||||
import { SyntheticsPrivateLocation } from '../private_location/synthetics_private_location';
|
||||
import { SyntheticsService } from '../synthetics_service';
|
||||
import { formatHeartbeatRequest, SyntheticsConfig } from '../formatters/format_configs';
|
||||
import { ConfigKey, MonitorFields, SyntheticsMonitorWithId } from '../../../common/runtime_types';
|
||||
|
||||
export class SyntheticsMonitorClient {
|
||||
public syntheticsService: SyntheticsService;
|
||||
|
||||
public privateLocationAPI: SyntheticsPrivateLocation;
|
||||
|
||||
constructor(syntheticsService: SyntheticsService, server: UptimeServerSetup) {
|
||||
this.syntheticsService = syntheticsService;
|
||||
this.privateLocationAPI = new SyntheticsPrivateLocation(server);
|
||||
}
|
||||
|
||||
async addMonitor(monitor: MonitorFields, id: string) {
|
||||
await this.syntheticsService.setupIndexTemplates();
|
||||
|
||||
const config = formatHeartbeatRequest({
|
||||
monitor,
|
||||
monitorId: id,
|
||||
customHeartbeatId: monitor[ConfigKey.CUSTOM_HEARTBEAT_ID],
|
||||
});
|
||||
|
||||
const { privateLocations, publicLocations } = this.parseLocations(config);
|
||||
|
||||
if (privateLocations.length > 0) {
|
||||
await this.privateLocationAPI.createMonitor(config);
|
||||
}
|
||||
|
||||
if (publicLocations.length > 0) {
|
||||
return await this.syntheticsService.addConfig(config);
|
||||
}
|
||||
}
|
||||
async editMonitor(editedMonitor: MonitorFields, id: string) {
|
||||
const editedConfig = formatHeartbeatRequest({
|
||||
monitor: editedMonitor,
|
||||
monitorId: id,
|
||||
customHeartbeatId: (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID],
|
||||
});
|
||||
|
||||
const { privateLocations, publicLocations } = this.parseLocations(editedConfig);
|
||||
|
||||
if (privateLocations.length > 0) {
|
||||
await this.privateLocationAPI.editMonitor(editedConfig);
|
||||
}
|
||||
|
||||
if (publicLocations.length > 0) {
|
||||
return await this.syntheticsService.editConfig(editedConfig);
|
||||
}
|
||||
|
||||
await this.syntheticsService.editConfig(editedConfig);
|
||||
}
|
||||
async deleteMonitor(monitor: SyntheticsMonitorWithId) {
|
||||
await this.privateLocationAPI.deleteMonitor(monitor);
|
||||
return await this.syntheticsService.deleteConfigs([monitor]);
|
||||
}
|
||||
|
||||
parseLocations(config: SyntheticsConfig) {
|
||||
const { locations } = config;
|
||||
|
||||
const privateLocations = locations.filter((loc) => !loc.isServiceManaged);
|
||||
const publicLocations = locations.filter((loc) => loc.isServiceManaged);
|
||||
|
||||
return { privateLocations, publicLocations };
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue