[Synthetics] Multi space monitors !! (#221568)

## Summary

Multi space monitors !!

Fixes https://github.com/elastic/kibana/issues/164294

User will be able to choose in which space monitors will be available !!

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/f01ac226-ed54-4e96-b6f4-27f0134a9be5"
/>


### Technical 
This is being done by registering another saved object type and for
existing monitors it will continue to work as right now but for newly
created monitors user will have ability to specify spaces or choose
multiple spaces or all.

### Testing

1. Create few monitors before this PR in multiple spaces
2. Create multiple monitors in multiple spaces after this PR
3. Make sure filtering, editing and deleting, creating works as expected
on both set of monitors

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2025-06-25 10:47:47 +02:00 committed by GitHub
parent b2d91b43f3
commit f317cec25b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 3470 additions and 1275 deletions

View file

@ -1159,6 +1159,34 @@
"type", "type",
"urls" "urls"
], ],
"synthetics-monitor-multi-space": [
"alert",
"alert.status",
"alert.status.enabled",
"alert.tls",
"alert.tls.enabled",
"config_id",
"custom_heartbeat_id",
"enabled",
"hash",
"hosts",
"id",
"journey_id",
"locations",
"locations.id",
"locations.label",
"maintenance_windows",
"name",
"origin",
"project_id",
"schedule",
"schedule.number",
"tags",
"throttling",
"throttling.label",
"type",
"urls"
],
"synthetics-param": [], "synthetics-param": [],
"synthetics-private-location": [], "synthetics-private-location": [],
"synthetics-privates-locations": [], "synthetics-privates-locations": [],

View file

@ -3832,6 +3832,136 @@
} }
} }
}, },
"synthetics-monitor-multi-space": {
"dynamic": false,
"properties": {
"alert": {
"properties": {
"status": {
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"tls": {
"properties": {
"enabled": {
"type": "boolean"
}
}
}
}
},
"config_id": {
"type": "keyword"
},
"custom_heartbeat_id": {
"type": "keyword"
},
"enabled": {
"type": "boolean"
},
"hash": {
"type": "keyword"
},
"hosts": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"id": {
"type": "keyword"
},
"journey_id": {
"type": "keyword"
},
"locations": {
"properties": {
"id": {
"fields": {
"text": {
"type": "text"
}
},
"ignore_above": 256,
"type": "keyword"
},
"label": {
"type": "text"
}
}
},
"maintenance_windows": {
"type": "keyword"
},
"name": {
"fields": {
"keyword": {
"ignore_above": 256,
"normalizer": "lowercase",
"type": "keyword"
}
},
"type": "text"
},
"origin": {
"type": "keyword"
},
"project_id": {
"fields": {
"text": {
"type": "text"
}
},
"type": "keyword"
},
"schedule": {
"properties": {
"number": {
"type": "integer"
}
}
},
"tags": {
"fields": {
"text": {
"type": "text"
}
},
"type": "keyword"
},
"throttling": {
"properties": {
"label": {
"type": "keyword"
}
}
},
"type": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"urls": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
}
}
},
"synthetics-param": { "synthetics-param": {
"dynamic": false, "dynamic": false,
"properties": {} "properties": {}

View file

@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration';
// set minimum number of registered saved objects to ensure no object types are removed after 8.8 // set minimum number of registered saved objects to ensure no object types are removed after 8.8
// declared in internal implementation explicitly to prevent unintended changes. // declared in internal implementation explicitly to prevent unintended changes.
export const SAVED_OBJECT_TYPES_COUNT = 135 as const; export const SAVED_OBJECT_TYPES_COUNT = 136 as const;

View file

@ -177,6 +177,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad", "spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad",
"synthetics-dynamic-settings": "7804b079cc502f16526f7c9491d1397cc1ec67db", "synthetics-dynamic-settings": "7804b079cc502f16526f7c9491d1397cc1ec67db",
"synthetics-monitor": "fdebfa2449d2b934972d1743dc78c34ae9ebc9c1", "synthetics-monitor": "fdebfa2449d2b934972d1743dc78c34ae9ebc9c1",
"synthetics-monitor-multi-space": "c8c9dab447ba8a7383041f55ba80757365d114c5",
"synthetics-param": "9776c9b571d35f0d0397e8915e035ea1dc026db7", "synthetics-param": "9776c9b571d35f0d0397e8915e035ea1dc026db7",
"synthetics-private-location": "27aaa44f792f70b734905e44e3e9b56bbeac7b86", "synthetics-private-location": "27aaa44f792f70b734905e44e3e9b56bbeac7b86",
"synthetics-privates-locations": "36036b881524108c7327fe14bd224c6e4d972cb5", "synthetics-privates-locations": "36036b881524108c7327fe14bd224c6e4d972cb5",

View file

@ -149,6 +149,7 @@ const previouslyRegisteredTypes = [
'space', 'space',
'spaces-usage-stats', 'spaces-usage-stats',
'synthetics-monitor', 'synthetics-monitor',
'synthetics-monitor-multi-space',
'synthetics-param', 'synthetics-param',
'synthetics-privates-locations', 'synthetics-privates-locations',
'synthetics-private-location', 'synthetics-private-location',

View file

@ -107,6 +107,7 @@ const STANDARD_LIST_TYPES = [
'cases-connector-mappings', 'cases-connector-mappings',
// synthetics based objects // synthetics based objects
'synthetics-monitor', 'synthetics-monitor',
'synthetics-monitor-multi-space',
'uptime-dynamic-settings', 'uptime-dynamic-settings',
'synthetics-privates-locations', 'synthetics-privates-locations',
'synthetics-private-location', 'synthetics-private-location',

View file

@ -45848,7 +45848,6 @@
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "Aperçu des moniteurs", "xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "Aperçu des moniteurs",
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "Statistiques des moniteurs", "xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "Statistiques des moniteurs",
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - Moniteur",
"xpack.synthetics.tableTitle.showing": "Affichage de {count} sur {total} {label}", "xpack.synthetics.tableTitle.showing": "Affichage de {count} sur {total} {label}",
"xpack.synthetics.tagsSelectPlaceholder": "Sélectionner des balises", "xpack.synthetics.tagsSelectPlaceholder": "Sélectionner des balises",
"xpack.synthetics.testDetails.after": "Après", "xpack.synthetics.testDetails.after": "Après",

View file

@ -45899,7 +45899,6 @@
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "モニター概要", "xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "モニター概要",
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "モニター統計", "xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "モニター統計",
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - 監視",
"xpack.synthetics.tableTitle.showing": "{count}/{total} {label}を表示中", "xpack.synthetics.tableTitle.showing": "{count}/{total} {label}を表示中",
"xpack.synthetics.tagsSelectPlaceholder": "タグを選択", "xpack.synthetics.tagsSelectPlaceholder": "タグを選択",
"xpack.synthetics.testDetails.after": "後", "xpack.synthetics.testDetails.after": "後",

View file

@ -45875,7 +45875,6 @@
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "监测概览", "xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "监测概览",
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "监测统计信息", "xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "监测统计信息",
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - 监测",
"xpack.synthetics.tableTitle.showing": "正在显示 {count} 个(共 {total} 个){label}", "xpack.synthetics.tableTitle.showing": "正在显示 {count} 个(共 {total} 个){label}",
"xpack.synthetics.tagsSelectPlaceholder": "选择标签", "xpack.synthetics.tagsSelectPlaceholder": "选择标签",
"xpack.synthetics.testDetails.after": "之后", "xpack.synthetics.testDetails.after": "之后",

View file

@ -52,7 +52,7 @@ const TagsList = ({
const tagsToDisplay = tags.slice(0, toDisplay); const tagsToDisplay = tags.slice(0, toDisplay);
return ( return (
<EuiFlexGroup wrap gutterSize="m" css={{ maxWidth: 400 }} alignItems="baseline"> <EuiFlexGroup wrap gutterSize="xs" css={{ maxWidth: 400 }} alignItems="baseline">
{tagsToDisplay.map((tag) => ( {tagsToDisplay.map((tag) => (
// filtering only makes sense in monitor list, where we have summary // filtering only makes sense in monitor list, where we have summary
<EuiFlexItem key={tag} grow={false}> <EuiFlexItem key={tag} grow={false}>

View file

@ -155,6 +155,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
[ConfigKey.LABELS]: {}, [ConfigKey.LABELS]: {},
[ConfigKey.MAX_ATTEMPTS]: 2, [ConfigKey.MAX_ATTEMPTS]: 2,
[ConfigKey.MAINTENANCE_WINDOWS]: [], [ConfigKey.MAINTENANCE_WINDOWS]: [],
[ConfigKey.KIBANA_SPACES]: [],
revision: 1, revision: 1,
}; };

View file

@ -79,6 +79,7 @@ export enum ConfigKey {
MONITOR_QUERY_ID = 'id', MONITOR_QUERY_ID = 'id',
MAX_ATTEMPTS = 'max_attempts', MAX_ATTEMPTS = 'max_attempts',
MAINTENANCE_WINDOWS = 'maintenance_windows', MAINTENANCE_WINDOWS = 'maintenance_windows',
KIBANA_SPACES = 'spaces',
} }
export const secretKeys = [ export const secretKeys = [

View file

@ -88,6 +88,7 @@ export const CommonFieldsCodec = t.intersection([
[ConfigKey.PARAMS]: t.string, [ConfigKey.PARAMS]: t.string,
[ConfigKey.LABELS]: t.record(t.string, t.string), [ConfigKey.LABELS]: t.record(t.string, t.string),
[ConfigKey.MAINTENANCE_WINDOWS]: t.array(t.string), [ConfigKey.MAINTENANCE_WINDOWS]: t.array(t.string),
[ConfigKey.KIBANA_SPACES]: t.array(t.string),
retest_on_failure: t.boolean, retest_on_failure: t.boolean,
}), }),
]); ]);
@ -357,7 +358,7 @@ const HeartbeatFieldsCodec = t.intersection([
'monitor.id': t.string, 'monitor.id': t.string,
'monitor.project.id': t.string, 'monitor.project.id': t.string,
'monitor.fleet_managed': t.boolean, 'monitor.fleet_managed': t.boolean,
meta: t.record(t.string, t.string), meta: t.record(t.string, t.union([t.string, t.array(t.string)])),
}), }),
]); ]);

View file

@ -52,7 +52,7 @@ export const OverviewStatusMetaDataCodec = t.intersection([
projectId: t.string, projectId: t.string,
updated_at: t.string, updated_at: t.string,
timestamp: t.string, timestamp: t.string,
spaceId: t.string, spaces: t.array(t.string),
urls: t.string, urls: t.string,
maintenanceWindows: t.array(t.string), maintenanceWindows: t.array(t.string),
}), }),

View file

@ -5,7 +5,15 @@
* 2.0. * 2.0.
*/ */
export const syntheticsMonitorType = 'synthetics-monitor'; export const legacySyntheticsMonitorTypeSingle = 'synthetics-monitor';
export const monitorAttributes = `${syntheticsMonitorType}.attributes`; export const legacyMonitorAttributes = `${legacySyntheticsMonitorTypeSingle}.attributes`;
export const syntheticsMonitorSavedObjectType = 'synthetics-monitor-multi-space';
export const syntheticsMonitorAttributes = `${syntheticsMonitorSavedObjectType}.attributes`;
export const syntheticsParamType = 'synthetics-param'; export const syntheticsParamType = 'synthetics-param';
export const syntheticsMonitorSOTypes = [
syntheticsMonitorSavedObjectType,
legacySyntheticsMonitorTypeSingle,
];

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment, { Moment } from 'moment';
export const SHORT_TS_LOCALE = 'en-short-locale';
export const SHORT_TIMESPAN_LOCALE = {
relativeTime: {
future: 'in %s',
past: '%s ago',
s: '%ds',
ss: '%ss',
m: '%dm',
mm: '%dm',
h: '%dh',
hh: '%dh',
d: '%dd',
dd: '%dd',
M: '%d Mon',
MM: '%d Mon',
y: '%d Yr',
yy: '%d Yr',
},
};
export const parseTimestamp = (tsValue: string): Moment => {
let parsed = Date.parse(tsValue);
if (isNaN(parsed)) {
parsed = parseInt(tsValue, 10);
}
return moment(parsed);
};
export const getShortTimeStamp = (timeStamp: moment.Moment, relative = false) => {
if (relative) {
const prevLocale: string = moment.locale() ?? 'en';
const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE;
if (!shortLocale) {
moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE);
}
let shortTimestamp;
if (typeof timeStamp === 'string') {
shortTimestamp = parseTimestamp(timeStamp).fromNow();
} else {
shortTimestamp = timeStamp.fromNow();
}
// Reset it so, it doesn't impact other part of the app
moment.locale(prevLocale);
return shortTimestamp;
} else {
if (moment().diff(timeStamp, 'd') >= 1) {
return timeStamp.format('ll LTS');
}
return timeStamp.format('LTS');
}
};

View file

@ -62,14 +62,14 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
// hash is always reset to empty string when monitor is edited // hash is always reset to empty string when monitor is edited
// this ensures that when the monitor is pushed again, the monitor // this ensures that when the monitor is pushed again, the monitor
// config in the process takes precedence // config in the process takes precedence
expect(omit(newConfiguration, ['updated_at'])).toEqual( expect(omit(newConfiguration, ['updated_at', 'created_at'])).toEqual(
omit( omit(
{ {
...originalMonitorConfiguration, ...originalMonitorConfiguration,
hash: '', hash: '',
revision: 2, revision: 2,
}, },
['updated_at'] ['updated_at', 'created_at']
) )
); );
}); });
@ -88,7 +88,7 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
// hash is always reset to empty string when monitor is edited // hash is always reset to empty string when monitor is edited
// this ensures that when the monitor is pushed again, the monitor // this ensures that when the monitor is pushed again, the monitor
// config in the process takes precedence // config in the process takes precedence
expect(omit(newConfiguration, ['updated_at'])).toEqual( expect(omit(newConfiguration, ['updated_at', 'created_at'])).toEqual(
omit( omit(
{ {
...originalMonitorConfiguration, ...originalMonitorConfiguration,
@ -104,7 +104,7 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
}, },
enabled: !originalMonitorConfiguration?.enabled, enabled: !originalMonitorConfiguration?.enabled,
}, },
['updated_at'] ['updated_at', 'created_at']
) )
); );
}); });
@ -112,13 +112,13 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
step('Monitor can be re-pushed and overwrite any changes', async () => { step('Monitor can be re-pushed and overwrite any changes', async () => {
await addTestMonitorProject(params.kibanaUrl, monitorName); await addTestMonitorProject(params.kibanaUrl, monitorName);
const repushedConfiguration = await services.getMonitor(monitorId); const repushedConfiguration = await services.getMonitor(monitorId);
expect(omit(repushedConfiguration, ['updated_at'])).toEqual( expect(omit(repushedConfiguration, ['updated_at', 'created_at'])).toEqual(
omit( omit(
{ {
...originalMonitorConfiguration, ...originalMonitorConfiguration,
revision: 4, revision: 4,
}, },
['updated_at'] ['updated_at', 'created_at']
) )
); );
}); });

View file

@ -48,7 +48,9 @@ export const cleanTestMonitors = async (params: Record<string, any>) => {
const server = getService('kibanaServer'); const server = getService('kibanaServer');
try { try {
await server.savedObjects.clean({ types: ['synthetics-monitor'] }); await server.savedObjects.clean({
types: ['synthetics-monitor', 'synthetics-monitor-multi-space'],
});
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(e); console.log(e);

View file

@ -202,7 +202,9 @@ export class SyntheticsServices {
const getService = this.params.getService; const getService = this.params.getService;
const server = getService('kibanaServer'); const server = getService('kibanaServer');
await server.savedObjects.clean({ types: ['synthetics-monitor', 'alert'] }); await server.savedObjects.clean({
types: ['synthetics-monitor', 'synthetics-monitor-multi-space', 'alert'],
});
await this.cleanUpAlerts(); await this.cleanUpAlerts();
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View file

@ -0,0 +1,56 @@
/*
* 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 { getUpdatedSpacesSelection } from './monitor_spaces';
const ALL_SPACES_ID = 'all-spaces-id';
const CURRENT_SPACE_ID = 'current-space-id';
describe('getUpdatedSpacesSelection', () => {
it('returns only allSpacesId if allSpacesId is selected', () => {
expect(
getUpdatedSpacesSelection(['foo', ALL_SPACES_ID, 'bar'], CURRENT_SPACE_ID, ALL_SPACES_ID)
).toEqual([ALL_SPACES_ID]);
});
it('returns currentSpaceId if nothing is selected', () => {
expect(getUpdatedSpacesSelection([], CURRENT_SPACE_ID, ALL_SPACES_ID)).toEqual([
CURRENT_SPACE_ID,
]);
});
it('adds currentSpaceId if not present', () => {
expect(getUpdatedSpacesSelection(['foo', 'bar'], CURRENT_SPACE_ID, ALL_SPACES_ID)).toEqual([
'foo',
'bar',
CURRENT_SPACE_ID,
]);
});
it('returns selectedIds if currentSpaceId is already present', () => {
expect(
getUpdatedSpacesSelection(['foo', CURRENT_SPACE_ID, 'bar'], CURRENT_SPACE_ID, ALL_SPACES_ID)
).toEqual(['foo', CURRENT_SPACE_ID, 'bar']);
});
it('returns selectedIds if no currentSpaceId is provided', () => {
expect(getUpdatedSpacesSelection(['foo', 'bar'], undefined, ALL_SPACES_ID)).toEqual([
'foo',
'bar',
]);
});
it('returns only allSpacesId if allSpacesId is the only selection', () => {
expect(getUpdatedSpacesSelection([ALL_SPACES_ID], CURRENT_SPACE_ID, ALL_SPACES_ID)).toEqual([
ALL_SPACES_ID,
]);
});
it('returns empty array if nothing is selected and no currentSpaceId', () => {
expect(getUpdatedSpacesSelection([], undefined, ALL_SPACES_ID)).toEqual([]);
});
});

View file

@ -0,0 +1,135 @@
/*
* 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 { useFormContext } from 'react-hook-form';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useEffect } from 'react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
import { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../../../plugin';
import { ConfigKey } from '../constants';
export interface MonitorSpacesProps {
onChange: (value: string[]) => void;
value?: string[] | null;
readOnly?: boolean;
}
/**
* Returns the updated list of selected space ids based on the selection logic.
* @param selectedIds Array of selected space ids
* @param currentSpaceId The id of the current space
* @param allSpacesId The id representing "All spaces"
*/
export function getUpdatedSpacesSelection(
selectedIds: string[],
currentSpaceId?: string,
allSpacesId?: string
): string[] {
if (allSpacesId && selectedIds.includes(allSpacesId)) {
// Only return allSpacesId if selected, ignore all others including currentSpaceId
return [allSpacesId];
}
// Remove allSpacesId if present (should only be present alone)
const filtered = allSpacesId ? selectedIds.filter((id) => id !== allSpacesId) : selectedIds;
if (filtered.length === 0 && currentSpaceId) {
return [currentSpaceId];
}
if (currentSpaceId && !filtered.includes(currentSpaceId)) {
return [...filtered, currentSpaceId];
}
return filtered;
}
export const MonitorSpaces = ({ value, onChange, ...rest }: MonitorSpacesProps) => {
const { space: currentSpace } = useKibanaSpace();
const { services } = useKibana<ClientPluginsStart>();
const [spacesList, setSpacesList] = React.useState<Array<{ id: string; label: string }>>([]);
const data = services.spaces?.ui.useSpaces();
const {
control,
formState: { isSubmitted },
trigger,
} = useFormContext();
const { isTouched, error } = control.getFieldState(ConfigKey.KIBANA_SPACES);
const showFieldInvalid = (isSubmitted || isTouched) && !!error;
useEffect(() => {
if (data?.spacesDataPromise) {
data.spacesDataPromise.then((spacesData) => {
setSpacesList([
allSpacesOption,
...[...spacesData.spacesMap].map(([spaceId, dataS]) => ({
id: spaceId,
label: dataS.name,
})),
]);
});
}
}, [data]);
// Ensure selected options always include the current space
const selectedIds = React.useMemo(() => {
if (!currentSpace) {
return value ?? [];
}
if (!value || value.length === 0) {
return [currentSpace.id];
}
if (value.includes(ALL_SPACES_ID)) {
// If "All spaces" is selected, return it alone
return [ALL_SPACES_ID];
}
return value.includes(currentSpace.id) ? value : [...value, currentSpace.id];
}, [value, currentSpace]);
// Compute if "All spaces" is selected
const isAllSpacesSelected = selectedIds.includes(ALL_SPACES_ID);
return (
<EuiComboBox<string>
fullWidth
aria-label={SPACES_LABEL}
placeholder={SPACES_LABEL}
isInvalid={showFieldInvalid}
onBlur={async () => {
await trigger();
}}
options={spacesList.map((option) =>
isAllSpacesSelected && option.id !== ALL_SPACES_ID ? { ...option, disabled: true } : option
)}
selectedOptions={spacesList.filter(({ id }) => selectedIds.includes(id))}
isClearable={true}
onChange={(selected) => {
const newSelectedIds = selected.map((option) => option.id!);
const updatedIds = getUpdatedSpacesSelection(
newSelectedIds,
currentSpace?.id,
allSpacesOption.id
);
onChange(updatedIds);
}}
/>
);
};
export const ALL_SPACES_LABEL = i18n.translate('xpack.synthetics.spaceList.allSpacesLabel', {
defaultMessage: `* All spaces`,
});
const allSpacesOption = {
id: ALL_SPACES_ID,
label: ALL_SPACES_LABEL,
};
const SPACES_LABEL = i18n.translate('xpack.synthetics.privateLocation.spacesLabel', {
defaultMessage: 'Spaces ',
});

View file

@ -32,6 +32,7 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { MaintenanceWindowsLink } from '../fields/maintenance_windows/create_maintenance_windows_btn'; import { MaintenanceWindowsLink } from '../fields/maintenance_windows/create_maintenance_windows_btn';
import { MaintenanceWindowsFieldProps } from '../fields/maintenance_windows/maintenance_windows'; import { MaintenanceWindowsFieldProps } from '../fields/maintenance_windows/maintenance_windows';
import { MonitorSpacesProps } from '../fields/monitor_spaces';
import { kibanaService } from '../../../../../utils/kibana_service'; import { kibanaService } from '../../../../../utils/kibana_service';
import { import {
PROFILE_OPTIONS, PROFILE_OPTIONS,
@ -63,6 +64,7 @@ import {
TextArea, TextArea,
ThrottlingWrapper, ThrottlingWrapper,
MaintenanceWindowsFieldWrapper, MaintenanceWindowsFieldWrapper,
KibanaSpacesWrapper,
} from './field_wrappers'; } from './field_wrappers';
import { useMonitorName } from '../../../hooks/use_monitor_name'; import { useMonitorName } from '../../../hooks/use_monitor_name';
import { import {
@ -1701,4 +1703,24 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
}), }),
labelAppend: <MaintenanceWindowsLink />, labelAppend: <MaintenanceWindowsLink />,
}, },
[ConfigKey.KIBANA_SPACES]: {
fieldKey: ConfigKey.KIBANA_SPACES,
component: KibanaSpacesWrapper,
label: i18n.translate('xpack.synthetics.monitorConfig.kibanaSpaces.label', {
defaultMessage: 'Kibana spaces',
}),
helpText: i18n.translate('xpack.synthetics.monitorConfig.kibanaSpaces.helpText', {
defaultMessage:
' Current space should always be part of list, unless All spaces is selected.',
}),
controlled: true,
props: ({ field, setValue, trigger }): MonitorSpacesProps => ({
readOnly,
value: field?.value || [],
onChange: async (spaces?: string[]) => {
setValue(ConfigKey.KIBANA_SPACES, spaces);
await trigger(ConfigKey.KIBANA_SPACES);
},
}),
},
}); });

