mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[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:
parent
b2d91b43f3
commit
f317cec25b
139 changed files with 3470 additions and 1275 deletions
|
@ -1159,6 +1159,34 @@
|
|||
"type",
|
||||
"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-private-location": [],
|
||||
"synthetics-privates-locations": [],
|
||||
|
|
|
@ -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": {
|
||||
"dynamic": false,
|
||||
"properties": {}
|
||||
|
|
|
@ -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
|
||||
// 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;
|
||||
|
|
|
@ -177,6 +177,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad",
|
||||
"synthetics-dynamic-settings": "7804b079cc502f16526f7c9491d1397cc1ec67db",
|
||||
"synthetics-monitor": "fdebfa2449d2b934972d1743dc78c34ae9ebc9c1",
|
||||
"synthetics-monitor-multi-space": "c8c9dab447ba8a7383041f55ba80757365d114c5",
|
||||
"synthetics-param": "9776c9b571d35f0d0397e8915e035ea1dc026db7",
|
||||
"synthetics-private-location": "27aaa44f792f70b734905e44e3e9b56bbeac7b86",
|
||||
"synthetics-privates-locations": "36036b881524108c7327fe14bd224c6e4d972cb5",
|
||||
|
|
|
@ -149,6 +149,7 @@ const previouslyRegisteredTypes = [
|
|||
'space',
|
||||
'spaces-usage-stats',
|
||||
'synthetics-monitor',
|
||||
'synthetics-monitor-multi-space',
|
||||
'synthetics-param',
|
||||
'synthetics-privates-locations',
|
||||
'synthetics-private-location',
|
||||
|
|
|
@ -107,6 +107,7 @@ const STANDARD_LIST_TYPES = [
|
|||
'cases-connector-mappings',
|
||||
// synthetics based objects
|
||||
'synthetics-monitor',
|
||||
'synthetics-monitor-multi-space',
|
||||
'uptime-dynamic-settings',
|
||||
'synthetics-privates-locations',
|
||||
'synthetics-private-location',
|
||||
|
|
|
@ -45848,7 +45848,6 @@
|
|||
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "Aperçu des moniteurs",
|
||||
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "Statistiques des moniteurs",
|
||||
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
|
||||
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - Moniteur",
|
||||
"xpack.synthetics.tableTitle.showing": "Affichage de {count} sur {total} {label}",
|
||||
"xpack.synthetics.tagsSelectPlaceholder": "Sélectionner des balises",
|
||||
"xpack.synthetics.testDetails.after": "Après",
|
||||
|
|
|
@ -45899,7 +45899,6 @@
|
|||
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "モニター概要",
|
||||
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "モニター統計",
|
||||
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
|
||||
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - 監視",
|
||||
"xpack.synthetics.tableTitle.showing": "{count}/{total} {label}を表示中",
|
||||
"xpack.synthetics.tagsSelectPlaceholder": "タグを選択",
|
||||
"xpack.synthetics.testDetails.after": "後",
|
||||
|
|
|
@ -45875,7 +45875,6 @@
|
|||
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "监测概览",
|
||||
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "监测统计信息",
|
||||
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
|
||||
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - 监测",
|
||||
"xpack.synthetics.tableTitle.showing": "正在显示 {count} 个(共 {total} 个){label}",
|
||||
"xpack.synthetics.tagsSelectPlaceholder": "选择标签",
|
||||
"xpack.synthetics.testDetails.after": "之后",
|
||||
|
|
|
@ -52,7 +52,7 @@ const TagsList = ({
|
|||
const tagsToDisplay = tags.slice(0, toDisplay);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup wrap gutterSize="m" css={{ maxWidth: 400 }} alignItems="baseline">
|
||||
<EuiFlexGroup wrap gutterSize="xs" css={{ maxWidth: 400 }} alignItems="baseline">
|
||||
{tagsToDisplay.map((tag) => (
|
||||
// filtering only makes sense in monitor list, where we have summary
|
||||
<EuiFlexItem key={tag} grow={false}>
|
||||
|
|
|
@ -155,6 +155,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
|
|||
[ConfigKey.LABELS]: {},
|
||||
[ConfigKey.MAX_ATTEMPTS]: 2,
|
||||
[ConfigKey.MAINTENANCE_WINDOWS]: [],
|
||||
[ConfigKey.KIBANA_SPACES]: [],
|
||||
revision: 1,
|
||||
};
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ export enum ConfigKey {
|
|||
MONITOR_QUERY_ID = 'id',
|
||||
MAX_ATTEMPTS = 'max_attempts',
|
||||
MAINTENANCE_WINDOWS = 'maintenance_windows',
|
||||
KIBANA_SPACES = 'spaces',
|
||||
}
|
||||
|
||||
export const secretKeys = [
|
||||
|
|
|
@ -88,6 +88,7 @@ export const CommonFieldsCodec = t.intersection([
|
|||
[ConfigKey.PARAMS]: t.string,
|
||||
[ConfigKey.LABELS]: t.record(t.string, t.string),
|
||||
[ConfigKey.MAINTENANCE_WINDOWS]: t.array(t.string),
|
||||
[ConfigKey.KIBANA_SPACES]: t.array(t.string),
|
||||
retest_on_failure: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
@ -357,7 +358,7 @@ const HeartbeatFieldsCodec = t.intersection([
|
|||
'monitor.id': t.string,
|
||||
'monitor.project.id': t.string,
|
||||
'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)])),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ export const OverviewStatusMetaDataCodec = t.intersection([
|
|||
projectId: t.string,
|
||||
updated_at: t.string,
|
||||
timestamp: t.string,
|
||||
spaceId: t.string,
|
||||
spaces: t.array(t.string),
|
||||
urls: t.string,
|
||||
maintenanceWindows: t.array(t.string),
|
||||
}),
|
||||
|
|
|
@ -5,7 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const syntheticsMonitorType = 'synthetics-monitor';
|
||||
export const monitorAttributes = `${syntheticsMonitorType}.attributes`;
|
||||
export const legacySyntheticsMonitorTypeSingle = 'synthetics-monitor';
|
||||
export const legacyMonitorAttributes = `${legacySyntheticsMonitorTypeSingle}.attributes`;
|
||||
|
||||
export const syntheticsMonitorSavedObjectType = 'synthetics-monitor-multi-space';
|
||||
export const syntheticsMonitorAttributes = `${syntheticsMonitorSavedObjectType}.attributes`;
|
||||
|
||||
export const syntheticsParamType = 'synthetics-param';
|
||||
|
||||
export const syntheticsMonitorSOTypes = [
|
||||
syntheticsMonitorSavedObjectType,
|
||||
legacySyntheticsMonitorTypeSingle,
|
||||
];
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
|
@ -62,14 +62,14 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
|
|||
// hash is always reset to empty string when monitor is edited
|
||||
// this ensures that when the monitor is pushed again, the monitor
|
||||
// config in the process takes precedence
|
||||
expect(omit(newConfiguration, ['updated_at'])).toEqual(
|
||||
expect(omit(newConfiguration, ['updated_at', 'created_at'])).toEqual(
|
||||
omit(
|
||||
{
|
||||
...originalMonitorConfiguration,
|
||||
hash: '',
|
||||
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
|
||||
// this ensures that when the monitor is pushed again, the monitor
|
||||
// config in the process takes precedence
|
||||
expect(omit(newConfiguration, ['updated_at'])).toEqual(
|
||||
expect(omit(newConfiguration, ['updated_at', 'created_at'])).toEqual(
|
||||
omit(
|
||||
{
|
||||
...originalMonitorConfiguration,
|
||||
|
@ -104,7 +104,7 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
|
|||
},
|
||||
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 () => {
|
||||
await addTestMonitorProject(params.kibanaUrl, monitorName);
|
||||
const repushedConfiguration = await services.getMonitor(monitorId);
|
||||
expect(omit(repushedConfiguration, ['updated_at'])).toEqual(
|
||||
expect(omit(repushedConfiguration, ['updated_at', 'created_at'])).toEqual(
|
||||
omit(
|
||||
{
|
||||
...originalMonitorConfiguration,
|
||||
revision: 4,
|
||||
},
|
||||
['updated_at']
|
||||
['updated_at', 'created_at']
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -48,7 +48,9 @@ export const cleanTestMonitors = async (params: Record<string, any>) => {
|
|||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
await server.savedObjects.clean({ types: ['synthetics-monitor'] });
|
||||
await server.savedObjects.clean({
|
||||
types: ['synthetics-monitor', 'synthetics-monitor-multi-space'],
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
|
|
|
@ -202,7 +202,9 @@ export class SyntheticsServices {
|
|||
const getService = this.params.getService;
|
||||
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();
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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 ',
|
||||
});
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { MaintenanceWindowsLink } from '../fields/maintenance_windows/create_maintenance_windows_btn';
|
||||
import { MaintenanceWindowsFieldProps } from '../fields/maintenance_windows/maintenance_windows';
|
||||
import { MonitorSpacesProps } from '../fields/monitor_spaces';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
import {
|
||||
PROFILE_OPTIONS,
|
||||
|
@ -63,6 +64,7 @@ import {
|
|||
TextArea,
|
||||
ThrottlingWrapper,
|
||||
MaintenanceWindowsFieldWrapper,
|
||||
KibanaSpacesWrapper,
|
||||
} from './field_wrappers';
|
||||
import { useMonitorName } from '../../../hooks/use_monitor_name';
|
||||
import {
|
||||
|
@ -1701,4 +1703,24 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
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);
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
EuiTextArea,
|
||||
EuiTextAreaProps,
|
||||
} from '@elastic/eui';
|
||||
import { MonitorSpaces, MonitorSpacesProps } from '../fields/monitor_spaces';
|
||||
import {
|
||||
MaintenanceWindowsField,
|
||||
MaintenanceWindowsFieldProps,
|
||||
|
@ -163,3 +164,7 @@ export const MaintenanceWindowsFieldWrapper = React.forwardRef<
|
|||
unknown,
|
||||
MaintenanceWindowsFieldProps
|
||||
>((props, _ref) => <MaintenanceWindowsField {...props} />);
|
||||
|
||||
export const KibanaSpacesWrapper = React.forwardRef<unknown, MonitorSpacesProps>((props, _ref) => (
|
||||
<MonitorSpaces {...props} />
|
||||
));
|
||||
|
|
|
@ -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 => ({
|
||||
[FormMonitorType.HTTP]: {
|
||||
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).responseChecks,
|
||||
TLS_OPTIONS(readOnly),
|
||||
KIBANA_SPACES_OPTIONS(readOnly),
|
||||
],
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
|
@ -246,6 +258,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
|
|||
TCP_ADVANCED(readOnly).requestConfig,
|
||||
TCP_ADVANCED(readOnly).responseChecks,
|
||||
TLS_OPTIONS(readOnly),
|
||||
KIBANA_SPACES_OPTIONS(readOnly),
|
||||
],
|
||||
},
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
|
@ -273,6 +286,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
|
|||
},
|
||||
MAINTENANCE_WINDOWS_OPTIONS(readOnly),
|
||||
...BROWSER_ADVANCED(readOnly),
|
||||
KIBANA_SPACES_OPTIONS(readOnly),
|
||||
],
|
||||
},
|
||||
[FormMonitorType.SINGLE]: {
|
||||
|
@ -300,6 +314,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
|
|||
},
|
||||
MAINTENANCE_WINDOWS_OPTIONS(readOnly),
|
||||
...BROWSER_ADVANCED(readOnly),
|
||||
KIBANA_SPACES_OPTIONS(readOnly),
|
||||
],
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
|
@ -319,6 +334,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
|
|||
DEFAULT_DATA_OPTIONS(readOnly),
|
||||
MAINTENANCE_WINDOWS_OPTIONS(readOnly),
|
||||
ICMP_ADVANCED(readOnly).requestConfig,
|
||||
KIBANA_SPACES_OPTIONS(readOnly),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import React, { FC, PropsWithChildren, useMemo } from 'react';
|
||||
import { EuiForm, EuiSpacer } from '@elastic/eui';
|
||||
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 { FormMonitorType, SyntheticsMonitor } from '../types';
|
||||
import { getDefaultFormFields, formatDefaultFormValues } from './defaults';
|
||||
import { ActionBar } from './submit';
|
||||
import { Disclaimer } from './disclaimer';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
export const MonitorForm: FC<
|
||||
PropsWithChildren<{
|
||||
|
@ -31,6 +35,14 @@ export const MonitorForm: FC<
|
|||
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
|
||||
* as dirty until validation unless dirtyFields is subscribed to */
|
||||
const {
|
||||
|
@ -38,17 +50,19 @@ export const MonitorForm: FC<
|
|||
} = methods;
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm
|
||||
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
|
||||
component="form"
|
||||
noValidate
|
||||
>
|
||||
{children}
|
||||
<EuiSpacer />
|
||||
<ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} />
|
||||
</EuiForm>
|
||||
<Disclaimer />
|
||||
</FormProvider>
|
||||
<ContextWrapper>
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm
|
||||
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
|
||||
component="form"
|
||||
noValidate
|
||||
>
|
||||
{children}
|
||||
<EuiSpacer />
|
||||
<ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} />
|
||||
</EuiForm>
|
||||
<Disclaimer />
|
||||
</FormProvider>
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -26,6 +26,11 @@ jest.mock('../../hooks/use_monitor_name', () => ({
|
|||
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', () => {
|
||||
const { FETCH_STATUS } = observabilitySharedPublic;
|
||||
|
||||
|
|
|
@ -168,4 +168,5 @@ export interface FieldMap {
|
|||
[ConfigKey.MAX_ATTEMPTS]: FieldMeta<ConfigKey.MAX_ATTEMPTS>;
|
||||
[ConfigKey.LABELS]: FieldMeta<ConfigKey.LABELS>;
|
||||
[ConfigKey.MAINTENANCE_WINDOWS]: FieldMeta<ConfigKey.MAINTENANCE_WINDOWS>;
|
||||
[ConfigKey.KIBANA_SPACES]: FieldMeta<ConfigKey.KIBANA_SPACES>;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ import React from 'react';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
import { FETCH_STATUS, TagsList } from '@kbn/observability-shared-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 { useEnablement } from '../../../../hooks';
|
||||
import { getMonitorSpaceToAppend, useEnablement } from '../../../../hooks';
|
||||
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
|
||||
import {
|
||||
isStatusEnabled,
|
||||
|
@ -49,7 +51,7 @@ export function useMonitorListColumns({
|
|||
setMonitorPendingDeletion: (configs: string[]) => void;
|
||||
}): Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> {
|
||||
const history = useHistory();
|
||||
const { http } = useKibana().services;
|
||||
const { http, spaces } = useKibana<ClientPluginsStart>().services;
|
||||
const canEditSynthetics = useCanEditSynthetics();
|
||||
|
||||
const { isServiceAllowed } = useEnablement();
|
||||
|
@ -69,6 +71,7 @@ export function useMonitorListColumns({
|
|||
|
||||
return publicLocations ? Boolean(canUsePublicLocations) : true;
|
||||
};
|
||||
const LazySpaceList = spaces?.ui.components.getSpaceList ?? (() => null);
|
||||
|
||||
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,
|
||||
name: i18n.translate('xpack.synthetics.management.monitorList.actions', {
|
||||
|
@ -206,9 +224,10 @@ export function useMonitorListColumns({
|
|||
isPublicLocationsAllowed(fields) &&
|
||||
isServiceAllowed,
|
||||
href: (fields) => {
|
||||
if ('spaceId' in fields && space?.id !== fields.spaceId) {
|
||||
const appendSpaceId = getMonitorSpaceToAppend(space, fields.spaces);
|
||||
if (!isEmpty(appendSpaceId)) {
|
||||
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]}`)!;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Criteria,
|
||||
EuiBasicTable,
|
||||
|
@ -16,6 +16,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 type { MonitorListSortField } from '../../../../../../../common/runtime_types/monitor_management/sort_field';
|
||||
import { DeleteMonitor } from './delete_monitor';
|
||||
|
@ -29,6 +31,7 @@ import {
|
|||
} from '../../../../../../../common/runtime_types';
|
||||
import { useMonitorListColumns } from './columns';
|
||||
import * as labels from './labels';
|
||||
import { ClientPluginsStart } from '../../../../../../plugin';
|
||||
|
||||
interface Props {
|
||||
pageState: MonitorListPageState;
|
||||
|
@ -40,6 +43,7 @@ interface Props {
|
|||
reloadPage: () => void;
|
||||
overviewStatus: OverviewStatusState | null;
|
||||
}
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
export const MonitorList = ({
|
||||
pageState: { pageIndex, pageSize, sortField, sortOrder },
|
||||
|
@ -108,9 +112,16 @@ export const MonitorList = ({
|
|||
onSelectionChange,
|
||||
initialSelected: selectedItems,
|
||||
};
|
||||
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const ContextWrapper = useMemo(
|
||||
() =>
|
||||
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
|
||||
[spacesApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextWrapper>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
|
||||
<MonitorListHeader
|
||||
recordRangeLabel={recordRangeLabel}
|
||||
|
@ -152,6 +163,6 @@ export const MonitorList = ({
|
|||
reloadPage={reloadPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -116,9 +116,9 @@ export function ActionsPopover({
|
|||
const detailUrl = useMonitorDetailLocator({
|
||||
configId: monitor.configId,
|
||||
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();
|
||||
|
||||
|
|
|
@ -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) },
|
||||
});
|
||||
};
|
|
@ -33,7 +33,7 @@ export const MonitorsTable = ({
|
|||
|
||||
const getRowProps = useCallback(
|
||||
(monitor: OverviewStatusMetaData): EuiTableRowProps => {
|
||||
const { configId, locationLabel, locationId, spaceId } = monitor;
|
||||
const { configId, locationLabel, locationId, spaces } = monitor;
|
||||
return {
|
||||
onClick: (e) => {
|
||||
// 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,
|
||||
location: locationLabel,
|
||||
locationId,
|
||||
spaceId,
|
||||
spaces,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,16 +9,16 @@ import React, { useCallback, useMemo } from 'react';
|
|||
import { EuiBasicTableColumn, EuiLink, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
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 { useMonitorHistogram } from '../../../../hooks/use_monitor_histogram';
|
||||
import {
|
||||
MonitorTypeEnum,
|
||||
OverviewStatusMetaData,
|
||||
} from '../../../../../../../../../common/runtime_types';
|
||||
import { OverviewStatusMetaData } from '../../../../../../../../../common/runtime_types';
|
||||
import { MonitorTypeBadge } from '../../../../../common/components/monitor_type_badge';
|
||||
import { getFilterForTypeMessage } from '../../../../management/monitor_list_table/labels';
|
||||
import { BadgeStatus } from '../../../../../common/components/monitor_status';
|
||||
import { FlyoutParamProps } from '../../types';
|
||||
import { MonitorsActions } from '../components/monitors_actions';
|
||||
import {
|
||||
|
@ -33,6 +33,8 @@ import {
|
|||
MONITOR_HISTORY,
|
||||
} from '../labels';
|
||||
import { MonitorsDuration } from '../components/monitors_duration';
|
||||
import { useKibanaSpace } from '../../../../../../../../hooks/use_kibana_space';
|
||||
import { ClientPluginsStart } from '../../../../../../../../plugin';
|
||||
|
||||
export const useMonitorsTableColumns = ({
|
||||
setFlyoutConfigCallback,
|
||||
|
@ -43,6 +45,12 @@ export const useMonitorsTableColumns = ({
|
|||
}) => {
|
||||
const history = useHistory();
|
||||
const { histogramsById, minInterval } = useMonitorHistogram({ items });
|
||||
const { space } = useKibanaSpace();
|
||||
const { spaces } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const {
|
||||
pageState: { showFromAllSpaces },
|
||||
} = useSelector(selectOverviewState);
|
||||
|
||||
const onClickMonitorFilter = useCallback(
|
||||
(filterName: string, filterValue: string) => {
|
||||
|
@ -60,31 +68,28 @@ export const useMonitorsTableColumns = ({
|
|||
|
||||
const openFlyout = useCallback(
|
||||
(monitor: OverviewStatusMetaData) => {
|
||||
const { configId, locationLabel, locationId, spaceId } = monitor;
|
||||
const { configId, locationLabel, locationId } = monitor;
|
||||
dispatch(
|
||||
setFlyoutConfigCallback({
|
||||
configId,
|
||||
id: configId,
|
||||
location: locationLabel,
|
||||
locationId,
|
||||
spaceId,
|
||||
spaces: monitor.spaces,
|
||||
})
|
||||
);
|
||||
},
|
||||
[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,
|
||||
render: (status: OverviewStatusMetaData['status'], monitor) => (
|
||||
<BadgeStatus
|
||||
status={status}
|
||||
isBrowserType={monitor.type === MonitorTypeEnum.BROWSER}
|
||||
onClickBadge={() => openFlyout(monitor)}
|
||||
/>
|
||||
render: (monitor: OverviewStatusMetaData) => (
|
||||
<MonitorStatusCol monitor={monitor} openFlyout={openFlyout} />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
render: (monitor: OverviewStatusMetaData) => <MonitorsActions monitor={monitor} />,
|
||||
align: 'right',
|
||||
width: '40px',
|
||||
},
|
||||
],
|
||||
[histogramsById, minInterval, onClickMonitorFilter, openFlyout]
|
||||
);
|
||||
];
|
||||
}, [
|
||||
histogramsById,
|
||||
minInterval,
|
||||
onClickMonitorFilter,
|
||||
openFlyout,
|
||||
showFromAllSpaces,
|
||||
space,
|
||||
spaces?.ui.components.getSpaceList,
|
||||
]);
|
||||
|
||||
return {
|
||||
columns,
|
||||
|
|
|
@ -33,7 +33,7 @@ import { MetricItemExtra } from './metric_item_extra';
|
|||
import { MetricItemIcon } from './metric_item_icon';
|
||||
import { FlyoutParamProps } from '../types';
|
||||
|
||||
const METRIC_ITEM_HEIGHT = 160;
|
||||
const METRIC_ITEM_HEIGHT = 170;
|
||||
|
||||
export const getColor = (euiTheme: EuiThemeComputed, isEnabled: boolean, status?: string) => {
|
||||
if (!isEnabled) {
|
||||
|
@ -171,7 +171,7 @@ export const MetricItem = ({
|
|||
id: monitor.configId,
|
||||
location: locationName,
|
||||
locationId: monitor.locationId,
|
||||
spaceId: monitor.spaceId,
|
||||
spaces: monitor.spaces,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -60,7 +60,7 @@ interface Props {
|
|||
id: string;
|
||||
location: string;
|
||||
locationId: string;
|
||||
spaceId?: string;
|
||||
spaces?: string[];
|
||||
onClose: () => void;
|
||||
onEnabledChange: () => void;
|
||||
onLocationChange: (params: FlyoutParamProps) => void;
|
||||
|
@ -220,7 +220,7 @@ export function LoadingState() {
|
|||
}
|
||||
|
||||
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 });
|
||||
|
||||
|
@ -235,13 +235,14 @@ export function MonitorDetailFlyout(props: Props) {
|
|||
|
||||
const setLocation = useCallback(
|
||||
(location: string, locationIdT: string) =>
|
||||
onLocationChange({ id, configId, location, locationId: locationIdT, spaceId }),
|
||||
[onLocationChange, id, configId, spaceId]
|
||||
onLocationChange({ id, configId, location, locationId: locationIdT, spaces }),
|
||||
[onLocationChange, id, configId, spaces]
|
||||
);
|
||||
|
||||
const detailLink = useMonitorDetailLocator({
|
||||
configId,
|
||||
locationId,
|
||||
spaces,
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
@ -265,10 +266,10 @@ export function MonitorDetailFlyout(props: Props) {
|
|||
dispatch(
|
||||
getMonitorAction.get({
|
||||
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);
|
||||
|
||||
|
@ -392,7 +393,7 @@ export const MaybeMonitorDetailsFlyout = ({
|
|||
id={flyoutConfig.id}
|
||||
location={flyoutConfig.location}
|
||||
locationId={flyoutConfig.locationId}
|
||||
spaceId={flyoutConfig.spaceId}
|
||||
spaces={flyoutConfig.spaces}
|
||||
onClose={hideFlyout}
|
||||
onEnabledChange={forceRefreshCallback}
|
||||
onLocationChange={setFlyoutConfigCallback}
|
||||
|
|
|
@ -44,7 +44,7 @@ import { MaybeMonitorDetailsFlyout } from './monitor_detail_flyout';
|
|||
import { OverviewGridCompactView } from './compact_view/overview_grid_compact_view';
|
||||
import { ViewButtons } from './view_buttons/view_buttons';
|
||||
|
||||
const ITEM_HEIGHT = 172;
|
||||
const ITEM_HEIGHT = 182;
|
||||
const ROW_COUNT = 4;
|
||||
const MAX_LIST_HEIGHT = 800;
|
||||
const MIN_BATCH_SIZE = 20;
|
||||
|
@ -189,7 +189,7 @@ export const OverviewGrid = memo(
|
|||
<EuiFlexGroup
|
||||
data-test-subj={`overview-grid-row-${listIndex}`}
|
||||
gutterSize="m"
|
||||
css={{ ...style }}
|
||||
css={{ ...style, marginLeft: 5 }}
|
||||
>
|
||||
{listData[listIndex].map((_, idx) => (
|
||||
<EuiFlexItem
|
||||
|
|
|
@ -10,5 +10,5 @@ export interface FlyoutParamProps {
|
|||
configId: string;
|
||||
location: string;
|
||||
locationId: string;
|
||||
spaceId?: string;
|
||||
spaces?: string[];
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export const SpaceSelector = <T extends FieldValues>({
|
|||
const showFieldInvalid = (isSubmitted || isTouched) && !!error;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data?.spacesDataPromise) {
|
||||
data.spacesDataPromise.then((spacesData) => {
|
||||
setSpacesList([
|
||||
allSpacesOption,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
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 { Store } from 'redux';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
|
||||
import { SyntheticsRefreshContextProvider } from './synthetics_refresh_context';
|
||||
import { SyntheticsDataViewContextProvider } from './synthetics_data_view_context';
|
||||
import { SyntheticsAppProps } from './synthetics_settings_context';
|
||||
import { storage, store } from '../state';
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
export const SyntheticsSharedContext: React.FC<
|
||||
React.PropsWithChildren<SyntheticsAppProps & { reload$?: Subject<boolean>; reduxStore?: Store }>
|
||||
> = ({ reduxStore, coreStart, setupPlugins, startPlugins, children, darkMode, reload$ }) => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const spacesApi = startPlugins.spaces;
|
||||
|
||||
const ContextWrapper = useMemo(
|
||||
() =>
|
||||
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
|
||||
[spacesApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
|
@ -60,7 +70,7 @@ export const SyntheticsSharedContext: React.FC<
|
|||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<ContextWrapper>{children}</ContextWrapper>
|
||||
</RedirectAppLinks>
|
||||
</SyntheticsDataViewContextProvider>
|
||||
</SyntheticsRefreshContextProvider>
|
||||
|
|
|
@ -9,16 +9,25 @@ import { useEffect, useState } from 'react';
|
|||
import { LocatorClient } from '@kbn/share-plugin/common/url_service/locators';
|
||||
import { syntheticsEditMonitorLocatorID } from '@kbn/observability-plugin/common';
|
||||
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 { 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({
|
||||
configId,
|
||||
locators,
|
||||
spaceId,
|
||||
spaces,
|
||||
}: {
|
||||
configId: string;
|
||||
spaceId?: string;
|
||||
spaces?: string[];
|
||||
locators?: LocatorClient;
|
||||
}) {
|
||||
const { space } = useKibanaSpace();
|
||||
|
@ -31,12 +40,12 @@ export function useEditMonitorLocator({
|
|||
async function generateUrl() {
|
||||
const url = await locator?.getUrl({
|
||||
configId,
|
||||
...(spaceId && spaceId !== space?.id ? { spaceId } : {}),
|
||||
...getMonitorSpaceToAppend(space, spaces),
|
||||
});
|
||||
setEditUrl(url);
|
||||
}
|
||||
generateUrl();
|
||||
}, [locator, configId, space, spaceId]);
|
||||
}, [locator, configId, space, spaces]);
|
||||
|
||||
return editUrl;
|
||||
}
|
||||
|
|
|
@ -8,17 +8,18 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { getMonitorSpaceToAppend } from './use_edit_monitor_locator';
|
||||
import { useKibanaSpace } from '../../../hooks/use_kibana_space';
|
||||
import { ClientPluginsStart } from '../../../plugin';
|
||||
|
||||
export function useMonitorDetailLocator({
|
||||
configId,
|
||||
locationId,
|
||||
spaceId,
|
||||
spaces,
|
||||
}: {
|
||||
configId: string;
|
||||
locationId?: string;
|
||||
spaceId?: string;
|
||||
spaces?: string[];
|
||||
}) {
|
||||
const { space } = useKibanaSpace();
|
||||
const [monitorUrl, setMonitorUrl] = useState<string | undefined>(undefined);
|
||||
|
@ -31,12 +32,12 @@ export function useMonitorDetailLocator({
|
|||
const url = await locator?.getUrl({
|
||||
configId,
|
||||
locationId,
|
||||
...(spaceId && spaceId !== space?.id ? { spaceId } : {}),
|
||||
...getMonitorSpaceToAppend(space, spaces),
|
||||
});
|
||||
setMonitorUrl(url);
|
||||
}
|
||||
generateUrl();
|
||||
}, [locator, configId, locationId, spaceId, space?.id]);
|
||||
}, [locator, configId, locationId, spaces, space?.id, space]);
|
||||
|
||||
return monitorUrl;
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ class ApiService {
|
|||
}
|
||||
|
||||
private parseApiUrl(apiUrl: string, spaceId?: string) {
|
||||
if (spaceId) {
|
||||
if (spaceId && spaceId !== 'default' && spaceId !== '*') {
|
||||
const basePath = kibanaService.coreSetup.http.basePath;
|
||||
return addSpaceIdToPath(basePath.serverBasePath, spaceId, apiUrl);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { KueryNode, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
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 {
|
||||
FINAL_SUMMARY_FILTER,
|
||||
|
@ -48,8 +49,8 @@ export async function queryFilterMonitors({
|
|||
getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
|
||||
getTimeSpanFilter(),
|
||||
{
|
||||
term: {
|
||||
'meta.space_id': spaceId,
|
||||
terms: {
|
||||
'meta.space_id': [spaceId, ALL_SPACES_ID],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
import moment from 'moment';
|
||||
import { SavedObjectsFindResult } 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 {
|
||||
AlertStatusConfigs,
|
||||
AlertPendingStatusConfigs,
|
||||
MissingPingMonitorInfo,
|
||||
} from '../../../../common/runtime_types/alert_rules/common';
|
||||
import { EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
|
||||
|
||||
export interface ConfigStats {
|
||||
up: number;
|
||||
|
@ -31,7 +31,10 @@ export const getMissingPingMonitorInfo = ({
|
|||
configId: string;
|
||||
locationId: string;
|
||||
}): (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) {
|
||||
// This should never happen
|
||||
return;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { times } from 'lodash';
|
|||
import { intersection } from 'lodash';
|
||||
import { SavedObjectsFindResult } 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 {
|
||||
AlertStatusConfigs,
|
||||
AlertStatusMetaData,
|
||||
|
|
|
@ -96,7 +96,7 @@ describe('StatusRuleExecutor', () => {
|
|||
expect(staleDownConfigs).toEqual({});
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
|
||||
filter: 'synthetics-monitor-multi-space.attributes.alert.status.enabled: true',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import { intersection, isEmpty } from 'lodash';
|
||||
import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
|
||||
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 {
|
||||
AlertOverviewStatus,
|
||||
|
@ -40,11 +41,10 @@ import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert';
|
|||
import { parseArrayFilters, parseLocationFilter } from '../../routes/common';
|
||||
import { SyntheticsServerSetup } from '../../types';
|
||||
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 { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
|
||||
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 { ALERT_DETAILS_URL, VIEW_IN_APP_URL } from '../action_variables';
|
||||
import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
|
||||
|
@ -105,7 +105,7 @@ export class StatusRuleExecutor {
|
|||
|
||||
async getMonitors() {
|
||||
const baseFilter = !this.hasCustomCondition
|
||||
? `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
|
||||
? `${syntheticsMonitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
|
||||
: '';
|
||||
|
||||
const configIds = await queryFilterMonitors({
|
||||
|
|
|
@ -62,7 +62,7 @@ describe('tlsRuleExecutor', () => {
|
|||
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
|
||||
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 = (
|
||||
ruleParams: TLSRuleParams = {}
|
||||
|
@ -110,7 +110,7 @@ describe('tlsRuleExecutor', () => {
|
|||
await tlsRule.getMonitors();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
expect(getAllMock).toHaveBeenCalledWith({
|
||||
filter: `${commonFilter} AND synthetics-monitor.attributes.type:(\"${monitorType}\")`,
|
||||
filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.type:(\"${monitorType}\")`,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,15 +14,16 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
|||
import type { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls';
|
||||
import moment from 'moment';
|
||||
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 { MonitorConfigRepository } from '../../services/monitor_config_repository';
|
||||
import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults';
|
||||
import { formatFilterString } from '../common';
|
||||
import { SyntheticsServerSetup } from '../../types';
|
||||
import { getSyntheticsCerts } from '../../queries/get_certs';
|
||||
import { savedObjectsAdapter } from '../../saved_objects';
|
||||
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 {
|
||||
CertResult,
|
||||
ConfigKey,
|
||||
|
@ -30,7 +31,6 @@ import {
|
|||
Ping,
|
||||
} from '../../../common/runtime_types';
|
||||
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 { SyntheticsEsClient } from '../../lib';
|
||||
import { queryFilterMonitors } from '../status_rule/queries/filter_monitors';
|
||||
|
@ -81,9 +81,9 @@ export class TLSRuleExecutor {
|
|||
}
|
||||
|
||||
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({
|
||||
spaceId: this.spaceId,
|
||||
|
@ -135,7 +135,7 @@ export class TLSRuleExecutor {
|
|||
async getExpiredCertificates() {
|
||||
const { enabledMonitorQueryIds } = await this.getMonitors();
|
||||
|
||||
const dynamicSettings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
|
||||
const dynamicSettings = await getSyntheticsDynamicSettings(this.soClient);
|
||||
|
||||
const expiryThreshold =
|
||||
this.params.certExpirationThreshold ??
|
||||
|
|
|
@ -15,11 +15,16 @@ import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
|||
import { DEPRECATED_ALERTING_CONSUMERS } 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 { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects';
|
||||
import {
|
||||
legacyPrivateLocationsSavedObjectName,
|
||||
privateLocationSavedObjectName,
|
||||
} from '../common/saved_objects/private_locations';
|
||||
import {
|
||||
legacySyntheticsMonitorTypeSingle,
|
||||
syntheticsMonitorSavedObjectType,
|
||||
syntheticsParamType,
|
||||
} from '../common/types/saved_objects';
|
||||
|
||||
import { PLUGIN } from '../common/constants/plugin';
|
||||
import {
|
||||
syntheticsSettingsObjectType,
|
||||
|
@ -93,7 +98,8 @@ export const syntheticsFeature = {
|
|||
savedObject: {
|
||||
all: [
|
||||
syntheticsSettingsObjectType,
|
||||
syntheticsMonitorType,
|
||||
legacySyntheticsMonitorTypeSingle,
|
||||
syntheticsMonitorSavedObjectType,
|
||||
syntheticsApiKeyObjectType,
|
||||
syntheticsParamType,
|
||||
|
||||
|
@ -124,7 +130,8 @@ export const syntheticsFeature = {
|
|||
read: [
|
||||
syntheticsParamType,
|
||||
syntheticsSettingsObjectType,
|
||||
syntheticsMonitorType,
|
||||
syntheticsMonitorSavedObjectType,
|
||||
legacySyntheticsMonitorTypeSingle,
|
||||
syntheticsApiKeyObjectType,
|
||||
privateLocationSavedObjectName,
|
||||
legacyPrivateLocationsSavedObjectName,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 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 { getSyntheticsCertsRoute } from './get_certificates';
|
||||
import { MonitorConfigRepository } from '../../services/monitor_config_repository';
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
|
||||
import { SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors';
|
||||
import { monitorAttributes } from '../../../common/types/saved_objects';
|
||||
import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { CertResult, GetCertsParams } from '../../../common/runtime_types';
|
||||
import { ConfigKey } from '../../../common/constants/monitor_management';
|
||||
|
@ -35,7 +35,7 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory<
|
|||
const queryParams = request.query;
|
||||
|
||||
const monitors = await monitorConfigRepository.getAll({
|
||||
filter: `${monitorAttributes}.${ConfigKey.ENABLED}: true`,
|
||||
filter: `${syntheticsMonitorAttributes}.${ConfigKey.ENABLED}: true`,
|
||||
});
|
||||
|
||||
if (monitors.length === 0) {
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('common utils', () => {
|
|||
configIds: ['1 4', '2 6', '5'],
|
||||
});
|
||||
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', () => {
|
||||
|
@ -23,7 +23,7 @@ describe('common utils', () => {
|
|||
tags: ['tag1', 'tag2'],
|
||||
});
|
||||
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', () => {
|
||||
|
@ -37,7 +37,7 @@ describe('common utils', () => {
|
|||
schedules: ['schedule1', 'schedule2'],
|
||||
});
|
||||
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', () => {
|
||||
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', () => {
|
||||
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', () => {
|
||||
expect(getSavedObjectKqlFilter({ field: 'tags', values: ['"apm', 'synthetics'] })).toBe(
|
||||
'synthetics-monitor.attributes.tags:("\\"apm" OR "synthetics")'
|
||||
'synthetics-monitor-multi-space.attributes.tags:("\\"apm" OR "synthetics")'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { schema, Type, TypeOf } from '@kbn/config-schema';
|
||||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { escapeQuotes } from '@kbn/es-query';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
|
@ -14,9 +13,8 @@ import { useLogicalAndFields } from '../../common/constants';
|
|||
import { RouteContext } from './types';
|
||||
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
|
||||
import { getAllLocations } from '../synthetics_service/get_all_locations';
|
||||
import { EncryptedSyntheticsMonitorAttributes } 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(
|
||||
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
|
||||
|
@ -75,36 +73,6 @@ export const SEARCH_FIELDS = [
|
|||
'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 {
|
||||
filter?: string;
|
||||
tags?: string | string[];
|
||||
|
@ -117,7 +85,8 @@ interface Filters {
|
|||
}
|
||||
|
||||
export const getMonitorFilters = async (
|
||||
context: RouteContext<Record<string, any>, OverviewStatusQuery>
|
||||
context: RouteContext<Record<string, any>, OverviewStatusQuery>,
|
||||
attr: string = syntheticsMonitorAttributes
|
||||
) => {
|
||||
const {
|
||||
tags,
|
||||
|
@ -141,7 +110,8 @@ export const getMonitorFilters = async (
|
|||
monitorQueryIds,
|
||||
locations,
|
||||
},
|
||||
useLogicalAndFor
|
||||
useLogicalAndFor,
|
||||
attr
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -156,7 +126,8 @@ export const parseArrayFilters = (
|
|||
monitorQueryIds,
|
||||
locations,
|
||||
}: Filters,
|
||||
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = []
|
||||
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [],
|
||||
attributes: string = syntheticsMonitorAttributes
|
||||
) => {
|
||||
const filtersStr = [
|
||||
filter,
|
||||
|
@ -164,17 +135,19 @@ export const parseArrayFilters = (
|
|||
field: 'tags',
|
||||
values: tags,
|
||||
operator: useLogicalAndFor.includes('tags') ? 'AND' : 'OR',
|
||||
attributes,
|
||||
}),
|
||||
getSavedObjectKqlFilter({ field: 'project_id', values: projects }),
|
||||
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes }),
|
||||
getSavedObjectKqlFilter({ field: 'project_id', values: projects, attributes }),
|
||||
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes, attributes }),
|
||||
getSavedObjectKqlFilter({
|
||||
field: 'locations.id',
|
||||
values: locations,
|
||||
operator: useLogicalAndFor.includes('locations') ? 'AND' : 'OR',
|
||||
attributes,
|
||||
}),
|
||||
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }),
|
||||
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }),
|
||||
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),
|
||||
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules, attributes }),
|
||||
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds, attributes }),
|
||||
getSavedObjectKqlFilter({ field: 'config_id', values: configIds, attributes }),
|
||||
]
|
||||
.filter((f) => !!f)
|
||||
.join(' AND ');
|
||||
|
@ -187,11 +160,13 @@ export const getSavedObjectKqlFilter = ({
|
|||
values,
|
||||
operator = 'OR',
|
||||
searchAtRoot = false,
|
||||
attributes = syntheticsMonitorAttributes,
|
||||
}: {
|
||||
field: string;
|
||||
values?: string | string[];
|
||||
operator?: string;
|
||||
searchAtRoot?: boolean;
|
||||
attributes?: string;
|
||||
}) => {
|
||||
if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) {
|
||||
return undefined;
|
||||
|
@ -204,7 +179,7 @@ export const getSavedObjectKqlFilter = ({
|
|||
if (searchAtRoot) {
|
||||
fieldKey = `${field}`;
|
||||
} else {
|
||||
fieldKey = `${monitorAttributes}.${field}`;
|
||||
fieldKey = `${attributes}.${field}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { parseDuration } from '@kbn/alerting-plugin/server';
|
||||
import { FindActionResult } from '@kbn/actions-plugin/server';
|
||||
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
|
||||
import { DynamicSettingsAttributes } from '../../runtime_types/settings';
|
||||
import { savedObjectsAdapter } from '../../saved_objects';
|
||||
import { populateAlertActions } from '../../../common/rules/alert_actions';
|
||||
import {
|
||||
SyntheticsMonitorStatusTranslations,
|
||||
|
@ -40,7 +40,7 @@ export class DefaultAlertService {
|
|||
|
||||
async getSettings() {
|
||||
if (!this.settings) {
|
||||
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
|
||||
this.settings = await getSyntheticsDynamicSettings(this.soClient);
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ export class DefaultAlertService {
|
|||
async getActionConnectors() {
|
||||
const actionsClient = (await this.context.actions)?.getActionsClient();
|
||||
if (!this.settings) {
|
||||
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
|
||||
this.settings = await getSyntheticsDynamicSettings(this.soClient);
|
||||
}
|
||||
let actionConnectors: FindActionResult[] = [];
|
||||
try {
|
||||
|
|
|
@ -5,25 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
|
||||
import { DefaultAlertService } from './default_alert_service';
|
||||
import { SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { savedObjectsAdapter } from '../../saved_objects';
|
||||
import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts';
|
||||
|
||||
export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
method: 'PUT',
|
||||
path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
|
||||
validate: {},
|
||||
handler: async ({
|
||||
request,
|
||||
context,
|
||||
server,
|
||||
savedObjectsClient,
|
||||
}): Promise<DEFAULT_ALERT_RESPONSE> => {
|
||||
handler: async ({ context, server, savedObjectsClient }): Promise<DEFAULT_ALERT_RESPONSE> => {
|
||||
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
|
||||
const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } =
|
||||
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
|
||||
const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } = await getSyntheticsDynamicSettings(
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
const updateStatusRulePromise = defaultAlertService.updateStatusRule(defaultStatusRuleEnabled);
|
||||
const updateTLSRulePromise = defaultAlertService.updateTlsRule(defaultTLSRuleEnabled);
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
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 { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
|
||||
|
@ -44,7 +48,7 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
|
|||
handler: async ({ savedObjectsClient, request }): Promise<any> => {
|
||||
const showFromAllSpaces = request.query?.showFromAllSpaces;
|
||||
const data = await savedObjectsClient.find({
|
||||
type: syntheticsMonitorType,
|
||||
type: [legacySyntheticsMonitorTypeSingle, syntheticsMonitorSavedObjectType],
|
||||
perPage: 0,
|
||||
aggs,
|
||||
...(showFromAllSpaces ? { namespaces: ['*'] } : {}),
|
||||
|
@ -87,31 +91,31 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
|
|||
const aggs = {
|
||||
monitorTypes: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
|
||||
field: `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.${ConfigKey.TAGS}`,
|
||||
field: `${syntheticsMonitorAttributes}.${ConfigKey.TAGS}`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
locations: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`,
|
||||
field: `${syntheticsMonitorAttributes}.${ConfigKey.LOCATIONS}.id`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`,
|
||||
field: `${syntheticsMonitorAttributes}.${ConfigKey.PROJECT_ID}`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.${ConfigKey.SCHEDULE}.number`,
|
||||
field: `${syntheticsMonitorAttributes}.${ConfigKey.SCHEDULE}.number`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -20,7 +20,7 @@ import { getConnectorTypesRoute } from './default_alerts/get_connector_types';
|
|||
import { getActionConnectorsRoute } from './default_alerts/get_action_connectors';
|
||||
import { SyntheticsRestApiRouteFactory } from './types';
|
||||
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 { inspectSyntheticsMonitorRoute } from './monitor_cruds/inspect_monitor';
|
||||
import { deletePackagePolicyRoute } from './monitor_cruds/delete_integration';
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
legacySyntheticsMonitorTypeSingle,
|
||||
syntheticsMonitorSavedObjectType,
|
||||
} from '../../../common/types/saved_objects';
|
||||
import { validatePermissions } from './edit_monitor';
|
||||
import {
|
||||
InvalidLocationError,
|
||||
|
@ -34,13 +38,25 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
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> => {
|
||||
const { request, response, server } = routeContext;
|
||||
const { request, response, server, spaceId } = routeContext;
|
||||
// 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);
|
||||
|
||||
|
@ -80,7 +96,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
request.body as CreateMonitorPayLoad
|
||||
);
|
||||
|
||||
const validationResult = validateMonitor(monitorWithDefaults);
|
||||
const validationResult = validateMonitor(monitorWithDefaults, spaceId);
|
||||
|
||||
if (!validationResult.valid || !validationResult.decodedMonitor) {
|
||||
const { reason: message, details } = validationResult;
|
||||
|
@ -91,7 +107,12 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
|
||||
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) {
|
||||
return response.forbidden({
|
||||
body: {
|
||||
|
@ -99,7 +120,6 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
},
|
||||
});
|
||||
}
|
||||
const nameError = await addMonitorAPI.validateUniqueMonitorName(normalizedMonitor.name);
|
||||
if (nameError) {
|
||||
return response.badRequest({
|
||||
body: { message: nameError, attributes: { details: nameError } },
|
||||
|
@ -109,6 +129,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
const { errors, newMonitor } = await addMonitorAPI.syncNewMonitor({
|
||||
id,
|
||||
normalizedMonitor,
|
||||
savedObjectType,
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { AddEditMonitorAPI } from './add_monitor_api';
|
||||
import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { SyntheticsService } from '../../../synthetics_service/synthetics_service';
|
||||
import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects';
|
||||
|
||||
describe('AddNewMonitorsPublicAPI', () => {
|
||||
it('should normalize schedule', async function () {
|
||||
|
@ -109,6 +110,7 @@ describe('AddNewMonitorsPublicAPI', () => {
|
|||
urls: '',
|
||||
labels: {},
|
||||
maintenance_windows: [],
|
||||
spaces: [],
|
||||
});
|
||||
});
|
||||
it('should normalize icmp', async () => {
|
||||
|
@ -147,6 +149,7 @@ describe('AddNewMonitorsPublicAPI', () => {
|
|||
wait: '1',
|
||||
labels: {},
|
||||
maintenance_windows: [],
|
||||
spaces: [],
|
||||
});
|
||||
});
|
||||
it('should normalize http', async () => {
|
||||
|
@ -207,6 +210,7 @@ describe('AddNewMonitorsPublicAPI', () => {
|
|||
username: '',
|
||||
labels: {},
|
||||
maintenance_windows: [],
|
||||
spaces: [],
|
||||
});
|
||||
});
|
||||
it('should normalize browser', async () => {
|
||||
|
@ -263,7 +267,61 @@ describe('AddNewMonitorsPublicAPI', () => {
|
|||
urls: '',
|
||||
labels: {},
|
||||
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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,11 +9,15 @@ import { v4 as uuidV4 } from 'uuid';
|
|||
import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
|
||||
import { isValidNamespace } from '@kbn/fleet-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
legacySyntheticsMonitorTypeSingle,
|
||||
syntheticsMonitorAttributes,
|
||||
syntheticsMonitorSavedObjectType,
|
||||
} from '../../../../common/types/saved_objects';
|
||||
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
|
||||
import { parseMonitorLocations } from './utils';
|
||||
import { MonitorValidationError } from '../monitor_validation';
|
||||
import { getSavedObjectKqlFilter } from '../../common';
|
||||
import { monitorAttributes } from '../../../../common/types/saved_objects';
|
||||
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
|
||||
import { ConfigKey } from '../../../../common/constants/monitor_management';
|
||||
import {
|
||||
|
@ -57,9 +61,11 @@ export class AddEditMonitorAPI {
|
|||
async syncNewMonitor({
|
||||
id,
|
||||
normalizedMonitor,
|
||||
savedObjectType,
|
||||
}: {
|
||||
id?: string;
|
||||
normalizedMonitor: SyntheticsMonitor;
|
||||
savedObjectType?: string;
|
||||
}) {
|
||||
const { server, syntheticsMonitorClient, spaceId } = this.routeContext;
|
||||
const newMonitorId = id ?? uuidV4();
|
||||
|
@ -74,6 +80,8 @@ export class AddEditMonitorAPI {
|
|||
const newMonitorPromise = this.routeContext.monitorConfigRepository.create({
|
||||
normalizedMonitor: monitorWithNamespace,
|
||||
id: newMonitorId,
|
||||
spaceId,
|
||||
savedObjectType,
|
||||
});
|
||||
|
||||
const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
|
||||
|
@ -205,7 +213,9 @@ export class AddEditMonitorAPI {
|
|||
const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name });
|
||||
const { total } = await monitorConfigRepository.find({
|
||||
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) {
|
||||
|
@ -217,7 +227,12 @@ export class AddEditMonitorAPI {
|
|||
}
|
||||
|
||||
initDefaultAlerts(name: string) {
|
||||
const { server, savedObjectsClient, context } = this.routeContext;
|
||||
const { server, savedObjectsClient, context, request } = this.routeContext;
|
||||
const { gettingStarted } = request.query;
|
||||
if (!gettingStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 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);
|
||||
|
@ -305,7 +320,10 @@ export class AddEditMonitorAPI {
|
|||
try {
|
||||
const encryptedMonitor = await monitorConfigRepository.get(newMonitorId);
|
||||
if (encryptedMonitor) {
|
||||
await monitorConfigRepository.delete(newMonitorId);
|
||||
await monitorConfigRepository.bulkDelete([
|
||||
{ id: newMonitorId, type: syntheticsMonitorSavedObjectType },
|
||||
{ id: newMonitorId, type: legacySyntheticsMonitorTypeSingle },
|
||||
]);
|
||||
|
||||
const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext);
|
||||
await deleteMonitorAPI.execute({
|
||||
|
|
|
@ -39,7 +39,8 @@ export const syncNewMonitorBulk = async ({
|
|||
privateLocations: SyntheticsPrivateLocations;
|
||||
spaceId: string;
|
||||
}) => {
|
||||
const { server, syntheticsMonitorClient, monitorConfigRepository } = routeContext;
|
||||
const { server, syntheticsMonitorClient, monitorConfigRepository, request } = routeContext;
|
||||
const { query } = request;
|
||||
let newMonitors: CreatedMonitors | null = null;
|
||||
|
||||
const monitorsToCreate = normalizedMonitors.map((monitor) => {
|
||||
|
@ -59,6 +60,7 @@ export const syncNewMonitorBulk = async ({
|
|||
const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([
|
||||
monitorConfigRepository.createBulk({
|
||||
monitors: monitorsToCreate,
|
||||
savedObjectType: query.savedObjectType,
|
||||
}),
|
||||
syntheticsMonitorClient.addMonitors(monitorsToCreate, privateLocations, spaceId),
|
||||
]);
|
||||
|
|
|
@ -81,6 +81,7 @@ export const syncEditedMonitorBulk = async ({
|
|||
[ConfigKey.MONITOR_QUERY_ID]:
|
||||
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
|
||||
} as unknown as MonitorFields,
|
||||
soType: decryptedPreviousMonitor.type,
|
||||
}));
|
||||
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
|
||||
monitorConfigRepository.bulkUpdate({
|
||||
|
@ -140,6 +141,7 @@ export const rollbackCompletely = async ({
|
|||
monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
|
||||
id: decryptedPreviousMonitor.id,
|
||||
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
|
||||
soType: decryptedPreviousMonitor.type,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -187,6 +189,7 @@ export const rollbackFailedUpdates = async ({
|
|||
.map(({ decryptedPreviousMonitor }) => ({
|
||||
id: decryptedPreviousMonitor.id,
|
||||
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
|
||||
soType: decryptedPreviousMonitor.type,
|
||||
}));
|
||||
|
||||
if (monitorsToRevert.length > 0) {
|
||||
|
|
|
@ -5,18 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { syncEditedMonitor } from './edit_monitor';
|
||||
import { SavedObject, SavedObjectsClientContract, KibanaRequest } from '@kbn/core/server';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import {
|
||||
EncryptedSyntheticsMonitorAttributes,
|
||||
SyntheticsMonitor,
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
} from '../../../common/runtime_types';
|
||||
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
|
||||
import { SyntheticsServerSetup } from '../../types';
|
||||
import { getRouteContextMock } from '../../mocks/route_context_mock';
|
||||
|
||||
jest.mock('../telemetry/monitor_upgrade_sender', () => ({
|
||||
sendTelemetryEvents: jest.fn(),
|
||||
|
@ -24,43 +20,6 @@ jest.mock('../telemetry/monitor_upgrade_sender', () => ({
|
|||
}));
|
||||
|
||||
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 = {
|
||||
type: 'http',
|
||||
enabled: true,
|
||||
|
@ -91,10 +50,7 @@ describe('syncEditedMonitor', () => {
|
|||
references: [],
|
||||
} as SavedObject<EncryptedSyntheticsMonitorAttributes>;
|
||||
|
||||
const syntheticsService = new SyntheticsService(serverMock);
|
||||
|
||||
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
|
||||
const { routeContext, syntheticsService, serverMock } = getRouteContextMock();
|
||||
syntheticsService.editConfig = jest.fn();
|
||||
syntheticsService.getMaintenanceWindows = jest.fn();
|
||||
|
||||
|
@ -103,13 +59,7 @@ describe('syncEditedMonitor', () => {
|
|||
normalizedMonitor: editedMonitor,
|
||||
decryptedPreviousMonitor:
|
||||
previousMonitor as unknown as SavedObject<SyntheticsMonitorWithSecretsAttributes>,
|
||||
routeContext: {
|
||||
syntheticsMonitorClient,
|
||||
server: serverMock,
|
||||
request: {} as unknown as KibanaRequest,
|
||||
savedObjectsClient:
|
||||
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
|
||||
} as any,
|
||||
routeContext,
|
||||
spaceId: 'test-space',
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema';
|
|||
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { syntheticsMonitorSavedObjectType } from '../../../common/types/saved_objects';
|
||||
import { invalidOriginError } from './add_monitor';
|
||||
import {
|
||||
InvalidLocationError,
|
||||
|
@ -15,11 +16,9 @@ import {
|
|||
} from '../../synthetics_service/project_monitor/normalizers/common_fields';
|
||||
import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
|
||||
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 { mergeSourceMonitor } from './formatters/saved_object_to_monitor';
|
||||
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
|
||||
import {
|
||||
MonitorFields,
|
||||
EncryptedSyntheticsMonitorAttributes,
|
||||
|
@ -35,7 +34,7 @@ import {
|
|||
sendTelemetryEvents,
|
||||
formatTelemetryUpdateEvent,
|
||||
} 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';
|
||||
|
||||
// Simplify return promise type and type it with runtime_types
|
||||
|
@ -59,7 +58,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
},
|
||||
},
|
||||
handler: async (routeContext): Promise<any> => {
|
||||
const { request, response, spaceId, server } = routeContext;
|
||||
const { request, response, spaceId, server, monitorConfigRepository } = routeContext;
|
||||
const { logger } = server;
|
||||
const monitor = request.body as SyntheticsMonitor;
|
||||
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
|
||||
* 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 */
|
||||
const previousMonitor = await getDecryptedMonitor(server, monitorId, spaceId);
|
||||
const normalizedPreviousMonitor = normalizeSecrets(previousMonitor).attributes;
|
||||
const { decryptedMonitor: decryptedMonitorPrevMonitor, normalizedMonitor: previousMonitor } =
|
||||
await monitorConfigRepository.getDecrypted(monitorId, spaceId);
|
||||
const normalizedPreviousMonitor = previousMonitor.attributes;
|
||||
|
||||
if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.internal) {
|
||||
return response.badRequest(getInvalidOriginError(monitor));
|
||||
|
@ -122,7 +122,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
previousMonitor.attributes.locations
|
||||
);
|
||||
|
||||
const validationResult = validateMonitor(editedMonitor as MonitorFields);
|
||||
const validationResult = validateMonitor(editedMonitor as MonitorFields, spaceId);
|
||||
|
||||
if (!validationResult.valid || !validationResult.decodedMonitor) {
|
||||
const { reason: message, details, payload } = validationResult;
|
||||
|
@ -153,7 +153,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
editedMonitor: editedMonitorSavedObject,
|
||||
} = await syncEditedMonitor({
|
||||
routeContext,
|
||||
decryptedPreviousMonitor: previousMonitor,
|
||||
decryptedPreviousMonitor: decryptedMonitorPrevMonitor,
|
||||
normalizedMonitor: monitorWithRevision,
|
||||
spaceId,
|
||||
});
|
||||
|
@ -162,7 +162,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
await rollbackUpdate({
|
||||
routeContext,
|
||||
configId: monitorId,
|
||||
attributes: previousMonitor.attributes,
|
||||
attributes: decryptedMonitorPrevMonitor.attributes,
|
||||
});
|
||||
throw hasError?.error;
|
||||
}
|
||||
|
@ -219,7 +219,11 @@ const rollbackUpdate = async ({
|
|||
}) => {
|
||||
const { savedObjectsClient, server } = routeContext;
|
||||
try {
|
||||
await savedObjectsClient.update<MonitorFields>(syntheticsMonitorType, configId, attributes);
|
||||
await savedObjectsClient.update<MonitorFields>(
|
||||
syntheticsMonitorSavedObjectType,
|
||||
configId,
|
||||
attributes
|
||||
);
|
||||
} catch (error) {
|
||||
server.logger.error(
|
||||
`Unable to rollback edit for Synthetics monitor with id ${configId}, Error: ${error.message}`,
|
||||
|
@ -241,20 +245,22 @@ export const syncEditedMonitor = async ({
|
|||
routeContext: RouteContext;
|
||||
spaceId: string;
|
||||
}) => {
|
||||
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext;
|
||||
const { server, savedObjectsClient, syntheticsMonitorClient, monitorConfigRepository } =
|
||||
routeContext;
|
||||
try {
|
||||
const monitorWithId = {
|
||||
...normalizedMonitor,
|
||||
[ConfigKey.MONITOR_QUERY_ID]:
|
||||
normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
|
||||
[ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id,
|
||||
[ConfigKey.KIBANA_SPACES]:
|
||||
normalizedMonitor[ConfigKey.KIBANA_SPACES] || decryptedPreviousMonitor.namespaces,
|
||||
};
|
||||
const formattedMonitor = formatSecrets(monitorWithId);
|
||||
|
||||
const editedSOPromise = savedObjectsClient.update<MonitorFields>(
|
||||
syntheticsMonitorType,
|
||||
const editedSOPromise = monitorConfigRepository.update(
|
||||
decryptedPreviousMonitor.id,
|
||||
formattedMonitor
|
||||
formattedMonitor,
|
||||
decryptedPreviousMonitor
|
||||
);
|
||||
|
||||
const allPrivateLocations = await getPrivateLocations(savedObjectsClient);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
|
||||
import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
|
@ -35,8 +34,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
handler: async ({
|
||||
request,
|
||||
response,
|
||||
server: { encryptedSavedObjects, coreStart },
|
||||
savedObjectsClient,
|
||||
server: { coreStart },
|
||||
spaceId,
|
||||
monitorConfigRepository,
|
||||
}): Promise<any> => {
|
||||
|
@ -54,17 +52,20 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
if (Boolean(canSave)) {
|
||||
// only user with write permissions can decrypt the monitor
|
||||
const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
|
||||
return { ...mapSavedObjectToMonitor({ monitor, internal }), spaceId };
|
||||
return {
|
||||
...mapSavedObjectToMonitor({ monitor: monitor.normalizedMonitor, internal }),
|
||||
spaceId,
|
||||
spaces: monitor.decryptedMonitor.namespaces,
|
||||
};
|
||||
} else {
|
||||
const monObj = await monitorConfigRepository.get(monitorId);
|
||||
return {
|
||||
...mapSavedObjectToMonitor({
|
||||
monitor: await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
|
||||
syntheticsMonitorType,
|
||||
monitorId
|
||||
),
|
||||
monitor: monObj,
|
||||
internal,
|
||||
}),
|
||||
spaceId,
|
||||
spaces: monObj.namespaces,
|
||||
};
|
||||
}
|
||||
} catch (getErr) {
|
||||
|
|
|
@ -4,10 +4,18 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
|
||||
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
|
||||
import { SyntheticsRestApiRouteFactory } from '../types';
|
||||
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 = () => ({
|
||||
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([
|
||||
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(),
|
||||
]);
|
||||
|
||||
|
@ -46,8 +67,9 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
|
|||
internal: request.query?.internal,
|
||||
});
|
||||
return {
|
||||
spaceId: monitor.namespaces?.[0],
|
||||
...mon,
|
||||
spaceId: monitor.namespaces?.[0],
|
||||
spaces: monitor.namespaces ?? [],
|
||||
};
|
||||
}),
|
||||
absoluteTotal,
|
||||
|
|
|
@ -39,7 +39,7 @@ export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
|
|||
...monitor,
|
||||
};
|
||||
|
||||
const validationResult = validateMonitor(monitorWithDefaults as MonitorFields);
|
||||
const validationResult = validateMonitor(monitorWithDefaults as MonitorFields, spaceId);
|
||||
|
||||
if (!validationResult.valid || !validationResult.decodedMonitor) {
|
||||
const { reason: message, details, payload } = validationResult;
|
||||
|
|
|
@ -204,7 +204,7 @@ describe('validateMonitor', () => {
|
|||
},
|
||||
locations: ['somewhere'],
|
||||
} as unknown as MonitorFields;
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
reason: 'Monitor type is invalid',
|
||||
|
@ -221,7 +221,7 @@ describe('validateMonitor', () => {
|
|||
},
|
||||
locations: ['somewhere'],
|
||||
} as unknown as MonitorFields;
|
||||
const result = validateMonitor(monitor);
|
||||
const result = validateMonitor(monitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
reason: 'Monitor type is invalid',
|
||||
|
@ -230,13 +230,16 @@ describe('validateMonitor', () => {
|
|||
});
|
||||
|
||||
it(`when schedule is not valid`, () => {
|
||||
const result = validateMonitor({
|
||||
...testICMPFields,
|
||||
schedule: {
|
||||
number: '4',
|
||||
unit: ScheduleUnit.MINUTES,
|
||||
},
|
||||
} as unknown as MonitorFields);
|
||||
const result = validateMonitor(
|
||||
{
|
||||
...testICMPFields,
|
||||
schedule: {
|
||||
number: '4',
|
||||
unit: ScheduleUnit.MINUTES,
|
||||
},
|
||||
} as unknown as MonitorFields,
|
||||
'default'
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
reason: 'Monitor schedule is invalid',
|
||||
|
@ -246,10 +249,13 @@ describe('validateMonitor', () => {
|
|||
});
|
||||
|
||||
it(`when timeout is not valid`, () => {
|
||||
const result = validateMonitor({
|
||||
...testICMPFields,
|
||||
timeout: '3m',
|
||||
} as unknown as MonitorFields);
|
||||
const result = validateMonitor(
|
||||
{
|
||||
...testICMPFields,
|
||||
timeout: '3m',
|
||||
} as unknown as MonitorFields,
|
||||
'default'
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
reason: 'Monitor is not a valid monitor of type icmp',
|
||||
|
@ -258,10 +264,13 @@ describe('validateMonitor', () => {
|
|||
});
|
||||
|
||||
it(`when location is not valid`, () => {
|
||||
const result = validateMonitor({
|
||||
...testICMPFields,
|
||||
locations: ['invalid-location'],
|
||||
} as unknown as MonitorFields);
|
||||
const result = validateMonitor(
|
||||
{
|
||||
...testICMPFields,
|
||||
locations: ['invalid-location'],
|
||||
} as unknown as MonitorFields,
|
||||
'default'
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
reason: 'Monitor is not a valid monitor of type icmp',
|
||||
|
@ -273,7 +282,7 @@ describe('validateMonitor', () => {
|
|||
describe('should validate', () => {
|
||||
it('when payload is a correct ICMP monitor', () => {
|
||||
const testMonitor = testICMPFields as MonitorFields;
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: true,
|
||||
reason: '',
|
||||
|
@ -284,7 +293,7 @@ describe('validateMonitor', () => {
|
|||
|
||||
it('when payload is a correct TCP monitor', () => {
|
||||
const testMonitor = testTCPFields as MonitorFields;
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: true,
|
||||
reason: '',
|
||||
|
@ -296,7 +305,7 @@ describe('validateMonitor', () => {
|
|||
it('when payload is a correct HTTP monitor', () => {
|
||||
const testMonitor = testHTTPFields as MonitorFields;
|
||||
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: true,
|
||||
reason: '',
|
||||
|
@ -307,7 +316,7 @@ describe('validateMonitor', () => {
|
|||
|
||||
it('when payload is not a correct Browser monitor', () => {
|
||||
const testMonitor = testBrowserFields as MonitorFields;
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
details: 'source.inline.script: Script is required for browser monitor.',
|
||||
|
@ -321,7 +330,7 @@ describe('validateMonitor', () => {
|
|||
...testBrowserFields,
|
||||
[ConfigKey.SOURCE_INLINE]: 'journey()',
|
||||
} as MonitorFields;
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
reason: 'Monitor is not a valid monitor of type browser',
|
||||
|
@ -336,7 +345,7 @@ describe('validateMonitor', () => {
|
|||
...testBrowserFields,
|
||||
[ConfigKey.SOURCE_INLINE]: 'step()',
|
||||
} as MonitorFields;
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
expect(result).toMatchObject({
|
||||
valid: true,
|
||||
reason: '',
|
||||
|
@ -355,7 +364,7 @@ describe('validateMonitor', () => {
|
|||
} as unknown as Partial<ICMPSimpleFields>),
|
||||
} 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(ConfigKey.HOSTS));
|
||||
|
@ -374,7 +383,7 @@ describe('validateMonitor', () => {
|
|||
} as unknown as Partial<TCPFields>),
|
||||
} as MonitorFields;
|
||||
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
|
||||
expect(result.details).toEqual(
|
||||
expect.stringContaining('Invalid field "host", must be a non-empty string.')
|
||||
|
@ -394,7 +403,7 @@ describe('validateMonitor', () => {
|
|||
} as unknown as Partial<HTTPFields>),
|
||||
} 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).toMatchObject({
|
||||
|
@ -412,7 +421,7 @@ describe('validateMonitor', () => {
|
|||
} as unknown as Partial<BrowserFields>),
|
||||
} as MonitorFields;
|
||||
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
|
||||
expect(result.details).toEqual(
|
||||
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 MonitorFields;
|
||||
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
valid: true,
|
||||
|
@ -451,7 +460,7 @@ describe('validateMonitor', () => {
|
|||
it('when parsed from serialized JSON', () => {
|
||||
const testMonitor = getJsonPayload() as MonitorFields;
|
||||
|
||||
const result = validateMonitor(testMonitor);
|
||||
const result = validateMonitor(testMonitor, 'default');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
valid: true,
|
||||
|
@ -463,10 +472,13 @@ describe('validateMonitor', () => {
|
|||
it('when parsed from serialized JSON for alert', () => {
|
||||
const testMonitor = getJsonPayload() as MonitorFields;
|
||||
|
||||
const result = validateMonitor({
|
||||
...testMonitor,
|
||||
alert: {},
|
||||
});
|
||||
const result = validateMonitor(
|
||||
{
|
||||
...testMonitor,
|
||||
alert: {},
|
||||
},
|
||||
'default'
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
|
@ -478,14 +490,17 @@ describe('validateMonitor', () => {
|
|||
it('when parsed from serialized JSON for alert invalid key', () => {
|
||||
const testMonitor = getJsonPayload() as MonitorFields;
|
||||
|
||||
const result = validateMonitor({
|
||||
...testMonitor,
|
||||
alert: {
|
||||
// @ts-ignore
|
||||
invalidKey: 'invalid',
|
||||
enabled: true,
|
||||
const result = validateMonitor(
|
||||
{
|
||||
...testMonitor,
|
||||
alert: {
|
||||
// @ts-ignore
|
||||
invalidKey: 'invalid',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
'default'
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
valid: false,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { isLeft } from 'fp-ts/Either';
|
||||
import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { omit, isEmpty } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { AlertConfigSchema } from '../../../common/runtime_types/monitor_management/alert_config_schema';
|
||||
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.
|
||||
* @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI.
|
||||
* @param spaceId
|
||||
*/
|
||||
export function validateMonitor(monitorFields: MonitorFields): ValidationResult {
|
||||
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields;
|
||||
export function validateMonitor(monitorFields: MonitorFields, spaceId: string): ValidationResult {
|
||||
const { [ConfigKey.MONITOR_TYPE]: monitorType, [ConfigKey.KIBANA_SPACES]: kSpaces } =
|
||||
monitorFields;
|
||||
|
||||
if (monitorType !== MonitorTypeEnum.BROWSER && !monitorFields.name) {
|
||||
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 {
|
||||
valid: true,
|
||||
reason: '',
|
||||
|
@ -230,6 +247,10 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => {
|
|||
let unsupportedKeys = Object.keys(rawConfig).filter((key) => !supportedKeys.includes(key));
|
||||
|
||||
const result = omit(rawConfig, unsupportedKeys);
|
||||
let kSpaces = rawConfig[ConfigKey.KIBANA_SPACES] as string[];
|
||||
if (kSpaces?.includes('*')) {
|
||||
kSpaces = ['*'];
|
||||
}
|
||||
|
||||
const formattedConfig = {
|
||||
...result,
|
||||
|
@ -237,6 +258,7 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => {
|
|||
private_locations: _privateLocations,
|
||||
retest_on_failure: _retestOnFailure,
|
||||
custom_heartbeat_id: _customHeartbeatId,
|
||||
...(kSpaces ? { [ConfigKey.KIBANA_SPACES]: kSpaces } : {}),
|
||||
} as CreateMonitorPayLoad;
|
||||
|
||||
const requestBodyCheck = formattedConfig[ConfigKey.REQUEST_BODY_CHECK];
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 { RouteContext, SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { ProjectMonitor } from '../../../../common/runtime_types';
|
||||
|
@ -20,6 +24,20 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
|
|||
method: 'PUT',
|
||||
path: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE,
|
||||
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({
|
||||
projectName: schema.string(),
|
||||
}),
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects';
|
||||
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { monitorAttributes } from '../../../../common/types/saved_objects';
|
||||
import { ConfigKey } from '../../../../common/runtime_types';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { getMonitors, getSavedObjectKqlFilter } from '../../common';
|
||||
import { getSavedObjectKqlFilter } from '../../common';
|
||||
import { validateSpaceId } from '../services/validate_space_id';
|
||||
|
||||
export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({
|
||||
|
@ -26,7 +26,7 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
|
|||
}),
|
||||
},
|
||||
handler: async (routeContext): Promise<any> => {
|
||||
const { request, response } = routeContext;
|
||||
const { request, response, monitorConfigRepository } = routeContext;
|
||||
const { projectName } = request.params;
|
||||
const { monitors: monitorsToDelete } = request.body;
|
||||
const decodedProjectName = decodeURI(projectName);
|
||||
|
@ -40,23 +40,19 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
|
|||
|
||||
await validateSpaceId(routeContext);
|
||||
|
||||
const deleteFilter = `${monitorAttributes}.${
|
||||
const deleteFilter = `${syntheticsMonitorAttributes}.${
|
||||
ConfigKey.PROJECT_ID
|
||||
}: "${decodedProjectName}" AND ${getSavedObjectKqlFilter({
|
||||
field: 'journey_id',
|
||||
values: monitorsToDelete.map((id: string) => `${id}`),
|
||||
})}`;
|
||||
|
||||
const { saved_objects: monitors } = await getMonitors(
|
||||
{
|
||||
...routeContext,
|
||||
request: {
|
||||
...request,
|
||||
query: { ...request.query, filter: deleteFilter, perPage: 500 },
|
||||
},
|
||||
},
|
||||
{ fields: [] }
|
||||
);
|
||||
const { saved_objects: monitors } =
|
||||
await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
|
||||
perPage: 500,
|
||||
filter: deleteFilter,
|
||||
fields: [],
|
||||
});
|
||||
|
||||
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);
|
||||
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { syntheticsMonitorSavedObjectType } from '../../../../common/types/saved_objects';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
|
||||
import { ConfigKey } from '../../../../common/runtime_types';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { getMonitors } from '../../common';
|
||||
import { SEARCH_FIELDS } from '../../common';
|
||||
|
||||
const querySchema = schema.object({
|
||||
search_after: schema.maybe(schema.string()),
|
||||
|
@ -29,6 +29,7 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory =
|
|||
const {
|
||||
request,
|
||||
server: { logger },
|
||||
monitorConfigRepository,
|
||||
} = routeContext;
|
||||
|
||||
const { projectName } = request.params;
|
||||
|
@ -37,25 +38,16 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory =
|
|||
const decodedSearchAfter = searchAfter ? decodeURI(searchAfter) : undefined;
|
||||
|
||||
try {
|
||||
const { saved_objects: monitors, total } = await getMonitors(
|
||||
{
|
||||
...routeContext,
|
||||
request: {
|
||||
...request,
|
||||
query: {
|
||||
...request.query,
|
||||
filter: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${decodedProjectName}"`,
|
||||
perPage,
|
||||
sortField: ConfigKey.JOURNEY_ID,
|
||||
sortOrder: 'asc',
|
||||
searchAfter: decodedSearchAfter ? [...decodedSearchAfter.split(',')] : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
const { saved_objects: monitors, total } =
|
||||
await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
|
||||
perPage,
|
||||
searchFields: SEARCH_FIELDS,
|
||||
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) => ({
|
||||
journey_id: monitor.attributes[ConfigKey.JOURNEY_ID],
|
||||
hash: monitor.attributes[ConfigKey.CONFIG_HASH] || '',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import pMap from 'p-map';
|
||||
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
|
||||
import { syntheticsMonitorSavedObjectType } from '../../../../common/types/saved_objects';
|
||||
import { validatePermissions } from '../edit_monitor';
|
||||
import {
|
||||
ConfigKey,
|
||||
|
@ -14,10 +15,7 @@ import {
|
|||
MonitorFields,
|
||||
SyntheticsMonitor,
|
||||
SyntheticsMonitorWithId,
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
} from '../../../../common/runtime_types';
|
||||
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
|
||||
import { normalizeSecrets } from '../../../synthetics_service/utils';
|
||||
import {
|
||||
formatTelemetryDeleteEvent,
|
||||
sendErrorTelemetryEvents,
|
||||
|
@ -50,19 +48,11 @@ export class DeleteMonitorAPI {
|
|||
}
|
||||
|
||||
async getMonitorToDelete(monitorId: string) {
|
||||
const { spaceId, savedObjectsClient, server } = this.routeContext;
|
||||
const { spaceId, savedObjectsClient, server, monitorConfigRepository } = this.routeContext;
|
||||
try {
|
||||
const encryptedSOClient = server.encryptedSavedObjects.getClient();
|
||||
const { normalizedMonitor } = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
|
||||
|
||||
const monitor =
|
||||
await encryptedSOClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
|
||||
syntheticsMonitorType,
|
||||
monitorId,
|
||||
{
|
||||
namespace: spaceId,
|
||||
}
|
||||
);
|
||||
return normalizeSecrets(monitor);
|
||||
return normalizedMonitor;
|
||||
} catch (e) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
this.result.push({
|
||||
|
@ -83,7 +73,7 @@ export class DeleteMonitorAPI {
|
|||
stackVersion: server.stackVersion,
|
||||
});
|
||||
return await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
|
||||
syntheticsMonitorType,
|
||||
syntheticsMonitorSavedObjectType,
|
||||
monitorId
|
||||
);
|
||||
}
|
||||
|
@ -146,7 +136,7 @@ export class DeleteMonitorAPI {
|
|||
);
|
||||
|
||||
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]);
|
||||
|
|
|
@ -8,7 +8,6 @@ import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
|
|||
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
|
||||
import { getUptimeESMockClient } from '../../queries/test_helpers';
|
||||
|
||||
import * as commonLibs from '../common';
|
||||
import * as allLocationsFn from '../../synthetics_service/get_all_locations';
|
||||
import { OverviewStatusService, SUMMARIES_PAGE_SIZE } from './overview_status_service';
|
||||
import times from 'lodash/times';
|
||||
|
@ -30,38 +29,15 @@ jest.spyOn(allLocationsFn, 'getAllLocations').mockResolvedValue({
|
|||
allLocations,
|
||||
});
|
||||
|
||||
jest.mock('../../saved_objects/synthetics_monitor/get_all_monitors', () => ({
|
||||
...jest.requireActual('../../saved_objects/synthetics_monitor/get_all_monitors'),
|
||||
jest.mock('../../saved_objects/synthetics_monitor/process_monitors', () => ({
|
||||
...jest.requireActual('../../saved_objects/synthetics_monitor/process_monitors'),
|
||||
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', () => {
|
||||
const testMonitors = [
|
||||
{
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
config_id: 'id1',
|
||||
id: 'id1',
|
||||
|
@ -78,6 +54,7 @@ describe('current status route', () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
namespaces: ['default'],
|
||||
attributes: {
|
||||
id: 'id2',
|
||||
config_id: 'id2',
|
||||
|
@ -187,7 +164,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 2",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "down",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -219,7 +198,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 1",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "up",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -241,7 +222,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 2",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "up",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -352,7 +335,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 2",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "down",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -384,7 +369,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 1",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "up",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -406,7 +393,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 2",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "up",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -467,7 +456,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 1",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "unknown",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -489,7 +480,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 2",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "unknown",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
@ -511,7 +504,9 @@ describe('current status route', () => {
|
|||
"name": "test monitor 2",
|
||||
"projectId": "project-id",
|
||||
"schedule": "1",
|
||||
"spaceId": undefined,
|
||||
"spaces": Array [
|
||||
"default",
|
||||
],
|
||||
"status": "unknown",
|
||||
"tags": Array [
|
||||
"tag-1",
|
||||
|
|
|
@ -10,9 +10,10 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
|||
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
|
||||
import { isEmpty } from 'lodash';
|
||||
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 { 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 { RouteContext } from '../types';
|
||||
import {
|
||||
|
@ -115,7 +116,7 @@ export class OverviewStatusService {
|
|||
];
|
||||
};
|
||||
const filters: QueryDslQueryContainer[] = [
|
||||
...(showFromAllSpaces ? [] : [{ term: { 'meta.space_id': spaceId } }]),
|
||||
...(showFromAllSpaces ? [] : [{ terms: { 'meta.space_id': [spaceId, ALL_SPACES_ID] } }]),
|
||||
...getTermFilter('monitor.type', monitorTypes),
|
||||
...getTermFilter('tags', tags),
|
||||
...getTermFilter('monitor.project.id', projects),
|
||||
|
@ -370,7 +371,7 @@ export class OverviewStatusService {
|
|||
projectId: monitor.attributes[ConfigKey.PROJECT_ID],
|
||||
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
|
||||
updated_at: monitor.updated_at,
|
||||
spaceId: monitor.namespaces?.[0],
|
||||
spaces: monitor.namespaces,
|
||||
urls: monitor.attributes[ConfigKey.URLS],
|
||||
maintenanceWindows: monitor.attributes[ConfigKey.MAINTENANCE_WINDOWS]?.map((mw) => mw),
|
||||
};
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { savedObjectsAdapter } from '../../saved_objects';
|
||||
import {
|
||||
getSyntheticsDynamicSettings,
|
||||
setSyntheticsDynamicSettings,
|
||||
} from '../../saved_objects/synthetics_settings';
|
||||
import { SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { DynamicSettings } from '../../../common/runtime_types';
|
||||
import { DynamicSettingsAttributes } from '../../runtime_types/settings';
|
||||
|
@ -20,8 +23,9 @@ export const createGetDynamicSettingsRoute: SyntheticsRestApiRouteFactory<
|
|||
path: SYNTHETICS_API_URLS.DYNAMIC_SETTINGS,
|
||||
validate: false,
|
||||
handler: async ({ savedObjectsClient }) => {
|
||||
const dynamicSettingsAttributes: DynamicSettingsAttributes =
|
||||
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
|
||||
const dynamicSettingsAttributes: DynamicSettingsAttributes = await getSyntheticsDynamicSettings(
|
||||
savedObjectsClient
|
||||
);
|
||||
return fromSettingsAttribute(dynamicSettingsAttributes);
|
||||
},
|
||||
});
|
||||
|
@ -35,9 +39,9 @@ export const createPostDynamicSettingsRoute: SyntheticsRestApiRouteFactory = ()
|
|||
writeAccess: true,
|
||||
handler: async ({ savedObjectsClient, request }): Promise<DynamicSettingsAttributes> => {
|
||||
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,
|
||||
...newSettings,
|
||||
} as DynamicSettingsAttributes);
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { getSavedObjectKqlFilter } from '../../common';
|
||||
import { PRIVATE_LOCATION_WRITE_API } from '../../../feature';
|
||||
import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations';
|
||||
import { getMonitorsByLocation } from './get_location_monitors';
|
||||
import { getPrivateLocationsAndAgentPolicies } from './get_private_locations';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
|
@ -28,7 +27,14 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory<undefined
|
|||
},
|
||||
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
|
||||
handler: async (routeContext) => {
|
||||
const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext;
|
||||
const {
|
||||
savedObjectsClient,
|
||||
syntheticsMonitorClient,
|
||||
request,
|
||||
response,
|
||||
server,
|
||||
monitorConfigRepository,
|
||||
} = routeContext;
|
||||
const internalSOClient = server.coreStart.savedObjects.createInternalRepository();
|
||||
|
||||
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 count = monitors.find((monitor) => monitor.id === locationId)?.count;
|
||||
const data = await monitorConfigRepository.find({
|
||||
perPage: 0,
|
||||
filter: locationFilter,
|
||||
});
|
||||
|
||||
if (data.total > 0) {
|
||||
return response.badRequest({
|
||||
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).`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,31 +6,36 @@
|
|||
*/
|
||||
|
||||
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 { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { monitorAttributes, syntheticsMonitorType } from '../../../../common/types/saved_objects';
|
||||
import { SyntheticsServerSetup } from '../../../types';
|
||||
import {
|
||||
legacyMonitorAttributes,
|
||||
syntheticsMonitorAttributes,
|
||||
syntheticsMonitorSOTypes,
|
||||
} from '../../../../common/types/saved_objects';
|
||||
|
||||
type Payload = Array<{
|
||||
id: string;
|
||||
count: number;
|
||||
}>;
|
||||
|
||||
interface ExpectedResponse {
|
||||
locations: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
interface Bucket {
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
const aggs = {
|
||||
locations_legacy: {
|
||||
terms: {
|
||||
field: `${legacyMonitorAttributes}.locations.id`,
|
||||
size: 20000,
|
||||
},
|
||||
},
|
||||
locations: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.locations.id`,
|
||||
size: 10000,
|
||||
field: `${syntheticsMonitorAttributes}.locations.id`,
|
||||
size: 20000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -40,27 +45,44 @@ export const getLocationMonitors: SyntheticsRestApiRouteFactory<Payload> = () =>
|
|||
path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS_MONITORS,
|
||||
|
||||
validate: {},
|
||||
handler: async ({ server }) => {
|
||||
return await getMonitorsByLocation(server);
|
||||
handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }) => {
|
||||
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,
|
||||
})) ?? []
|
||||
);
|
||||
};
|
||||
|
|
|
@ -151,6 +151,7 @@ describe('updatePrivateLocationMonitors', () => {
|
|||
schedule: { number: '10', unit: 'm' },
|
||||
namespace: FIRST_SPACE_ID,
|
||||
},
|
||||
namespaces: [FIRST_SPACE_ID],
|
||||
},
|
||||
{
|
||||
id: SECOND_MONITOR_ID,
|
||||
|
@ -166,6 +167,7 @@ describe('updatePrivateLocationMonitors', () => {
|
|||
schedule: { number: '5', unit: 'm' },
|
||||
namespace: SECOND_SPACE_ID,
|
||||
},
|
||||
namespaces: [SECOND_SPACE_ID],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -101,21 +101,21 @@ export const updatePrivateLocationMonitors = async ({
|
|||
monitorWithRevision,
|
||||
};
|
||||
|
||||
const namespace = m.attributes.namespace;
|
||||
const spaceId = m.namespaces?.[0] || 'default'; // Default to 'default' if no namespace is found
|
||||
return {
|
||||
...acc,
|
||||
[namespace]: [...(acc[namespace] || []), monitorToUpdate],
|
||||
[spaceId]: [...(acc[spaceId] || []), monitorToUpdate],
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const promises = Object.keys(updatedMonitorsPerSpace).map((namespace) => [
|
||||
const promises = Object.keys(updatedMonitorsPerSpace).map((spaceId) => [
|
||||
syncEditedMonitorBulk({
|
||||
monitorsToUpdate: updatedMonitorsPerSpace[namespace],
|
||||
monitorsToUpdate: updatedMonitorsPerSpace[spaceId],
|
||||
privateLocations: allPrivateLocations,
|
||||
routeContext,
|
||||
spaceId: namespace,
|
||||
spaceId,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
})) ?? [],
|
||||
};
|
||||
},
|
||||
});
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
|
||||
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
|
||||
|
@ -26,9 +25,9 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
|
|||
handler: async ({
|
||||
request,
|
||||
response,
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
savedObjectsClient,
|
||||
spaceId,
|
||||
}): Promise<any> => {
|
||||
const monitor = request.body as MonitorFields;
|
||||
const { monitorId } = request.params;
|
||||
|
@ -36,9 +35,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
|
|||
return response.badRequest({ body: { message: 'Monitor data is empty.' } });
|
||||
}
|
||||
|
||||
const validationResult = validateMonitor(monitor);
|
||||
|
||||
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
|
||||
const validationResult = validateMonitor(monitor, spaceId);
|
||||
|
||||
const decodedMonitor = validationResult.decodedMonitor;
|
||||
if (!validationResult.valid || !decodedMonitor) {
|
||||
|
|
|
@ -8,13 +8,11 @@ import { schema } from '@kbn/config-schema';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
|
||||
import { IKibanaResponse } from '@kbn/core-http-server';
|
||||
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
|
||||
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
|
||||
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { TestNowResponse } from '../../../common/types';
|
||||
import { ConfigKey, MonitorFields } from '../../../common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
|
||||
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
|
||||
import { getMonitorNotFoundResponse } from './service_errors';
|
||||
|
||||
|
@ -37,14 +35,19 @@ export const triggerTestNow = async (
|
|||
monitorId: string,
|
||||
routeContext: RouteContext
|
||||
): Promise<TestNowResponse | IKibanaResponse<any>> => {
|
||||
const { server, spaceId, syntheticsMonitorClient, savedObjectsClient, response } = routeContext;
|
||||
const {
|
||||
spaceId,
|
||||
syntheticsMonitorClient,
|
||||
savedObjectsClient,
|
||||
response,
|
||||
monitorConfigRepository,
|
||||
} = routeContext;
|
||||
|
||||
try {
|
||||
const monitorWithSecrets = await getDecryptedMonitor(server, monitorId, spaceId);
|
||||
const normalizedMonitor = normalizeSecrets(monitorWithSecrets);
|
||||
const { normalizedMonitor } = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
|
||||
|
||||
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } =
|
||||
monitorWithSecrets.attributes;
|
||||
normalizedMonitor.attributes;
|
||||
|
||||
const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
|
||||
savedObjectsClient,
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
* 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';
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
ConfigKey,
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
} 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<
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
|
|
|
@ -165,6 +165,7 @@ describe('Monitor migrations v8.7.0 -> v8.8.0', () => {
|
|||
urls: 'https://elastic.co',
|
||||
labels: {},
|
||||
maintenance_windows: [],
|
||||
spaces: [],
|
||||
},
|
||||
coreMigrationVersion: '8.8.0',
|
||||
created_at: '2023-03-31T20:31:24.177Z',
|
||||
|
|
|
@ -25,8 +25,8 @@ import {
|
|||
} from '../../../../common/constants/monitor_defaults';
|
||||
import {
|
||||
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
|
||||
SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
|
||||
} from '../../synthetics_monitor';
|
||||
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
|
||||
} from '../../synthetics_monitor/legacy_synthetics_monitor';
|
||||
import { validateMonitor } from '../../../routes/monitor_cruds/monitor_validation';
|
||||
import {
|
||||
formatSecrets,
|
||||
|
@ -89,7 +89,7 @@ export const migration880 = (encryptedSavedObjects: EncryptedSavedObjectsPluginS
|
|||
return migrated;
|
||||
},
|
||||
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
|
||||
// zip url fields
|
||||
|
||||
const validationResult = validateMonitor({
|
||||
...fields,
|
||||
[ConfigKey.METADATA]: updatedMetadata,
|
||||
} as MonitorFields);
|
||||
const validationResult = validateMonitor(
|
||||
{
|
||||
...fields,
|
||||
[ConfigKey.METADATA]: updatedMetadata,
|
||||
} as MonitorFields,
|
||||
fields[ConfigKey.ORIGINAL_SPACE]!
|
||||
);
|
||||
|
||||
if (!validationResult.valid || !validationResult.decodedMonitor) {
|
||||
throw new Error(
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
|
||||
import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE } from '../../synthetics_monitor/legacy_synthetics_monitor';
|
||||
import {
|
||||
ConfigKey,
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
} from '../../../../common/runtime_types';
|
||||
import { SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor';
|
||||
|
||||
export type SyntheticsMonitor890 = Omit<
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
|
@ -51,7 +51,7 @@ export const migration890 = (encryptedSavedObjects: EncryptedSavedObjectsPluginS
|
|||
|
||||
return migrated;
|
||||
},
|
||||
inputType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
|
||||
migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
|
||||
inputType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
|
||||
migratedType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,35 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsErrorHelpers,
|
||||
SavedObjectsServiceSetup,
|
||||
} from '@kbn/core/server';
|
||||
import { SavedObjectsServiceSetup } from '@kbn/core/server';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
|
||||
import { fromSettingsAttribute } from '../routes/settings/dynamic_settings';
|
||||
import {
|
||||
syntheticsSettings,
|
||||
syntheticsSettingsObjectId,
|
||||
syntheticsSettingsObjectType,
|
||||
uptimeSettingsObjectId,
|
||||
uptimeSettingsObjectType,
|
||||
} from './synthetics_settings';
|
||||
getSyntheticsMonitorConfigSavedObjectType,
|
||||
SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
|
||||
} from './synthetics_monitor/synthetics_monitor_config';
|
||||
import { syntheticsSettings } from './synthetics_settings';
|
||||
import {
|
||||
SYNTHETICS_SECRET_ENCRYPTED_TYPE,
|
||||
SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE,
|
||||
syntheticsParamSavedObjectType,
|
||||
} from './synthetics_param';
|
||||
import {
|
||||
LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE,
|
||||
PRIVATE_LOCATION_SAVED_OBJECT_TYPE,
|
||||
} from './private_locations';
|
||||
import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../constants/settings';
|
||||
import { DynamicSettingsAttributes } from '../runtime_types/settings';
|
||||
import {
|
||||
getSyntheticsMonitorSavedObjectType,
|
||||
SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
|
||||
} from './synthetics_monitor';
|
||||
getLegacySyntheticsMonitorSavedObjectType,
|
||||
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
|
||||
} from './synthetics_monitor/legacy_synthetics_monitor';
|
||||
import { syntheticsServiceApiKey } from './service_api_key';
|
||||
|
||||
export const registerSyntheticsSavedObjects = (
|
||||
|
@ -43,64 +34,27 @@ export const registerSyntheticsSavedObjects = (
|
|||
savedObjectsService.registerType(LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE);
|
||||
savedObjectsService.registerType(PRIVATE_LOCATION_SAVED_OBJECT_TYPE);
|
||||
|
||||
savedObjectsService.registerType(getSyntheticsMonitorSavedObjectType(encryptedSavedObjects));
|
||||
savedObjectsService.registerType(syntheticsServiceApiKey);
|
||||
savedObjectsService.registerType(syntheticsParamSavedObjectType);
|
||||
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({
|
||||
type: syntheticsServiceApiKey.name,
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
attributesToIncludeInAAD: new Set(['id', 'name']),
|
||||
});
|
||||
|
||||
encryptedSavedObjects.registerType(SYNTHETICS_MONITOR_ENCRYPTED_TYPE);
|
||||
encryptedSavedObjects.registerType(SYNTHETICS_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;
|
||||
}
|
||||
// global params saved object type
|
||||
savedObjectsService.registerType(syntheticsParamSavedObjectType);
|
||||
encryptedSavedObjects.registerType(SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE);
|
||||
};
|
||||
|
|
|
@ -4,300 +4,3 @@
|
|||
* 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 { 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,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
|
@ -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,
|
||||
]),
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { processMonitors } from './get_all_monitors';
|
||||
import { processMonitors } from './process_monitors';
|
||||
import * as getLocations from '../../synthetics_service/get_all_locations';
|
||||
|
||||
describe('processMonitors', () => {
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue