[Synthetics] Refactor bulk delete monitor and params routes !! (#195420)

## Summary

Refactor bulk delete monitor and params routes !! 

We need to remove usage for body from DELETE route.

### Params

Params can be bulk delete now with POST request to
`/params/_bulk_delete` endpoint

### Monitors
Monitors can be bulk delete now with POST request to
`/monitors/_bulk_delete` endpoint
This commit is contained in:
Shahzad 2024-11-07 10:51:56 +01:00 committed by GitHub
parent 12dc8c1bb8
commit d25a2b442a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 403 additions and 197 deletions

View file

@ -17,9 +17,6 @@ Deletes one or more monitors from the Synthetics app.
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[delete-monitor-api-path-params]]
=== {api-path-parms-title}
@ -27,7 +24,6 @@ You must have `all` privileges for the *Synthetics* feature in the *{observabili
`config_id`::
(Required, string) The ID of the monitor that you want to delete.
Here is an example of a DELETE request to delete a monitor by ID:
[source,sh]
@ -37,7 +33,7 @@ DELETE /api/synthetics/monitors/monitor1-id
==== Bulk Delete Monitors
You can delete multiple monitors by sending a list of config ids to a DELETE request to the `/api/synthetics/monitors` endpoint.
You can delete multiple monitors by sending a list of config ids to a POST request to the `/api/synthetics/monitors/_bulk_delete` endpoint.
[[monitors-delete-request-body]]
@ -49,11 +45,11 @@ The request body should contain an array of monitors IDs that you want to delete
(Required, array of strings) An array of monitor IDs to delete.
Here is an example of a DELETE request to delete a list of monitors by ID:
Here is an example of a POST request to delete a list of monitors by ID:
[source,sh]
--------------------------------------------------
DELETE /api/synthetics/monitors
POST /api/synthetics/monitors/_bulk_delete
{
"ids": [
"monitor1-id",

View file

@ -8,9 +8,9 @@ Deletes one or more parameters from the Synthetics app.
=== {api-request-title}
`DELETE <kibana host>:<port>/api/synthetics/params`
`DELETE <kibana host>:<port>/api/synthetics/params/<param_id>`
`DELETE <kibana host>:<port>/s/<space_id>/api/synthetics/params`
`DELETE <kibana host>:<port>/s/<space_id>/api/synthetics/params/<param_id>`
=== {api-prereq-title}
@ -20,26 +20,19 @@ You must have `all` privileges for the *Synthetics* feature in the *{observabili
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[parameters-delete-request-body]]
==== Request Body
[[parameters-delete-path-param]]
==== Path Parameters
The request body should contain an array of parameter IDs that you want to delete.
`ids`::
(Required, array of strings) An array of parameter IDs to delete.
`param_id`::
(Required, string) An id of parameter to delete.
Here is an example of a DELETE request to delete a list of parameters by ID:
Here is an example of a DELETE request to delete a parameter by its ID:
[source,sh]
--------------------------------------------------
DELETE /api/synthetics/params
{
"ids": [
"param1-id",
"param2-id"
]
}
DELETE /api/synthetics/params/param_id1
--------------------------------------------------
[[parameters-delete-response-example]]
@ -58,10 +51,21 @@ Here's an example response for deleting multiple parameters:
{
"id": "param1-id",
"deleted": true
},
{
"id": "param2-id",
"deleted": true
}
]
--------------------------------------------------
--------------------------------------------------
==== Bulk delete parameters
To delete multiple parameters, you can send a POST request to `/api/synthetics/params/_bulk_delete` with an array of parameter IDs to delete via body.
Here is an example of a POST request to delete multiple parameters:
[source,sh]
--------------------------------------------------
POST /api/synthetics/params/_bulk_delete
{
"ids": ["param1-id", "param2-id"]
}
--------------------------------------------------

View file

@ -19,6 +19,8 @@ export const SyntheticsParamsReadonlyCodec = t.intersection([
}),
]);
export const SyntheticsParamsReadonlyCodecList = t.array(SyntheticsParamsReadonlyCodec);
export type SyntheticsParamsReadonly = t.TypeOf<typeof SyntheticsParamsReadonlyCodec>;
export const SyntheticsParamsCodec = t.intersection([

View file

@ -5,16 +5,17 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { EuiConfirmModal } from '@elastic/eui';
import { FETCH_STATUS, useFetcher } from '@kbn/observability-shared-plugin/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { getGlobalParamAction, deleteGlobalParams } from '../../../state/global_params';
import { useDispatch, useSelector } from 'react-redux';
import {
getGlobalParamAction,
deleteGlobalParamsAction,
selectGlobalParamState,
} from '../../../state/global_params';
import { syncGlobalParamsAction } from '../../../state/settings';
import { kibanaService } from '../../../../../utils/kibana_service';
import { NO_LABEL, YES_LABEL } from '../../monitors_page/management/monitor_list_table/labels';
import { ListParamItem } from './params_list';
@ -25,19 +26,8 @@ export const DeleteParam = ({
items: ListParamItem[];
setIsDeleteModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const [isDeleting, setIsDeleting] = useState<boolean>(false);
const dispatch = useDispatch();
const handleConfirmDelete = () => {
setIsDeleting(true);
};
const { status } = useFetcher(() => {
if (isDeleting) {
return deleteGlobalParams(items.map(({ id }) => id));
}
}, [items, isDeleting]);
const { isDeleting, listOfParams } = useSelector(selectGlobalParamState);
const name = items
.map(({ key }) => key)
@ -45,51 +35,12 @@ export const DeleteParam = ({
.slice(0, 50);
useEffect(() => {
if (!isDeleting) {
return;
}
const { coreStart, toasts } = kibanaService;
if (status === FETCH_STATUS.FAILURE) {
toasts.addDanger(
{
title: toMountPoint(
<p data-test-subj="uptimeDeleteParamFailure">
{' '}
{i18n.translate('xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name', {
defaultMessage: 'Param {name} failed to delete.',
values: { name },
})}
</p>,
coreStart
),
},
{ toastLifeTimeMs: 3000 }
);
} else if (status === FETCH_STATUS.SUCCESS) {
toasts.addSuccess(
{
title: toMountPoint(
<p data-test-subj="uptimeDeleteParamSuccess">
{i18n.translate('xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name', {
defaultMessage: 'Param {name} deleted successfully.',
values: { name },
})}
</p>,
coreStart
),
},
{ toastLifeTimeMs: 3000 }
);
dispatch(syncGlobalParamsAction.get());
}
if (status === FETCH_STATUS.SUCCESS || status === FETCH_STATUS.FAILURE) {
setIsDeleting(false);
if (!isDeleting && (listOfParams ?? []).length === 0) {
setIsDeleteModalVisible(false);
dispatch(getGlobalParamAction.get());
dispatch(syncGlobalParamsAction.get());
}
}, [setIsDeleting, isDeleting, status, setIsDeleteModalVisible, name, dispatch]);
}, [isDeleting, setIsDeleteModalVisible, name, dispatch, listOfParams]);
return (
<EuiConfirmModal
@ -98,7 +49,9 @@ export const DeleteParam = ({
values: { name },
})}
onCancel={() => setIsDeleteModalVisible(false)}
onConfirm={handleConfirmDelete}
onConfirm={() => {
dispatch(deleteGlobalParamsAction.get(items.map(({ id }) => id)));
}}
cancelButtonText={NO_LABEL}
confirmButtonText={YES_LABEL}
buttonColor="danger"

View file

@ -83,7 +83,11 @@ export const ParamsList = () => {
render: (val: string[]) => {
const tags = val ?? [];
if (tags.length === 0) {
return <EuiText>--</EuiText>;
return (
<EuiText>
{i18n.translate('xpack.synthetics.columns.TextLabel', { defaultMessage: '--' })}
</EuiText>
);
}
return (
<EuiFlexGroup gutterSize="xs" wrap>
@ -105,7 +109,11 @@ export const ParamsList = () => {
render: (val: string[]) => {
const namespaces = val ?? [];
if (namespaces.length === 0) {
return <EuiText>--</EuiText>;
return (
<EuiText>
{i18n.translate('xpack.synthetics.columns.TextLabel', { defaultMessage: '--' })}
</EuiText>
);
}
return (
<EuiFlexGroup gutterSize="xs" wrap>
@ -184,6 +192,7 @@ export const ParamsList = () => {
isEditingItem={isEditingItem}
setIsEditingItem={setIsEditingItem}
items={items}
key="add-param-flyout"
/>,
];
};

View file

@ -23,3 +23,8 @@ export const editGlobalParamAction = createAsyncAction<
},
SyntheticsParams
>('EDIT GLOBAL PARAM');
export const deleteGlobalParamsAction = createAsyncAction<
string[],
Array<{ id: string; deleted: boolean }>
>('DELETE GLOBAL PARAMS');

View file

@ -13,6 +13,7 @@ import {
SyntheticsParams,
SyntheticsParamsCodec,
SyntheticsParamsReadonlyCodec,
SyntheticsParamsReadonlyCodecList,
} from '../../../../../common/runtime_types';
import { apiService } from '../../../../utils/api_service/api_service';
@ -20,14 +21,14 @@ export const getGlobalParams = async (): Promise<SyntheticsParams[]> => {
return apiService.get<SyntheticsParams[]>(
SYNTHETICS_API_URLS.PARAMS,
{ version: INITIAL_REST_VERSION },
SyntheticsParamsReadonlyCodec
SyntheticsParamsReadonlyCodecList
);
};
export const addGlobalParam = async (
paramRequest: SyntheticsParamRequest
): Promise<SyntheticsParams> =>
apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest, SyntheticsParamsCodec, {
apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest, SyntheticsParamsReadonlyCodec, {
version: INITIAL_REST_VERSION,
});
@ -53,11 +54,13 @@ export const editGlobalParam = async ({
);
};
export const deleteGlobalParams = async (ids: string[]): Promise<DeleteParamsResponse[]> =>
apiService.delete(
SYNTHETICS_API_URLS.PARAMS,
{ version: INITIAL_REST_VERSION },
export const deleteGlobalParams = async (ids: string[]): Promise<DeleteParamsResponse[]> => {
return await apiService.post(
SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete',
{
ids,
}
},
null,
{ version: INITIAL_REST_VERSION }
);
};

View file

@ -8,8 +8,13 @@
import { takeLeading } from 'redux-saga/effects';
import { i18n } from '@kbn/i18n';
import { fetchEffectFactory } from '../utils/fetch_effect';
import { addGlobalParam, editGlobalParam, getGlobalParams } from './api';
import { addNewGlobalParamAction, editGlobalParamAction, getGlobalParamAction } from './actions';
import { addGlobalParam, deleteGlobalParams, editGlobalParam, getGlobalParams } from './api';
import {
addNewGlobalParamAction,
deleteGlobalParamsAction,
editGlobalParamAction,
getGlobalParamAction,
} from './actions';
export function* getGlobalParamEffect() {
yield takeLeading(
@ -69,3 +74,26 @@ const editSuccessMessage = i18n.translate('xpack.synthetics.settings.editParams.
const editFailureMessage = i18n.translate('xpack.synthetics.settings.editParams.fail', {
defaultMessage: 'Failed to edit global parameter.',
});
// deleteGlobalParams
export function* deleteGlobalParamsEffect() {
yield takeLeading(
deleteGlobalParamsAction.get,
fetchEffectFactory(
deleteGlobalParams,
deleteGlobalParamsAction.success,
deleteGlobalParamsAction.fail,
deleteSuccessMessage,
deleteFailureMessage
)
);
}
const deleteSuccessMessage = i18n.translate('xpack.synthetics.settings.deleteParams.success', {
defaultMessage: 'Successfully deleted global parameters.',
});
const deleteFailureMessage = i18n.translate('xpack.synthetics.settings.deleteParams.fail', {
defaultMessage: 'Failed to delete global parameters.',
});

View file

@ -8,7 +8,12 @@
import { createReducer } from '@reduxjs/toolkit';
import { SyntheticsParams } from '../../../../../common/runtime_types';
import { IHttpSerializedFetchError } from '..';
import { addNewGlobalParamAction, editGlobalParamAction, getGlobalParamAction } from './actions';
import {
addNewGlobalParamAction,
deleteGlobalParamsAction,
editGlobalParamAction,
getGlobalParamAction,
} from './actions';
export interface GlobalParamsState {
isLoading?: boolean;
@ -16,6 +21,7 @@ export interface GlobalParamsState {
addError: IHttpSerializedFetchError | null;
editError: IHttpSerializedFetchError | null;
isSaving?: boolean;
isDeleting?: boolean;
savedData?: SyntheticsParams;
}
@ -23,6 +29,7 @@ const initialState: GlobalParamsState = {
isLoading: false,
addError: null,
isSaving: false,
isDeleting: false,
editError: null,
listOfParams: [],
};
@ -62,6 +69,16 @@ export const globalParamsReducer = createReducer(initialState, (builder) => {
.addCase(editGlobalParamAction.fail, (state, action) => {
state.isSaving = false;
state.editError = action.payload;
})
.addCase(deleteGlobalParamsAction.get, (state) => {
state.isDeleting = true;
})
.addCase(deleteGlobalParamsAction.success, (state) => {
state.isDeleting = false;
state.listOfParams = [];
})
.addCase(deleteGlobalParamsAction.fail, (state) => {
state.isDeleting = false;
});
});

View file

@ -60,12 +60,13 @@ export const fetchDeleteMonitor = async ({
}): Promise<void> => {
const baseUrl = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS;
return await apiService.delete(
baseUrl,
{ version: INITIAL_REST_VERSION, spaceId },
return await apiService.post(
baseUrl + '/_bulk_delete',
{
ids: configIds,
}
},
undefined,
{ version: INITIAL_REST_VERSION, spaceId }
);
};

View file

@ -7,7 +7,12 @@
import { all, fork } from 'redux-saga/effects';
import { getCertsListEffect } from './certs';
import { addGlobalParamEffect, editGlobalParamEffect, getGlobalParamEffect } from './global_params';
import {
addGlobalParamEffect,
deleteGlobalParamsEffect,
editGlobalParamEffect,
getGlobalParamEffect,
} from './global_params';
import { fetchManualTestRunsEffect } from './manual_test_runs/effects';
import {
enableDefaultAlertingEffect,
@ -66,6 +71,7 @@ export const rootEffect = function* root(): Generator {
fork(fetchManualTestRunsEffect),
fork(addGlobalParamEffect),
fork(editGlobalParamEffect),
fork(deleteGlobalParamsEffect),
fork(getGlobalParamEffect),
fork(getCertsListEffect),
fork(getDefaultAlertingEffect),

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { deleteSyntheticsParamsBulkRoute } from './settings/params/delete_params_bulk';
import { deleteSyntheticsMonitorBulkRoute } from './monitor_cruds/bulk_cruds/delete_monitor_bulk';
import {
createGetDynamicSettingsRoute,
createPostDynamicSettingsRoute,
@ -113,4 +115,6 @@ export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] =
addSyntheticsMonitorRoute,
editSyntheticsMonitorRoute,
deleteSyntheticsMonitorRoute,
deleteSyntheticsMonitorBulkRoute,
deleteSyntheticsParamsBulkRoute,
];

View file

@ -10,7 +10,6 @@ import { SavedObjectsBulkResponse } from '@kbn/core-saved-objects-api-server';
import { v4 as uuidV4 } from 'uuid';
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { deleteMonitorBulk } from './delete_monitor_bulk';
import { SyntheticsServerSetup } from '../../../types';
import { RouteContext } from '../../types';
import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
@ -190,9 +189,10 @@ export const deleteMonitorIfCreated = async ({
newMonitorId
);
if (encryptedMonitor) {
await deleteMonitorBulk({
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);
await deleteMonitorAPI.deleteMonitorBulk({
monitors: [encryptedMonitor],
routeContext,
});
}
} catch (e) {

View file

@ -4,63 +4,40 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from '@kbn/core-saved-objects-server';
import {
formatTelemetryDeleteEvent,
sendTelemetryEvents,
} from '../../telemetry/monitor_upgrade_sender';
import {
ConfigKey,
MonitorFields,
SyntheticsMonitor,
EncryptedSyntheticsMonitorAttributes,
SyntheticsMonitorWithId,
} from '../../../../common/runtime_types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { RouteContext } from '../../types';
export const deleteMonitorBulk = async ({
monitors,
routeContext,
}: {
monitors: Array<SavedObject<SyntheticsMonitor | EncryptedSyntheticsMonitorAttributes>>;
routeContext: RouteContext;
}) => {
const { savedObjectsClient, server, spaceId, syntheticsMonitorClient } = routeContext;
const { logger, telemetry, stackVersion } = server;
import { schema } from '@kbn/config-schema';
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { SyntheticsRestApiRouteFactory } from '../../types';
try {
const deleteSyncPromise = syntheticsMonitorClient.deleteMonitors(
monitors.map((normalizedMonitor) => ({
...normalizedMonitor.attributes,
id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID],
})) as SyntheticsMonitorWithId[],
savedObjectsClient,
spaceId
);
export const deleteSyntheticsMonitorBulkRoute: SyntheticsRestApiRouteFactory<
Array<{ id: string; deleted: boolean }>,
Record<string, string>,
Record<string, string>,
{ ids: string[] }
> = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/_bulk_delete',
validate: {},
validation: {
request: {
body: schema.object({
ids: schema.arrayOf(schema.string(), {
minSize: 1,
}),
}),
},
},
handler: async (routeContext): Promise<any> => {
const { request } = routeContext;
const deletePromises = savedObjectsClient.bulkDelete(
monitors.map((monitor) => ({ type: syntheticsMonitorType, id: monitor.id }))
);
const { ids: idsToDelete } = request.body || {};
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromises]);
monitors.forEach((monitor) => {
sendTelemetryEvents(
logger,
telemetry,
formatTelemetryDeleteEvent(
monitor,
stackVersion,
new Date().toISOString(),
Boolean((monitor.attributes as MonitorFields)[ConfigKey.SOURCE_INLINE]),
errors
)
);
const { errors, result } = await deleteMonitorAPI.execute({
monitorIds: idsToDelete,
});
return { errors, result };
} catch (e) {
throw e;
}
};
return { result, errors };
},
});

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { DeleteMonitorAPI } from './services/delete_monitor_api';
import { SyntheticsRestApiRouteFactory } from '../types';
@ -41,30 +42,39 @@ export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory<
if (ids && queryId) {
return response.badRequest({
body: { message: 'id must be provided either via param or body.' },
body: {
message: i18n.translate('xpack.synthetics.deleteMonitor.errorMultipleIdsProvided', {
defaultMessage: 'id must be provided either via param or body.',
}),
},
});
}
const idsToDelete = [...(ids ?? []), ...(queryId ? [queryId] : [])];
if (idsToDelete.length === 0) {
return response.badRequest({
body: { message: 'id must be provided via param or body.' },
body: {
message: i18n.translate('xpack.synthetics.deleteMonitor.errorMultipleIdsProvided', {
defaultMessage: 'id must be provided either via param or body.',
}),
},
});
}
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);
try {
const { errors } = await deleteMonitorAPI.execute({
monitorIds: idsToDelete,
});
const { errors } = await deleteMonitorAPI.execute({
monitorIds: idsToDelete,
});
if (errors && errors.length > 0) {
return response.ok({
body: { message: 'error pushing monitor to the service', attributes: { errors } },
});
}
} catch (getErr) {
throw getErr;
if (errors && errors.length > 0) {
return response.ok({
body: {
message: i18n.translate('xpack.synthetics.deleteMonitor.errorPushingMonitorToService', {
defaultMessage: 'Error pushing monitor to the service',
}),
attributes: { errors },
},
});
}
return deleteMonitorAPI.result;

View file

@ -6,12 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { DeleteMonitorAPI } from './services/delete_monitor_api';
import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { ConfigKey } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitors, getSavedObjectKqlFilter } from '../common';
import { deleteMonitorBulk } from './bulk_cruds/delete_monitor_bulk';
import { validateSpaceId } from './services/validate_space_id';
export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({
@ -58,9 +58,10 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
{ fields: [] }
);
await deleteMonitorBulk({
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);
await deleteMonitorAPI.deleteMonitorBulk({
monitors,
routeContext,
});
return {

View file

@ -7,16 +7,22 @@
import pMap from 'p-map';
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { deleteMonitorBulk } from '../bulk_cruds/delete_monitor_bulk';
import { validatePermissions } from '../edit_monitor';
import {
ConfigKey,
EncryptedSyntheticsMonitorAttributes,
MonitorFields,
SyntheticsMonitor,
SyntheticsMonitorWithId,
SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { normalizeSecrets } from '../../../synthetics_service/utils';
import { sendErrorTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
import {
formatTelemetryDeleteEvent,
sendErrorTelemetryEvents,
sendTelemetryEvents,
} from '../../telemetry/monitor_upgrade_sender';
import { RouteContext } from '../../types';
export class DeleteMonitorAPI {
@ -100,9 +106,8 @@ export class DeleteMonitorAPI {
}
try {
const { errors, result } = await deleteMonitorBulk({
const { errors, result } = await this.deleteMonitorBulk({
monitors,
routeContext: this.routeContext,
});
result.statuses?.forEach((res) => {
@ -112,11 +117,55 @@ export class DeleteMonitorAPI {
});
});
return { errors };
return { errors, result: this.result };
} catch (e) {
server.logger.error(`Unable to delete Synthetics monitor with error ${e.message}`);
server.logger.error(e);
throw e;
}
}
async deleteMonitorBulk({
monitors,
}: {
monitors: Array<SavedObject<SyntheticsMonitor | EncryptedSyntheticsMonitorAttributes>>;
}) {
const { savedObjectsClient, server, spaceId, syntheticsMonitorClient } = this.routeContext;
const { logger, telemetry, stackVersion } = server;
try {
const deleteSyncPromise = syntheticsMonitorClient.deleteMonitors(
monitors.map((normalizedMonitor) => ({
...normalizedMonitor.attributes,
id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID],
})) as SyntheticsMonitorWithId[],
savedObjectsClient,
spaceId
);
const deletePromises = savedObjectsClient.bulkDelete(
monitors.map((monitor) => ({ type: syntheticsMonitorType, id: monitor.id }))
);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromises]);
monitors.forEach((monitor) => {
sendTelemetryEvents(
logger,
telemetry,
formatTelemetryDeleteEvent(
monitor,
stackVersion,
new Date().toISOString(),
Boolean((monitor.attributes as MonitorFields)[ConfigKey.SOURCE_INLINE]),
errors
)
);
});
return { errors, result };
} catch (e) {
throw e;
}
}
}

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsParamType } from '../../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
@ -13,25 +14,51 @@ import { DeleteParamsResponse } from '../../../../common/runtime_types';
export const deleteSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
DeleteParamsResponse[],
unknown,
{ id?: string },
unknown,
{ ids: string[] }
> = () => ({
method: 'DELETE',
path: SYNTHETICS_API_URLS.PARAMS,
path: SYNTHETICS_API_URLS.PARAMS + '/{id?}',
validate: {},
validation: {
request: {
body: schema.object({
ids: schema.arrayOf(schema.string()),
body: schema.nullable(
schema.object({
ids: schema.arrayOf(schema.string(), {
minSize: 1,
}),
})
),
params: schema.object({
id: schema.maybe(schema.string()),
}),
},
},
handler: async ({ savedObjectsClient, request }) => {
const { ids } = request.body;
handler: async ({ savedObjectsClient, request, response }) => {
const { ids } = request.body ?? {};
const { id: paramId } = request.params ?? {};
if (ids && paramId) {
return response.badRequest({
body: i18n.translate('xpack.synthetics.deleteParam.errorMultipleIdsProvided', {
defaultMessage: `Both param id and body parameters cannot be provided`,
}),
});
}
const idsToDelete = ids ?? [paramId];
if (idsToDelete.length === 0) {
return response.badRequest({
body: i18n.translate('xpack.synthetics.deleteParam.errorNoIdsProvided', {
defaultMessage: `No param ids provided`,
}),
});
}
const result = await savedObjectsClient.bulkDelete(
ids.map((id) => ({ type: syntheticsParamType, id })),
idsToDelete.map((id) => ({ type: syntheticsParamType, id })),
{ force: true }
);
return result.statuses.map(({ id, success }) => ({ id, deleted: success }));

View file

@ -0,0 +1,39 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsParamType } from '../../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { DeleteParamsResponse } from '../../../../common/runtime_types';
export const deleteSyntheticsParamsBulkRoute: SyntheticsRestApiRouteFactory<
DeleteParamsResponse[],
unknown,
unknown,
{ ids: string[] }
> = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete',
validate: {},
validation: {
request: {
body: schema.object({
ids: schema.arrayOf(schema.string()),
}),
},
},
handler: async ({ savedObjectsClient, request }) => {
const { ids } = request.body;
const result = await savedObjectsClient.bulkDelete(
ids.map((id) => ({ type: syntheticsParamType, id })),
{ force: true }
);
return result.statuses.map(({ id, success }) => ({ id, deleted: success }));
},
});

View file

@ -44331,8 +44331,6 @@
"xpack.synthetics.paramForm.namespaces": "Espaces de noms",
"xpack.synthetics.paramForm.sharedAcrossSpacesLabel": "Partager entre les espaces",
"xpack.synthetics.paramManagement.deleteParamNameLabel": "Supprimer le paramètre \"{name}\" ?",
"xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name": "Impossible de supprimer le paramètre {name}.",
"xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name": "Paramètre {name} supprimé avec succès.",
"xpack.synthetics.params.description": "Définissez les variables et paramètres que vous pouvez utiliser dans la configuration du navigateur et des moniteurs légers, tels que des informations d'identification ou des URL. {learnMore}",
"xpack.synthetics.params.unprivileged.unprivilegedDescription": "Vous devez disposer de privilèges supplémentaires pour voir les paramètres d'utilisation et de conservation des données des applications Synthetics. {docsLink}",
"xpack.synthetics.pingList.collapseRow": "Réduire",

View file

@ -44069,8 +44069,6 @@
"xpack.synthetics.paramForm.namespaces": "名前空間",
"xpack.synthetics.paramForm.sharedAcrossSpacesLabel": "複数のスペース間で共有",
"xpack.synthetics.paramManagement.deleteParamNameLabel": "\"{name}\"パラメーターを削除しますか?",
"xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name": "パラメーター{name}の削除に失敗しました。",
"xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name": "パラメーター\"{name}\"が正常に削除されました。",
"xpack.synthetics.params.description": "ブラウザーや軽量モニターの設定に使用できる変数やパラメーター認証情報やURLなどを定義します。{learnMore}",
"xpack.synthetics.params.unprivileged.unprivilegedDescription": "Syntheticsアプリデータの使用状況と保持設定を表示する追加の権限が必要です。{docsLink}",
"xpack.synthetics.pingList.collapseRow": "縮小",

View file

@ -44119,8 +44119,6 @@
"xpack.synthetics.paramForm.namespaces": "命名空间",
"xpack.synthetics.paramForm.sharedAcrossSpacesLabel": "跨工作区共享",
"xpack.synthetics.paramManagement.deleteParamNameLabel": "删除“{name}”参数?",
"xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name": "无法删除参数 {name}。",
"xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name": "已成功删除参数 {name}。",
"xpack.synthetics.params.description": "定义可在浏览器和轻量级监测的配置中使用的变量和参数,如凭据或 URL。{learnMore}",
"xpack.synthetics.params.unprivileged.unprivilegedDescription": "您需要其他权限才能查看 Synthetics 应用数据使用情况和保留设置。{docsLink}",
"xpack.synthetics.pingList.collapseRow": "折叠",

View file

@ -353,5 +353,45 @@ export default function ({ getService }: FtrProviderContext) {
expect(param.key).to.not.empty();
});
});
it('should handle bulk deleting params', async () => {
await kServer.savedObjects.clean({ types: [syntheticsParamType] });
const params = [
{ key: 'param1', value: 'value1' },
{ key: 'param2', value: 'value2' },
{ key: 'param3', value: 'value3' },
];
for (const param of params) {
await supertestAPI
.post(SYNTHETICS_API_URLS.PARAMS)
.set('kbn-xsrf', 'true')
.send(param)
.expect(200);
}
const getResponse = await supertestAPI
.get(SYNTHETICS_API_URLS.PARAMS)
.set('kbn-xsrf', 'true')
.expect(200);
expect(getResponse.body.length).to.eql(3);
const ids = getResponse.body.map((param: any) => param.id);
await supertestAPI
.post(SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete')
.set('kbn-xsrf', 'true')
.send({ ids })
.expect(200);
const getResponseAfterDelete = await supertestAPI
.get(SYNTHETICS_API_URLS.PARAMS)
.set('kbn-xsrf', 'true')
.expect(200);
expect(getResponseAfterDelete.body.length).to.eql(0);
});
});
}

View file

@ -121,6 +121,33 @@ export default function ({ getService }: FtrProviderContext) {
await supertest.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId).expect(404);
});
it('deletes multiple monitors by bulk delete', async () => {
const { id: monitorId } = await saveMonitor(httpMonitorJson as MonitorFields);
const { id: monitorId2 } = await saveMonitor({
...httpMonitorJson,
name: 'another -2',
} as MonitorFields);
const deleteResponse = await monitorTestService.deleteMonitorBulk(
[monitorId2, monitorId],
200
);
expect(
deleteResponse.body.result.sort((a: { id: string }, b: { id: string }) =>
a.id > b.id ? 1 : -1
)
).eql(
[
{ id: monitorId2, deleted: true },
{ id: monitorId, deleted: true },
].sort((a, b) => (a.id > b.id ? 1 : -1))
);
// Hit get endpoint and expect 404 as well
await supertest.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId).expect(404);
});
it('returns 404 if monitor id is not found', async () => {
const invalidMonitorId = 'invalid-id';
const expected404Message = `Monitor id ${invalidMonitorId} not found!`;

View file

@ -230,4 +230,17 @@ export class SyntheticsMonitorTestService {
expect(deleteResponse.status).to.eql(statusCode);
return deleteResponse;
}
async deleteMonitorBulk(monitorIds: string[], statusCode = 200, spaceId?: string) {
const deleteResponse = await this.supertest
.post(
spaceId
? `/s/${spaceId}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/_bulk_delete`
: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/_bulk_delete'
)
.send({ ids: monitorIds })
.set('kbn-xsrf', 'true');
expect(deleteResponse.status).to.eql(statusCode);
return deleteResponse;
}
}

View file

@ -287,8 +287,9 @@ export default function ({ getService }: FtrProviderContext) {
const deleteResponse = await supertestAPI
.delete(SYNTHETICS_API_URLS.PARAMS)
.set('kbn-xsrf', 'true')
.send({ ids })
.expect(200);
.send({ ids });
expect(deleteResponse.status).eql(200);
expect(deleteResponse.body).to.have.length(2);