View file

@ -28,6 +28,7 @@ import {
EuiTextArea, EuiTextArea,
EuiTextAreaProps, EuiTextAreaProps,
} from '@elastic/eui'; } from '@elastic/eui';
import { MonitorSpaces, MonitorSpacesProps } from '../fields/monitor_spaces';
import { import {
MaintenanceWindowsField, MaintenanceWindowsField,
MaintenanceWindowsFieldProps, MaintenanceWindowsFieldProps,
@ -163,3 +164,7 @@ export const MaintenanceWindowsFieldWrapper = React.forwardRef<
unknown, unknown,
MaintenanceWindowsFieldProps MaintenanceWindowsFieldProps
>((props, _ref) => <MaintenanceWindowsField {...props} />); >((props, _ref) => <MaintenanceWindowsField {...props} />);
export const KibanaSpacesWrapper = React.forwardRef<unknown, MonitorSpacesProps>((props, _ref) => (
<MonitorSpaces {...props} />
));

View file

@ -203,6 +203,17 @@ const TLS_OPTIONS = (readOnly: boolean): AdvancedFieldGroup => ({
], ],
}); });
const KIBANA_SPACES_OPTIONS = (readOnly: boolean): AdvancedFieldGroup => ({
title: i18n.translate('xpack.synthetics.monitorConfig.section.kibanaSpaces.title', {
defaultMessage: 'Kibana Spaces',
}),
description: i18n.translate('xpack.synthetics.monitorConfig.kibanaSpaces.description', {
defaultMessage:
'Select the Kibana spaces where this monitor should be available. Current space should always be part of list, unless All spaces is selected.',
}),
components: [FIELD(readOnly)[ConfigKey.KIBANA_SPACES]],
});
export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
[FormMonitorType.HTTP]: { [FormMonitorType.HTTP]: {
step1: [FIELD(readOnly)[ConfigKey.FORM_MONITOR_TYPE]], step1: [FIELD(readOnly)[ConfigKey.FORM_MONITOR_TYPE]],
@ -225,6 +236,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
HTTP_ADVANCED(readOnly).responseConfig, HTTP_ADVANCED(readOnly).responseConfig,
HTTP_ADVANCED(readOnly).responseChecks, HTTP_ADVANCED(readOnly).responseChecks,
TLS_OPTIONS(readOnly), TLS_OPTIONS(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
], ],
}, },
[FormMonitorType.TCP]: { [FormMonitorType.TCP]: {
@ -246,6 +258,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
TCP_ADVANCED(readOnly).requestConfig, TCP_ADVANCED(readOnly).requestConfig,
TCP_ADVANCED(readOnly).responseChecks, TCP_ADVANCED(readOnly).responseChecks,
TLS_OPTIONS(readOnly), TLS_OPTIONS(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
], ],
}, },
[FormMonitorType.MULTISTEP]: { [FormMonitorType.MULTISTEP]: {
@ -273,6 +286,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
}, },
MAINTENANCE_WINDOWS_OPTIONS(readOnly), MAINTENANCE_WINDOWS_OPTIONS(readOnly),
...BROWSER_ADVANCED(readOnly), ...BROWSER_ADVANCED(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
], ],
}, },
[FormMonitorType.SINGLE]: { [FormMonitorType.SINGLE]: {
@ -300,6 +314,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
}, },
MAINTENANCE_WINDOWS_OPTIONS(readOnly), MAINTENANCE_WINDOWS_OPTIONS(readOnly),
...BROWSER_ADVANCED(readOnly), ...BROWSER_ADVANCED(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
], ],
}, },
[FormMonitorType.ICMP]: { [FormMonitorType.ICMP]: {
@ -319,6 +334,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
DEFAULT_DATA_OPTIONS(readOnly), DEFAULT_DATA_OPTIONS(readOnly),
MAINTENANCE_WINDOWS_OPTIONS(readOnly), MAINTENANCE_WINDOWS_OPTIONS(readOnly),
ICMP_ADVANCED(readOnly).requestConfig, ICMP_ADVANCED(readOnly).requestConfig,
KIBANA_SPACES_OPTIONS(readOnly),
], ],
}, },
}); });

View file

@ -5,14 +5,18 @@
* 2.0. * 2.0.
*/ */
import React, { FC, PropsWithChildren } from 'react'; import React, { FC, PropsWithChildren, useMemo } from 'react';
import { EuiForm, EuiSpacer } from '@elastic/eui'; import { EuiForm, EuiSpacer } from '@elastic/eui';
import { FormProvider } from 'react-hook-form'; import { FormProvider } from 'react-hook-form';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped'; import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
import { FormMonitorType, SyntheticsMonitor } from '../types'; import { FormMonitorType, SyntheticsMonitor } from '../types';
import { getDefaultFormFields, formatDefaultFormValues } from './defaults'; import { getDefaultFormFields, formatDefaultFormValues } from './defaults';
import { ActionBar } from './submit'; import { ActionBar } from './submit';
import { Disclaimer } from './disclaimer'; import { Disclaimer } from './disclaimer';
import { ClientPluginsStart } from '../../../../../plugin';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const MonitorForm: FC< export const MonitorForm: FC<
PropsWithChildren<{ PropsWithChildren<{
@ -31,6 +35,14 @@ export const MonitorForm: FC<
shouldFocusError: false, shouldFocusError: false,
}); });
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
/* React hook form doesn't seem to register a field /* React hook form doesn't seem to register a field
* as dirty until validation unless dirtyFields is subscribed to */ * as dirty until validation unless dirtyFields is subscribed to */
const { const {
@ -38,17 +50,19 @@ export const MonitorForm: FC<
} = methods; } = methods;
return ( return (
<FormProvider {...methods}> <ContextWrapper>
<EuiForm <FormProvider {...methods}>
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)} <EuiForm
component="form" isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
noValidate component="form"
> noValidate
{children} >
<EuiSpacer /> {children}
<ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} /> <EuiSpacer />
</EuiForm> <ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} />
<Disclaimer /> </EuiForm>
</FormProvider> <Disclaimer />
</FormProvider>
</ContextWrapper>
); );
}; };

View file

