diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml
index 4a1c4aee7947..93c4f5f98801 100644
--- a/oas_docs/output/kibana.yaml
+++ b/oas_docs/output/kibana.yaml
@@ -56759,6 +56759,69 @@ paths:
summary: Get a private location
tags:
- synthetics
+ put:
+ description: |
+ Update an existing private location's label.
+ You must have `all` privileges for the Synthetics and Uptime feature in the Observability section of the Kibana feature privileges.
+ When a private location's label is updated, all monitors using this location will also be updated to maintain data consistency.
+ operationId: put-private-location
+ parameters:
+ - description: The unique identifier of the private location to be updated.
+ in: path
+ name: id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ examples:
+ putPrivateLocationRequestExample1:
+ description: Update a private location's label.
+ value: |-
+ {
+ "label": "Updated Private Location Name"
+ }
+ schema:
+ type: object
+ properties:
+ label:
+ description: A new label for the private location. Must be at least 1 character long.
+ minLength: 1
+ type: string
+ required:
+ - label
+ required: true
+ responses:
+ '200':
+ content:
+ application/json:
+ examples:
+ putPrivateLocationResponseExample1:
+ value: |-
+ {
+ "label": "Updated Private Location Name",
+ "id": "test-private-location-id",
+ "agentPolicyId": "test-private-location-id",
+ "isServiceManaged": false,
+ "isInvalid": false,
+ "tags": ["private", "testing", "updated"],
+ "geo": {
+ "lat": 37.7749,
+ "lon": -122.4194
+ },
+ "spaces": ["*"]
+ }
+ schema:
+ $ref: '#/components/schemas/Synthetics_getPrivateLocation'
+ description: A successful response.
+ '400':
+ description: If the `label` is shorter than 1 character the API will return a 400 Bad Request response with a corresponding error message.
+ '404':
+ description: If the private location with the specified ID does not exist, the API will return a 404 Not Found response.
+ summary: Update a private location
+ tags:
+ - synthetics
/api/task_manager/_health:
get:
description: |
diff --git a/x-pack/solutions/observability/plugins/synthetics/common/constants/ui.ts b/x-pack/solutions/observability/plugins/synthetics/common/constants/ui.ts
index 6f3fd250dcfe..ed7c652923db 100644
--- a/x-pack/solutions/observability/plugins/synthetics/common/constants/ui.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/common/constants/ui.ts
@@ -28,7 +28,7 @@ export const GETTING_STARTED_ROUTE = '/monitors/getting-started';
export const SETTINGS_ROUTE = '/settings';
-export const PRIVATE_LOCATIOSN_ROUTE = '/settings/private-locations';
+export const PRIVATE_LOCATIONS_ROUTE = '/settings/private-locations';
export const SYNTHETICS_SETTINGS_ROUTE = '/settings/:tabId';
diff --git a/x-pack/solutions/observability/plugins/synthetics/docs/openapi/synthetic_apis.yaml b/x-pack/solutions/observability/plugins/synthetics/docs/openapi/synthetic_apis.yaml
index d639f4cbc5f1..04f7d9a162bc 100644
--- a/x-pack/solutions/observability/plugins/synthetics/docs/openapi/synthetic_apis.yaml
+++ b/x-pack/solutions/observability/plugins/synthetics/docs/openapi/synthetic_apis.yaml
@@ -1024,6 +1024,71 @@ paths:
},
"namespace": "default"
}
+ put:
+ summary: Update a private location
+ operationId: put-private-location
+ description: >
+ Update an existing private location's label.
+
+ You must have `all` privileges for the Synthetics and Uptime feature in the Observability section of the Kibana feature privileges.
+
+ When a private location's label is updated, all monitors using this location will also be updated to maintain data consistency.
+ tags:
+ - synthetics
+ parameters:
+ - in: path
+ name: id
+ description: The unique identifier of the private location to be updated.
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - label
+ properties:
+ label:
+ type: string
+ minLength: 1
+ description: A new label for the private location. Must be at least 1 character long.
+ examples:
+ putPrivateLocationRequestExample1:
+ description: Update a private location's label.
+ value: |-
+ {
+ "label": "Updated Private Location Name"
+ }
+ responses:
+ '200':
+ description: A successful response.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/getPrivateLocation"
+ examples:
+ putPrivateLocationResponseExample1:
+ value: |-
+ {
+ "label": "Updated Private Location Name",
+ "id": "test-private-location-id",
+ "agentPolicyId": "test-private-location-id",
+ "isServiceManaged": false,
+ "isInvalid": false,
+ "tags": ["private", "testing", "updated"],
+ "geo": {
+ "lat": 37.7749,
+ "lon": -122.4194
+ },
+ "spaces": ["*"]
+ }
+ '400':
+ description: If the `label` is shorter than 1 character the API will return a 400 Bad Request response with a corresponding error message.
+ '404':
+ description: If the private location with the specified ID does not exist, the API will return a 404 Not Found response.
components:
schemas:
commonMonitorFields:
diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/private_locations.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/private_locations.journey.ts
index cdc596199157..d1ea98301f11 100644
--- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/private_locations.journey.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/private_locations.journey.ts
@@ -15,6 +15,7 @@ import { syntheticsAppPageProvider } from '../page_objects/synthetics_app';
journey(`PrivateLocationsSettings`, async ({ page, params }) => {
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
const services = new SyntheticsServices(params);
+ const NEW_LOCATION_LABEL = 'Updated Test Location';
page.setDefaultTimeout(2 * 30000);
@@ -87,11 +88,33 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => {
});
});
+ step('Edit private location label and verify disabled fields', async () => {
+ // Click on the edit button for the location
+ await page.click('[data-test-subj="action-edit"]');
+
+ // Verify that agent policy selector is disabled
+ expect(await page.locator('[aria-label="Select agent policy"]').isDisabled()).toBe(true);
+
+ // Verify that tags field is disabled
+ expect(await page.locator('[aria-label="Tags"]').isDisabled()).toBe(false);
+
+ // Verify that spaces selector is disabled
+ expect(await page.locator('[aria-label="Spaces "]').isDisabled()).toBe(true);
+
+ await page.fill('[aria-label="Location name"]', NEW_LOCATION_LABEL);
+
+ // Save the changes
+ await page.click('[data-test-subj="syntheticsLocationFlyoutSaveButton"]');
+
+ // Wait for the save to complete and verify the updated label appears in the table
+ await page.waitForSelector(`td:has-text("${NEW_LOCATION_LABEL}")`);
+ });
+
step('Integration cannot be edited in Fleet', async () => {
await page.goto(`${params.kibanaUrl}/app/integrations/detail/synthetics/policies`);
await page.waitForSelector('h1:has-text("Elastic Synthetics")');
- await page.click('text="test-monitor-Test private-default"');
+ await page.click(`text="test-monitor-${NEW_LOCATION_LABEL}-default"`);
await page.waitForSelector('h1:has-text("Edit Elastic Synthetics integration")');
await page.waitForSelector('text="This package policy is managed by the Synthetics app."');
});
@@ -111,14 +134,14 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => {
await page.click('h1:has-text("Settings")');
await page.click('text=Private Locations');
await page.waitForSelector('td:has-text("1")');
- await page.waitForSelector('td:has-text("Test private")');
+ await page.waitForSelector(`td:has-text("${NEW_LOCATION_LABEL}")`);
await page.click('.euiTableRowCell .euiToolTipAnchor');
await page.click('button:has-text("Tags")');
await page.click('[aria-label="Tags"] >> text=Area51');
await page.click(
'main div:has-text("Private locations allow you to run monitors from your own premises. They require")'
);
- await page.click('text=Test private');
+ await page.click(`text=${NEW_LOCATION_LABEL}`);
await page.click('.euiTableRowCell .euiToolTipAnchor');
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx
index 1a35504699cb..cc4e845726e4 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx
@@ -21,8 +21,9 @@ describe('GettingStartedPage', () => {
loading: false,
privateLocations: [],
deleteLoading: false,
- onSubmit: jest.fn(),
- onDelete: jest.fn(),
+ onCreateLocationAPI: jest.fn(),
+ onDeleteLocationAPI: jest.fn(),
+ onEditLocationAPI: jest.fn(),
createLoading: false,
});
jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true);
@@ -82,7 +83,7 @@ describe('GettingStartedPage', () => {
loading: false,
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: true,
+ isPrivateLocationFlyoutVisible: true,
},
agentPolicies: {
data: [],
@@ -112,7 +113,7 @@ describe('GettingStartedPage', () => {
data: [{}],
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: true,
+ isPrivateLocationFlyoutVisible: true,
},
},
});
@@ -151,7 +152,7 @@ describe('GettingStartedPage', () => {
data: [{}],
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: true,
+ isPrivateLocationFlyoutVisible: true,
},
},
}
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx
index c102536e0454..53563eb1fb30 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx
@@ -27,11 +27,14 @@ import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_
import { getServiceLocations, cleanMonitorListState } from '../../state';
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui';
import { SimpleMonitorForm } from './simple_monitor_form';
-import { AddLocationFlyout, NewLocation } from '../settings/private_locations/add_location_flyout';
+import {
+ AddOrEditLocationFlyout,
+ NewLocation,
+} from '../settings/private_locations/add_or_edit_location_flyout';
import type { ClientPluginsStart } from '../../../../plugin';
import { getAgentPoliciesAction, selectAgentPolicies } from '../../state/agent_policies';
-import { selectAddingNewPrivateLocation } from '../../state/settings/selectors';
-import { setIsCreatePrivateLocationFlyoutVisible } from '../../state/private_locations/actions';
+import { setIsPrivateLocationFlyoutVisible } from '../../state/private_locations/actions';
+import { selectPrivateLocationFlyoutVisible } from '../../state/private_locations/selectors';
export const GettingStartedPage = () => {
const dispatch = useDispatch();
@@ -127,23 +130,23 @@ export const GettingStartedOnPrem = () => {
useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview
- const isAddingNewLocation = useSelector(selectAddingNewPrivateLocation);
+ const isPrivateLocationFlyoutVisible = useSelector(selectPrivateLocationFlyoutVisible);
- const setIsAddingNewLocation = useCallback(
- (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
+ const setIsFlyoutOpen = useCallback(
+ (val: boolean) => dispatch(setIsPrivateLocationFlyoutVisible(val)),
[dispatch]
);
- const { onSubmit, privateLocations } = usePrivateLocationsAPI();
+ const { onCreateLocationAPI, privateLocations } = usePrivateLocationsAPI();
const handleSubmit = (formData: NewLocation) => {
- onSubmit(formData);
+ onCreateLocationAPI(formData);
};
// make sure flyout is closed when first visiting the page
useEffect(() => {
- setIsAddingNewLocation(false);
- }, [setIsAddingNewLocation]);
+ setIsFlyoutOpen(false);
+ }, [setIsFlyoutOpen]);
return (
<>
@@ -163,7 +166,7 @@ export const GettingStartedOnPrem = () => {
fill
iconType="plusInCircleFilled"
data-test-subj="gettingStartedAddLocationButton"
- onClick={() => setIsAddingNewLocation(true)}
+ onClick={() => setIsFlyoutOpen(true)}
>
{CREATE_LOCATION_LABEL}
@@ -173,9 +176,9 @@ export const GettingStartedOnPrem = () => {
footer={}
/>
- {isAddingNewLocation ? (
- setIsFlyoutOpen(false)}
onSubmit={handleSubmit}
privateLocations={privateLocations}
/>
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx
index 51fecfef7f94..54dbc743dab0 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx
@@ -16,9 +16,13 @@ import { ClientPluginsStart } from '../../../../../plugin';
interface SpaceSelectorProps {
helpText: string;
+ isDisabled?: boolean;
}
-export const SpaceSelector = ({ helpText }: SpaceSelectorProps) => {
+export const SpaceSelector = ({
+ helpText,
+ isDisabled = false,
+}: SpaceSelectorProps) => {
const NAMESPACES_NAME = 'spaces' as Path;
const { services } = useKibana();
const [spacesList, setSpacesList] = React.useState>([]);
@@ -61,6 +65,7 @@ export const SpaceSelector = ({ helpText }: SpaceSelector
rules={{ required: true }}
render={({ field }) => (
;
+ isDisabled?: boolean;
}) {
return (
@@ -27,6 +29,7 @@ export function TagsField({
control={control}
render={({ field }) => (
;
const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}>;
-export const AddLocationFlyout = ({
+export const AddOrEditLocationFlyout = ({
onSubmit,
- setIsOpen,
+ onCloseFlyout,
privateLocations,
+ privateLocationToEdit,
}: {
onSubmit: (val: NewLocation) => void;
- setIsOpen: (val: boolean) => void;
+ onCloseFlyout: () => void;
privateLocations: PrivateLocation[];
+ privateLocationToEdit?: PrivateLocation;
}) => {
const form = useFormWrapped({
mode: 'onSubmit',
reValidateMode: 'onChange',
shouldFocusError: true,
- defaultValues: {
+ defaultValues: privateLocationToEdit || {
label: '',
agentPolicyId: '',
geo: {
@@ -61,7 +63,7 @@ export const AddLocationFlyout = ({
const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext();
- const { createLoading } = useSelector(selectPrivateLocationsState);
+ const { createLoading, editLoading } = useSelector(selectPrivateLocationsState);
const { spaces: spacesApi } = useKibana().services;
@@ -72,33 +74,35 @@ export const AddLocationFlyout = ({
);
const { handleSubmit } = form;
- const closeFlyout = () => {
- setIsOpen(false);
- };
return (
-
+
- {ADD_PRIVATE_LOCATION}
+
+ {privateLocationToEdit !== undefined ? EDIT_PRIVATE_LOCATION : ADD_PRIVATE_LOCATION}
+
-
+
{CANCEL_LABEL}
@@ -106,10 +110,10 @@ export const AddLocationFlyout = ({
{SAVE_LABEL}
@@ -131,6 +135,13 @@ const ADD_PRIVATE_LOCATION = i18n.translate(
}
);
+const EDIT_PRIVATE_LOCATION = i18n.translate(
+ 'xpack.synthetics.monitorManagement.editPrivateLocations',
+ {
+ defaultMessage: 'Edit private location',
+ }
+);
+
const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.cancelLabel', {
defaultMessage: 'Cancel',
});
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx
index 9d871c7507ad..ab16bc925627 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx
@@ -12,19 +12,19 @@ import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { NoPermissionsTooltip } from '../../common/components/permissions';
import { useSyntheticsSettingsContext } from '../../../contexts';
-import { PRIVATE_LOCATIOSN_ROUTE } from '../../../../../../common/constants';
+import { PRIVATE_LOCATIONS_ROUTE } from '../../../../../../common/constants';
import {
- setIsCreatePrivateLocationFlyoutVisible,
+ setIsPrivateLocationFlyoutVisible,
setManageFlyoutOpen,
} from '../../../state/private_locations/actions';
export const EmptyLocations = ({
inFlyout = true,
- setIsAddingNew,
+ setIsFlyoutOpen,
redirectToSettings,
}: {
inFlyout?: boolean;
- setIsAddingNew?: (val: boolean) => void;
+ setIsFlyoutOpen?: (val: boolean) => void;
redirectToSettings?: boolean;
}) => {
const dispatch = useDispatch();
@@ -52,7 +52,7 @@ export const EmptyLocations = ({
fill
isDisabled={!canSave}
href={history.createHref({
- pathname: PRIVATE_LOCATIOSN_ROUTE,
+ pathname: PRIVATE_LOCATIONS_ROUTE,
})}
>
{ADD_LOCATION}
@@ -65,9 +65,9 @@ export const EmptyLocations = ({
color="primary"
fill
onClick={() => {
- setIsAddingNew?.(true);
+ setIsFlyoutOpen?.(true);
dispatch(setManageFlyoutOpen(true));
- dispatch(setIsCreatePrivateLocationFlyoutVisible(true));
+ dispatch(setIsPrivateLocationFlyoutVisible(true));
}}
>
{ADD_LOCATION}
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts
index 162f8ff59f87..e2d688d87637 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts
@@ -7,10 +7,12 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { NewLocation } from '../add_location_flyout';
+import type { EditPrivateLocationAttributes } from '../../../../../../../server/routes/settings/private_locations/edit_private_location';
+import { NewLocation } from '../add_or_edit_location_flyout';
import {
createPrivateLocationAction,
deletePrivateLocationAction,
+ editPrivateLocationAction,
getPrivateLocationsAction,
} from '../../../../state/private_locations/actions';
import { selectPrivateLocationsState } from '../../../../state/private_locations/selectors';
@@ -30,17 +32,22 @@ export const usePrivateLocationsAPI = () => {
}
}, [data, dispatch]);
- const onSubmit = (newLoc: NewLocation) => {
+ const onCreateLocationAPI = (newLoc: NewLocation) => {
dispatch(createPrivateLocationAction.get(newLoc));
};
- const onDelete = (id: string) => {
+ const onEditLocationAPI = (locationId: string, newAttributes: EditPrivateLocationAttributes) => {
+ dispatch(editPrivateLocationAction.get({ locationId, newAttributes }));
+ };
+
+ const onDeleteLocationAPI = (id: string) => {
dispatch(deletePrivateLocationAction.get(id));
};
return {
- onSubmit,
- onDelete,
+ onCreateLocationAPI,
+ onEditLocationAPI,
+ onDeleteLocationAPI,
deleteLoading,
loading,
createLoading,
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx
index 48b4b1e6dbad..052b69017ac4 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx
@@ -18,7 +18,13 @@ import { PrivateLocation } from '../../../../../../common/runtime_types';
import { AgentPolicyNeeded } from './agent_policy_needed';
import { PolicyHostsField } from './policy_hosts';
-export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => {
+export const LocationForm = ({
+ privateLocations,
+ privateLocationToEdit,
+}: {
+ privateLocations: PrivateLocation[];
+ privateLocationToEdit?: PrivateLocation;
+}) => {
const { data } = useSelector(selectAgentPolicies);
const { control, register } = useFormContext();
const { errors } = useFormState();
@@ -28,6 +34,8 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
return [...acc, ...tags];
}, [] as string[]);
+ const isEditingLocation = privateLocationToEdit !== undefined;
+
return (
<>
{data?.length === 0 && }
@@ -48,7 +56,8 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
message: NAME_REQUIRED,
},
validate: (val: string) => {
- return privateLocations.some((loc) => loc.label === val)
+ return privateLocations.some((loc) => loc.label === val) &&
+ val !== privateLocationToEdit?.label
? NAME_ALREADY_EXISTS
: undefined;
},
@@ -56,13 +65,13 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
/>
-
+
-
+
>
);
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx
index af51265feee2..684bbbcb8746 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx
@@ -8,6 +8,7 @@
import React, { useState } from 'react';
import {
EuiBadge,
+ EuiBasicTableColumn,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
@@ -31,7 +32,7 @@ import { DeleteLocation } from './delete_location';
import { useLocationMonitors } from './hooks/use_location_monitors';
import { PolicyName } from './policy_name';
import { LOCATION_NAME_LABEL } from './location_form';
-import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
+import { setIsPrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
import { ClientPluginsStart } from '../../../../../plugin';
interface ListItem extends PrivateLocation {
@@ -41,10 +42,12 @@ interface ListItem extends PrivateLocation {
export const PrivateLocationsTable = ({
deleteLoading,
onDelete,
+ onEdit,
privateLocations,
}: {
deleteLoading?: boolean;
onDelete: (id: string) => void;
+ onEdit: (privateLocation: PrivateLocation) => void;
privateLocations: PrivateLocation[];
}) => {
const dispatch = useDispatch();
@@ -65,7 +68,7 @@ export const PrivateLocationsTable = ({
return new Set([...acc, ...tags]);
}, new Set());
- const columns = [
+ const columns: Array> = [
{
field: 'label',
name: LOCATION_NAME_LABEL,
@@ -114,6 +117,15 @@ export const PrivateLocationsTable = ({
{
name: ACTIONS_LABEL,
actions: [
+ {
+ name: EDIT_LOCATION,
+ description: EDIT_LOCATION,
+ isPrimary: true,
+ 'data-test-subj': 'action-edit',
+ onClick: onEdit,
+ icon: 'pencil',
+ type: 'icon',
+ },
{
name: DELETE_LOCATION,
description: DELETE_LOCATION,
@@ -138,7 +150,7 @@ export const PrivateLocationsTable = ({
monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0,
}));
- const setIsAddingNew = (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val));
+ const openFlyout = () => dispatch(setIsPrivateLocationFlyoutVisible(true));
const renderToolRight = () => {
return [
@@ -152,7 +164,7 @@ export const PrivateLocationsTable = ({
data-test-subj={'addPrivateLocationButton'}
isLoading={loading}
disabled={!canSave || !canManagePrivateLocations}
- onClick={() => setIsAddingNew(true)}
+ onClick={openFlyout}
iconType="plusInCircle"
>
{ADD_LABEL}
@@ -236,6 +248,10 @@ const DELETE_LOCATION = i18n.translate(
}
);
+const EDIT_LOCATION = i18n.translate('xpack.synthetics.settingsRoute.privateLocations.editLabel', {
+ defaultMessage: 'Edit private location',
+});
+
const ADD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
defaultMessage: 'Create location',
});
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx
index dd0b5d8b9993..49abc93ce233 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx
@@ -15,14 +15,14 @@ import { selectAgentPolicies } from '../../../state/agent_policies';
export const ManageEmptyState: FC<
PropsWithChildren<{
privateLocations: PrivateLocation[];
- setIsAddingNew?: (val: boolean) => void;
+ setIsFlyoutOpen?: (val: boolean) => void;
showNeedAgentPolicy?: boolean;
showEmptyLocations?: boolean;
}>
> = ({
children,
privateLocations,
- setIsAddingNew,
+ setIsFlyoutOpen,
showNeedAgentPolicy = true,
showEmptyLocations = true,
}) => {
@@ -33,7 +33,7 @@ export const ManageEmptyState: FC<
}
if (privateLocations.length === 0 && showEmptyLocations) {
- return ;
+ return ;
}
return <>{children}>;
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx
index a6d6ccfb1af7..271af590dcf7 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx
@@ -28,9 +28,10 @@ describe('', () => {
});
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
loading: false,
- onSubmit: jest.fn(),
+ onCreateLocationAPI: jest.fn(),
+ onEditLocationAPI: jest.fn(),
privateLocations: [],
- onDelete: jest.fn(),
+ onDeleteLocationAPI: jest.fn(),
deleteLoading: false,
createLoading: false,
});
@@ -60,7 +61,7 @@ describe('', () => {
error: null,
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: false,
+ isPrivateLocationFlyoutVisible: false,
},
},
});
@@ -95,7 +96,7 @@ describe('', () => {
error: null,
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: false,
+ isPrivateLocationFlyoutVisible: false,
},
},
});
@@ -125,7 +126,8 @@ describe('', () => {
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
loading: false,
- onSubmit: jest.fn(),
+ onCreateLocationAPI: jest.fn(),
+ onEditLocationAPI: jest.fn(),
privateLocations: [
{
label: privateLocationName,
@@ -134,7 +136,7 @@ describe('', () => {
isServiceManaged: false,
},
],
- onDelete: jest.fn(),
+ onDeleteLocationAPI: jest.fn(),
deleteLoading: false,
createLoading: false,
});
@@ -146,7 +148,7 @@ describe('', () => {
error: null,
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: false,
+ isPrivateLocationFlyoutVisible: false,
},
},
});
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx
index 4478c973f7ea..550b6ee52a5c 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx
@@ -8,15 +8,23 @@ import React, { useEffect, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
+import { isEqual } from 'lodash';
+import { PrivateLocation } from '../../../../../../common/runtime_types';
import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout';
import { PrivateLocationsTable } from './locations_table';
import { ManageEmptyState } from './manage_empty_state';
-import { AddLocationFlyout, NewLocation } from './add_location_flyout';
+import { AddOrEditLocationFlyout, NewLocation } from './add_or_edit_location_flyout';
import { usePrivateLocationsAPI } from './hooks/use_locations_api';
-import { selectAddingNewPrivateLocation } from '../../../state/private_locations/selectors';
+import {
+ selectPrivateLocationFlyoutVisible,
+ selectPrivateLocationToEdit,
+} from '../../../state/private_locations/selectors';
import { getServiceLocations } from '../../../state';
import { getAgentPoliciesAction } from '../../../state/agent_policies';
-import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
+import {
+ setIsPrivateLocationFlyoutVisible as setIsPrivateLocationFlyoutVisible,
+ setPrivateLocationToEdit,
+} from '../../../state/private_locations/actions';
import { ClientPluginsStart } from '../../../../../plugin';
const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}>;
@@ -33,23 +41,53 @@ export const ManagePrivateLocations = () => {
[spacesApi]
);
- const isAddingNew = useSelector(selectAddingNewPrivateLocation);
- const setIsAddingNew = useCallback(
- (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
+ const isPrivateLocationFlyoutVisible = useSelector(selectPrivateLocationFlyoutVisible);
+ const privateLocationToEdit = useSelector(selectPrivateLocationToEdit);
+ const setIsFlyoutOpen = useCallback(
+ (val: boolean) => dispatch(setIsPrivateLocationFlyoutVisible(val)),
[dispatch]
);
- const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = usePrivateLocationsAPI();
+ const {
+ onCreateLocationAPI,
+ onEditLocationAPI,
+ loading,
+ privateLocations,
+ onDeleteLocationAPI,
+ deleteLoading,
+ } = usePrivateLocationsAPI();
useEffect(() => {
dispatch(getAgentPoliciesAction.get());
dispatch(getServiceLocations());
// make sure flyout is closed when first visiting the page
- dispatch(setIsCreatePrivateLocationFlyoutVisible(false));
+ dispatch(setIsPrivateLocationFlyoutVisible(false));
}, [dispatch]);
const handleSubmit = (formData: NewLocation) => {
- onSubmit(formData);
+ if (privateLocationToEdit) {
+ const isLabelChanged = formData.label !== privateLocationToEdit.label;
+ const areTagsChanged = !isEqual(formData.tags, privateLocationToEdit.tags);
+ if (!isLabelChanged && !areTagsChanged) {
+ onCloseFlyout();
+ } else {
+ onEditLocationAPI(privateLocationToEdit.id, { label: formData.label, tags: formData.tags });
+ }
+ } else {
+ onCreateLocationAPI(formData);
+ }
+ };
+
+ const onEditLocation = (privateLocation: PrivateLocation) => {
+ dispatch(setPrivateLocationToEdit(privateLocation));
+ setIsFlyoutOpen(true);
+ };
+
+ const onCloseFlyout = () => {
+ if (privateLocationToEdit) {
+ dispatch(setPrivateLocationToEdit(undefined));
+ }
+ setIsFlyoutOpen(false);
};
return (
@@ -57,20 +95,22 @@ export const ManagePrivateLocations = () => {
{loading ? (
) : (
-
+
)}
- {isAddingNew ? (
-
) : null}
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx
index 42f6a463e228..bb520225611b 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx
@@ -29,7 +29,13 @@ import { selectAgentPolicies } from '../../../state/agent_policies';
export const AGENT_POLICY_FIELD_NAME = 'agentPolicyId';
-export const PolicyHostsField = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => {
+export const PolicyHostsField = ({
+ privateLocations,
+ isDisabled,
+}: {
+ privateLocations: PrivateLocation[];
+ isDisabled?: boolean;
+}) => {
const { data } = useSelector(selectAgentPolicies);
const { basePath } = useSyntheticsSettingsContext();
@@ -53,7 +59,7 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
inputDisplay: (
{item.name}
@@ -74,7 +80,7 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
<>
{item.name}
@@ -124,6 +130,7 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
rules={{ required: true }}
render={({ field }) => (
(
'[PRIVATE LOCATIONS] GET'
@@ -18,12 +19,24 @@ export const createPrivateLocationAction = createAsyncAction('EDIT PRIVATE LOCATION');
+
export const deletePrivateLocationAction = createAsyncAction(
'DELETE PRIVATE LOCATION'
);
export const setManageFlyoutOpen = createAction('SET MANAGE FLYOUT OPEN');
-export const setIsCreatePrivateLocationFlyoutVisible = createAction(
- 'SET IS CREATE PRIVATE LOCATION FLYOUT VISIBLE'
+export const setIsPrivateLocationFlyoutVisible = createAction(
+ 'SET IS CREATE OR EDIT PRIVATE LOCATION FLYOUT VISIBLE'
+);
+
+export const setPrivateLocationToEdit = createAction(
+ 'SET PRIVATE LOCATION TO EDIT'
);
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts
index afa722302c89..6acb4b2e07e5 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
+import type { EditPrivateLocationAttributes } from '../../../../../server/routes/settings/private_locations/edit_private_location';
+import { NewLocation } from '../../components/settings/private_locations/add_or_edit_location_flyout';
import { AgentPolicyInfo } from '../../../../../common/types';
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
@@ -23,6 +24,23 @@ export const createSyntheticsPrivateLocation = async (
});
};
+export const editSyntheticsPrivateLocation = async ({
+ locationId,
+ newAttributes,
+}: {
+ locationId: string;
+ newAttributes: EditPrivateLocationAttributes;
+}): Promise => {
+ return apiService.put(
+ `${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${locationId}`,
+ newAttributes,
+ undefined,
+ {
+ version: INITIAL_REST_VERSION,
+ }
+ );
+};
+
export const getSyntheticsPrivateLocations = async (): Promise => {
return await apiService.get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, {
version: INITIAL_REST_VERSION,
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts
index 0642e1d697ca..2ba40ff664c0 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts
@@ -11,11 +11,13 @@ import { fetchEffectFactory } from '../utils/fetch_effect';
import {
createSyntheticsPrivateLocation,
deleteSyntheticsPrivateLocation,
+ editSyntheticsPrivateLocation,
getSyntheticsPrivateLocations,
} from './api';
import {
createPrivateLocationAction,
deletePrivateLocationAction,
+ editPrivateLocationAction,
getPrivateLocationsAction,
} from './actions';
@@ -47,6 +49,23 @@ export function* createPrivateLocationEffect() {
);
}
+export function* editPrivateLocationEffect() {
+ yield takeLeading(
+ editPrivateLocationAction.get,
+ fetchEffectFactory(
+ editSyntheticsPrivateLocation,
+ editPrivateLocationAction.success,
+ editPrivateLocationAction.fail,
+ i18n.translate('xpack.synthetics.editPrivateLocationSuccess', {
+ defaultMessage: 'Successfully edited private location.',
+ }),
+ i18n.translate('xpack.synthetics.editPrivateLocationFailure', {
+ defaultMessage: 'Failed to edit private location.',
+ })
+ )
+ );
+}
+
export function* deletePrivateLocationEffect() {
yield takeLeading(
deletePrivateLocationAction.get,
@@ -61,5 +80,6 @@ export function* deletePrivateLocationEffect() {
export const privateLocationsEffects = [
fetchPrivateLocationsEffect,
createPrivateLocationEffect,
+ editPrivateLocationEffect,
deletePrivateLocationEffect,
];
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts
index 45c9ba4ece57..ef1a3284b36e 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts
@@ -7,19 +7,26 @@
import { createReducer } from '@reduxjs/toolkit';
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
-import { createPrivateLocationAction, deletePrivateLocationAction } from './actions';
-import { setIsCreatePrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
+import {
+ createPrivateLocationAction,
+ deletePrivateLocationAction,
+ editPrivateLocationAction,
+ setPrivateLocationToEdit,
+} from './actions';
+import { setIsPrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
import { IHttpSerializedFetchError } from '../utils/http_error';
export interface PrivateLocationsState {
data?: SyntheticsPrivateLocations | null;
loading: boolean;
createLoading?: boolean;
+ editLoading?: boolean;
deleteLoading?: boolean;
error: IHttpSerializedFetchError | null;
isManageFlyoutOpen?: boolean;
- isCreatePrivateLocationFlyoutVisible?: boolean;
+ isPrivateLocationFlyoutVisible?: boolean;
newLocation?: PrivateLocation;
+ privateLocationToEdit?: PrivateLocation;
}
const initialState: PrivateLocationsState = {
@@ -27,8 +34,10 @@ const initialState: PrivateLocationsState = {
loading: false,
error: null,
isManageFlyoutOpen: false,
- isCreatePrivateLocationFlyoutVisible: false,
+ isPrivateLocationFlyoutVisible: false,
createLoading: false,
+ editLoading: false,
+ privateLocationToEdit: undefined,
};
export const privateLocationsStateReducer = createReducer(initialState, (builder) => {
@@ -51,12 +60,26 @@ export const privateLocationsStateReducer = createReducer(initialState, (builder
state.newLocation = action.payload;
state.createLoading = false;
state.data = null;
- state.isCreatePrivateLocationFlyoutVisible = false;
+ state.isPrivateLocationFlyoutVisible = false;
})
.addCase(createPrivateLocationAction.fail, (state, action) => {
state.error = action.payload;
state.createLoading = false;
})
+ .addCase(editPrivateLocationAction.get, (state) => {
+ state.editLoading = true;
+ })
+ .addCase(editPrivateLocationAction.success, (state, action) => {
+ state.editLoading = false;
+ state.privateLocationToEdit = undefined;
+ state.data = null;
+ state.isPrivateLocationFlyoutVisible = false;
+ })
+ .addCase(editPrivateLocationAction.fail, (state, action) => {
+ state.editLoading = false;
+ state.privateLocationToEdit = undefined;
+ state.error = action.payload;
+ })
.addCase(deletePrivateLocationAction.get, (state) => {
state.deleteLoading = true;
})
@@ -68,7 +91,10 @@ export const privateLocationsStateReducer = createReducer(initialState, (builder
state.error = action.payload;
state.deleteLoading = false;
})
- .addCase(setIsCreatePrivateLocationFlyoutVisible, (state, action) => {
- state.isCreatePrivateLocationFlyoutVisible = action.payload;
+ .addCase(setIsPrivateLocationFlyoutVisible, (state, action) => {
+ state.isPrivateLocationFlyoutVisible = action.payload;
+ })
+ .addCase(setPrivateLocationToEdit, (state, action) => {
+ state.privateLocationToEdit = action.payload;
});
});
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts
index a9ce77217579..5b85c0e7dbdd 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts
@@ -11,8 +11,11 @@ import { AppState } from '..';
const getState = (appState: AppState) => appState.privateLocations;
export const selectAgentPolicies = createSelector(getState, (state) => state);
-export const selectAddingNewPrivateLocation = (state: AppState) =>
- state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
+export const selectPrivateLocationFlyoutVisible = (state: AppState) =>
+ state.privateLocations.isPrivateLocationFlyoutVisible ?? false;
+
+export const selectPrivateLocationToEdit = (state: AppState) =>
+ state.privateLocations.privateLocationToEdit;
export const selectPrivateLocationsLoading = (state: AppState) =>
state.privateLocations.loading ?? false;
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts
index 195c1380ebc9..298c7a5dbf10 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts
@@ -14,7 +14,7 @@ const getState = (appState: AppState) => appState.agentPolicies;
export const selectAgentPolicies = createSelector(getState, (state) => state);
export const selectAddingNewPrivateLocation = (state: AppState) =>
- state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
+ state.privateLocations.isPrivateLocationFlyoutVisible ?? false;
export const selectLocationMonitors = (state: AppState) => ({
locationMonitors: state.dynamicSettings.locationMonitors,
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
index 248e4f2060ac..6b1a75c067c8 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
@@ -116,7 +116,7 @@ export const mockState: SyntheticsAppState = {
data: null,
},
privateLocations: {
- isCreatePrivateLocationFlyoutVisible: false,
+ isPrivateLocationFlyoutVisible: false,
loading: false,
error: null,
data: [],
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts b/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts
index f42947b8252c..6f0f52545b95 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts
@@ -12,6 +12,7 @@ import { PrivateLocationAttributes } from '../runtime_types/private_locations';
import { PrivateLocationObject } from '../routes/settings/private_locations/add_private_location';
import { RouteContext } from '../routes/types';
import { privateLocationSavedObjectName } from '../../common/saved_objects/private_locations';
+import { EditPrivateLocationAttributes } from '../routes/settings/private_locations/edit_private_location';
export class PrivateLocationRepository {
internalSOClient: ISavedObjectsRepository;
@@ -33,6 +34,26 @@ export class PrivateLocationRepository {
}
);
}
+
+ async getPrivateLocation(locationId: string) {
+ const { savedObjectsClient } = this.routeContext;
+
+ return savedObjectsClient.get(
+ privateLocationSavedObjectName,
+ locationId
+ );
+ }
+
+ async editPrivateLocation(locationId: string, newAttributes: EditPrivateLocationAttributes) {
+ const { savedObjectsClient } = this.routeContext;
+
+ return savedObjectsClient.update(
+ privateLocationSavedObjectName,
+ locationId,
+ newAttributes
+ );
+ }
+
async validatePrivateLocation() {
const { response, request, server } = this.routeContext;
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/index.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/index.ts
index 19b45f7b662d..bae05cb89fbd 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/routes/index.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/index.ts
@@ -55,6 +55,7 @@ import { getDefaultAlertingRoute } from './default_alerts/get_default_alert';
import { createNetworkEventsRoute } from './network_events';
import { addPrivateLocationRoute } from './settings/private_locations/add_private_location';
import { deletePrivateLocationRoute } from './settings/private_locations/delete_private_location';
+import { editPrivateLocationRoute } from './settings/private_locations/edit_private_location';
import { getPrivateLocationsRoute } from './settings/private_locations/get_private_locations';
import { getSyntheticsFilters } from './filters/filters';
import { getAllSyntheticsMonitorRoute } from './monitor_cruds/get_monitors_list';
@@ -112,6 +113,7 @@ export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] =
deleteSyntheticsParamsRoute,
addPrivateLocationRoute,
deletePrivateLocationRoute,
+ editPrivateLocationRoute,
getPrivateLocationsRoute,
getAllSyntheticsMonitorRoute,
getSyntheticsMonitorRoute,
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts
index d4725af1edfd..61f609b1966c 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts
@@ -83,7 +83,10 @@ export const syncEditedMonitorBulk = async ({
} as unknown as MonitorFields,
}));
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
- monitorConfigRepository.bulkUpdate({ monitors: data }),
+ monitorConfigRepository.bulkUpdate({
+ monitors: data,
+ namespace: spaceId !== routeContext.spaceId ? spaceId : undefined,
+ }),
syncUpdatedMonitors({ monitorsToUpdate, routeContext, spaceId, privateLocations }),
]);
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts
new file mode 100644
index 000000000000..3ae484b33cbf
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/edit_private_location.ts
@@ -0,0 +1,163 @@
+/*
+ * 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 { TypeOf, schema } from '@kbn/config-schema';
+import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server';
+import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants';
+import { i18n } from '@kbn/i18n';
+import { isEqual } from 'lodash';
+import { getPrivateLocations } from '../../../synthetics_service/get_private_locations';
+import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
+import { PrivateLocationRepository } from '../../../repositories/private_location_repository';
+import { PRIVATE_LOCATION_WRITE_API } from '../../../feature';
+import { SyntheticsRestApiRouteFactory } from '../../types';
+import { SYNTHETICS_API_URLS } from '../../../../common/constants';
+import { toClientContract, updatePrivateLocationMonitors } from './helpers';
+import { PrivateLocation } from '../../../../common/runtime_types';
+import { parseArrayFilters } from '../../common';
+
+const EditPrivateLocationSchema = schema.object({
+ label: schema.maybe(
+ schema.string({
+ minLength: 1,
+ })
+ ),
+ tags: schema.maybe(schema.arrayOf(schema.string())),
+});
+
+const EditPrivateLocationQuery = schema.object({
+ locationId: schema.string(),
+});
+
+export type EditPrivateLocationAttributes = Pick<
+ PrivateLocationAttributes,
+ keyof TypeOf
+>;
+
+const isPrivateLocationLabelChanged = (oldLabel: string, newLabel?: string): newLabel is string => {
+ return typeof newLabel === 'string' && oldLabel !== newLabel;
+};
+
+const isPrivateLocationChanged = ({
+ privateLocation,
+ newParams,
+}: {
+ privateLocation: SavedObject;
+ newParams: TypeOf;
+}) => {
+ const isLabelChanged = isPrivateLocationLabelChanged(
+ privateLocation.attributes.label,
+ newParams.label
+ );
+ const areTagsChanged =
+ Array.isArray(newParams.tags) &&
+ (!privateLocation.attributes.tags ||
+ (privateLocation.attributes.tags &&
+ !isEqual(privateLocation.attributes.tags, newParams.tags)));
+
+ return isLabelChanged || areTagsChanged;
+};
+
+export const editPrivateLocationRoute: SyntheticsRestApiRouteFactory<
+ PrivateLocation,
+ TypeOf,
+ any,
+ TypeOf
+> = () => ({
+ method: 'PUT',
+ path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + '/{locationId}',
+ validate: {},
+ validation: {
+ request: {
+ body: EditPrivateLocationSchema,
+ params: EditPrivateLocationQuery,
+ },
+ },
+ requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
+ handler: async (routeContext) => {
+ const { response, request, savedObjectsClient, server } = routeContext;
+ const { locationId } = request.params;
+ const { label: newLocationLabel, tags: newTags } = request.body;
+
+ const repo = new PrivateLocationRepository(routeContext);
+
+ try {
+ const { filtersStr } = parseArrayFilters({
+ locations: [locationId],
+ });
+ const [existingLocation, monitorsInLocation] = await Promise.all([
+ repo.getPrivateLocation(locationId),
+ routeContext.monitorConfigRepository.findDecryptedMonitors({
+ spaceId: ALL_SPACES_ID,
+ filter: filtersStr,
+ }),
+ ]);
+
+ let newLocation: Awaited> | undefined;
+
+ if (
+ isPrivateLocationChanged({ privateLocation: existingLocation, newParams: request.body })
+ ) {
+ // This privileges check is done only when changing the label, because changing the label will update also the monitors in that location
+ if (isPrivateLocationLabelChanged(existingLocation.attributes.label, newLocationLabel)) {
+ const monitorsSpaces = monitorsInLocation.map(({ namespaces }) => namespaces![0]);
+
+ const checkSavedObjectsPrivileges =
+ server.security.authz.checkSavedObjectsPrivilegesWithRequest(request);
+
+ const { hasAllRequested } = await checkSavedObjectsPrivileges(
+ 'saved_object:synthetics-monitor/bulk_update',
+ monitorsSpaces
+ );
+
+ if (!hasAllRequested) {
+ return response.forbidden({
+ body: {
+ message: i18n.translate('xpack.synthetics.editPrivateLocation.forbidden', {
+ defaultMessage:
+ 'You do not have sufficient permissions to update monitors in all required spaces. This private location is used by monitors in spaces where you lack update privileges.',
+ }),
+ },
+ });
+ }
+ }
+
+ newLocation = await repo.editPrivateLocation(locationId, {
+ label: newLocationLabel || existingLocation.attributes.label,
+ tags: newTags || existingLocation.attributes.tags,
+ });
+
+ if (isPrivateLocationLabelChanged(existingLocation.attributes.label, newLocationLabel)) {
+ await updatePrivateLocationMonitors({
+ locationId,
+ newLocationLabel,
+ allPrivateLocations: await getPrivateLocations(savedObjectsClient),
+ routeContext,
+ monitorsInLocation,
+ });
+ }
+ }
+
+ return toClientContract({
+ ...existingLocation,
+ attributes: {
+ ...existingLocation.attributes,
+ ...(newLocation ? newLocation.attributes : {}),
+ },
+ });
+ } catch (error) {
+ if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
+ return response.notFound({
+ body: {
+ message: `Private location with id ${locationId} does not exist.`,
+ },
+ });
+ }
+ throw error;
+ }
+ },
+});
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts
index 84c531cb9ce7..5a271747c8d3 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts
@@ -5,7 +5,20 @@
* 2.0.
*/
-import { allLocationsToClientContract } from './helpers';
+import { allLocationsToClientContract, updatePrivateLocationMonitors } from './helpers';
+import { RouteContext } from '../../types';
+
+// Mock the syncEditedMonitorBulk module
+jest.mock('../../monitor_cruds/bulk_cruds/edit_monitor_bulk', () => ({
+ syncEditedMonitorBulk: jest.fn().mockResolvedValue({
+ failedConfigs: [],
+ errors: [],
+ editedMonitors: [],
+ }),
+}));
+
+// Import the mocked function
+import { syncEditedMonitorBulk } from '../../monitor_cruds/bulk_cruds/edit_monitor_bulk';
const testLocations = {
locations: [
@@ -114,3 +127,107 @@ describe('toClientContract', () => {
]);
});
});
+
+describe('updatePrivateLocationMonitors', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const LOCATION_ID = 'test-location-id';
+ const NEW_LABEL = 'New location label';
+ const FIRST_SPACE_ID = 'firstSpaceId';
+ const SECOND_SPACE_ID = 'secondSpaceId';
+ const FIRST_MONITOR_ID = 'monitor-1';
+ const SECOND_MONITOR_ID = 'monitor-2';
+ const mockMonitors = [
+ {
+ id: FIRST_MONITOR_ID,
+ attributes: {
+ name: 'Test Monitor 1',
+ locations: [{ id: LOCATION_ID, label: 'Old Label' }],
+ // Other required monitor fields
+ type: 'http',
+ enabled: true,
+ schedule: { number: '10', unit: 'm' },
+ namespace: FIRST_SPACE_ID,
+ },
+ },
+ {
+ id: SECOND_MONITOR_ID,
+ attributes: {
+ name: 'Test Monitor 2',
+ locations: [
+ { id: LOCATION_ID, label: 'Old Label' },
+ { id: 'different-location', label: 'Different Location' },
+ ],
+ // Other required monitor fields
+ type: 'http',
+ enabled: true,
+ schedule: { number: '5', unit: 'm' },
+ namespace: SECOND_SPACE_ID,
+ },
+ },
+ ];
+
+ it('updates monitor locations with the new label', async () => {
+ const PRIVATE_LOCATIONS = [] as any[];
+ const ROUTE_CONTEXT = {} as RouteContext;
+ // Call the function
+ await updatePrivateLocationMonitors({
+ locationId: LOCATION_ID,
+ newLocationLabel: NEW_LABEL,
+ allPrivateLocations: PRIVATE_LOCATIONS,
+ routeContext: ROUTE_CONTEXT,
+ monitorsInLocation: mockMonitors as any,
+ });
+
+ // Verify that syncEditedMonitorBulk was called
+ expect(syncEditedMonitorBulk).toHaveBeenCalledTimes(2);
+
+ // Check first call for first space
+ expect(syncEditedMonitorBulk).toHaveBeenCalledWith({
+ monitorsToUpdate: expect.arrayContaining([
+ expect.objectContaining({
+ decryptedPreviousMonitor: mockMonitors[0],
+ normalizedMonitor: expect.any(Object),
+ monitorWithRevision: expect.objectContaining({
+ locations: [
+ expect.objectContaining({
+ id: LOCATION_ID,
+ label: NEW_LABEL,
+ }),
+ ],
+ }),
+ }),
+ ]),
+ privateLocations: PRIVATE_LOCATIONS,
+ routeContext: ROUTE_CONTEXT,
+ spaceId: FIRST_SPACE_ID,
+ });
+
+ // Check second call for second space
+ expect(syncEditedMonitorBulk).toHaveBeenCalledWith({
+ monitorsToUpdate: expect.arrayContaining([
+ expect.objectContaining({
+ decryptedPreviousMonitor: mockMonitors[1],
+ normalizedMonitor: expect.any(Object),
+ monitorWithRevision: expect.objectContaining({
+ locations: [
+ expect.objectContaining({
+ id: LOCATION_ID,
+ label: NEW_LABEL,
+ }),
+ expect.objectContaining({
+ id: 'different-location',
+ label: 'Different Location',
+ }),
+ ],
+ }),
+ }),
+ ]),
+ privateLocations: PRIVATE_LOCATIONS,
+ routeContext: ROUTE_CONTEXT,
+ spaceId: SECOND_SPACE_ID,
+ });
+ });
+});
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts
index 956fdec42335..85579112411d 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts
@@ -4,14 +4,24 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { SavedObject } from '@kbn/core/server';
+import { SavedObject, SavedObjectsFindResult } from '@kbn/core/server';
+import { formatSecrets, normalizeSecrets } from '../../../synthetics_service/utils';
import { AgentPolicyInfo } from '../../../../common/types';
-import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
+import type {
+ SyntheticsMonitor,
+ SyntheticsMonitorWithSecretsAttributes,
+ SyntheticsPrivateLocations,
+} from '../../../../common/runtime_types';
import type {
SyntheticsPrivateLocationsAttributes,
PrivateLocationAttributes,
} from '../../../runtime_types/private_locations';
import { PrivateLocation } from '../../../../common/runtime_types';
+import {
+ MonitorConfigUpdate,
+ syncEditedMonitorBulk,
+} from '../../monitor_cruds/bulk_cruds/edit_monitor_bulk';
+import { RouteContext } from '../../types';
export const toClientContract = (
locationObject: SavedObject
@@ -60,3 +70,54 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio
spaces: location.spaces,
};
};
+
+// This should be called when changing the label of a private location because the label is also stored
+// in the locations array of monitors attributes
+export const updatePrivateLocationMonitors = async ({
+ locationId,
+ newLocationLabel,
+ allPrivateLocations,
+ routeContext,
+ monitorsInLocation,
+}: {
+ locationId: string;
+ newLocationLabel: string;
+ allPrivateLocations: SyntheticsPrivateLocations;
+ routeContext: RouteContext;
+ monitorsInLocation: Array>;
+}) => {
+ const updatedMonitorsPerSpace = monitorsInLocation.reduce>(
+ (acc, m) => {
+ const decryptedMonitorsWithNormalizedSecrets: SavedObject =
+ normalizeSecrets(m);
+ const normalizedMonitor = decryptedMonitorsWithNormalizedSecrets.attributes;
+ const newLocations = m.attributes.locations.map((l) =>
+ l.id !== locationId ? l : { ...l, label: newLocationLabel }
+ );
+ const monitorWithRevision = formatSecrets({ ...normalizedMonitor, locations: newLocations });
+ const monitorToUpdate: MonitorConfigUpdate = {
+ normalizedMonitor,
+ decryptedPreviousMonitor: m,
+ monitorWithRevision,
+ };
+
+ const namespace = m.attributes.namespace;
+ return {
+ ...acc,
+ [namespace]: [...(acc[namespace] || []), monitorToUpdate],
+ };
+ },
+ {}
+ );
+
+ const promises = Object.keys(updatedMonitorsPerSpace).map((namespace) => [
+ syncEditedMonitorBulk({
+ monitorsToUpdate: updatedMonitorsPerSpace[namespace],
+ privateLocations: allPrivateLocations,
+ routeContext,
+ spaceId: namespace,
+ }),
+ ]);
+
+ return Promise.all(promises.flat());
+};
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts
index 5c8901d802e9..487d58e95ef0 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts
@@ -82,17 +82,20 @@ export class MonitorConfigRepository {
async bulkUpdate({
monitors,
+ namespace,
}: {
monitors: Array<{
attributes: MonitorFields;
id: string;
}>;
+ namespace?: string;
}) {
- return await this.soClient.bulkUpdate(
+ return this.soClient.bulkUpdate(
monitors.map(({ attributes, id }) => ({
type: syntheticsMonitorType,
id,
attributes,
+ namespace,
}))
);
}
diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/utils/secrets.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/utils/secrets.ts
index 90a11812ff19..b60622670224 100644
--- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/utils/secrets.ts
+++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/utils/secrets.ts
@@ -42,7 +42,7 @@ export function normalizeMonitorSecretAttributes(
const normalizedMonitorAttributes = {
...defaultFields,
...monitor,
- ...JSON.parse(monitor.secrets || ''),
+ ...JSON.parse(monitor.secrets || '{}'),
};
delete normalizedMonitorAttributes.secrets;
return normalizedMonitorAttributes;
diff --git a/x-pack/solutions/observability/plugins/uptime/common/constants/ui.ts b/x-pack/solutions/observability/plugins/uptime/common/constants/ui.ts
index 19d980dfa153..145477becc42 100644
--- a/x-pack/solutions/observability/plugins/uptime/common/constants/ui.ts
+++ b/x-pack/solutions/observability/plugins/uptime/common/constants/ui.ts
@@ -27,7 +27,7 @@ export const GETTING_STARTED_ROUTE = '/monitors/getting-started';
export const SETTINGS_ROUTE = '/settings';
-export const PRIVATE_LOCATIOSN_ROUTE = '/settings/private-locations';
+export const PRIVATE_LOCATIONS_ROUTE = '/settings/private-locations';
export const SYNTHETICS_SETTINGS_ROUTE = '/settings/:tabId';
diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/synthetics_api_security.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/synthetics_api_security.ts
index 3969dcae8821..1ee6f203336c 100644
--- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/synthetics_api_security.ts
+++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/synthetics_api_security.ts
@@ -39,7 +39,10 @@ export default function ({ getService }: FtrProviderContext) {
let resp;
const { statusCodes, SPACE_ID, username, password, writeAccess, readUser } = options;
let tags = !writeAccess ? '[uptime-read]' : options.tags ?? '[uptime-read,uptime-write]';
- if ((method === 'POST' || method === 'DELETE') && path.includes('private_locations')) {
+ if (
+ (method === 'POST' || method === 'DELETE' || method === 'PUT') &&
+ path.includes('private_locations')
+ ) {
tags = readUser
? '[private-location-write,uptime-write]'
: '[uptime-read,private-location-write,uptime-write]';
diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts
new file mode 100644
index 000000000000..efef66b610bf
--- /dev/null
+++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_private_location.ts
@@ -0,0 +1,174 @@
+/*
+ * 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 { RoleCredentials } from '@kbn/ftr-common-functional-services';
+import { PrivateLocation, ServiceLocation } from '@kbn/synthetics-plugin/common/runtime_types';
+import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
+import expect from '@kbn/expect';
+import rawExpect from 'expect';
+import { PackagePolicy } from '@kbn/fleet-plugin/common';
+import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
+import { getFixtureJson } from './helpers/get_fixture_json';
+import { PrivateLocationTestService } from '../../../services/synthetics_private_location';
+import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor';
+import { SupertestWithRoleScopeType } from '../../../services';
+
+export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
+ describe('EditPrivateLocation', function () {
+ const kibanaServer = getService('kibanaServer');
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const samlAuth = getService('samlAuth');
+ const roleScopedSupertest = getService('roleScopedSupertest');
+ let supertestEditorWithApiKey: SupertestWithRoleScopeType;
+
+ let testFleetPolicyID: string;
+ let editorUser: RoleCredentials;
+ let privateLocations: PrivateLocation[] = [];
+ const testPolicyName = 'Fleet test server policy' + Date.now();
+
+ let newMonitor: { id: string; name: string };
+ const testPrivateLocations = new PrivateLocationTestService(getService);
+ const NEW_LOCATION_LABEL = 'Barcelona';
+ const NEW_TAGS = ['myAwesomeTag'];
+
+ before(async () => {
+ supertestEditorWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('editor', {
+ withInternalHeaders: true,
+ });
+
+ await kibanaServer.savedObjects.cleanStandardList();
+ await testPrivateLocations.installSyntheticsPackage();
+ editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor');
+ });
+
+ after(async () => {
+ await supertestEditorWithApiKey.destroy();
+ await samlAuth.invalidateM2mApiKeyWithRoleScope(editorUser);
+ await kibanaServer.savedObjects.cleanStandardList();
+ });
+
+ it('adds a test fleet policy', async () => {
+ const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName);
+ testFleetPolicyID = apiResponse.body.item.id;
+ });
+
+ it('add a test private location', async () => {
+ privateLocations = await testPrivateLocations.setTestLocations([testFleetPolicyID]);
+
+ const apiResponse = await supertestEditorWithApiKey
+ .get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS)
+ .expect(200);
+
+ const testResponse: Array = [
+ {
+ id: testFleetPolicyID,
+ isServiceManaged: false,
+ isInvalid: false,
+ label: privateLocations[0].label,
+ geo: {
+ lat: 0,
+ lon: 0,
+ },
+ agentPolicyId: testFleetPolicyID,
+ spaces: ['default'],
+ },
+ ];
+
+ rawExpect(apiResponse.body.locations).toEqual(rawExpect.arrayContaining(testResponse));
+ });
+
+ it('adds a monitor in private location', async () => {
+ newMonitor = {
+ ...getFixtureJson('http_monitor'),
+ namespace: 'default',
+ locations: [privateLocations[0]],
+ };
+
+ const { body, rawBody } = await addMonitorAPIHelper(
+ supertestWithoutAuth,
+ newMonitor,
+ 200,
+ editorUser,
+ samlAuth
+ );
+ expect(body).eql(omitMonitorKeys(newMonitor));
+ newMonitor.id = rawBody.id;
+ });
+
+ it('successfully edits a private location label', async () => {
+ const privateLocation = privateLocations[0];
+
+ // Edit the private location
+ const editResponse = await supertestEditorWithApiKey
+ .put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocation.id}`)
+ .send({ label: NEW_LOCATION_LABEL, tags: NEW_TAGS })
+ .expect(200);
+
+ // Verify the response contains the updated label
+ expect(editResponse.body.label).to.be(NEW_LOCATION_LABEL);
+ expect(editResponse.body.tags).to.eql(NEW_TAGS);
+ expect(editResponse.body.id).to.be(privateLocation.id);
+ expect(editResponse.body.agentPolicyId).to.be(privateLocation.agentPolicyId);
+
+ // Verify the location was actually updated by getting it
+ const getResponse = await supertestEditorWithApiKey
+ .get(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocation.id}`)
+ .expect(200);
+
+ expect(getResponse.body.label).to.be(NEW_LOCATION_LABEL);
+ });
+
+ it('verifies that monitor location label is updated when the private location label changes', async () => {
+ // Get the monitor with the updated location label
+ const getMonitorResponse = await supertestEditorWithApiKey
+ .get(`${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${newMonitor.id}`)
+ .expect(200);
+
+ // Verify the monitor's location has the updated label
+ const monitor = getMonitorResponse.body;
+ expect(monitor.locations).to.have.length(1);
+ expect(monitor.locations[0].id).to.be(privateLocations[0].id);
+ expect(monitor.locations[0].label).to.be(NEW_LOCATION_LABEL);
+ });
+
+ it('verifies that package policies are updated when the private location label changes', async () => {
+ const apiResponse = await supertestEditorWithApiKey.get(
+ '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
+ );
+
+ const packagePolicy: PackagePolicy = apiResponse.body.items.find(
+ (pkgPolicy: PackagePolicy) =>
+ pkgPolicy.id === newMonitor.id + '-' + testFleetPolicyID + '-default'
+ );
+ expect(packagePolicy.name).to.contain(NEW_LOCATION_LABEL);
+ });
+
+ it('returns 404 when trying to edit a non-existent private location', async () => {
+ const nonExistentId = 'non-existent-id';
+
+ const response = await supertestEditorWithApiKey
+ .put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${nonExistentId}`)
+ .send({ label: NEW_LOCATION_LABEL })
+ .expect(404);
+
+ expect(response.body.message).to.contain(
+ `Private location with id ${nonExistentId} does not exist.`
+ );
+ });
+
+ it('returns 400 when trying to edit with an empty label', async () => {
+ const response = await supertestEditorWithApiKey
+ .put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocations[0].id}`)
+ .send({ label: '' })
+ .expect(400);
+
+ expect(response.body.message).to.contain(
+ '[request body.label]: value has length [0] but it must have a minimum length of [1].'
+ );
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts
index 0e79a70376f0..2dd2e0b50812 100644
--- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts
@@ -31,5 +31,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
loadTestFile(require.resolve('./sync_global_params'));
loadTestFile(require.resolve('./synthetics_enablement'));
loadTestFile(require.resolve('./test_now_monitor'));
+ loadTestFile(require.resolve('./edit_private_location'));
});
}