diff --git a/docs/api/synthetics/monitors/delete-monitor-api.asciidoc b/docs/api/synthetics/monitors/delete-monitor-api.asciidoc index 70861fcd60a36..74798b40830b7 100644 --- a/docs/api/synthetics/monitors/delete-monitor-api.asciidoc +++ b/docs/api/synthetics/monitors/delete-monitor-api.asciidoc @@ -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 <>. -You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the -<>. - [[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", diff --git a/docs/api/synthetics/params/delete-param.asciidoc b/docs/api/synthetics/params/delete-param.asciidoc index 4c7d7911ec180..031a47501a8a8 100644 --- a/docs/api/synthetics/params/delete-param.asciidoc +++ b/docs/api/synthetics/params/delete-param.asciidoc @@ -8,9 +8,9 @@ Deletes one or more parameters from the Synthetics app. === {api-request-title} -`DELETE :/api/synthetics/params` +`DELETE :/api/synthetics/params/` -`DELETE :/s//api/synthetics/params` +`DELETE :/s//api/synthetics/params/` === {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 <>. -[[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 } ] --------------------------------------------------- \ No newline at end of file +-------------------------------------------------- + +==== 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"] +} +-------------------------------------------------- + + diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts index 5393fed135b7d..4107bc1efbb2a 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/synthetics_params.ts @@ -19,6 +19,8 @@ export const SyntheticsParamsReadonlyCodec = t.intersection([ }), ]); +export const SyntheticsParamsReadonlyCodecList = t.array(SyntheticsParamsReadonlyCodec); + export type SyntheticsParamsReadonly = t.TypeOf; export const SyntheticsParamsCodec = t.intersection([ diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx index 814fb13a99ba9..2615a65ef289c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx @@ -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>; }) => { - const [isDeleting, setIsDeleting] = useState(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( -

- {' '} - {i18n.translate('xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name', { - defaultMessage: 'Param {name} failed to delete.', - values: { name }, - })} -

, - coreStart - ), - }, - { toastLifeTimeMs: 3000 } - ); - } else if (status === FETCH_STATUS.SUCCESS) { - toasts.addSuccess( - { - title: toMountPoint( -

- {i18n.translate('xpack.synthetics.paramManagement.paramDeleteSuccessMessage.name', { - defaultMessage: 'Param {name} deleted successfully.', - values: { name }, - })} -

, - 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 ( setIsDeleteModalVisible(false)} - onConfirm={handleConfirmDelete} + onConfirm={() => { + dispatch(deleteGlobalParamsAction.get(items.map(({ id }) => id))); + }} cancelButtonText={NO_LABEL} confirmButtonText={YES_LABEL} buttonColor="danger" diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx index 2ff3ea547ae9f..b16dbcd686d91 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx @@ -83,7 +83,11 @@ export const ParamsList = () => { render: (val: string[]) => { const tags = val ?? []; if (tags.length === 0) { - return --; + return ( + + {i18n.translate('xpack.synthetics.columns.TextLabel', { defaultMessage: '--' })} + + ); } return ( @@ -105,7 +109,11 @@ export const ParamsList = () => { render: (val: string[]) => { const namespaces = val ?? []; if (namespaces.length === 0) { - return --; + return ( + + {i18n.translate('xpack.synthetics.columns.TextLabel', { defaultMessage: '--' })} + + ); } return ( @@ -184,6 +192,7 @@ export const ParamsList = () => { isEditingItem={isEditingItem} setIsEditingItem={setIsEditingItem} items={items} + key="add-param-flyout" />, ]; }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts index b1388bc2674b9..0faef0079657a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/actions.ts @@ -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'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts index 33eb4622bf6c5..1badb74dff26f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/api.ts @@ -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 => { return apiService.get( SYNTHETICS_API_URLS.PARAMS, { version: INITIAL_REST_VERSION }, - SyntheticsParamsReadonlyCodec + SyntheticsParamsReadonlyCodecList ); }; export const addGlobalParam = async ( paramRequest: SyntheticsParamRequest ): Promise => - 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 => - apiService.delete( - SYNTHETICS_API_URLS.PARAMS, - { version: INITIAL_REST_VERSION }, +export const deleteGlobalParams = async (ids: string[]): Promise => { + return await apiService.post( + SYNTHETICS_API_URLS.PARAMS + '/_bulk_delete', { ids, - } + }, + null, + { version: INITIAL_REST_VERSION } ); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts index d5249fcfc4519..f5f0c6e4ee951 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/effects.ts @@ -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.', +}); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts index 89b3a0b7e1904..a1e2e07ff955f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/global_params/index.ts @@ -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; }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts index 344897dd0eb1d..bef569bf0da39 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts @@ -60,12 +60,13 @@ export const fetchDeleteMonitor = async ({ }): Promise => { 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 } ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts index 1a565fe772aa6..e38a1b5ad918f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts @@ -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), diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts index 48e0de1c7fba4..f9d178befeb46 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts @@ -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, ]; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts index 2ecbbf83d471c..03c7ede49ceba 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts @@ -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) { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts index 9a031b3e7111a..ba6426de740d3 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts @@ -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>; - 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, + Record, + { 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 => { + 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 }; + }, +}); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts index f40f06f66b1ff..b989d16e4f194 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor.ts @@ -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; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts index 7b36780937694..a56f66842a703 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/delete_monitor_project.ts @@ -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 { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts index bd162fc043592..4fc527f930832 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts @@ -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>; + }) { + 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; + } + } } diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts index 78d24d9452ae9..1a504b263861b 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_param.ts @@ -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 })); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts new file mode 100644 index 0000000000000..2cafaf0a1af99 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/params/delete_params_bulk.ts @@ -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 })); + }, +}); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 35fa0b829cbee..5c676abad57c3 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -44340,8 +44340,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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 64a5214cdd770..5be78e48921dd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -44078,8 +44078,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": "縮小", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f42192834b229..3ceb78da7d2cd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -44128,8 +44128,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": "折叠", diff --git a/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts b/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts index 7b27aaa621f46..0aae85864bf16 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_edit_params.ts @@ -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); + }); }); } diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts index c96175d2982b3..f8781295e8005 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor.ts @@ -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!`; diff --git a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts index 1c7376e41c4d7..c0c15024b5401 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts @@ -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; + } } diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index 8e0aaeff21580..e0a79a8905ee8 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -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);