@ -26,6 +26,11 @@ jest.mock('../../hooks/use_monitor_name', () => ({
useMonitorName: jest.fn().mockReturnValue({ nameAlreadyExists: false }), useMonitorName: jest.fn().mockReturnValue({ nameAlreadyExists: false }),
})); }));
jest.mock('../../../../hooks/use_kibana_space', () => ({
...jest.requireActual('../../../../hooks/use_kibana_space'),
useKibanaSpace: jest.fn().mockReturnValue({ id: 'default' }),
}));
describe('MonitorEditPage', () => { describe('MonitorEditPage', () => {
const { FETCH_STATUS } = observabilitySharedPublic; const { FETCH_STATUS } = observabilitySharedPublic;

View file

@ -168,4 +168,5 @@ export interface FieldMap {
[ConfigKey.MAX_ATTEMPTS]: FieldMeta<ConfigKey.MAX_ATTEMPTS>; [ConfigKey.MAX_ATTEMPTS]: FieldMeta<ConfigKey.MAX_ATTEMPTS>;
[ConfigKey.LABELS]: FieldMeta<ConfigKey.LABELS>; [ConfigKey.LABELS]: FieldMeta<ConfigKey.LABELS>;
[ConfigKey.MAINTENANCE_WINDOWS]: FieldMeta<ConfigKey.MAINTENANCE_WINDOWS>; [ConfigKey.MAINTENANCE_WINDOWS]: FieldMeta<ConfigKey.MAINTENANCE_WINDOWS>;
[ConfigKey.KIBANA_SPACES]: FieldMeta<ConfigKey.KIBANA_SPACES>;
} }

View file

@ -11,8 +11,10 @@ import React from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { FETCH_STATUS, TagsList } from '@kbn/observability-shared-plugin/public'; import { FETCH_STATUS, TagsList } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public';
import { isEmpty } from 'lodash';
import { ClientPluginsStart } from '../../../../../../plugin';
import { useKibanaSpace } from '../../../../../../hooks/use_kibana_space'; import { useKibanaSpace } from '../../../../../../hooks/use_kibana_space';
import { useEnablement } from '../../../../hooks'; import { getMonitorSpaceToAppend, useEnablement } from '../../../../hooks';
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities'; import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
import { import {
isStatusEnabled, isStatusEnabled,
@ -49,7 +51,7 @@ export function useMonitorListColumns({
setMonitorPendingDeletion: (configs: string[]) => void; setMonitorPendingDeletion: (configs: string[]) => void;
}): Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> { }): Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> {
const history = useHistory(); const history = useHistory();
const { http } = useKibana().services; const { http, spaces } = useKibana<ClientPluginsStart>().services;
const canEditSynthetics = useCanEditSynthetics(); const canEditSynthetics = useCanEditSynthetics();
const { isServiceAllowed } = useEnablement(); const { isServiceAllowed } = useEnablement();
@ -69,6 +71,7 @@ export function useMonitorListColumns({
return publicLocations ? Boolean(canUsePublicLocations) : true; return publicLocations ? Boolean(canUsePublicLocations) : true;
}; };
const LazySpaceList = spaces?.ui.components.getSpaceList ?? (() => null);
const columns: Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> = [ const columns: Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> = [
{ {
@ -171,6 +174,21 @@ export function useMonitorListColumns({
/> />
), ),
}, },
{
name: i18n.translate('xpack.synthetics.management.monitorList.spacesColumnTitle', {
defaultMessage: 'Spaces',
}),
field: 'spaces',
sortable: false,
render: (monSpaces: string[]) => {
return (
<LazySpaceList
namespaces={monSpaces ?? (space ? [space?.id] : [])}
behaviorContext="outside-space"
/>
);
},
},
{ {
align: 'right' as const, align: 'right' as const,
name: i18n.translate('xpack.synthetics.management.monitorList.actions', { name: i18n.translate('xpack.synthetics.management.monitorList.actions', {
@ -206,9 +224,10 @@ export function useMonitorListColumns({
isPublicLocationsAllowed(fields) && isPublicLocationsAllowed(fields) &&
isServiceAllowed, isServiceAllowed,
href: (fields) => { href: (fields) => {
if ('spaceId' in fields && space?.id !== fields.spaceId) { const appendSpaceId = getMonitorSpaceToAppend(space, fields.spaces);
if (!isEmpty(appendSpaceId)) {
return http?.basePath.prepend( return http?.basePath.prepend(
`edit-monitor/${fields[ConfigKey.CONFIG_ID]}?spaceId=${fields.spaceId}` `edit-monitor/${fields[ConfigKey.CONFIG_ID]}?spaceId=${fields.spaces?.[0]}`
)!; )!;
} }
return http?.basePath.prepend(`edit-monitor/${fields[ConfigKey.CONFIG_ID]}`)!; return http?.basePath.prepend(`edit-monitor/${fields[ConfigKey.CONFIG_ID]}`)!;

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { import {
Criteria, Criteria,
EuiBasicTable, EuiBasicTable,
@ -16,6 +16,8 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { MonitorListHeader } from './monitor_list_header'; import { MonitorListHeader } from './monitor_list_header';
import type { MonitorListSortField } from '../../../../../../../common/runtime_types/monitor_management/sort_field'; import type { MonitorListSortField } from '../../../../../../../common/runtime_types/monitor_management/sort_field';
import { DeleteMonitor } from './delete_monitor'; import { DeleteMonitor } from './delete_monitor';
@ -29,6 +31,7 @@ import {
} from '../../../../../../../common/runtime_types'; } from '../../../../../../../common/runtime_types';
import { useMonitorListColumns } from './columns'; import { useMonitorListColumns } from './columns';
import * as labels from './labels'; import * as labels from './labels';
import { ClientPluginsStart } from '../../../../../../plugin';
interface Props { interface Props {
pageState: MonitorListPageState; pageState: MonitorListPageState;
@ -40,6 +43,7 @@ interface Props {
reloadPage: () => void; reloadPage: () => void;
overviewStatus: OverviewStatusState | null; overviewStatus: OverviewStatusState | null;
} }
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const MonitorList = ({ export const MonitorList = ({
pageState: { pageIndex, pageSize, sortField, sortOrder }, pageState: { pageIndex, pageSize, sortField, sortOrder },
@ -108,9 +112,16 @@ export const MonitorList = ({
onSelectionChange, onSelectionChange,
initialSelected: selectedItems, initialSelected: selectedItems,
}; };
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
return ( return (
<> <ContextWrapper>
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none"> <EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<MonitorListHeader <MonitorListHeader
recordRangeLabel={recordRangeLabel} recordRangeLabel={recordRangeLabel}
@ -152,6 +163,6 @@ export const MonitorList = ({
reloadPage={reloadPage} reloadPage={reloadPage}
/> />
)} )}
</> </ContextWrapper>
); );
}; };

View file

@ -116,9 +116,9 @@ export function ActionsPopover({
const detailUrl = useMonitorDetailLocator({ const detailUrl = useMonitorDetailLocator({
configId: monitor.configId, configId: monitor.configId,
locationId: locationId ?? monitor.locationId, locationId: locationId ?? monitor.locationId,
spaceId: monitor.spaceId, spaces: monitor.spaces,
}); });
const editUrl = useEditMonitorLocator({ configId: monitor.configId, spaceId: monitor.spaceId }); const editUrl = useEditMonitorLocator({ configId: monitor.configId, spaces: monitor.spaces });
const canEditSynthetics = useCanEditSynthetics(); const canEditSynthetics = useCanEditSynthetics();

View file

@ -0,0 +1,69 @@
/*
* 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 { EuiHorizontalRule, EuiText, EuiToolTip, EuiSpacer } from '@elastic/eui';
import { Moment } from 'moment';
import { i18n } from '@kbn/i18n';
import {
getShortTimeStamp,
parseTimestamp,
} from '../../../../../../../../../common/utils/date_util';
import {
MonitorTypeEnum,
OverviewStatusMetaData,
} from '../../../../../../../../../common/runtime_types';
import { BadgeStatus } from '../../../../../common/components/monitor_status';
export const MonitorStatusCol = ({
monitor,
openFlyout,
}: {
monitor: OverviewStatusMetaData;
openFlyout: (monitor: OverviewStatusMetaData) => void;
}) => {
const timestamp = monitor.timestamp ? parseTimestamp(monitor.timestamp) : null;
return (
<div>
<BadgeStatus
status={monitor.status}
isBrowserType={monitor.type === MonitorTypeEnum.BROWSER}
onClickBadge={() => openFlyout(monitor)}
/>
<EuiSpacer size="xs" />
{timestamp ? (
<EuiToolTip
content={
<>
<EuiText color="text" size="xs">
<strong> {timestamp.fromNow()}</strong>
</EuiText>
<EuiHorizontalRule margin="xs" />
<EuiText color="ghost" size="xs">
{timestamp.toLocaleString()}
</EuiText>
</>
}
>
<EuiText size="xs" color="subdued" className="eui-textNoWrap">
{getCheckedLabel(timestamp)}
</EuiText>
</EuiToolTip>
) : (
'--'
)}
</div>
);
};
const getCheckedLabel = (timestamp: Moment) => {
return i18n.translate('xpack.synthetics.monitorList.statusColumn.checkedTimestamp', {
defaultMessage: 'Checked {timestamp}',
values: { timestamp: getShortTimeStamp(timestamp) },
});
};

View file

@ -33,7 +33,7 @@ export const MonitorsTable = ({
const getRowProps = useCallback( const getRowProps = useCallback(
(monitor: OverviewStatusMetaData): EuiTableRowProps => { (monitor: OverviewStatusMetaData): EuiTableRowProps => {
const { configId, locationLabel, locationId, spaceId } = monitor; const { configId, locationLabel, locationId, spaces } = monitor;
return { return {
onClick: (e) => { onClick: (e) => {
// This is a workaround to prevent the flyout from opening when clicking on the action buttons // This is a workaround to prevent the flyout from opening when clicking on the action buttons
@ -49,7 +49,7 @@ export const MonitorsTable = ({
id: configId, id: configId,
location: locationLabel, location: locationLabel,
locationId, locationId,
spaceId, spaces,
}) })
); );
} }

View file

@ -9,16 +9,16 @@ import React, { useCallback, useMemo } from 'react';
import { EuiBasicTableColumn, EuiLink, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiBasicTableColumn, EuiLink, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { TagsList } from '@kbn/observability-shared-plugin/public'; import { TagsList } from '@kbn/observability-shared-plugin/public';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { MonitorStatusCol } from '../components/monitor_status_col';
import { selectOverviewState } from '../../../../../../state';
import { MonitorBarSeries } from '../components/monitor_bar_series'; import { MonitorBarSeries } from '../components/monitor_bar_series';
import { useMonitorHistogram } from '../../../../hooks/use_monitor_histogram'; import { useMonitorHistogram } from '../../../../hooks/use_monitor_histogram';
import { import { OverviewStatusMetaData } from '../../../../../../../../../common/runtime_types';
MonitorTypeEnum,
OverviewStatusMetaData,
} from '../../../../../../../../../common/runtime_types';
import { MonitorTypeBadge } from '../../../../../common/components/monitor_type_badge'; import { MonitorTypeBadge } from '../../../../../common/components/monitor_type_badge';
import { getFilterForTypeMessage } from '../../../../management/monitor_list_table/labels'; import { getFilterForTypeMessage } from '../../../../management/monitor_list_table/labels';
import { BadgeStatus } from '../../../../../common/components/monitor_status';
import { FlyoutParamProps } from '../../types'; import { FlyoutParamProps } from '../../types';
import { MonitorsActions } from '../components/monitors_actions'; import { MonitorsActions } from '../components/monitors_actions';
import { import {
@ -33,6 +33,8 @@ import {
MONITOR_HISTORY, MONITOR_HISTORY,
} from '../labels'; } from '../labels';
import { MonitorsDuration } from '../components/monitors_duration'; import { MonitorsDuration } from '../components/monitors_duration';
import { useKibanaSpace } from '../../../../../../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../../../../../../plugin';
export const useMonitorsTableColumns = ({ export const useMonitorsTableColumns = ({
setFlyoutConfigCallback, setFlyoutConfigCallback,
@ -43,6 +45,12 @@ export const useMonitorsTableColumns = ({
}) => { }) => {
const history = useHistory(); const history = useHistory();
const { histogramsById, minInterval } = useMonitorHistogram({ items }); const { histogramsById, minInterval } = useMonitorHistogram({ items });
const { space } = useKibanaSpace();
const { spaces } = useKibana<ClientPluginsStart>().services;
const {
pageState: { showFromAllSpaces },
} = useSelector(selectOverviewState);
const onClickMonitorFilter = useCallback( const onClickMonitorFilter = useCallback(
(filterName: string, filterValue: string) => { (filterName: string, filterValue: string) => {
@ -60,31 +68,28 @@ export const useMonitorsTableColumns = ({
const openFlyout = useCallback( const openFlyout = useCallback(
(monitor: OverviewStatusMetaData) => { (monitor: OverviewStatusMetaData) => {
const { configId, locationLabel, locationId, spaceId } = monitor; const { configId, locationLabel, locationId } = monitor;
dispatch( dispatch(
setFlyoutConfigCallback({ setFlyoutConfigCallback({
configId, configId,
id: configId, id: configId,
location: locationLabel, location: locationLabel,
locationId, locationId,
spaceId, spaces: monitor.spaces,
}) })
); );
}, },
[dispatch, setFlyoutConfigCallback] [dispatch, setFlyoutConfigCallback]
); );
const columns: Array<EuiBasicTableColumn<OverviewStatusMetaData>> = useMemo( const columns: Array<EuiBasicTableColumn<OverviewStatusMetaData>> = useMemo(() => {
() => [ const LazySpaceList = spaces?.ui.components.getSpaceList ?? (() => null);
return [
{ {
field: 'status',
name: STATUS, name: STATUS,
render: (status: OverviewStatusMetaData['status'], monitor) => ( render: (monitor: OverviewStatusMetaData) => (
<BadgeStatus <MonitorStatusCol monitor={monitor} openFlyout={openFlyout} />
status={status}
isBrowserType={monitor.type === MonitorTypeEnum.BROWSER}
onClickBadge={() => openFlyout(monitor)}
/>
), ),
}, },
{ {
@ -174,15 +179,41 @@ export const useMonitorsTableColumns = ({
); );
}, },
}, },
...(showFromAllSpaces
? [
{
name: i18n.translate('xpack.synthetics.management.monitorList.spacesColumnTitle', {
defaultMessage: 'Spaces',
}),
field: 'spaces',
sortable: true,
render: (monSpaces: string[]) => {
return (
<LazySpaceList
namespaces={monSpaces ?? (space ? [space?.id] : [])}
behaviorContext="outside-space"
/>
);
},
},
]
: []),
{ {
name: ACTIONS, name: ACTIONS,
render: (monitor: OverviewStatusMetaData) => <MonitorsActions monitor={monitor} />, render: (monitor: OverviewStatusMetaData) => <MonitorsActions monitor={monitor} />,
align: 'right', align: 'right',
width: '40px', width: '40px',
}, },
], ];
[histogramsById, minInterval, onClickMonitorFilter, openFlyout] }, [
); histogramsById,
minInterval,
onClickMonitorFilter,
openFlyout,
showFromAllSpaces,
space,
spaces?.ui.components.getSpaceList,
]);
return { return {
columns, columns,

View file

@ -33,7 +33,7 @@ import { MetricItemExtra } from './metric_item_extra';
import { MetricItemIcon } from './metric_item_icon'; import { MetricItemIcon } from './metric_item_icon';
import { FlyoutParamProps } from '../types'; import { FlyoutParamProps } from '../types';
const METRIC_ITEM_HEIGHT = 160; const METRIC_ITEM_HEIGHT = 170;
export const getColor = (euiTheme: EuiThemeComputed, isEnabled: boolean, status?: string) => { export const getColor = (euiTheme: EuiThemeComputed, isEnabled: boolean, status?: string) => {
if (!isEnabled) { if (!isEnabled) {
@ -171,7 +171,7 @@ export const MetricItem = ({
id: monitor.configId, id: monitor.configId,
location: locationName, location: locationName,
locationId: monitor.locationId, locationId: monitor.locationId,
spaceId: monitor.spaceId, spaces: monitor.spaces,
}); });
} }
}} }}

View file

@ -60,7 +60,7 @@ interface Props {
id: string; id: string;
location: string; location: string;
locationId: string; locationId: string;
spaceId?: string; spaces?: string[];
onClose: () => void; onClose: () => void;
onEnabledChange: () => void; onEnabledChange: () => void;
onLocationChange: (params: FlyoutParamProps) => void; onLocationChange: (params: FlyoutParamProps) => void;
@ -220,7 +220,7 @@ export function LoadingState() {
} }
export function MonitorDetailFlyout(props: Props) { export function MonitorDetailFlyout(props: Props) {
const { id, configId, onLocationChange, locationId, spaceId } = props; const { id, configId, onLocationChange, locationId, spaces } = props;
const { status: overviewStatus } = useOverviewStatus({ scopeStatusByLocation: true }); const { status: overviewStatus } = useOverviewStatus({ scopeStatusByLocation: true });
@ -235,13 +235,14 @@ export function MonitorDetailFlyout(props: Props) {
const setLocation = useCallback( const setLocation = useCallback(
(location: string, locationIdT: string) => (location: string, locationIdT: string) =>
onLocationChange({ id, configId, location, locationId: locationIdT, spaceId }), onLocationChange({ id, configId, location, locationId: locationIdT, spaces }),
[onLocationChange, id, configId, spaceId] [onLocationChange, id, configId, spaces]
); );
const detailLink = useMonitorDetailLocator({ const detailLink = useMonitorDetailLocator({
configId, configId,
locationId, locationId,
spaces,
}); });
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -265,10 +266,10 @@ export function MonitorDetailFlyout(props: Props) {
dispatch( dispatch(
getMonitorAction.get({ getMonitorAction.get({
monitorId: configId, monitorId: configId,
...(spaceId && spaceId !== space?.id ? { spaceId } : {}), ...(space && spaces?.length && !spaces?.includes(space?.id) ? { spaceId: spaces[0] } : {}),
}) })
); );
}, [configId, dispatch, space?.id, spaceId, upsertSuccess]); }, [configId, dispatch, space, space?.id, spaces, upsertSuccess]);
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
@ -392,7 +393,7 @@ export const MaybeMonitorDetailsFlyout = ({
id={flyoutConfig.id} id={flyoutConfig.id}
location={flyoutConfig.location} location={flyoutConfig.location}
locationId={flyoutConfig.locationId} locationId={flyoutConfig.locationId}
spaceId={flyoutConfig.spaceId} spaces={flyoutConfig.spaces}
onClose={hideFlyout} onClose={hideFlyout}
onEnabledChange={forceRefreshCallback} onEnabledChange={forceRefreshCallback}
onLocationChange={setFlyoutConfigCallback} onLocationChange={setFlyoutConfigCallback}

View file

@ -44,7 +44,7 @@ import { MaybeMonitorDetailsFlyout } from './monitor_detail_flyout';
import { OverviewGridCompactView } from './compact_view/overview_grid_compact_view'; import { OverviewGridCompactView } from './compact_view/overview_grid_compact_view';
import { ViewButtons } from './view_buttons/view_buttons'; import { ViewButtons } from './view_buttons/view_buttons';
const ITEM_HEIGHT = 172; const ITEM_HEIGHT = 182;
const ROW_COUNT = 4; const ROW_COUNT = 4;
const MAX_LIST_HEIGHT = 800; const MAX_LIST_HEIGHT = 800;
const MIN_BATCH_SIZE = 20; const MIN_BATCH_SIZE = 20;
@ -189,7 +189,7 @@ export const OverviewGrid = memo(
<EuiFlexGroup <EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`} data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m" gutterSize="m"
css={{ ...style }} css={{ ...style, marginLeft: 5 }}
> >
{listData[listIndex].map((_, idx) => ( {listData[listIndex].map((_, idx) => (
<EuiFlexItem <EuiFlexItem

View file

@ -10,5 +10,5 @@ export interface FlyoutParamProps {
configId: string; configId: string;
location: string; location: string;
locationId: string; locationId: string;
spaceId?: string; spaces?: string[];
} }

View file

@ -38,7 +38,7 @@ export const SpaceSelector = <T extends FieldValues>({
const showFieldInvalid = (isSubmitted || isTouched) && !!error; const showFieldInvalid = (isSubmitted || isTouched) && !!error;
useEffect(() => { useEffect(() => {
if (data) { if (data?.spacesDataPromise) {
data.spacesDataPromise.then((spacesData) => { data.spacesDataPromise.then((spacesData) => {
setSpacesList([ setSpacesList([
allSpacesOption, allSpacesOption,

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import React from 'react'; import React, { useMemo } from 'react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { Provider as ReduxProvider } from 'react-redux'; import { Provider as ReduxProvider } from 'react-redux';
@ -13,16 +13,26 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { Store } from 'redux'; import { Store } from 'redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { SyntheticsRefreshContextProvider } from './synthetics_refresh_context'; import { SyntheticsRefreshContextProvider } from './synthetics_refresh_context';
import { SyntheticsDataViewContextProvider } from './synthetics_data_view_context'; import { SyntheticsDataViewContextProvider } from './synthetics_data_view_context';
import { SyntheticsAppProps } from './synthetics_settings_context'; import { SyntheticsAppProps } from './synthetics_settings_context';
import { storage, store } from '../state'; import { storage, store } from '../state';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const SyntheticsSharedContext: React.FC< export const SyntheticsSharedContext: React.FC<
React.PropsWithChildren<SyntheticsAppProps & { reload$?: Subject<boolean>; reduxStore?: Store }> React.PropsWithChildren<SyntheticsAppProps & { reload$?: Subject<boolean>; reduxStore?: Store }>
> = ({ reduxStore, coreStart, setupPlugins, startPlugins, children, darkMode, reload$ }) => { > = ({ reduxStore, coreStart, setupPlugins, startPlugins, children, darkMode, reload$ }) => {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const spacesApi = startPlugins.spaces;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
return ( return (
<KibanaContextProvider <KibanaContextProvider
services={{ services={{
@ -60,7 +70,7 @@ export const SyntheticsSharedContext: React.FC<
height: '100%', height: '100%',
}} }}
> >
{children} <ContextWrapper>{children}</ContextWrapper>
</RedirectAppLinks> </RedirectAppLinks>
</SyntheticsDataViewContextProvider> </SyntheticsDataViewContextProvider>
</SyntheticsRefreshContextProvider> </SyntheticsRefreshContextProvider>

View file

@ -9,16 +9,25 @@ import { useEffect, useState } from 'react';
import { LocatorClient } from '@kbn/share-plugin/common/url_service/locators'; import { LocatorClient } from '@kbn/share-plugin/common/url_service/locators';
import { syntheticsEditMonitorLocatorID } from '@kbn/observability-plugin/common'; import { syntheticsEditMonitorLocatorID } from '@kbn/observability-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Space } from '@kbn/spaces-plugin/common';
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
import { useKibanaSpace } from '../../../hooks/use_kibana_space'; import { useKibanaSpace } from '../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../plugin'; import { ClientPluginsStart } from '../../../plugin';
export const getMonitorSpaceToAppend = (space?: Space, spaces?: string[]) => {
if (spaces?.includes(ALL_SPACES_ID)) {
return {};
}
return space && spaces?.length && !spaces?.includes(space?.id) ? { spaceId: spaces[0] } : {};
};
export function useEditMonitorLocator({ export function useEditMonitorLocator({
configId, configId,
locators, locators,
spaceId, spaces,
}: { }: {
configId: string; configId: string;
spaceId?: string; spaces?: string[];
locators?: LocatorClient; locators?: LocatorClient;
}) { }) {
const { space } = useKibanaSpace(); const { space } = useKibanaSpace();
@ -31,12 +40,12 @@ export function useEditMonitorLocator({
async function generateUrl() { async function generateUrl() {
const url = await locator?.getUrl({ const url = await locator?.getUrl({
configId, configId,
...(spaceId && spaceId !== space?.id ? { spaceId } : {}), ...getMonitorSpaceToAppend(space, spaces),
}); });
setEditUrl(url); setEditUrl(url);
} }
generateUrl(); generateUrl();
}, [locator, configId, space, spaceId]); }, [locator, configId, space, spaces]);
return editUrl; return editUrl;
} }

View file

@ -8,17 +8,18 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common'; import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getMonitorSpaceToAppend } from './use_edit_monitor_locator';
import { useKibanaSpace } from '../../../hooks/use_kibana_space'; import { useKibanaSpace } from '../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../plugin'; import { ClientPluginsStart } from '../../../plugin';
export function useMonitorDetailLocator({ export function useMonitorDetailLocator({
configId, configId,
locationId, locationId,
spaceId, spaces,
}: { }: {
configId: string; configId: string;
locationId?: string; locationId?: string;
spaceId?: string; spaces?: string[];
}) { }) {
const { space } = useKibanaSpace(); const { space } = useKibanaSpace();
const [monitorUrl, setMonitorUrl] = useState<string | undefined>(undefined); const [monitorUrl, setMonitorUrl] = useState<string | undefined>(undefined);
@ -31,12 +32,12 @@ export function useMonitorDetailLocator({
const url = await locator?.getUrl({ const url = await locator?.getUrl({
configId, configId,
locationId, locationId,
...(spaceId && spaceId !== space?.id ? { spaceId } : {}), ...getMonitorSpaceToAppend(space, spaces),
}); });
setMonitorUrl(url); setMonitorUrl(url);
} }
generateUrl(); generateUrl();
}, [locator, configId, locationId, spaceId, space?.id]); }, [locator, configId, locationId, spaces, space?.id, space]);
return monitorUrl; return monitorUrl;
} }

View file

@ -72,7 +72,7 @@ class ApiService {
} }
private parseApiUrl(apiUrl: string, spaceId?: string) { private parseApiUrl(apiUrl: string, spaceId?: string) {
if (spaceId) { if (spaceId && spaceId !== 'default' && spaceId !== '*') {
const basePath = kibanaService.coreSetup.http.basePath; const basePath = kibanaService.coreSetup.http.basePath;
return addSpaceIdToPath(basePath.serverBasePath, spaceId, apiUrl); return addSpaceIdToPath(basePath.serverBasePath, spaceId, apiUrl);
} }

View file

@ -8,6 +8,7 @@
import { KueryNode, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { KueryNode, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status'; import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { SyntheticsEsClient } from '../../../lib'; import { SyntheticsEsClient } from '../../../lib';
import { import {
FINAL_SUMMARY_FILTER, FINAL_SUMMARY_FILTER,
@ -48,8 +49,8 @@ export async function queryFilterMonitors({
getRangeFilter({ from: 'now-24h/m', to: 'now/m' }), getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
getTimeSpanFilter(), getTimeSpanFilter(),
{ {
term: { terms: {
'meta.space_id': spaceId, 'meta.space_id': [spaceId, ALL_SPACES_ID],
}, },
}, },
{ {

View file

@ -8,13 +8,13 @@
import moment from 'moment'; import moment from 'moment';
import { SavedObjectsFindResult } from '@kbn/core/server'; import { SavedObjectsFindResult } from '@kbn/core/server';
import { Logger } from '@kbn/core/server'; import { Logger } from '@kbn/core/server';
import { MonitorData } from '../../../saved_objects/synthetics_monitor/get_all_monitors'; import { MonitorData } from '../../../saved_objects/synthetics_monitor/process_monitors';
import { import {
AlertStatusConfigs, AlertStatusConfigs,
AlertPendingStatusConfigs, AlertPendingStatusConfigs,
MissingPingMonitorInfo, MissingPingMonitorInfo,
} from '../../../../common/runtime_types/alert_rules/common'; } from '../../../../common/runtime_types/alert_rules/common';
import { EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
export interface ConfigStats { export interface ConfigStats {
up: number; up: number;
@ -31,7 +31,10 @@ export const getMissingPingMonitorInfo = ({
configId: string; configId: string;
locationId: string; locationId: string;
}): (MissingPingMonitorInfo & { createdAt?: string }) | undefined => { }): (MissingPingMonitorInfo & { createdAt?: string }) | undefined => {
const monitor = monitors.find((m) => m.id === configId); const monitor = monitors.find(
// for project monitors, we can match by id or by monitor query id
(m) => m.id === configId || m.attributes[ConfigKey.MONITOR_QUERY_ID] === configId
);
if (!monitor) { if (!monitor) {
// This should never happen // This should never happen
return; return;

View file

@ -10,7 +10,7 @@ import { times } from 'lodash';
import { intersection } from 'lodash'; import { intersection } from 'lodash';
import { SavedObjectsFindResult } from '@kbn/core/server'; import { SavedObjectsFindResult } from '@kbn/core/server';
import { Logger } from '@kbn/core/server'; import { Logger } from '@kbn/core/server';
import { MonitorData } from '../../../saved_objects/synthetics_monitor/get_all_monitors'; import { MonitorData } from '../../../saved_objects/synthetics_monitor/process_monitors';
import { import {
AlertStatusConfigs, AlertStatusConfigs,
AlertStatusMetaData, AlertStatusMetaData,

View file

@ -96,7 +96,7 @@ describe('StatusRuleExecutor', () => {
expect(staleDownConfigs).toEqual({}); expect(staleDownConfigs).toEqual({});
expect(spy).toHaveBeenCalledWith({ expect(spy).toHaveBeenCalledWith({
filter: 'synthetics-monitor.attributes.alert.status.enabled: true', filter: 'synthetics-monitor-multi-space.attributes.alert.status.enabled: true',
}); });
}); });

View file

@ -13,6 +13,7 @@ import { Logger } from '@kbn/core/server';
import { intersection, isEmpty } from 'lodash'; import { intersection, isEmpty } from 'lodash';
import { getAlertDetailsUrl } from '@kbn/observability-plugin/common'; import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status'; import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
import { MonitorConfigRepository } from '../../services/monitor_config_repository'; import { MonitorConfigRepository } from '../../services/monitor_config_repository';
import { import {
AlertOverviewStatus, AlertOverviewStatus,
@ -40,11 +41,10 @@ import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert';
import { parseArrayFilters, parseLocationFilter } from '../../routes/common'; import { parseArrayFilters, parseLocationFilter } from '../../routes/common';
import { SyntheticsServerSetup } from '../../types'; import { SyntheticsServerSetup } from '../../types';
import { SyntheticsEsClient } from '../../lib'; import { SyntheticsEsClient } from '../../lib';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { getConditionType } from '../../../common/rules/status_rule'; import { getConditionType } from '../../../common/rules/status_rule';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { AlertConfigKey } from '../../../common/constants/monitor_management'; import { AlertConfigKey } from '../../../common/constants/monitor_management';
import { ALERT_DETAILS_URL, VIEW_IN_APP_URL } from '../action_variables'; import { ALERT_DETAILS_URL, VIEW_IN_APP_URL } from '../action_variables';
import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts'; import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
@ -105,7 +105,7 @@ export class StatusRuleExecutor {
async getMonitors() { async getMonitors() {
const baseFilter = !this.hasCustomCondition const baseFilter = !this.hasCustomCondition
? `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true` ? `${syntheticsMonitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
: ''; : '';
const configIds = await queryFilterMonitors({ const configIds = await queryFilterMonitors({

View file

@ -62,7 +62,7 @@ describe('tlsRuleExecutor', () => {
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock); const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
const commonFilter = const commonFilter =
'synthetics-monitor.attributes.alert.tls.enabled: true and (synthetics-monitor.attributes.type: http or synthetics-monitor.attributes.type: tcp)'; 'synthetics-monitor-multi-space.attributes.alert.tls.enabled: true and (synthetics-monitor-multi-space.attributes.type: http or synthetics-monitor-multi-space.attributes.type: tcp)';
const getTLSRuleExecutorParams = ( const getTLSRuleExecutorParams = (
ruleParams: TLSRuleParams = {} ruleParams: TLSRuleParams = {}
@ -110,7 +110,7 @@ describe('tlsRuleExecutor', () => {
await tlsRule.getMonitors(); await tlsRule.getMonitors();
expect(getAllMock).toHaveBeenCalledWith({ expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.id:(\"${monitorId}\")`, filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.id:(\"${monitorId}\")`,
}); });
}); });
@ -123,7 +123,7 @@ describe('tlsRuleExecutor', () => {
await tlsRule.getMonitors(); await tlsRule.getMonitors();
expect(getAllMock).toHaveBeenCalledWith({ expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.tags:(\"${tag}\")`, filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.tags:(\"${tag}\")`,
}); });
}); });
@ -138,7 +138,7 @@ describe('tlsRuleExecutor', () => {
await tlsRule.getMonitors(); await tlsRule.getMonitors();
expect(getAllMock).toHaveBeenCalledWith({ expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.type:(\"${monitorType}\")`, filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.type:(\"${monitorType}\")`,
}); });
}); });

View file

@ -14,15 +14,16 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls'; import type { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls';
import moment from 'moment'; import moment from 'moment';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
import { TLSRuleInspect } from '../../../common/runtime_types/alert_rules/common'; import { TLSRuleInspect } from '../../../common/runtime_types/alert_rules/common';
import { MonitorConfigRepository } from '../../services/monitor_config_repository'; import { MonitorConfigRepository } from '../../services/monitor_config_repository';
import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults'; import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults';
import { formatFilterString } from '../common'; import { formatFilterString } from '../common';
import { SyntheticsServerSetup } from '../../types'; import { SyntheticsServerSetup } from '../../types';
import { getSyntheticsCerts } from '../../queries/get_certs'; import { getSyntheticsCerts } from '../../queries/get_certs';
import { savedObjectsAdapter } from '../../saved_objects';
import { DYNAMIC_SETTINGS_DEFAULTS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants'; import { DYNAMIC_SETTINGS_DEFAULTS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { import {
CertResult, CertResult,
ConfigKey, ConfigKey,
@ -30,7 +31,6 @@ import {
Ping, Ping,
} from '../../../common/runtime_types'; } from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { AlertConfigKey } from '../../../common/constants/monitor_management'; import { AlertConfigKey } from '../../../common/constants/monitor_management';
import { SyntheticsEsClient } from '../../lib'; import { SyntheticsEsClient } from '../../lib';
import { queryFilterMonitors } from '../status_rule/queries/filter_monitors'; import { queryFilterMonitors } from '../status_rule/queries/filter_monitors';
@ -81,9 +81,9 @@ export class TLSRuleExecutor {
} }
async getMonitors() { async getMonitors() {
const HTTP_OR_TCP = `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`; const HTTP_OR_TCP = `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`;
const baseFilter = `${monitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`; const baseFilter = `${syntheticsMonitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`;
const configIds = await queryFilterMonitors({ const configIds = await queryFilterMonitors({
spaceId: this.spaceId, spaceId: this.spaceId,
@ -135,7 +135,7 @@ export class TLSRuleExecutor {
async getExpiredCertificates() { async getExpiredCertificates() {
const { enabledMonitorQueryIds } = await this.getMonitors(); const { enabledMonitorQueryIds } = await this.getMonitors();
const dynamicSettings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient); const dynamicSettings = await getSyntheticsDynamicSettings(this.soClient);
const expiryThreshold = const expiryThreshold =
this.params.certExpirationThreshold ?? this.params.certExpirationThreshold ??

View file

@ -15,11 +15,16 @@ import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils'; import { DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
import { UPTIME_RULE_TYPE_IDS, SYNTHETICS_RULE_TYPE_IDS } from '@kbn/rule-data-utils'; import { UPTIME_RULE_TYPE_IDS, SYNTHETICS_RULE_TYPE_IDS } from '@kbn/rule-data-utils';
import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { KibanaFeatureScope } from '@kbn/features-plugin/common';
import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects';
import { import {
legacyPrivateLocationsSavedObjectName, legacyPrivateLocationsSavedObjectName,
privateLocationSavedObjectName, privateLocationSavedObjectName,
} from '../common/saved_objects/private_locations'; } from '../common/saved_objects/private_locations';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
syntheticsParamType,
} from '../common/types/saved_objects';
import { PLUGIN } from '../common/constants/plugin'; import { PLUGIN } from '../common/constants/plugin';
import { import {
syntheticsSettingsObjectType, syntheticsSettingsObjectType,
@ -93,7 +98,8 @@ export const syntheticsFeature = {
savedObject: { savedObject: {
all: [ all: [
syntheticsSettingsObjectType, syntheticsSettingsObjectType,
syntheticsMonitorType, legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
syntheticsApiKeyObjectType, syntheticsApiKeyObjectType,
syntheticsParamType, syntheticsParamType,
@ -124,7 +130,8 @@ export const syntheticsFeature = {
read: [ read: [
syntheticsParamType, syntheticsParamType,
syntheticsSettingsObjectType, syntheticsSettingsObjectType,
syntheticsMonitorType, syntheticsMonitorSavedObjectType,
legacySyntheticsMonitorTypeSingle,
syntheticsApiKeyObjectType, syntheticsApiKeyObjectType,
privateLocationSavedObjectName, privateLocationSavedObjectName,
legacyPrivateLocationsSavedObjectName, legacyPrivateLocationsSavedObjectName,

View file

@ -0,0 +1,38 @@
/*
* 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 { KibanaRequest } from '@kbn/core-http-server';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { MonitorConfigRepository } from '../services/monitor_config_repository';
import { SyntheticsServerSetup } from '../types';
import { SyntheticsService } from '../synthetics_service/synthetics_service';
import { SyntheticsMonitorClient } from '../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { getServerMock } from './server_mock';
export const getRouteContextMock = () => {
const serverMock: SyntheticsServerSetup = getServerMock();
const syntheticsService = new SyntheticsService(serverMock);
const monitorConfigRepo = new MonitorConfigRepository(
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
serverMock.encryptedSavedObjects.getClient()
);
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
return {
routeContext: {
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
monitorConfigRepository: monitorConfigRepo,
} as any,
syntheticsService,
serverMock,
};
};

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { loggerMock } from '@kbn/logging-mocks';
import { SyntheticsServerSetup } from '../types';
import { mockEncryptedSO } from '../synthetics_service/utils/mocks';
export const getServerMock = () => {
const logger = loggerMock.create();
const serverMock: SyntheticsServerSetup = {
syntheticsEsClient: { search: jest.fn() },
stackVersion: null,
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
get: jest.fn(),
update: jest.fn(),
createPointInTimeFinder: jest.fn().mockImplementation(({ perPage, type: soType }) => ({
close: jest.fn(async () => {}),
find: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
yield {
saved_objects: [],
};
},
}),
})),
},
logger,
config: {
service: {
username: 'dev',
password: '12345',
},
},
fleet: {
packagePolicyService: {
get: jest.fn().mockReturnValue({}),
getByIDs: jest.fn().mockReturnValue([]),
buildPackagePolicyFromPackage: jest.fn().mockReturnValue({}),
},
},
encryptedSavedObjects: mockEncryptedSO(),
} as unknown as SyntheticsServerSetup;
return serverMock;
};

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import * as getAllMonitors from '../../saved_objects/synthetics_monitor/get_all_monitors'; import * as getAllMonitors from '../../saved_objects/synthetics_monitor/process_monitors';
import * as getCerts from '../../queries/get_certs'; import * as getCerts from '../../queries/get_certs';
import { getSyntheticsCertsRoute } from './get_certificates'; import { getSyntheticsCertsRoute } from './get_certificates';
import { MonitorConfigRepository } from '../../services/monitor_config_repository'; import { MonitorConfigRepository } from '../../services/monitor_config_repository';

View file

@ -6,9 +6,9 @@
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../types'; import { SyntheticsRestApiRouteFactory } from '../types';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { CertResult, GetCertsParams } from '../../../common/runtime_types'; import { CertResult, GetCertsParams } from '../../../common/runtime_types';
import { ConfigKey } from '../../../common/constants/monitor_management'; import { ConfigKey } from '../../../common/constants/monitor_management';
@ -35,7 +35,7 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory<
const queryParams = request.query; const queryParams = request.query;
const monitors = await monitorConfigRepository.getAll({ const monitors = await monitorConfigRepository.getAll({
filter: `${monitorAttributes}.${ConfigKey.ENABLED}: true`, filter: `${syntheticsMonitorAttributes}.${ConfigKey.ENABLED}: true`,
}); });
if (monitors.length === 0) { if (monitors.length === 0) {

View file

@ -14,7 +14,7 @@ describe('common utils', () => {
configIds: ['1 4', '2 6', '5'], configIds: ['1 4', '2 6', '5'],
}); });
expect(filters.filtersStr).toMatchInlineSnapshot( expect(filters.filtersStr).toMatchInlineSnapshot(
`"synthetics-monitor.attributes.config_id:(\\"1 4\\" OR \\"2 6\\" OR \\"5\\")"` `"synthetics-monitor-multi-space.attributes.config_id:(\\"1 4\\" OR \\"2 6\\" OR \\"5\\")"`
); );
}); });
it('tests parseArrayFilters with tags and configIds', () => { it('tests parseArrayFilters with tags and configIds', () => {
@ -23,7 +23,7 @@ describe('common utils', () => {
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],
}); });
expect(filters.filtersStr).toMatchInlineSnapshot( expect(filters.filtersStr).toMatchInlineSnapshot(
`"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"` `"synthetics-monitor-multi-space.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor-multi-space.attributes.config_id:(\\"1\\" OR \\"2\\")"`
); );
}); });
it('tests parseArrayFilters with all options', () => { it('tests parseArrayFilters with all options', () => {
@ -37,7 +37,7 @@ describe('common utils', () => {
schedules: ['schedule1', 'schedule2'], schedules: ['schedule1', 'schedule2'],
}); });
expect(filters.filtersStr).toMatchInlineSnapshot( expect(filters.filtersStr).toMatchInlineSnapshot(
`"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.project_id:(\\"project1\\" OR \\"project2\\") AND synthetics-monitor.attributes.type:(\\"type1\\" OR \\"type2\\") AND synthetics-monitor.attributes.locations.id:(\\"loc1\\" OR \\"loc2\\") AND synthetics-monitor.attributes.schedule.number:(\\"schedule1\\" OR \\"schedule2\\") AND synthetics-monitor.attributes.id:(\\"query1\\" OR \\"query2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"` `"synthetics-monitor-multi-space.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor-multi-space.attributes.project_id:(\\"project1\\" OR \\"project2\\") AND synthetics-monitor-multi-space.attributes.type:(\\"type1\\" OR \\"type2\\") AND synthetics-monitor-multi-space.attributes.locations.id:(\\"loc1\\" OR \\"loc2\\") AND synthetics-monitor-multi-space.attributes.schedule.number:(\\"schedule1\\" OR \\"schedule2\\") AND synthetics-monitor-multi-space.attributes.id:(\\"query1\\" OR \\"query2\\") AND synthetics-monitor-multi-space.attributes.config_id:(\\"1\\" OR \\"2\\")"`
); );
}); });
}); });
@ -49,7 +49,7 @@ describe('getSavedObjectKqlFilter', () => {
it('returns KQL string if values are provided', () => { it('returns KQL string if values are provided', () => {
expect(getSavedObjectKqlFilter({ field: 'tags', values: 'apm' })).toBe( expect(getSavedObjectKqlFilter({ field: 'tags', values: 'apm' })).toBe(
'synthetics-monitor.attributes.tags:"apm"' 'synthetics-monitor-multi-space.attributes.tags:"apm"'
); );
}); });
@ -61,13 +61,13 @@ describe('getSavedObjectKqlFilter', () => {
it('handles array values', () => { it('handles array values', () => {
expect(getSavedObjectKqlFilter({ field: 'tags', values: ['apm', 'synthetics'] })).toBe( expect(getSavedObjectKqlFilter({ field: 'tags', values: ['apm', 'synthetics'] })).toBe(
'synthetics-monitor.attributes.tags:("apm" OR "synthetics")' 'synthetics-monitor-multi-space.attributes.tags:("apm" OR "synthetics")'
); );
}); });
it('escapes quotes', () => { it('escapes quotes', () => {
expect(getSavedObjectKqlFilter({ field: 'tags', values: ['"apm', 'synthetics'] })).toBe( expect(getSavedObjectKqlFilter({ field: 'tags', values: ['"apm', 'synthetics'] })).toBe(
'synthetics-monitor.attributes.tags:("\\"apm" OR "synthetics")' 'synthetics-monitor-multi-space.attributes.tags:("\\"apm" OR "synthetics")'
); );
}); });
}); });

View file

@ -6,7 +6,6 @@
*/ */
import { schema, Type, TypeOf } from '@kbn/config-schema'; import { schema, Type, TypeOf } from '@kbn/config-schema';
import { SavedObjectsFindResponse } from '@kbn/core/server';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { escapeQuotes } from '@kbn/es-query'; import { escapeQuotes } from '@kbn/es-query';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
@ -14,9 +13,8 @@ import { useLogicalAndFields } from '../../common/constants';
import { RouteContext } from './types'; import { RouteContext } from './types';
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field'; import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
import { getAllLocations } from '../synthetics_service/get_all_locations'; import { getAllLocations } from '../synthetics_service/get_all_locations';
import { EncryptedSyntheticsMonitorAttributes } from '../../common/runtime_types';
import { PrivateLocation, ServiceLocation } from '../../common/runtime_types'; import { PrivateLocation, ServiceLocation } from '../../common/runtime_types';
import { monitorAttributes } from '../../common/types/saved_objects'; import { syntheticsMonitorAttributes } from '../../common/types/saved_objects';
const StringOrArraySchema = schema.maybe( const StringOrArraySchema = schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
@ -75,36 +73,6 @@ export const SEARCH_FIELDS = [
'project_id.text', 'project_id.text',
]; ];
export const getMonitors = async (
context: RouteContext<MonitorsQuery>,
{ fields }: { fields?: string[] } = {}
): Promise<SavedObjectsFindResponse<EncryptedSyntheticsMonitorAttributes>> => {
const {
perPage = 50,
page,
sortField,
sortOrder,
query,
searchAfter,
showFromAllSpaces,
} = context.request.query;
const { filtersStr } = await getMonitorFilters(context);
return context.monitorConfigRepository.find({
perPage,
page,
sortField: parseMappingKey(sortField),
sortOrder,
searchFields: SEARCH_FIELDS,
search: query,
filter: filtersStr,
searchAfter,
fields,
...(showFromAllSpaces && { namespaces: ['*'] }),
});
};
interface Filters { interface Filters {
filter?: string; filter?: string;
tags?: string | string[]; tags?: string | string[];
@ -117,7 +85,8 @@ interface Filters {
} }
export const getMonitorFilters = async ( export const getMonitorFilters = async (
context: RouteContext<Record<string, any>, OverviewStatusQuery> context: RouteContext<Record<string, any>, OverviewStatusQuery>,
attr: string = syntheticsMonitorAttributes
) => { ) => {
const { const {
tags, tags,
@ -141,7 +110,8 @@ export const getMonitorFilters = async (
monitorQueryIds, monitorQueryIds,
locations, locations,
}, },
useLogicalAndFor useLogicalAndFor,
attr
); );
}; };
@ -156,7 +126,8 @@ export const parseArrayFilters = (
monitorQueryIds, monitorQueryIds,
locations, locations,
}: Filters, }: Filters,
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [] useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [],
attributes: string = syntheticsMonitorAttributes
) => { ) => {
const filtersStr = [ const filtersStr = [
filter, filter,
@ -164,17 +135,19 @@ export const parseArrayFilters = (
field: 'tags', field: 'tags',
values: tags, values: tags,
operator: useLogicalAndFor.includes('tags') ? 'AND' : 'OR', operator: useLogicalAndFor.includes('tags') ? 'AND' : 'OR',
attributes,
}), }),
getSavedObjectKqlFilter({ field: 'project_id', values: projects }), getSavedObjectKqlFilter({ field: 'project_id', values: projects, attributes }),
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes }), getSavedObjectKqlFilter({ field: 'type', values: monitorTypes, attributes }),
getSavedObjectKqlFilter({ getSavedObjectKqlFilter({
field: 'locations.id', field: 'locations.id',
values: locations, values: locations,
operator: useLogicalAndFor.includes('locations') ? 'AND' : 'OR', operator: useLogicalAndFor.includes('locations') ? 'AND' : 'OR',
attributes,
}), }),
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }), getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules, attributes }),
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }), getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds, attributes }),
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }), getSavedObjectKqlFilter({ field: 'config_id', values: configIds, attributes }),
] ]
.filter((f) => !!f) .filter((f) => !!f)
.join(' AND '); .join(' AND ');
@ -187,11 +160,13 @@ export const getSavedObjectKqlFilter = ({
values, values,
operator = 'OR', operator = 'OR',
searchAtRoot = false, searchAtRoot = false,
attributes = syntheticsMonitorAttributes,
}: { }: {
field: string; field: string;
values?: string | string[]; values?: string | string[];
operator?: string; operator?: string;
searchAtRoot?: boolean; searchAtRoot?: boolean;
attributes?: string;
}) => { }) => {
if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) { if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) {
return undefined; return undefined;
@ -204,7 +179,7 @@ export const getSavedObjectKqlFilter = ({
if (searchAtRoot) { if (searchAtRoot) {
fieldKey = `${field}`; fieldKey = `${field}`;
} else { } else {
fieldKey = `${monitorAttributes}.${field}`; fieldKey = `${attributes}.${field}`;
} }
if (Array.isArray(values)) { if (Array.isArray(values)) {

View file

@ -8,8 +8,8 @@
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { parseDuration } from '@kbn/alerting-plugin/server'; import { parseDuration } from '@kbn/alerting-plugin/server';
import { FindActionResult } from '@kbn/actions-plugin/server'; import { FindActionResult } from '@kbn/actions-plugin/server';
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
import { DynamicSettingsAttributes } from '../../runtime_types/settings'; import { DynamicSettingsAttributes } from '../../runtime_types/settings';
import { savedObjectsAdapter } from '../../saved_objects';
import { populateAlertActions } from '../../../common/rules/alert_actions'; import { populateAlertActions } from '../../../common/rules/alert_actions';
import { import {
SyntheticsMonitorStatusTranslations, SyntheticsMonitorStatusTranslations,
@ -40,7 +40,7 @@ export class DefaultAlertService {
async getSettings() { async getSettings() {
if (!this.settings) { if (!this.settings) {
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient); this.settings = await getSyntheticsDynamicSettings(this.soClient);
} }
return this.settings; return this.settings;
} }
@ -254,7 +254,7 @@ export class DefaultAlertService {
async getActionConnectors() { async getActionConnectors() {
const actionsClient = (await this.context.actions)?.getActionsClient(); const actionsClient = (await this.context.actions)?.getActionsClient();
if (!this.settings) { if (!this.settings) {
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient); this.settings = await getSyntheticsDynamicSettings(this.soClient);
} }
let actionConnectors: FindActionResult[] = []; let actionConnectors: FindActionResult[] = [];
try { try {

View file

@ -5,25 +5,21 @@
* 2.0. * 2.0.
*/ */
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
import { DefaultAlertService } from './default_alert_service'; import { DefaultAlertService } from './default_alert_service';
import { SyntheticsRestApiRouteFactory } from '../types'; import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { savedObjectsAdapter } from '../../saved_objects';
import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts'; import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts';
export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({ export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'PUT', method: 'PUT',
path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING, path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
validate: {}, validate: {},
handler: async ({ handler: async ({ context, server, savedObjectsClient }): Promise<DEFAULT_ALERT_RESPONSE> => {
request,
context,
server,
savedObjectsClient,
}): Promise<DEFAULT_ALERT_RESPONSE> => {
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } = const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } = await getSyntheticsDynamicSettings(
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient); savedObjectsClient
);
const updateStatusRulePromise = defaultAlertService.updateStatusRule(defaultStatusRuleEnabled); const updateStatusRulePromise = defaultAlertService.updateStatusRule(defaultStatusRuleEnabled);
const updateTLSRulePromise = defaultAlertService.updateTlsRule(defaultTLSRuleEnabled); const updateTLSRulePromise = defaultAlertService.updateTlsRule(defaultTLSRuleEnabled);

View file

@ -6,7 +6,11 @@
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../types'; import { SyntheticsRestApiRouteFactory } from '../types';
import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects'; import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorAttributes,
syntheticsMonitorSavedObjectType,
} from '../../../common/types/saved_objects';
import { ConfigKey, MonitorFiltersResult } from '../../../common/runtime_types'; import { ConfigKey, MonitorFiltersResult } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -44,7 +48,7 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
handler: async ({ savedObjectsClient, request }): Promise<any> => { handler: async ({ savedObjectsClient, request }): Promise<any> => {
const showFromAllSpaces = request.query?.showFromAllSpaces; const showFromAllSpaces = request.query?.showFromAllSpaces;
const data = await savedObjectsClient.find({ const data = await savedObjectsClient.find({
type: syntheticsMonitorType, type: [legacySyntheticsMonitorTypeSingle, syntheticsMonitorSavedObjectType],
perPage: 0, perPage: 0,
aggs, aggs,
...(showFromAllSpaces ? { namespaces: ['*'] } : {}), ...(showFromAllSpaces ? { namespaces: ['*'] } : {}),
@ -87,31 +91,31 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
const aggs = { const aggs = {
monitorTypes: { monitorTypes: {
terms: { terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`, field: `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000, size: 10000,
}, },
}, },
tags: { tags: {
terms: { terms: {
field: `${monitorAttributes}.${ConfigKey.TAGS}`, field: `${syntheticsMonitorAttributes}.${ConfigKey.TAGS}`,
size: 10000, size: 10000,
}, },
}, },
locations: { locations: {
terms: { terms: {
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`, field: `${syntheticsMonitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000, size: 10000,
}, },
}, },
projects: { projects: {
terms: { terms: {
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`, field: `${syntheticsMonitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000, size: 10000,
}, },
}, },
schedules: { schedules: {
terms: { terms: {
field: `${monitorAttributes}.${ConfigKey.SCHEDULE}.number`, field: `${syntheticsMonitorAttributes}.${ConfigKey.SCHEDULE}.number`,
size: 10000, size: 10000,
}, },
}, },

View file

@ -20,7 +20,7 @@ import { getConnectorTypesRoute } from './default_alerts/get_connector_types';
import { getActionConnectorsRoute } from './default_alerts/get_action_connectors'; import { getActionConnectorsRoute } from './default_alerts/get_action_connectors';
import { SyntheticsRestApiRouteFactory } from './types'; import { SyntheticsRestApiRouteFactory } from './types';
import { getSyntheticsCertsRoute } from './certs/get_certificates'; import { getSyntheticsCertsRoute } from './certs/get_certificates';
import { getSyntheticsSuggestionsRoute } from './suggestions/route'; import { getSyntheticsSuggestionsRoute } from './suggestions/suggestions_route';
import { getAgentPoliciesRoute } from './settings/private_locations/get_agent_policies'; import { getAgentPoliciesRoute } from './settings/private_locations/get_agent_policies';
import { inspectSyntheticsMonitorRoute } from './monitor_cruds/inspect_monitor'; import { inspectSyntheticsMonitorRoute } from './monitor_cruds/inspect_monitor';
import { deletePackagePolicyRoute } from './monitor_cruds/delete_integration'; import { deletePackagePolicyRoute } from './monitor_cruds/delete_integration';

View file

@ -7,6 +7,10 @@
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
} from '../../../common/types/saved_objects';
import { validatePermissions } from './edit_monitor'; import { validatePermissions } from './edit_monitor';
import { import {
InvalidLocationError, InvalidLocationError,
@ -34,13 +38,25 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
defaultValue: false, defaultValue: false,
}) })
), ),
// primarily used for testing purposes, to specify the type of saved object
savedObjectType: schema.maybe(
schema.oneOf(
[
schema.literal(syntheticsMonitorSavedObjectType),
schema.literal(legacySyntheticsMonitorTypeSingle),
],
{
defaultValue: syntheticsMonitorSavedObjectType,
}
)
),
}), }),
}, },
}, },
handler: async (routeContext): Promise<any> => { handler: async (routeContext): Promise<any> => {
const { request, response, server } = routeContext; const { request, response, server, spaceId } = routeContext;
// usually id is auto generated, but this is useful for testing // usually id is auto generated, but this is useful for testing
const { id, internal } = request.query; const { id, internal, savedObjectType } = request.query;
const addMonitorAPI = new AddEditMonitorAPI(routeContext); const addMonitorAPI = new AddEditMonitorAPI(routeContext);
@ -80,7 +96,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
request.body as CreateMonitorPayLoad request.body as CreateMonitorPayLoad
); );
const validationResult = validateMonitor(monitorWithDefaults); const validationResult = validateMonitor(monitorWithDefaults, spaceId);
if (!validationResult.valid || !validationResult.decodedMonitor) { if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details } = validationResult; const { reason: message, details } = validationResult;
@ -91,7 +107,12 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
const normalizedMonitor = validationResult.decodedMonitor; const normalizedMonitor = validationResult.decodedMonitor;
const err = await validatePermissions(routeContext, normalizedMonitor.locations); // Parallelize permission and unique name validation
const [err, nameError] = await Promise.all([
validatePermissions(routeContext, normalizedMonitor.locations),
addMonitorAPI.validateUniqueMonitorName(normalizedMonitor.name),
]);
if (err) { if (err) {
return response.forbidden({ return response.forbidden({
body: { body: {
@ -99,7 +120,6 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
}, },
}); });
} }
const nameError = await addMonitorAPI.validateUniqueMonitorName(normalizedMonitor.name);
if (nameError) { if (nameError) {
return response.badRequest({ return response.badRequest({
body: { message: nameError, attributes: { details: nameError } }, body: { message: nameError, attributes: { details: nameError } },
@ -109,6 +129,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
const { errors, newMonitor } = await addMonitorAPI.syncNewMonitor({ const { errors, newMonitor } = await addMonitorAPI.syncNewMonitor({
id, id,
normalizedMonitor, normalizedMonitor,
savedObjectType,
}); });
if (errors && errors.length > 0) { if (errors && errors.length > 0) {

View file

@ -8,6 +8,7 @@
import { AddEditMonitorAPI } from './add_monitor_api'; import { AddEditMonitorAPI } from './add_monitor_api';
import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { SyntheticsService } from '../../../synthetics_service/synthetics_service'; import { SyntheticsService } from '../../../synthetics_service/synthetics_service';
import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects';
describe('AddNewMonitorsPublicAPI', () => { describe('AddNewMonitorsPublicAPI', () => {
it('should normalize schedule', async function () { it('should normalize schedule', async function () {
@ -109,6 +110,7 @@ describe('AddNewMonitorsPublicAPI', () => {
urls: '', urls: '',
labels: {}, labels: {},
maintenance_windows: [], maintenance_windows: [],
spaces: [],
}); });
}); });
it('should normalize icmp', async () => { it('should normalize icmp', async () => {
@ -147,6 +149,7 @@ describe('AddNewMonitorsPublicAPI', () => {
wait: '1', wait: '1',
labels: {}, labels: {},
maintenance_windows: [], maintenance_windows: [],
spaces: [],
}); });
}); });
it('should normalize http', async () => { it('should normalize http', async () => {
@ -207,6 +210,7 @@ describe('AddNewMonitorsPublicAPI', () => {
username: '', username: '',
labels: {}, labels: {},
maintenance_windows: [], maintenance_windows: [],
spaces: [],
}); });
}); });
it('should normalize browser', async () => { it('should normalize browser', async () => {
@ -263,7 +267,61 @@ describe('AddNewMonitorsPublicAPI', () => {
urls: '', urls: '',
labels: {}, labels: {},
maintenance_windows: [], maintenance_windows: [],
spaces: [],
}); });
}); });
}); });
describe('validateUniqueMonitorName', () => {
it('should return an error message if the monitor name already exists', async () => {
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async () => ({ total: 1 }),
},
} as any);
const result = await api.validateUniqueMonitorName('test-monitor');
expect(result).toBe('Monitor name must be unique, "test-monitor" already exists.');
});
it('should not return an error message if the monitor name is unique', async () => {
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async () => ({ total: 0 }),
},
} as any);
const result = await api.validateUniqueMonitorName('unique-monitor');
expect(result).toBeUndefined();
});
it('should not return an error message if the monitor name is the same as the one being edited', async () => {
let receivedFilter: string | undefined;
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async (options: { filter: string }) => {
receivedFilter = options.filter;
return { total: 0 };
},
},
} as any);
const result = await api.validateUniqueMonitorName('test-monitor', 'monitor-id');
expect(result).toBeUndefined();
expect(receivedFilter).toBe(
`${syntheticsMonitorAttributes}.name.keyword:"test-monitor" and not (${syntheticsMonitorAttributes}.config_id: monitor-id)`
);
});
it('should return an error message if the monitor name is used by another monitor when editing', async () => {
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async () => ({ total: 1 }),
},
} as any);
const result = await api.validateUniqueMonitorName('test-monitor', 'monitor-id');
expect(result).toBe('Monitor name must be unique, "test-monitor" already exists.');
});
});
}); });

View file

@ -9,11 +9,15 @@ import { v4 as uuidV4 } from 'uuid';
import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types'; import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
import { isValidNamespace } from '@kbn/fleet-plugin/common'; import { isValidNamespace } from '@kbn/fleet-plugin/common';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorAttributes,
syntheticsMonitorSavedObjectType,
} from '../../../../common/types/saved_objects';
import { DeleteMonitorAPI } from '../services/delete_monitor_api'; import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { parseMonitorLocations } from './utils'; import { parseMonitorLocations } from './utils';
import { MonitorValidationError } from '../monitor_validation'; import { MonitorValidationError } from '../monitor_validation';
import { getSavedObjectKqlFilter } from '../../common'; import { getSavedObjectKqlFilter } from '../../common';
import { monitorAttributes } from '../../../../common/types/saved_objects';
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations'; import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
import { ConfigKey } from '../../../../common/constants/monitor_management'; import { ConfigKey } from '../../../../common/constants/monitor_management';
import { import {
@ -57,9 +61,11 @@ export class AddEditMonitorAPI {
async syncNewMonitor({ async syncNewMonitor({
id, id,
normalizedMonitor, normalizedMonitor,
savedObjectType,
}: { }: {
id?: string; id?: string;
normalizedMonitor: SyntheticsMonitor; normalizedMonitor: SyntheticsMonitor;
savedObjectType?: string;
}) { }) {
const { server, syntheticsMonitorClient, spaceId } = this.routeContext; const { server, syntheticsMonitorClient, spaceId } = this.routeContext;
const newMonitorId = id ?? uuidV4(); const newMonitorId = id ?? uuidV4();
@ -74,6 +80,8 @@ export class AddEditMonitorAPI {
const newMonitorPromise = this.routeContext.monitorConfigRepository.create({ const newMonitorPromise = this.routeContext.monitorConfigRepository.create({
normalizedMonitor: monitorWithNamespace, normalizedMonitor: monitorWithNamespace,
id: newMonitorId, id: newMonitorId,
spaceId,
savedObjectType,
}); });
const syncErrorsPromise = syntheticsMonitorClient.addMonitors( const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
@ -205,7 +213,9 @@ export class AddEditMonitorAPI {
const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name }); const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name });
const { total } = await monitorConfigRepository.find({ const { total } = await monitorConfigRepository.find({
perPage: 0, perPage: 0,
filter: id ? `${kqlFilter} and not (${monitorAttributes}.config_id: ${id})` : kqlFilter, filter: id
? `${kqlFilter} and not (${syntheticsMonitorAttributes}.config_id: ${id})`
: kqlFilter,
}); });
if (total > 0) { if (total > 0) {
@ -217,7 +227,12 @@ export class AddEditMonitorAPI {
} }
initDefaultAlerts(name: string) { initDefaultAlerts(name: string) {
const { server, savedObjectsClient, context } = this.routeContext; const { server, savedObjectsClient, context, request } = this.routeContext;
const { gettingStarted } = request.query;
if (!gettingStarted) {
return;
}
try { try {
// we do this async, so we don't block the user, error handling will be done on the UI via separate api // we do this async, so we don't block the user, error handling will be done on the UI via separate api
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
@ -305,7 +320,10 @@ export class AddEditMonitorAPI {
try { try {
const encryptedMonitor = await monitorConfigRepository.get(newMonitorId); const encryptedMonitor = await monitorConfigRepository.get(newMonitorId);
if (encryptedMonitor) { if (encryptedMonitor) {
await monitorConfigRepository.delete(newMonitorId); await monitorConfigRepository.bulkDelete([
{ id: newMonitorId, type: syntheticsMonitorSavedObjectType },
{ id: newMonitorId, type: legacySyntheticsMonitorTypeSingle },
]);
const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext); const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext);
await deleteMonitorAPI.execute({ await deleteMonitorAPI.execute({

View file

@ -39,7 +39,8 @@ export const syncNewMonitorBulk = async ({
privateLocations: SyntheticsPrivateLocations; privateLocations: SyntheticsPrivateLocations;
spaceId: string; spaceId: string;
}) => { }) => {
const { server, syntheticsMonitorClient, monitorConfigRepository } = routeContext; const { server, syntheticsMonitorClient, monitorConfigRepository, request } = routeContext;
const { query } = request;
let newMonitors: CreatedMonitors | null = null; let newMonitors: CreatedMonitors | null = null;
const monitorsToCreate = normalizedMonitors.map((monitor) => { const monitorsToCreate = normalizedMonitors.map((monitor) => {
@ -59,6 +60,7 @@ export const syncNewMonitorBulk = async ({
const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([ const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([
monitorConfigRepository.createBulk({ monitorConfigRepository.createBulk({
monitors: monitorsToCreate, monitors: monitorsToCreate,
savedObjectType: query.savedObjectType,
}), }),
syntheticsMonitorClient.addMonitors(monitorsToCreate, privateLocations, spaceId), syntheticsMonitorClient.addMonitors(monitorsToCreate, privateLocations, spaceId),
]); ]);

View file

@ -81,6 +81,7 @@ export const syncEditedMonitorBulk = async ({
[ConfigKey.MONITOR_QUERY_ID]: [ConfigKey.MONITOR_QUERY_ID]:
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id, monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
} as unknown as MonitorFields, } as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
})); }));
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([ const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
monitorConfigRepository.bulkUpdate({ monitorConfigRepository.bulkUpdate({
@ -140,6 +141,7 @@ export const rollbackCompletely = async ({
monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({ monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id, id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields, attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
})), })),
}); });
} catch (error) { } catch (error) {
@ -187,6 +189,7 @@ export const rollbackFailedUpdates = async ({
.map(({ decryptedPreviousMonitor }) => ({ .map(({ decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id, id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields, attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
})); }));
if (monitorsToRevert.length > 0) { if (monitorsToRevert.length > 0) {

View file

@ -5,18 +5,14 @@
* 2.0. * 2.0.
*/ */
import { loggerMock } from '@kbn/logging-mocks';
import { syncEditedMonitor } from './edit_monitor'; import { syncEditedMonitor } from './edit_monitor';
import { SavedObject, SavedObjectsClientContract, KibanaRequest } from '@kbn/core/server'; import { SavedObject } from '@kbn/core/server';
import { import {
EncryptedSyntheticsMonitorAttributes, EncryptedSyntheticsMonitorAttributes,
SyntheticsMonitor, SyntheticsMonitor,
SyntheticsMonitorWithSecretsAttributes, SyntheticsMonitorWithSecretsAttributes,
} from '../../../common/runtime_types'; } from '../../../common/runtime_types';
import { SyntheticsService } from '../../synthetics_service/synthetics_service'; import { getRouteContextMock } from '../../mocks/route_context_mock';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
import { SyntheticsServerSetup } from '../../types';
jest.mock('../telemetry/monitor_upgrade_sender', () => ({ jest.mock('../telemetry/monitor_upgrade_sender', () => ({
sendTelemetryEvents: jest.fn(), sendTelemetryEvents: jest.fn(),
@ -24,43 +20,6 @@ jest.mock('../telemetry/monitor_upgrade_sender', () => ({
})); }));
describe('syncEditedMonitor', () => { describe('syncEditedMonitor', () => {
const logger = loggerMock.create();
const serverMock: SyntheticsServerSetup = {
syntheticsEsClient: { search: jest.fn() },
stackVersion: null,
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
get: jest.fn(),
update: jest.fn(),
createPointInTimeFinder: jest.fn().mockImplementation(({ perPage, type: soType }) => ({
close: jest.fn(async () => {}),
find: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
yield {
saved_objects: [],
};
},
}),
})),
},
logger,
config: {
service: {
username: 'dev',
password: '12345',
},
},
fleet: {
packagePolicyService: {
get: jest.fn().mockReturnValue({}),
getByIDs: jest.fn().mockReturnValue([]),
buildPackagePolicyFromPackage: jest.fn().mockReturnValue({}),
},
},
encryptedSavedObjects: mockEncryptedSO(),
} as unknown as SyntheticsServerSetup;
const editedMonitor = { const editedMonitor = {
type: 'http', type: 'http',
enabled: true, enabled: true,
@ -91,10 +50,7 @@ describe('syncEditedMonitor', () => {
references: [], references: [],
} as SavedObject<EncryptedSyntheticsMonitorAttributes>; } as SavedObject<EncryptedSyntheticsMonitorAttributes>;
const syntheticsService = new SyntheticsService(serverMock); const { routeContext, syntheticsService, serverMock } = getRouteContextMock();
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
syntheticsService.editConfig = jest.fn(); syntheticsService.editConfig = jest.fn();
syntheticsService.getMaintenanceWindows = jest.fn(); syntheticsService.getMaintenanceWindows = jest.fn();
@ -103,13 +59,7 @@ describe('syncEditedMonitor', () => {
normalizedMonitor: editedMonitor, normalizedMonitor: editedMonitor,
decryptedPreviousMonitor: decryptedPreviousMonitor:
previousMonitor as unknown as SavedObject<SyntheticsMonitorWithSecretsAttributes>, previousMonitor as unknown as SavedObject<SyntheticsMonitorWithSecretsAttributes>,
routeContext: { routeContext,
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
} as any,
spaceId: 'test-space', spaceId: 'test-space',
}); });

View file

@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema';
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server'; import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { syntheticsMonitorSavedObjectType } from '../../../common/types/saved_objects';
import { invalidOriginError } from './add_monitor'; import { invalidOriginError } from './add_monitor';
import { import {
InvalidLocationError, InvalidLocationError,
@ -15,11 +16,9 @@ import {
} from '../../synthetics_service/project_monitor/normalizers/common_fields'; } from '../../synthetics_service/project_monitor/normalizers/common_fields';
import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monitor_api'; import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
import { ELASTIC_MANAGED_LOCATIONS_DISABLED } from './project_monitor/add_monitor_project'; import { ELASTIC_MANAGED_LOCATIONS_DISABLED } from './project_monitor/add_monitor_project';
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
import { getPrivateLocations } from '../../synthetics_service/get_private_locations'; import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
import { mergeSourceMonitor } from './formatters/saved_object_to_monitor'; import { mergeSourceMonitor } from './formatters/saved_object_to_monitor';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types'; import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { import {
MonitorFields, MonitorFields,
EncryptedSyntheticsMonitorAttributes, EncryptedSyntheticsMonitorAttributes,
@ -35,7 +34,7 @@ import {
sendTelemetryEvents, sendTelemetryEvents,
formatTelemetryUpdateEvent, formatTelemetryUpdateEvent,
} from '../telemetry/monitor_upgrade_sender'; } from '../telemetry/monitor_upgrade_sender';
import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets'; import { formatSecrets } from '../../synthetics_service/utils/secrets';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
// Simplify return promise type and type it with runtime_types // Simplify return promise type and type it with runtime_types
@ -59,7 +58,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
}, },
}, },
handler: async (routeContext): Promise<any> => { handler: async (routeContext): Promise<any> => {
const { request, response, spaceId, server } = routeContext; const { request, response, spaceId, server, monitorConfigRepository } = routeContext;
const { logger } = server; const { logger } = server;
const monitor = request.body as SyntheticsMonitor; const monitor = request.body as SyntheticsMonitor;
const reqQuery = request.query as { internal?: boolean }; const reqQuery = request.query as { internal?: boolean };
@ -90,8 +89,9 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
/* Decrypting the previous monitor before editing ensures that all existing fields remain /* Decrypting the previous monitor before editing ensures that all existing fields remain
* on the object, even in flows where decryption does not take place, such as the enabled tab * on the object, even in flows where decryption does not take place, such as the enabled tab
* on the monitor list table. We do not decrypt monitors in bulk for the monitor list table */ * on the monitor list table. We do not decrypt monitors in bulk for the monitor list table */
const previousMonitor = await getDecryptedMonitor(server, monitorId, spaceId); const { decryptedMonitor: decryptedMonitorPrevMonitor, normalizedMonitor: previousMonitor } =
const normalizedPreviousMonitor = normalizeSecrets(previousMonitor).attributes; await monitorConfigRepository.getDecrypted(monitorId, spaceId);
const normalizedPreviousMonitor = previousMonitor.attributes;
if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.internal) { if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.internal) {
return response.badRequest(getInvalidOriginError(monitor)); return response.badRequest(getInvalidOriginError(monitor));
@ -122,7 +122,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
previousMonitor.attributes.locations previousMonitor.attributes.locations
); );
const validationResult = validateMonitor(editedMonitor as MonitorFields); const validationResult = validateMonitor(editedMonitor as MonitorFields, spaceId);
if (!validationResult.valid || !validationResult.decodedMonitor) { if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult; const { reason: message, details, payload } = validationResult;
@ -153,7 +153,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
editedMonitor: editedMonitorSavedObject, editedMonitor: editedMonitorSavedObject,
} = await syncEditedMonitor({ } = await syncEditedMonitor({
routeContext, routeContext,
decryptedPreviousMonitor: previousMonitor, decryptedPreviousMonitor: decryptedMonitorPrevMonitor,
normalizedMonitor: monitorWithRevision, normalizedMonitor: monitorWithRevision,
spaceId, spaceId,
}); });
@ -162,7 +162,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
await rollbackUpdate({ await rollbackUpdate({
routeContext, routeContext,
configId: monitorId, configId: monitorId,
attributes: previousMonitor.attributes, attributes: decryptedMonitorPrevMonitor.attributes,
}); });
throw hasError?.error; throw hasError?.error;
} }
@ -219,7 +219,11 @@ const rollbackUpdate = async ({
}) => { }) => {
const { savedObjectsClient, server } = routeContext; const { savedObjectsClient, server } = routeContext;
try { try {
await savedObjectsClient.update<MonitorFields>(syntheticsMonitorType, configId, attributes); await savedObjectsClient.update<MonitorFields>(
syntheticsMonitorSavedObjectType,
configId,
attributes
);
} catch (error) { } catch (error) {
server.logger.error( server.logger.error(
`Unable to rollback edit for Synthetics monitor with id ${configId}, Error: ${error.message}`, `Unable to rollback edit for Synthetics monitor with id ${configId}, Error: ${error.message}`,
@ -241,20 +245,22 @@ export const syncEditedMonitor = async ({
routeContext: RouteContext; routeContext: RouteContext;
spaceId: string; spaceId: string;
}) => { }) => {
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext; const { server, savedObjectsClient, syntheticsMonitorClient, monitorConfigRepository } =
routeContext;
try { try {
const monitorWithId = { const monitorWithId = {
...normalizedMonitor, ...normalizedMonitor,
[ConfigKey.MONITOR_QUERY_ID]: [ConfigKey.MONITOR_QUERY_ID]:
normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id, normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
[ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id, [ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id,
[ConfigKey.KIBANA_SPACES]:
normalizedMonitor[ConfigKey.KIBANA_SPACES] || decryptedPreviousMonitor.namespaces,
}; };
const formattedMonitor = formatSecrets(monitorWithId); const formattedMonitor = formatSecrets(monitorWithId);
const editedSOPromise = monitorConfigRepository.update(
const editedSOPromise = savedObjectsClient.update<MonitorFields>(
syntheticsMonitorType,
decryptedPreviousMonitor.id, decryptedPreviousMonitor.id,
formattedMonitor formattedMonitor,
decryptedPreviousMonitor
); );
const allPrivateLocations = await getPrivateLocations(savedObjectsClient); const allPrivateLocations = await getPrivateLocations(savedObjectsClient);

View file

@ -7,7 +7,6 @@
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SyntheticsRestApiRouteFactory } from '../types'; import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config'; import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -35,8 +34,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
handler: async ({ handler: async ({
request, request,
response, response,
server: { encryptedSavedObjects, coreStart }, server: { coreStart },
savedObjectsClient,
spaceId, spaceId,
monitorConfigRepository, monitorConfigRepository,
}): Promise<any> => { }): Promise<any> => {
@ -54,17 +52,20 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
if (Boolean(canSave)) { if (Boolean(canSave)) {
// only user with write permissions can decrypt the monitor // only user with write permissions can decrypt the monitor
const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId); const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
return { ...mapSavedObjectToMonitor({ monitor, internal }), spaceId }; return {
...mapSavedObjectToMonitor({ monitor: monitor.normalizedMonitor, internal }),
spaceId,
spaces: monitor.decryptedMonitor.namespaces,
};
} else { } else {
const monObj = await monitorConfigRepository.get(monitorId);
return { return {
...mapSavedObjectToMonitor({ ...mapSavedObjectToMonitor({
monitor: await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>( monitor: monObj,
syntheticsMonitorType,
monitorId
),
internal, internal,
}), }),
spaceId, spaceId,
spaces: monObj.namespaces,
}; };
} }
} catch (getErr) { } catch (getErr) {

View file

@ -4,10 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { SyntheticsRestApiRouteFactory } from '../types'; import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitors, isMonitorsQueryFiltered, QuerySchema } from '../common'; import {
getMonitorFilters,
isMonitorsQueryFiltered,
MonitorsQuery,
parseMappingKey,
QuerySchema,
SEARCH_FIELDS,
} from '../common';
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET', method: 'GET',
@ -28,9 +36,22 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
}); });
} }
}; };
const queryParams = routeContext.request.query as MonitorsQuery;
const { filtersStr } = await getMonitorFilters(routeContext);
const [queryResultSavedObjects, totalCount] = await Promise.all([ const [queryResultSavedObjects, totalCount] = await Promise.all([
getMonitors(routeContext), monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage: queryParams.perPage ?? 50,
page: queryParams.page ?? 1,
sortField: parseMappingKey(queryParams.sortField),
sortOrder: queryParams.sortOrder,
searchFields: SEARCH_FIELDS,
search: queryParams.query,
filter: filtersStr,
searchAfter: queryParams.searchAfter,
...(queryParams.showFromAllSpaces && { namespaces: ['*'] }),
}),
totalCountQuery(), totalCountQuery(),
]); ]);
@ -46,8 +67,9 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
internal: request.query?.internal, internal: request.query?.internal,
}); });
return { return {
spaceId: monitor.namespaces?.[0],
...mon, ...mon,
spaceId: monitor.namespaces?.[0],
spaces: monitor.namespaces ?? [],
}; };
}), }),
absoluteTotal, absoluteTotal,

View file

@ -39,7 +39,7 @@ export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
...monitor, ...monitor,
}; };
const validationResult = validateMonitor(monitorWithDefaults as MonitorFields); const validationResult = validateMonitor(monitorWithDefaults as MonitorFields, spaceId);
if (!validationResult.valid || !validationResult.decodedMonitor) { if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult; const { reason: message, details, payload } = validationResult;

View file

@ -204,7 +204,7 @@ describe('validateMonitor', () => {
}, },
locations: ['somewhere'], locations: ['somewhere'],
} as unknown as MonitorFields; } as unknown as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
reason: 'Monitor type is invalid', reason: 'Monitor type is invalid',
@ -221,7 +221,7 @@ describe('validateMonitor', () => {
}, },
locations: ['somewhere'], locations: ['somewhere'],
} as unknown as MonitorFields; } as unknown as MonitorFields;
const result = validateMonitor(monitor); const result = validateMonitor(monitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
reason: 'Monitor type is invalid', reason: 'Monitor type is invalid',
@ -230,13 +230,16 @@ describe('validateMonitor', () => {
}); });
it(`when schedule is not valid`, () => { it(`when schedule is not valid`, () => {
const result = validateMonitor({ const result = validateMonitor(
...testICMPFields, {
schedule: { ...testICMPFields,
number: '4', schedule: {
unit: ScheduleUnit.MINUTES, number: '4',
}, unit: ScheduleUnit.MINUTES,
} as unknown as MonitorFields); },
} as unknown as MonitorFields,
'default'
);
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
reason: 'Monitor schedule is invalid', reason: 'Monitor schedule is invalid',
@ -246,10 +249,13 @@ describe('validateMonitor', () => {
}); });
it(`when timeout is not valid`, () => { it(`when timeout is not valid`, () => {
const result = validateMonitor({ const result = validateMonitor(
...testICMPFields, {
timeout: '3m', ...testICMPFields,
} as unknown as MonitorFields); timeout: '3m',
} as unknown as MonitorFields,
'default'
);
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
reason: 'Monitor is not a valid monitor of type icmp', reason: 'Monitor is not a valid monitor of type icmp',
@ -258,10 +264,13 @@ describe('validateMonitor', () => {
}); });
it(`when location is not valid`, () => { it(`when location is not valid`, () => {
const result = validateMonitor({ const result = validateMonitor(
...testICMPFields, {
locations: ['invalid-location'], ...testICMPFields,
} as unknown as MonitorFields); locations: ['invalid-location'],
} as unknown as MonitorFields,
'default'
);
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
reason: 'Monitor is not a valid monitor of type icmp', reason: 'Monitor is not a valid monitor of type icmp',
@ -273,7 +282,7 @@ describe('validateMonitor', () => {
describe('should validate', () => { describe('should validate', () => {
it('when payload is a correct ICMP monitor', () => { it('when payload is a correct ICMP monitor', () => {
const testMonitor = testICMPFields as MonitorFields; const testMonitor = testICMPFields as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: true, valid: true,
reason: '', reason: '',
@ -284,7 +293,7 @@ describe('validateMonitor', () => {
it('when payload is a correct TCP monitor', () => { it('when payload is a correct TCP monitor', () => {
const testMonitor = testTCPFields as MonitorFields; const testMonitor = testTCPFields as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: true, valid: true,
reason: '', reason: '',
@ -296,7 +305,7 @@ describe('validateMonitor', () => {
it('when payload is a correct HTTP monitor', () => { it('when payload is a correct HTTP monitor', () => {
const testMonitor = testHTTPFields as MonitorFields; const testMonitor = testHTTPFields as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: true, valid: true,
reason: '', reason: '',
@ -307,7 +316,7 @@ describe('validateMonitor', () => {
it('when payload is not a correct Browser monitor', () => { it('when payload is not a correct Browser monitor', () => {
const testMonitor = testBrowserFields as MonitorFields; const testMonitor = testBrowserFields as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
details: 'source.inline.script: Script is required for browser monitor.', details: 'source.inline.script: Script is required for browser monitor.',
@ -321,7 +330,7 @@ describe('validateMonitor', () => {
...testBrowserFields, ...testBrowserFields,
[ConfigKey.SOURCE_INLINE]: 'journey()', [ConfigKey.SOURCE_INLINE]: 'journey()',
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
reason: 'Monitor is not a valid monitor of type browser', reason: 'Monitor is not a valid monitor of type browser',
@ -336,7 +345,7 @@ describe('validateMonitor', () => {
...testBrowserFields, ...testBrowserFields,
[ConfigKey.SOURCE_INLINE]: 'step()', [ConfigKey.SOURCE_INLINE]: 'step()',
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: true, valid: true,
reason: '', reason: '',
@ -355,7 +364,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<ICMPSimpleFields>), } as unknown as Partial<ICMPSimpleFields>),
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual(expect.stringContaining('Invalid value')); expect(result.details).toEqual(expect.stringContaining('Invalid value'));
expect(result.details).toEqual(expect.stringContaining(ConfigKey.HOSTS)); expect(result.details).toEqual(expect.stringContaining(ConfigKey.HOSTS));
@ -374,7 +383,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<TCPFields>), } as unknown as Partial<TCPFields>),
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual( expect(result.details).toEqual(
expect.stringContaining('Invalid field "host", must be a non-empty string.') expect.stringContaining('Invalid field "host", must be a non-empty string.')
@ -394,7 +403,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<HTTPFields>), } as unknown as Partial<HTTPFields>),
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual('Invalid field "url", must be a non-empty string.'); expect(result.details).toEqual('Invalid field "url", must be a non-empty string.');
expect(result).toMatchObject({ expect(result).toMatchObject({
@ -412,7 +421,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<BrowserFields>), } as unknown as Partial<BrowserFields>),
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual( expect(result.details).toEqual(
expect.stringContaining('source.inline.script: Inline script must be a non-empty string') expect.stringContaining('source.inline.script: Inline script must be a non-empty string')
@ -436,7 +445,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<TCPFields>), } as unknown as Partial<TCPFields>),
} as MonitorFields; } as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: true, valid: true,
@ -451,7 +460,7 @@ describe('validateMonitor', () => {
it('when parsed from serialized JSON', () => { it('when parsed from serialized JSON', () => {
const testMonitor = getJsonPayload() as MonitorFields; const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor(testMonitor); const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: true, valid: true,
@ -463,10 +472,13 @@ describe('validateMonitor', () => {
it('when parsed from serialized JSON for alert', () => { it('when parsed from serialized JSON for alert', () => {
const testMonitor = getJsonPayload() as MonitorFields; const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor({ const result = validateMonitor(
...testMonitor, {
alert: {}, ...testMonitor,
}); alert: {},
},
'default'
);
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,
@ -478,14 +490,17 @@ describe('validateMonitor', () => {
it('when parsed from serialized JSON for alert invalid key', () => { it('when parsed from serialized JSON for alert invalid key', () => {
const testMonitor = getJsonPayload() as MonitorFields; const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor({ const result = validateMonitor(
...testMonitor, {
alert: { ...testMonitor,
// @ts-ignore alert: {
invalidKey: 'invalid', // @ts-ignore
enabled: true, invalidKey: 'invalid',
enabled: true,
},
}, },
}); 'default'
);
expect(result).toMatchObject({ expect(result).toMatchObject({
valid: false, valid: false,

View file

@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { isLeft } from 'fp-ts/Either'; import { isLeft } from 'fp-ts/Either';
import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { omit } from 'lodash'; import { omit, isEmpty } from 'lodash';
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { AlertConfigSchema } from '../../../common/runtime_types/monitor_management/alert_config_schema'; import { AlertConfigSchema } from '../../../common/runtime_types/monitor_management/alert_config_schema';
import { CreateMonitorPayLoad } from './add_monitor/add_monitor_api'; import { CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
@ -69,9 +69,11 @@ export class MonitorValidationError extends Error {
/** /**
* Validates monitor fields with respect to the relevant Codec identified by object's 'type' property. * Validates monitor fields with respect to the relevant Codec identified by object's 'type' property.
* @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI. * @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI.
* @param spaceId
*/ */
export function validateMonitor(monitorFields: MonitorFields): ValidationResult { export function validateMonitor(monitorFields: MonitorFields, spaceId: string): ValidationResult {
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields; const { [ConfigKey.MONITOR_TYPE]: monitorType, [ConfigKey.KIBANA_SPACES]: kSpaces } =
monitorFields;
if (monitorType !== MonitorTypeEnum.BROWSER && !monitorFields.name) { if (monitorType !== MonitorTypeEnum.BROWSER && !monitorFields.name) {
monitorFields.name = monitorFields.urls || monitorFields.hosts; monitorFields.name = monitorFields.urls || monitorFields.hosts;
@ -159,6 +161,21 @@ export function validateMonitor(monitorFields: MonitorFields): ValidationResult
} }
} }
if (spaceId && !isEmpty(kSpaces)) {
// we throw error if kSpaces is not empty and spaceId is not present
if (kSpaces && !kSpaces.includes(spaceId) && !kSpaces.includes('*')) {
return {
valid: false,
reason: i18n.translate('xpack.synthetics.createMonitor.validation.invalidSpace', {
defaultMessage:
'Invalid space ID provided in monitor configuration. It should always include the current space ID.',
}),
details: '',
payload: monitorFields,
};
}
}
return { return {
valid: true, valid: true,
reason: '', reason: '',
@ -230,6 +247,10 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => {
let unsupportedKeys = Object.keys(rawConfig).filter((key) => !supportedKeys.includes(key)); let unsupportedKeys = Object.keys(rawConfig).filter((key) => !supportedKeys.includes(key));
const result = omit(rawConfig, unsupportedKeys); const result = omit(rawConfig, unsupportedKeys);
let kSpaces = rawConfig[ConfigKey.KIBANA_SPACES] as string[];
if (kSpaces?.includes('*')) {
kSpaces = ['*'];
}
const formattedConfig = { const formattedConfig = {
...result, ...result,
@ -237,6 +258,7 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => {
private_locations: _privateLocations, private_locations: _privateLocations,
retest_on_failure: _retestOnFailure, retest_on_failure: _retestOnFailure,
custom_heartbeat_id: _customHeartbeatId, custom_heartbeat_id: _customHeartbeatId,
...(kSpaces ? { [ConfigKey.KIBANA_SPACES]: kSpaces } : {}),
} as CreateMonitorPayLoad; } as CreateMonitorPayLoad;
const requestBodyCheck = formattedConfig[ConfigKey.REQUEST_BODY_CHECK]; const requestBodyCheck = formattedConfig[ConfigKey.REQUEST_BODY_CHECK];

View file

@ -7,6 +7,10 @@
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
} from '../../../../common/types/saved_objects';
import { validateSpaceId } from '../services/validate_space_id'; import { validateSpaceId } from '../services/validate_space_id';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../../types'; import { RouteContext, SyntheticsRestApiRouteFactory } from '../../types';
import { ProjectMonitor } from '../../../../common/runtime_types'; import { ProjectMonitor } from '../../../../common/runtime_types';
@ -20,6 +24,20 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
method: 'PUT', method: 'PUT',
path: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE, path: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE,
validate: { validate: {
query: schema.object({
// primarily used for testing purposes, to specify the type of saved object
savedObjectType: schema.maybe(
schema.oneOf(
[
schema.literal(syntheticsMonitorSavedObjectType),
schema.literal(legacySyntheticsMonitorTypeSingle),
],
{
defaultValue: syntheticsMonitorSavedObjectType,
}
)
),
}),
params: schema.object({ params: schema.object({
projectName: schema.string(), projectName: schema.string(),
}), }),

View file

@ -6,12 +6,12 @@
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects';
import { DeleteMonitorAPI } from '../services/delete_monitor_api'; import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { SyntheticsRestApiRouteFactory } from '../../types'; import { SyntheticsRestApiRouteFactory } from '../../types';
import { monitorAttributes } from '../../../../common/types/saved_objects'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
import { ConfigKey } from '../../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { getMonitors, getSavedObjectKqlFilter } from '../../common'; import { getSavedObjectKqlFilter } from '../../common';
import { validateSpaceId } from '../services/validate_space_id'; import { validateSpaceId } from '../services/validate_space_id';
export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({
@ -26,7 +26,7 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
}), }),
}, },
handler: async (routeContext): Promise<any> => { handler: async (routeContext): Promise<any> => {
const { request, response } = routeContext; const { request, response, monitorConfigRepository } = routeContext;
const { projectName } = request.params; const { projectName } = request.params;
const { monitors: monitorsToDelete } = request.body; const { monitors: monitorsToDelete } = request.body;
const decodedProjectName = decodeURI(projectName); const decodedProjectName = decodeURI(projectName);
@ -40,23 +40,19 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
await validateSpaceId(routeContext); await validateSpaceId(routeContext);
const deleteFilter = `${monitorAttributes}.${ const deleteFilter = `${syntheticsMonitorAttributes}.${
ConfigKey.PROJECT_ID ConfigKey.PROJECT_ID
}: "${decodedProjectName}" AND ${getSavedObjectKqlFilter({ }: "${decodedProjectName}" AND ${getSavedObjectKqlFilter({
field: 'journey_id', field: 'journey_id',
values: monitorsToDelete.map((id: string) => `${id}`), values: monitorsToDelete.map((id: string) => `${id}`),
})}`; })}`;
const { saved_objects: monitors } = await getMonitors( const { saved_objects: monitors } =
{ await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
...routeContext, perPage: 500,
request: { filter: deleteFilter,
...request, fields: [],
query: { ...request.query, filter: deleteFilter, perPage: 500 }, });
},
},
{ fields: [] }
);
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext); const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);

View file

@ -5,11 +5,11 @@
* 2.0. * 2.0.
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { syntheticsMonitorSavedObjectType } from '../../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../../types'; import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
import { ConfigKey } from '../../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { getMonitors } from '../../common'; import { SEARCH_FIELDS } from '../../common';
const querySchema = schema.object({ const querySchema = schema.object({
search_after: schema.maybe(schema.string()), search_after: schema.maybe(schema.string()),
@ -29,6 +29,7 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory =
const { const {
request, request,
server: { logger }, server: { logger },
monitorConfigRepository,
} = routeContext; } = routeContext;
const { projectName } = request.params; const { projectName } = request.params;
@ -37,25 +38,16 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory =
const decodedSearchAfter = searchAfter ? decodeURI(searchAfter) : undefined; const decodedSearchAfter = searchAfter ? decodeURI(searchAfter) : undefined;
try { try {
const { saved_objects: monitors, total } = await getMonitors( const { saved_objects: monitors, total } =
{ await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
...routeContext, perPage,
request: { searchFields: SEARCH_FIELDS,
...request,
query: {
...request.query,
filter: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${decodedProjectName}"`,
perPage,
sortField: ConfigKey.JOURNEY_ID,
sortOrder: 'asc',
searchAfter: decodedSearchAfter ? [...decodedSearchAfter.split(',')] : undefined,
},
},
},
{
fields: [ConfigKey.JOURNEY_ID, ConfigKey.CONFIG_HASH], fields: [ConfigKey.JOURNEY_ID, ConfigKey.CONFIG_HASH],
} filter: `${syntheticsMonitorSavedObjectType}.attributes.${ConfigKey.PROJECT_ID}: "${decodedProjectName}"`,
); sortField: ConfigKey.JOURNEY_ID,
sortOrder: 'asc',
searchAfter: decodedSearchAfter ? decodedSearchAfter.split(',') : undefined,
});
const projectMonitors = monitors.map((monitor) => ({ const projectMonitors = monitors.map((monitor) => ({
journey_id: monitor.attributes[ConfigKey.JOURNEY_ID], journey_id: monitor.attributes[ConfigKey.JOURNEY_ID],
hash: monitor.attributes[ConfigKey.CONFIG_HASH] || '', hash: monitor.attributes[ConfigKey.CONFIG_HASH] || '',

View file

@ -7,6 +7,7 @@
import pMap from 'p-map'; import pMap from 'p-map';
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { syntheticsMonitorSavedObjectType } from '../../../../common/types/saved_objects';
import { validatePermissions } from '../edit_monitor'; import { validatePermissions } from '../edit_monitor';
import { import {
ConfigKey, ConfigKey,
@ -14,10 +15,7 @@ import {
MonitorFields, MonitorFields,
SyntheticsMonitor, SyntheticsMonitor,
SyntheticsMonitorWithId, SyntheticsMonitorWithId,
SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types'; } from '../../../../common/runtime_types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { normalizeSecrets } from '../../../synthetics_service/utils';
import { import {
formatTelemetryDeleteEvent, formatTelemetryDeleteEvent,
sendErrorTelemetryEvents, sendErrorTelemetryEvents,
@ -50,19 +48,11 @@ export class DeleteMonitorAPI {
} }
async getMonitorToDelete(monitorId: string) { async getMonitorToDelete(monitorId: string) {
const { spaceId, savedObjectsClient, server } = this.routeContext; const { spaceId, savedObjectsClient, server, monitorConfigRepository } = this.routeContext;
try { try {
const encryptedSOClient = server.encryptedSavedObjects.getClient(); const { normalizedMonitor } = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
const monitor = return normalizedMonitor;
await encryptedSOClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: spaceId,
}
);
return normalizeSecrets(monitor);
} catch (e) { } catch (e) {
if (SavedObjectsErrorHelpers.isNotFoundError(e)) { if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
this.result.push({ this.result.push({
@ -83,7 +73,7 @@ export class DeleteMonitorAPI {
stackVersion: server.stackVersion, stackVersion: server.stackVersion,
}); });
return await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>( return await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType, syntheticsMonitorSavedObjectType,
monitorId monitorId
); );
} }
@ -146,7 +136,7 @@ export class DeleteMonitorAPI {
); );
const deletePromise = this.routeContext.monitorConfigRepository.bulkDelete( const deletePromise = this.routeContext.monitorConfigRepository.bulkDelete(
monitors.map((monitor) => monitor.id) monitors.map((monitor) => ({ id: monitor.id, type: monitor.type }))
); );
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromise]); const [errors, result] = await Promise.all([deleteSyncPromise, deletePromise]);

View file

@ -8,7 +8,6 @@ import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { getUptimeESMockClient } from '../../queries/test_helpers'; import { getUptimeESMockClient } from '../../queries/test_helpers';
import * as commonLibs from '../common';
import * as allLocationsFn from '../../synthetics_service/get_all_locations'; import * as allLocationsFn from '../../synthetics_service/get_all_locations';
import { OverviewStatusService, SUMMARIES_PAGE_SIZE } from './overview_status_service'; import { OverviewStatusService, SUMMARIES_PAGE_SIZE } from './overview_status_service';
import times from 'lodash/times'; import times from 'lodash/times';
@ -30,38 +29,15 @@ jest.spyOn(allLocationsFn, 'getAllLocations').mockResolvedValue({
allLocations, allLocations,
}); });
jest.mock('../../saved_objects/synthetics_monitor/get_all_monitors', () => ({ jest.mock('../../saved_objects/synthetics_monitor/process_monitors', () => ({
...jest.requireActual('../../saved_objects/synthetics_monitor/get_all_monitors'), ...jest.requireActual('../../saved_objects/synthetics_monitor/process_monitors'),
getAllMonitors: jest.fn(), getAllMonitors: jest.fn(),
})); }));
jest.spyOn(commonLibs, 'getMonitors').mockResolvedValue({
per_page: 10,
saved_objects: [
{
id: 'mon-1',
attributes: {
enabled: false,
locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }],
},
},
{
id: 'mon-2',
attributes: {
enabled: true,
locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }],
schedule: {
number: '10',
unit: 'm',
},
},
},
],
} as any);
describe('current status route', () => { describe('current status route', () => {
const testMonitors = [ const testMonitors = [
{ {
namespaces: ['default'],
attributes: { attributes: {
config_id: 'id1', config_id: 'id1',
id: 'id1', id: 'id1',
@ -78,6 +54,7 @@ describe('current status route', () => {
}, },
}, },
{ {
namespaces: ['default'],
attributes: { attributes: {
id: 'id2', id: 'id2',
config_id: 'id2', config_id: 'id2',
@ -187,7 +164,9 @@ describe('current status route', () => {
"name": "test monitor 2", "name": "test monitor 2",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "down", "status": "down",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -219,7 +198,9 @@ describe('current status route', () => {
"name": "test monitor 1", "name": "test monitor 1",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "up", "status": "up",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -241,7 +222,9 @@ describe('current status route', () => {
"name": "test monitor 2", "name": "test monitor 2",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "up", "status": "up",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -352,7 +335,9 @@ describe('current status route', () => {
"name": "test monitor 2", "name": "test monitor 2",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "down", "status": "down",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -384,7 +369,9 @@ describe('current status route', () => {
"name": "test monitor 1", "name": "test monitor 1",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "up", "status": "up",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -406,7 +393,9 @@ describe('current status route', () => {
"name": "test monitor 2", "name": "test monitor 2",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "up", "status": "up",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -467,7 +456,9 @@ describe('current status route', () => {
"name": "test monitor 1", "name": "test monitor 1",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "unknown", "status": "unknown",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -489,7 +480,9 @@ describe('current status route', () => {
"name": "test monitor 2", "name": "test monitor 2",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "unknown", "status": "unknown",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",
@ -511,7 +504,9 @@ describe('current status route', () => {
"name": "test monitor 2", "name": "test monitor 2",
"projectId": "project-id", "projectId": "project-id",
"schedule": "1", "schedule": "1",
"spaceId": undefined, "spaces": Array [
"default",
],
"status": "unknown", "status": "unknown",
"tags": Array [ "tags": Array [
"tag-1", "tag-1",

View file

@ -10,9 +10,10 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server'; import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span'; import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { asMutableArray } from '../../../common/utils/as_mutable_array';
import { getMonitorFilters, OverviewStatusQuery } from '../common'; import { getMonitorFilters, OverviewStatusQuery } from '../common';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { ConfigKey } from '../../../common/constants/monitor_management'; import { ConfigKey } from '../../../common/constants/monitor_management';
import { RouteContext } from '../types'; import { RouteContext } from '../types';
import { import {
@ -115,7 +116,7 @@ export class OverviewStatusService {
]; ];
}; };
const filters: QueryDslQueryContainer[] = [ const filters: QueryDslQueryContainer[] = [
...(showFromAllSpaces ? [] : [{ term: { 'meta.space_id': spaceId } }]), ...(showFromAllSpaces ? [] : [{ terms: { 'meta.space_id': [spaceId, ALL_SPACES_ID] } }]),
...getTermFilter('monitor.type', monitorTypes), ...getTermFilter('monitor.type', monitorTypes),
...getTermFilter('tags', tags), ...getTermFilter('tags', tags),
...getTermFilter('monitor.project.id', projects), ...getTermFilter('monitor.project.id', projects),
@ -370,7 +371,7 @@ export class OverviewStatusService {
projectId: monitor.attributes[ConfigKey.PROJECT_ID], projectId: monitor.attributes[ConfigKey.PROJECT_ID],
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]), isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
updated_at: monitor.updated_at, updated_at: monitor.updated_at,
spaceId: monitor.namespaces?.[0], spaces: monitor.namespaces,
urls: monitor.attributes[ConfigKey.URLS], urls: monitor.attributes[ConfigKey.URLS],
maintenanceWindows: monitor.attributes[ConfigKey.MAINTENANCE_WINDOWS]?.map((mw) => mw), maintenanceWindows: monitor.attributes[ConfigKey.MAINTENANCE_WINDOWS]?.map((mw) => mw),
}; };

View file

@ -7,7 +7,10 @@
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { savedObjectsAdapter } from '../../saved_objects'; import {
getSyntheticsDynamicSettings,
setSyntheticsDynamicSettings,
} from '../../saved_objects/synthetics_settings';
import { SyntheticsRestApiRouteFactory } from '../types'; import { SyntheticsRestApiRouteFactory } from '../types';
import { DynamicSettings } from '../../../common/runtime_types'; import { DynamicSettings } from '../../../common/runtime_types';
import { DynamicSettingsAttributes } from '../../runtime_types/settings'; import { DynamicSettingsAttributes } from '../../runtime_types/settings';
@ -20,8 +23,9 @@ export const createGetDynamicSettingsRoute: SyntheticsRestApiRouteFactory<
path: SYNTHETICS_API_URLS.DYNAMIC_SETTINGS, path: SYNTHETICS_API_URLS.DYNAMIC_SETTINGS,
validate: false, validate: false,
handler: async ({ savedObjectsClient }) => { handler: async ({ savedObjectsClient }) => {
const dynamicSettingsAttributes: DynamicSettingsAttributes = const dynamicSettingsAttributes: DynamicSettingsAttributes = await getSyntheticsDynamicSettings(
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient); savedObjectsClient
);
return fromSettingsAttribute(dynamicSettingsAttributes); return fromSettingsAttribute(dynamicSettingsAttributes);
}, },
}); });
@ -35,9 +39,9 @@ export const createPostDynamicSettingsRoute: SyntheticsRestApiRouteFactory = ()
writeAccess: true, writeAccess: true,
handler: async ({ savedObjectsClient, request }): Promise<DynamicSettingsAttributes> => { handler: async ({ savedObjectsClient, request }): Promise<DynamicSettingsAttributes> => {
const newSettings = request.body; const newSettings = request.body;
const prevSettings = await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient); const prevSettings = await getSyntheticsDynamicSettings(savedObjectsClient);
const attr = await savedObjectsAdapter.setSyntheticsDynamicSettings(savedObjectsClient, { const attr = await setSyntheticsDynamicSettings(savedObjectsClient, {
...prevSettings, ...prevSettings,
...newSettings, ...newSettings,
} as DynamicSettingsAttributes); } as DynamicSettingsAttributes);

View file

@ -6,10 +6,9 @@
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { isEmpty } from 'lodash'; import { getSavedObjectKqlFilter } from '../../common';
import { PRIVATE_LOCATION_WRITE_API } from '../../../feature'; import { PRIVATE_LOCATION_WRITE_API } from '../../../feature';
import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations';
import { getMonitorsByLocation } from './get_location_monitors';
import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations';
import { SyntheticsRestApiRouteFactory } from '../../types'; import { SyntheticsRestApiRouteFactory } from '../../types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../../common/constants';
@ -28,7 +27,14 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory<undefined
}, },
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API], requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
handler: async (routeContext) => { handler: async (routeContext) => {
const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext; const {
savedObjectsClient,
syntheticsMonitorClient,
request,
response,
server,
monitorConfigRepository,
} = routeContext;
const internalSOClient = server.coreStart.savedObjects.createInternalRepository(); const internalSOClient = server.coreStart.savedObjects.createInternalRepository();
await migrateLegacyPrivateLocations(internalSOClient, server.logger); await migrateLegacyPrivateLocations(internalSOClient, server.logger);
@ -49,13 +55,17 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory<undefined
}); });
} }
const monitors = await getMonitorsByLocation(server, locationId); const locationFilter = getSavedObjectKqlFilter({ field: 'locations.id', values: locationId });
if (!isEmpty(monitors)) { const data = await monitorConfigRepository.find({
const count = monitors.find((monitor) => monitor.id === locationId)?.count; perPage: 0,
filter: locationFilter,
});
if (data.total > 0) {
return response.badRequest({ return response.badRequest({
body: { body: {
message: `Private location with id ${locationId} cannot be deleted because it is used by ${count} monitor(s).`, message: `Private location with id ${locationId} cannot be deleted because it is used by ${data.total} monitor(s).`,
}, },
}); });
} }

View file

@ -6,31 +6,36 @@
*/ */
import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants'; import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants';
import { getSavedObjectKqlFilter } from '../../common'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations';
import { SyntheticsRestApiRouteFactory } from '../../types'; import { SyntheticsRestApiRouteFactory } from '../../types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { monitorAttributes, syntheticsMonitorType } from '../../../../common/types/saved_objects'; import {
import { SyntheticsServerSetup } from '../../../types'; legacyMonitorAttributes,
syntheticsMonitorAttributes,
syntheticsMonitorSOTypes,
} from '../../../../common/types/saved_objects';
type Payload = Array<{ type Payload = Array<{
id: string; id: string;
count: number; count: number;
}>; }>;
interface ExpectedResponse { interface Bucket {
locations: { key: string;
buckets: Array<{ doc_count: number;
key: string;
doc_count: number;
}>;
};
} }
const aggs = { const aggs = {
locations_legacy: {
terms: {
field: `${legacyMonitorAttributes}.locations.id`,
size: 20000,
},
},
locations: { locations: {
terms: { terms: {
field: `${monitorAttributes}.locations.id`, field: `${syntheticsMonitorAttributes}.locations.id`,
size: 10000, size: 20000,
}, },
}, },
}; };
@ -40,27 +45,44 @@ export const getLocationMonitors: SyntheticsRestApiRouteFactory<Payload> = () =>
path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS_MONITORS, path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS_MONITORS,
validate: {}, validate: {},
handler: async ({ server }) => { handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }) => {
return await getMonitorsByLocation(server); const soClient = server.coreStart.savedObjects.createInternalRepository();
const { locations } = await getPrivateLocationsAndAgentPolicies(
savedObjectsClient,
syntheticsMonitorClient
);
const locationMonitors = await soClient.find({
type: syntheticsMonitorSOTypes,
perPage: 0,
aggs,
namespaces: [ALL_SPACES_ID],
});
const aggsResp = locationMonitors.aggregations as
| {
locations_legacy?: { buckets: Bucket[] };
locations?: { buckets: Bucket[] };
}
| undefined;
// Merge counts from both buckets
const counts: Record<string, number> = {};
aggsResp?.locations_legacy?.buckets.forEach(({ key, doc_count: docCount }) => {
counts[key] = (counts[key] || 0) + docCount;
});
aggsResp?.locations?.buckets.forEach(({ key, doc_count: docCount }) => {
counts[key] = (counts[key] || 0) + docCount;
});
return Object.entries(counts)
.map(([id, count]) => ({
id,
count,
}))
.filter(({ id }) =>
locations.some((location) => location.id === id || location.label === id)
);
}, },
}); });
export const getMonitorsByLocation = async (server: SyntheticsServerSetup, locationId?: string) => {
const soClient = server.coreStart.savedObjects.createInternalRepository();
const locationFilter = getSavedObjectKqlFilter({ field: 'locations.id', values: locationId });
const locationMonitors = await soClient.find<unknown, ExpectedResponse>({
type: syntheticsMonitorType,
perPage: 0,
aggs,
filter: locationFilter,
namespaces: [ALL_SPACES_ID],
});
return (
locationMonitors.aggregations?.locations.buckets.map(({ key: id, doc_count: count }) => ({
id,
count,
})) ?? []
);
};

View file

@ -151,6 +151,7 @@ describe('updatePrivateLocationMonitors', () => {
schedule: { number: '10', unit: 'm' }, schedule: { number: '10', unit: 'm' },
namespace: FIRST_SPACE_ID, namespace: FIRST_SPACE_ID,
}, },
namespaces: [FIRST_SPACE_ID],
}, },
{ {
id: SECOND_MONITOR_ID, id: SECOND_MONITOR_ID,
@ -166,6 +167,7 @@ describe('updatePrivateLocationMonitors', () => {
schedule: { number: '5', unit: 'm' }, schedule: { number: '5', unit: 'm' },
namespace: SECOND_SPACE_ID, namespace: SECOND_SPACE_ID,
}, },
namespaces: [SECOND_SPACE_ID],
}, },
]; ];

View file

@ -101,21 +101,21 @@ export const updatePrivateLocationMonitors = async ({
monitorWithRevision, monitorWithRevision,
}; };
const namespace = m.attributes.namespace; const spaceId = m.namespaces?.[0] || 'default'; // Default to 'default' if no namespace is found
return { return {
...acc, ...acc,
[namespace]: [...(acc[namespace] || []), monitorToUpdate], [spaceId]: [...(acc[spaceId] || []), monitorToUpdate],
}; };
}, },
{} {}
); );
const promises = Object.keys(updatedMonitorsPerSpace).map((namespace) => [ const promises = Object.keys(updatedMonitorsPerSpace).map((spaceId) => [
syncEditedMonitorBulk({ syncEditedMonitorBulk({
monitorsToUpdate: updatedMonitorsPerSpace[namespace], monitorsToUpdate: updatedMonitorsPerSpace[spaceId],
privateLocations: allPrivateLocations, privateLocations: allPrivateLocations,
routeContext, routeContext,
spaceId: namespace, spaceId,
}), }),
]); ]);

View file

@ -1,166 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SyntheticsRestApiRouteFactory } from '../types';
import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
ConfigKey,
MonitorFiltersResult,
EncryptedSyntheticsMonitorAttributes,
} from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { QuerySchema, getMonitorFilters, SEARCH_FIELDS } from '../common';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
type Buckets = Array<{
key: string;
doc_count: number;
}>;
interface AggsResponse {
locationsAggs: {
buckets: Buckets;
};
tagsAggs: {
buckets: Buckets;
};
projectsAggs: {
buckets: Buckets;
};
monitorTypesAggs: {
buckets: Buckets;
};
monitorIdsAggs: {
buckets: Array<{
key: string;
doc_count: number;
name: {
hits: {
hits: Array<{
_source: {
[syntheticsMonitorType]: {
[ConfigKey.NAME]: string;
};
};
}>;
};
};
}>;
};
}
export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
MonitorFiltersResult
> = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.SUGGESTIONS,
validate: {
query: QuerySchema,
},
handler: async (route): Promise<any> => {
const { monitorConfigRepository } = route;
const { query } = route.request.query;
const { filtersStr } = await getMonitorFilters(route);
const { allLocations = [] } = await getAllLocations(route);
const data = await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage: 0,
filter: filtersStr ? `${filtersStr}` : undefined,
aggs,
search: query ? `${query}*` : undefined,
searchFields: SEARCH_FIELDS,
});
const { monitorTypesAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } =
(data?.aggregations as AggsResponse) ?? {};
const allLocationsMap = new Map(allLocations.map((obj) => [obj.id, obj.label]));
return {
monitorIds: monitorIdsAggs?.buckets?.map(({ key, doc_count: count, name }) => ({
label: name?.hits?.hits[0]?._source?.[syntheticsMonitorType]?.[ConfigKey.NAME] || key,
value: key,
count,
})),
tags:
tagsAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
locations:
locationsAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: allLocationsMap.get(key) || key,
value: key,
count,
})) ?? [],
projects:
projectsAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
monitorTypes:
monitorTypesAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
};
},
});
const aggs = {
tagsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.TAGS}`,
size: 10000,
exclude: [''],
},
},
monitorTypeAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
locationsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
exclude: [''],
},
},
projectsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
exclude: [''],
},
},
monitorTypesAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
monitorIdsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_QUERY_ID}`,
size: 10000,
exclude: [''],
},
aggs: {
name: {
top_hits: {
_source: [`${syntheticsMonitorType}.${ConfigKey.NAME}`],
size: 1,
},
},
},
},
};

View file

@ -0,0 +1,261 @@
/*
* 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 {
syntheticsMonitorAttributes,
syntheticsMonitorSavedObjectType,
legacySyntheticsMonitorTypeSingle,
legacyMonitorAttributes,
} from '../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../types';
import {
ConfigKey,
MonitorFiltersResult,
EncryptedSyntheticsMonitorAttributes,
} from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { QuerySchema, getMonitorFilters, SEARCH_FIELDS } from '../common';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
type Buckets = Array<{
key: string;
doc_count: number;
}>;
type MonitorTypes =
| typeof syntheticsMonitorSavedObjectType
| typeof legacySyntheticsMonitorTypeSingle;
interface AggsResponse {
locationsAggs: {
buckets: Buckets;
};
tagsAggs: {
buckets: Buckets;
};
projectsAggs: {
buckets: Buckets;
};
monitorTypesAggs: {
buckets: Buckets;
};
monitorIdsAggs: {
buckets: Array<{
key: string;
doc_count: number;
name: {
hits: {
hits: Array<{
_source: {
[key in MonitorTypes]: {
[ConfigKey.NAME]: string;
};
};
}>;
};
};
}>;
};
}
// Helper to sum buckets by key
function sumBuckets(bucketsA: Buckets = [], bucketsB: Buckets = []): Buckets {
const map = new Map<string, number>();
for (const { key, doc_count: docCount } of bucketsA) {
map.set(key, docCount);
}
for (const { key, doc_count: docCount } of bucketsB) {
map.set(key, (map.get(key) || 0) + docCount);
}
return Array.from(map.entries()).map(([key, docCount]) => ({ key, doc_count: docCount }));
}
// Helper to sum monitorIdsAggs buckets
function sumMonitorIdsBuckets(
bucketsA: AggsResponse['monitorIdsAggs']['buckets'] = [],
bucketsB: AggsResponse['monitorIdsAggs']['buckets'] = []
): AggsResponse['monitorIdsAggs']['buckets'] {
const map = new Map<string, { doc_count: number; name?: any }>();
for (const b of bucketsA) {
map.set(b.key, { doc_count: b.doc_count, name: b.name });
}
for (const b of bucketsB) {
if (map.has(b.key)) {
map.get(b.key)!.doc_count += b.doc_count;
} else {
map.set(b.key, { doc_count: b.doc_count, name: b.name });
}
}
return Array.from(map.entries()).map(([key, { doc_count: docCount, name }]) => ({
key,
doc_count: docCount,
name,
}));
}
// Helper to generate aggs for new or legacy monitors
function getAggs(isLegacy: boolean) {
const attributes = isLegacy ? legacyMonitorAttributes : syntheticsMonitorAttributes;
const savedObjectType = isLegacy
? legacySyntheticsMonitorTypeSingle
: syntheticsMonitorSavedObjectType;
return {
tagsAggs: {
terms: {
field: `${attributes}.${ConfigKey.TAGS}`,
size: 10000,
exclude: [''],
},
},
monitorTypeAggs: {
terms: {
field: `${attributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
locationsAggs: {
terms: {
field: `${attributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
exclude: [''],
},
},
projectsAggs: {
terms: {
field: `${attributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
exclude: [''],
},
},
monitorTypesAggs: {
terms: {
field: `${attributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
monitorIdsAggs: {
terms: {
field: `${attributes}.${ConfigKey.MONITOR_QUERY_ID}`,
size: 10000,
exclude: [''],
},
aggs: {
name: {
top_hits: {
_source: [`${savedObjectType}.${ConfigKey.NAME}`],
size: 1,
},
},
},
},
};
}
export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
MonitorFiltersResult
> = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.SUGGESTIONS,
validate: {
query: QuerySchema,
},
handler: async (route): Promise<any> => {
const { savedObjectsClient } = route;
const { query } = route.request.query;
const { filtersStr } = await getMonitorFilters(route, syntheticsMonitorAttributes);
const { allLocations = [] } = await getAllLocations(route);
// Find for new monitors
const data = await savedObjectsClient.find<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorSavedObjectType,
perPage: 0,
filter: filtersStr ? filtersStr : undefined,
aggs: getAggs(false),
search: query ? `${query}*` : undefined,
searchFields: SEARCH_FIELDS,
});
const { filtersStr: legacyFilterStr } = await getMonitorFilters(route, legacyMonitorAttributes);
// Find for legacy monitors
const legacyData = await savedObjectsClient.find<any>({
type: legacySyntheticsMonitorTypeSingle,
perPage: 0,
filter: legacyFilterStr ? legacyFilterStr : undefined,
aggs: getAggs(true),
search: query ? `${query}*` : undefined,
searchFields: SEARCH_FIELDS,
});
// Extract aggs
const { monitorTypesAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } =
(data?.aggregations as AggsResponse) ?? {};
const {
monitorTypesAggs: legacyMonitorTypesAggs,
tagsAggs: legacyTagsAggs,
locationsAggs: legacyLocationsAggs,
projectsAggs: legacyProjectsAggs,
monitorIdsAggs: legacyMonitorIdsAggs,
} = (legacyData?.aggregations as AggsResponse) ?? {};
const allLocationsMap = new Map(allLocations.map((obj) => [obj.id, obj.label]));
// Sum buckets
const summedTags = sumBuckets(tagsAggs?.buckets, legacyTagsAggs?.buckets);
const summedLocations = sumBuckets(locationsAggs?.buckets, legacyLocationsAggs?.buckets);
const summedProjects = sumBuckets(projectsAggs?.buckets, legacyProjectsAggs?.buckets);
const summedMonitorTypes = sumBuckets(
monitorTypesAggs?.buckets,
legacyMonitorTypesAggs?.buckets
);
const summedMonitorIds = sumMonitorIdsBuckets(
monitorIdsAggs?.buckets,
legacyMonitorIdsAggs?.buckets
);
return {
monitorIds: summedMonitorIds?.map(({ key, doc_count: count, name }) => {
const source = name?.hits?.hits[0]?._source || {};
return {
label:
source?.[syntheticsMonitorSavedObjectType]?.[ConfigKey.NAME] ||
source?.[legacySyntheticsMonitorTypeSingle]?.[ConfigKey.NAME] ||
key,
value: key,
count,
};
}),
tags:
summedTags?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
locations:
summedLocations?.map(({ key, doc_count: count }) => ({
label: allLocationsMap.get(key) || key,
value: key,
count,
})) ?? [],
projects:
summedProjects?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
monitorTypes:
summedMonitorTypes?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
};
},
});

View file

@ -5,7 +5,6 @@
* 2.0. * 2.0.
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations'; import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils'; import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
@ -26,9 +25,9 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
handler: async ({ handler: async ({
request, request,
response, response,
server,
syntheticsMonitorClient, syntheticsMonitorClient,
savedObjectsClient, savedObjectsClient,
spaceId,
}): Promise<any> => { }): Promise<any> => {
const monitor = request.body as MonitorFields; const monitor = request.body as MonitorFields;
const { monitorId } = request.params; const { monitorId } = request.params;
@ -36,9 +35,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
return response.badRequest({ body: { message: 'Monitor data is empty.' } }); return response.badRequest({ body: { message: 'Monitor data is empty.' } });
} }
const validationResult = validateMonitor(monitor); const validationResult = validateMonitor(monitor, spaceId);
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
const decodedMonitor = validationResult.decodedMonitor; const decodedMonitor = validationResult.decodedMonitor;
if (!validationResult.valid || !decodedMonitor) { if (!validationResult.valid || !decodedMonitor) {

View file

@ -8,13 +8,11 @@ import { schema } from '@kbn/config-schema';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { IKibanaResponse } from '@kbn/core-http-server'; import { IKibanaResponse } from '@kbn/core-http-server';
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations'; import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types'; import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import { TestNowResponse } from '../../../common/types'; import { TestNowResponse } from '../../../common/types';
import { ConfigKey, MonitorFields } from '../../../common/runtime_types'; import { ConfigKey, MonitorFields } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils'; import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
import { getMonitorNotFoundResponse } from './service_errors'; import { getMonitorNotFoundResponse } from './service_errors';
@ -37,14 +35,19 @@ export const triggerTestNow = async (
monitorId: string, monitorId: string,
routeContext: RouteContext routeContext: RouteContext
): Promise<TestNowResponse | IKibanaResponse<any>> => { ): Promise<TestNowResponse | IKibanaResponse<any>> => {
const { server, spaceId, syntheticsMonitorClient, savedObjectsClient, response } = routeContext; const {
spaceId,
syntheticsMonitorClient,
savedObjectsClient,
response,
monitorConfigRepository,
} = routeContext;
try { try {
const monitorWithSecrets = await getDecryptedMonitor(server, monitorId, spaceId); const { normalizedMonitor } = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
const normalizedMonitor = normalizeSecrets(monitorWithSecrets);
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } = const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } =
monitorWithSecrets.attributes; normalizedMonitor.attributes;
const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor( const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
savedObjectsClient, savedObjectsClient,

View file

@ -5,4 +5,7 @@
* 2.0. * 2.0.
*/ */
export { savedObjectsAdapter } from './saved_objects'; export {
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from './synthetics_monitor/legacy_synthetics_monitor';

View file

@ -10,7 +10,7 @@ import {
ConfigKey, ConfigKey,
SyntheticsMonitorWithSecretsAttributes, SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types'; } from '../../../../common/runtime_types';
import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor'; import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor/legacy_synthetics_monitor';
export type SyntheticsMonitorWithSecretsAttributes860 = Omit< export type SyntheticsMonitorWithSecretsAttributes860 = Omit<
SyntheticsMonitorWithSecretsAttributes, SyntheticsMonitorWithSecretsAttributes,

View file

@ -165,6 +165,7 @@ describe('Monitor migrations v8.7.0 -> v8.8.0', () => {
urls: 'https://elastic.co', urls: 'https://elastic.co',
labels: {}, labels: {},
maintenance_windows: [], maintenance_windows: [],
spaces: [],
}, },
coreMigrationVersion: '8.8.0', coreMigrationVersion: '8.8.0',
created_at: '2023-03-31T20:31:24.177Z', created_at: '2023-03-31T20:31:24.177Z',

View file

@ -25,8 +25,8 @@ import {
} from '../../../../common/constants/monitor_defaults'; } from '../../../../common/constants/monitor_defaults';
import { import {
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE, LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
SYNTHETICS_MONITOR_ENCRYPTED_TYPE, LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from '../../synthetics_monitor'; } from '../../synthetics_monitor/legacy_synthetics_monitor';
import { validateMonitor } from '../../../routes/monitor_cruds/monitor_validation'; import { validateMonitor } from '../../../routes/monitor_cruds/monitor_validation';
import { import {
formatSecrets, formatSecrets,
@ -89,7 +89,7 @@ export const migration880 = (encryptedSavedObjects: EncryptedSavedObjectsPluginS
return migrated; return migrated;
}, },
inputType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE, inputType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE, migratedType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
}); });
}; };
@ -117,10 +117,13 @@ const omitZipUrlFields = (fields: BrowserFields) => {
// will return only fields that match the current type defs, which omit // will return only fields that match the current type defs, which omit
// zip url fields // zip url fields
const validationResult = validateMonitor({ const validationResult = validateMonitor(
...fields, {
[ConfigKey.METADATA]: updatedMetadata, ...fields,
} as MonitorFields); [ConfigKey.METADATA]: updatedMetadata,
} as MonitorFields,
fields[ConfigKey.ORIGINAL_SPACE]!
);
if (!validationResult.valid || !validationResult.decodedMonitor) { if (!validationResult.valid || !validationResult.decodedMonitor) {
throw new Error( throw new Error(

View file

@ -6,11 +6,11 @@
*/ */
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE } from '../../synthetics_monitor/legacy_synthetics_monitor';
import { import {
ConfigKey, ConfigKey,
SyntheticsMonitorWithSecretsAttributes, SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types'; } from '../../../../common/runtime_types';
import { SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor';
export type SyntheticsMonitor890 = Omit< export type SyntheticsMonitor890 = Omit<
SyntheticsMonitorWithSecretsAttributes, SyntheticsMonitorWithSecretsAttributes,
@ -51,7 +51,7 @@ export const migration890 = (encryptedSavedObjects: EncryptedSavedObjectsPluginS
return migrated; return migrated;
}, },
inputType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE, inputType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE, migratedType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
}); });
}; };

View file

@ -5,35 +5,26 @@
* 2.0. * 2.0.
*/ */
import { import { SavedObjectsServiceSetup } from '@kbn/core/server';
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
SavedObjectsServiceSetup,
} from '@kbn/core/server';
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { fromSettingsAttribute } from '../routes/settings/dynamic_settings';
import { import {
syntheticsSettings, getSyntheticsMonitorConfigSavedObjectType,
syntheticsSettingsObjectId, SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
syntheticsSettingsObjectType, } from './synthetics_monitor/synthetics_monitor_config';
uptimeSettingsObjectId, import { syntheticsSettings } from './synthetics_settings';
uptimeSettingsObjectType,
} from './synthetics_settings';
import { import {
SYNTHETICS_SECRET_ENCRYPTED_TYPE, SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE,
syntheticsParamSavedObjectType, syntheticsParamSavedObjectType,
} from './synthetics_param'; } from './synthetics_param';
import { import {
LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE, LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE,
PRIVATE_LOCATION_SAVED_OBJECT_TYPE, PRIVATE_LOCATION_SAVED_OBJECT_TYPE,
} from './private_locations'; } from './private_locations';
import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../constants/settings';
import { DynamicSettingsAttributes } from '../runtime_types/settings';
import { import {
getSyntheticsMonitorSavedObjectType, getLegacySyntheticsMonitorSavedObjectType,
SYNTHETICS_MONITOR_ENCRYPTED_TYPE, LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from './synthetics_monitor'; } from './synthetics_monitor/legacy_synthetics_monitor';
import { syntheticsServiceApiKey } from './service_api_key'; import { syntheticsServiceApiKey } from './service_api_key';
export const registerSyntheticsSavedObjects = ( export const registerSyntheticsSavedObjects = (
@ -43,64 +34,27 @@ export const registerSyntheticsSavedObjects = (
savedObjectsService.registerType(LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE); savedObjectsService.registerType(LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE);
savedObjectsService.registerType(PRIVATE_LOCATION_SAVED_OBJECT_TYPE); savedObjectsService.registerType(PRIVATE_LOCATION_SAVED_OBJECT_TYPE);
savedObjectsService.registerType(getSyntheticsMonitorSavedObjectType(encryptedSavedObjects));
savedObjectsService.registerType(syntheticsServiceApiKey);
savedObjectsService.registerType(syntheticsParamSavedObjectType);
savedObjectsService.registerType(syntheticsSettings); savedObjectsService.registerType(syntheticsSettings);
// legacy synthetics monitor saved object type which is single namespace
savedObjectsService.registerType(
getLegacySyntheticsMonitorSavedObjectType(encryptedSavedObjects)
);
encryptedSavedObjects.registerType(LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE);
// synthetics monitor config saved object type which supports multiple namespace
savedObjectsService.registerType(getSyntheticsMonitorConfigSavedObjectType());
encryptedSavedObjects.registerType(SYNTHETICS_MONITOR_ENCRYPTED_TYPE);
// service api key saved object type
savedObjectsService.registerType(syntheticsServiceApiKey);
encryptedSavedObjects.registerType({ encryptedSavedObjects.registerType({
type: syntheticsServiceApiKey.name, type: syntheticsServiceApiKey.name,
attributesToEncrypt: new Set(['apiKey']), attributesToEncrypt: new Set(['apiKey']),
attributesToIncludeInAAD: new Set(['id', 'name']), attributesToIncludeInAAD: new Set(['id', 'name']),
}); });
encryptedSavedObjects.registerType(SYNTHETICS_MONITOR_ENCRYPTED_TYPE); // global params saved object type
encryptedSavedObjects.registerType(SYNTHETICS_SECRET_ENCRYPTED_TYPE); savedObjectsService.registerType(syntheticsParamSavedObjectType);
}; encryptedSavedObjects.registerType(SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE);
export const savedObjectsAdapter = {
getSyntheticsDynamicSettings: async (
client: SavedObjectsClientContract
): Promise<DynamicSettingsAttributes> => {
try {
const obj = await client.get<DynamicSettingsAttributes>(
syntheticsSettingsObjectType,
syntheticsSettingsObjectId
);
return fromSettingsAttribute(obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES);
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
// If the object doesn't exist, check to see if uptime settings exist
return getUptimeDynamicSettings(client);
}
throw getErr;
}
},
setSyntheticsDynamicSettings: async (
client: SavedObjectsClientContract,
settings: DynamicSettingsAttributes
) => {
const settingsObject = await client.create<DynamicSettingsAttributes>(
syntheticsSettingsObjectType,
settings,
{
id: syntheticsSettingsObjectId,
overwrite: true,
}
);
return settingsObject.attributes;
},
};
const getUptimeDynamicSettings = async (client: SavedObjectsClientContract) => {
try {
const obj = await client.get<DynamicSettingsAttributes>(
uptimeSettingsObjectType,
uptimeSettingsObjectId
);
return obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES;
} catch (getErr) {
return DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES;
}
}; };

View file

@ -4,300 +4,3 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectsType } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { SyntheticsMonitorWithSecretsAttributes } from '../../common/runtime_types';
import { SyntheticsServerSetup } from '../types';
import { syntheticsMonitorType } from '../../common/types/saved_objects';
import { ConfigKey, LegacyConfigKey, secretKeys } from '../../common/constants/monitor_management';
import { monitorMigrations } from './migrations/monitors';
const attributesToIncludeInAAD = new Set([
ConfigKey.APM_SERVICE_NAME,
ConfigKey.CUSTOM_HEARTBEAT_ID,
ConfigKey.CONFIG_ID,
ConfigKey.CONFIG_HASH,
ConfigKey.ENABLED,
ConfigKey.FORM_MONITOR_TYPE,
ConfigKey.HOSTS,
ConfigKey.IGNORE_HTTPS_ERRORS,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.JOURNEY_FILTERS_MATCH,
ConfigKey.JOURNEY_FILTERS_TAGS,
ConfigKey.JOURNEY_ID,
ConfigKey.MAX_REDIRECTS,
ConfigKey.MODE,
ConfigKey.MONITOR_TYPE,
ConfigKey.NAME,
ConfigKey.NAMESPACE,
ConfigKey.LOCATIONS,
ConfigKey.PLAYWRIGHT_OPTIONS,
ConfigKey.ORIGINAL_SPACE,
ConfigKey.PORT,
ConfigKey.PROXY_URL,
ConfigKey.PROXY_USE_LOCAL_RESOLVER,
ConfigKey.RESPONSE_BODY_INDEX,
ConfigKey.RESPONSE_HEADERS_INDEX,
ConfigKey.RESPONSE_BODY_MAX_BYTES,
ConfigKey.RESPONSE_STATUS_CHECK,
ConfigKey.REQUEST_METHOD_CHECK,
ConfigKey.REVISION,
ConfigKey.SCHEDULE,
ConfigKey.SCREENSHOTS,
ConfigKey.IPV4,
ConfigKey.IPV6,
ConfigKey.PROJECT_ID,
ConfigKey.TEXT_ASSERTION,
ConfigKey.TLS_CERTIFICATE_AUTHORITIES,
ConfigKey.TLS_CERTIFICATE,
ConfigKey.TLS_VERIFICATION_MODE,
ConfigKey.TLS_VERSION,
ConfigKey.TAGS,
ConfigKey.TIMEOUT,
ConfigKey.THROTTLING_CONFIG,
ConfigKey.URLS,
ConfigKey.WAIT,
ConfigKey.MONITOR_QUERY_ID,
]);
export const LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: syntheticsMonitorType,
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,
]),
attributesToIncludeInAAD: new Set([
LegacyConfigKey.SOURCE_ZIP_URL,
LegacyConfigKey.SOURCE_ZIP_USERNAME,
LegacyConfigKey.SOURCE_ZIP_PASSWORD,
LegacyConfigKey.SOURCE_ZIP_FOLDER,
LegacyConfigKey.SOURCE_ZIP_PROXY_URL,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE,
LegacyConfigKey.ZIP_URL_TLS_KEY,
LegacyConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE,
LegacyConfigKey.ZIP_URL_TLS_VERIFICATION_MODE,
LegacyConfigKey.ZIP_URL_TLS_VERSION,
LegacyConfigKey.THROTTLING_CONFIG,
LegacyConfigKey.IS_THROTTLING_ENABLED,
LegacyConfigKey.DOWNLOAD_SPEED,
LegacyConfigKey.UPLOAD_SPEED,
LegacyConfigKey.LATENCY,
...attributesToIncludeInAAD,
]),
};
export const SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: syntheticsMonitorType,
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,
]),
attributesToIncludeInAAD,
};
export const getSyntheticsMonitorSavedObjectType = (
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectsType => {
return {
name: syntheticsMonitorType,
hidden: false,
namespaceType: 'single',
migrations: {
'8.6.0': monitorMigrations['8.6.0'](encryptedSavedObjects),
'8.8.0': monitorMigrations['8.8.0'](encryptedSavedObjects),
'8.9.0': monitorMigrations['8.9.0'](encryptedSavedObjects),
},
mappings: {
dynamic: false,
properties: {
name: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
normalizer: 'lowercase',
},
},
},
type: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
urls: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
hosts: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
journey_id: {
type: 'keyword',
},
project_id: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
origin: {
type: 'keyword',
},
hash: {
type: 'keyword',
},
locations: {
properties: {
id: {
type: 'keyword',
ignore_above: 256,
fields: {
text: {
type: 'text',
},
},
},
label: {
type: 'text',
},
},
},
custom_heartbeat_id: {
type: 'keyword',
},
id: {
type: 'keyword',
},
config_id: {
type: 'keyword',
},
tags: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
schedule: {
properties: {
number: {
type: 'integer',
},
},
},
enabled: {
type: 'boolean',
},
alert: {
properties: {
status: {
properties: {
enabled: {
type: 'boolean',
},
},
},
tls: {
properties: {
enabled: {
type: 'boolean',
},
},
},
},
},
throttling: {
properties: {
label: {
type: 'keyword',
},
},
},
maintenance_windows: {
type: 'keyword',
},
},
},
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: (savedObject) =>
savedObject.attributes.name +
' - ' +
i18n.translate('xpack.synthetics.syntheticsMonitors.label', {
defaultMessage: 'Synthetics - Monitor',
}),
},
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
config_id: { type: 'keyword' },
},
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
maintenance_windows: { type: 'keyword' },
},
},
],
},
},
};
};
export const getDecryptedMonitor = async (
server: SyntheticsServerSetup,
monitorId: string,
spaceId: string
) => {
const encryptedClient = server.encryptedSavedObjects.getClient();
return await encryptedClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: spaceId,
}
);
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export {
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from './legacy_synthetics_monitor';
export * from './synthetics_monitor_config';

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectsType } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { attributesToIncludeInAAD } from './synthetics_monitor_config';
import { monitorConfigMappings } from './monitor_mappings';
import { legacySyntheticsMonitorTypeSingle } from '../../../common/types/saved_objects';
import { LegacyConfigKey, secretKeys } from '../../../common/constants/monitor_management';
import { monitorMigrations } from '../migrations/monitors';
export const LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE = {
type: legacySyntheticsMonitorTypeSingle,
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,
]),
attributesToIncludeInAAD,
};
export const getLegacySyntheticsMonitorSavedObjectType = (
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectsType => {
return {
name: legacySyntheticsMonitorTypeSingle,
hidden: false,
namespaceType: 'single',
migrations: {
'8.6.0': monitorMigrations['8.6.0'](encryptedSavedObjects),
'8.8.0': monitorMigrations['8.8.0'](encryptedSavedObjects),
'8.9.0': monitorMigrations['8.9.0'](encryptedSavedObjects),
},
mappings: monitorConfigMappings,
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: (savedObject) =>
i18n.translate('xpack.synthetics.syntheticsMonitors.label.name', {
defaultMessage: '{name} - Synthetics - Monitor',
values: { name: savedObject.attributes.name },
}),
},
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
config_id: { type: 'keyword' },
},
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
maintenance_windows: { type: 'keyword' },
},
},
],
},
},
};
};
export const LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: legacySyntheticsMonitorTypeSingle,
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,
]),
attributesToIncludeInAAD: new Set([
LegacyConfigKey.SOURCE_ZIP_URL,
LegacyConfigKey.SOURCE_ZIP_USERNAME,
LegacyConfigKey.SOURCE_ZIP_PASSWORD,
LegacyConfigKey.SOURCE_ZIP_FOLDER,
LegacyConfigKey.SOURCE_ZIP_PROXY_URL,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE,
LegacyConfigKey.ZIP_URL_TLS_KEY,
LegacyConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE,
LegacyConfigKey.ZIP_URL_TLS_VERIFICATION_MODE,
LegacyConfigKey.ZIP_URL_TLS_VERSION,
LegacyConfigKey.THROTTLING_CONFIG,
LegacyConfigKey.IS_THROTTLING_ENABLED,
LegacyConfigKey.DOWNLOAD_SPEED,
LegacyConfigKey.UPLOAD_SPEED,
LegacyConfigKey.LATENCY,
...attributesToIncludeInAAD,
]),
};

View file

@ -0,0 +1,139 @@
/*
* 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 type { SavedObjectsTypeMappingDefinition } from '@kbn/core-saved-objects-server';
export const monitorConfigMappings: SavedObjectsTypeMappingDefinition = {
dynamic: false,
properties: {
name: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
normalizer: 'lowercase',
},
},
},
type: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
urls: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
hosts: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
journey_id: {
type: 'keyword',
},
project_id: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
origin: {
type: 'keyword',
},
hash: {
type: 'keyword',
},
locations: {
properties: {
id: {
type: 'keyword',
ignore_above: 256,
fields: {
text: {
type: 'text',
},
},
},
label: {
type: 'text',
},
},
},
custom_heartbeat_id: {
type: 'keyword',
},
id: {
type: 'keyword',
},
config_id: {
type: 'keyword',
},
tags: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
schedule: {
properties: {
number: {
type: 'integer',
},
},
},
enabled: {
type: 'boolean',
},
alert: {
properties: {
status: {
properties: {
enabled: {
type: 'boolean',
},
},
},
tls: {
properties: {
enabled: {
type: 'boolean',
},
},
},
},
},
throttling: {
properties: {
label: {
type: 'keyword',
},
},
},
maintenance_windows: {
type: 'keyword',
},
},
};

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { processMonitors } from './get_all_monitors'; import { processMonitors } from './process_monitors';
import * as getLocations from '../../synthetics_service/get_all_locations'; import * as getLocations from '../../synthetics_service/get_all_locations';
describe('processMonitors', () => { describe('processMonitors', () => {

View file

@ -0,0 +1,93 @@
/*
* 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 { SavedObjectsType } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { monitorConfigMappings } from './monitor_mappings';
import { syntheticsMonitorSavedObjectType } from '../../../common/types/saved_objects';
import { ConfigKey, secretKeys } from '../../../common/constants/monitor_management';
export const getSyntheticsMonitorConfigSavedObjectType = (): SavedObjectsType => {
return {
name: syntheticsMonitorSavedObjectType,
hidden: false,
namespaceType: 'multiple',
mappings: monitorConfigMappings,
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: (savedObject) =>
i18n.translate('xpack.synthetics.syntheticsMonitors.multiple.label', {
defaultMessage: '{name} - (Synthetics Monitor)',
values: { name: savedObject.attributes.name },
}),
},
modelVersions: {},
};
};
export const attributesToIncludeInAAD = new Set([
ConfigKey.APM_SERVICE_NAME,
ConfigKey.CUSTOM_HEARTBEAT_ID,
ConfigKey.CONFIG_ID,
ConfigKey.CONFIG_HASH,
ConfigKey.ENABLED,
ConfigKey.FORM_MONITOR_TYPE,
ConfigKey.HOSTS,
ConfigKey.IGNORE_HTTPS_ERRORS,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.JOURNEY_FILTERS_MATCH,
ConfigKey.JOURNEY_FILTERS_TAGS,
ConfigKey.JOURNEY_ID,
ConfigKey.MAX_REDIRECTS,
ConfigKey.MODE,
ConfigKey.MONITOR_TYPE,
ConfigKey.NAME,
ConfigKey.NAMESPACE,
ConfigKey.LOCATIONS,
ConfigKey.PLAYWRIGHT_OPTIONS,
ConfigKey.ORIGINAL_SPACE,
ConfigKey.PORT,
ConfigKey.PROXY_URL,
ConfigKey.PROXY_USE_LOCAL_RESOLVER,
ConfigKey.RESPONSE_BODY_INDEX,
ConfigKey.RESPONSE_HEADERS_INDEX,
ConfigKey.RESPONSE_BODY_MAX_BYTES,
ConfigKey.RESPONSE_STATUS_CHECK,
ConfigKey.REQUEST_METHOD_CHECK,
ConfigKey.REVISION,
ConfigKey.SCHEDULE,
ConfigKey.SCREENSHOTS,
ConfigKey.IPV4,
ConfigKey.IPV6,
ConfigKey.PROJECT_ID,
ConfigKey.TEXT_ASSERTION,
ConfigKey.TLS_CERTIFICATE_AUTHORITIES,
ConfigKey.TLS_CERTIFICATE,
ConfigKey.TLS_VERIFICATION_MODE,
ConfigKey.TLS_VERSION,
ConfigKey.TAGS,
ConfigKey.TIMEOUT,
ConfigKey.THROTTLING_CONFIG,
ConfigKey.URLS,
ConfigKey.WAIT,
ConfigKey.MONITOR_QUERY_ID,
]);
export const SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: syntheticsMonitorSavedObjectType,
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,
]),
attributesToIncludeInAAD,
};

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