mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Synthetics] Edit private locations labels and tags in Synthetics (#221515)
This PR closes #221508. https://github.com/user-attachments/assets/0b57487a-7188-4722-99dc-5cb44c15f129 - Added a new API endpoint to edit the label of private locations. - When a label is updated, all monitors deployed in that location are automatically updated to reflect the change. - The UI now allows users to edit only the label of a private location. - Added comprehensive API tests to cover the new functionality. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b759ebba3d
commit
b1b8fb0a88
37 changed files with 994 additions and 111 deletions
|
@ -56759,6 +56759,69 @@ paths:
|
||||||
summary: Get a private location
|
summary: Get a private location
|
||||||
tags:
|
tags:
|
||||||
- synthetics
|
- 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:
|
/api/task_manager/_health:
|
||||||
get:
|
get:
|
||||||
description: |
|
description: |
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const GETTING_STARTED_ROUTE = '/monitors/getting-started';
|
||||||
|
|
||||||
export const SETTINGS_ROUTE = '/settings';
|
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';
|
export const SYNTHETICS_SETTINGS_ROUTE = '/settings/:tabId';
|
||||||
|
|
||||||
|
|
|
@ -1024,6 +1024,71 @@ paths:
|
||||||
},
|
},
|
||||||
"namespace": "default"
|
"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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
commonMonitorFields:
|
commonMonitorFields:
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { syntheticsAppPageProvider } from '../page_objects/synthetics_app';
|
||||||
journey(`PrivateLocationsSettings`, async ({ page, params }) => {
|
journey(`PrivateLocationsSettings`, async ({ page, params }) => {
|
||||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
|
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
|
||||||
const services = new SyntheticsServices(params);
|
const services = new SyntheticsServices(params);
|
||||||
|
const NEW_LOCATION_LABEL = 'Updated Test Location';
|
||||||
|
|
||||||
page.setDefaultTimeout(2 * 30000);
|
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 () => {
|
step('Integration cannot be edited in Fleet', async () => {
|
||||||
await page.goto(`${params.kibanaUrl}/app/integrations/detail/synthetics/policies`);
|
await page.goto(`${params.kibanaUrl}/app/integrations/detail/synthetics/policies`);
|
||||||
await page.waitForSelector('h1:has-text("Elastic Synthetics")');
|
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('h1:has-text("Edit Elastic Synthetics integration")');
|
||||||
await page.waitForSelector('text="This package policy is managed by the Synthetics app."');
|
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('h1:has-text("Settings")');
|
||||||
await page.click('text=Private Locations');
|
await page.click('text=Private Locations');
|
||||||
await page.waitForSelector('td:has-text("1")');
|
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('.euiTableRowCell .euiToolTipAnchor');
|
||||||
await page.click('button:has-text("Tags")');
|
await page.click('button:has-text("Tags")');
|
||||||
await page.click('[aria-label="Tags"] >> text=Area51');
|
await page.click('[aria-label="Tags"] >> text=Area51');
|
||||||
await page.click(
|
await page.click(
|
||||||
'main div:has-text("Private locations allow you to run monitors from your own premises. They require")'
|
'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');
|
await page.click('.euiTableRowCell .euiToolTipAnchor');
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,9 @@ describe('GettingStartedPage', () => {
|
||||||
loading: false,
|
loading: false,
|
||||||
privateLocations: [],
|
privateLocations: [],
|
||||||
deleteLoading: false,
|
deleteLoading: false,
|
||||||
onSubmit: jest.fn(),
|
onCreateLocationAPI: jest.fn(),
|
||||||
onDelete: jest.fn(),
|
onDeleteLocationAPI: jest.fn(),
|
||||||
|
onEditLocationAPI: jest.fn(),
|
||||||
createLoading: false,
|
createLoading: false,
|
||||||
});
|
});
|
||||||
jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true);
|
jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true);
|
||||||
|
@ -82,7 +83,7 @@ describe('GettingStartedPage', () => {
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: true,
|
isPrivateLocationFlyoutVisible: true,
|
||||||
},
|
},
|
||||||
agentPolicies: {
|
agentPolicies: {
|
||||||
data: [],
|
data: [],
|
||||||
|
@ -112,7 +113,7 @@ describe('GettingStartedPage', () => {
|
||||||
data: [{}],
|
data: [{}],
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: true,
|
isPrivateLocationFlyoutVisible: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -151,7 +152,7 @@ describe('GettingStartedPage', () => {
|
||||||
data: [{}],
|
data: [{}],
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: true,
|
isPrivateLocationFlyoutVisible: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,11 +27,14 @@ import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_
|
||||||
import { getServiceLocations, cleanMonitorListState } from '../../state';
|
import { getServiceLocations, cleanMonitorListState } from '../../state';
|
||||||
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui';
|
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui';
|
||||||
import { SimpleMonitorForm } from './simple_monitor_form';
|
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 type { ClientPluginsStart } from '../../../../plugin';
|
||||||
import { getAgentPoliciesAction, selectAgentPolicies } from '../../state/agent_policies';
|
import { getAgentPoliciesAction, selectAgentPolicies } from '../../state/agent_policies';
|
||||||
import { selectAddingNewPrivateLocation } from '../../state/settings/selectors';
|
import { setIsPrivateLocationFlyoutVisible } from '../../state/private_locations/actions';
|
||||||
import { setIsCreatePrivateLocationFlyoutVisible } from '../../state/private_locations/actions';
|
import { selectPrivateLocationFlyoutVisible } from '../../state/private_locations/selectors';
|
||||||
|
|
||||||
export const GettingStartedPage = () => {
|
export const GettingStartedPage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
@ -127,23 +130,23 @@ export const GettingStartedOnPrem = () => {
|
||||||
|
|
||||||
useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview
|
useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview
|
||||||
|
|
||||||
const isAddingNewLocation = useSelector(selectAddingNewPrivateLocation);
|
const isPrivateLocationFlyoutVisible = useSelector(selectPrivateLocationFlyoutVisible);
|
||||||
|
|
||||||
const setIsAddingNewLocation = useCallback(
|
const setIsFlyoutOpen = useCallback(
|
||||||
(val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
|
(val: boolean) => dispatch(setIsPrivateLocationFlyoutVisible(val)),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { onSubmit, privateLocations } = usePrivateLocationsAPI();
|
const { onCreateLocationAPI, privateLocations } = usePrivateLocationsAPI();
|
||||||
|
|
||||||
const handleSubmit = (formData: NewLocation) => {
|
const handleSubmit = (formData: NewLocation) => {
|
||||||
onSubmit(formData);
|
onCreateLocationAPI(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
// make sure flyout is closed when first visiting the page
|
// make sure flyout is closed when first visiting the page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsAddingNewLocation(false);
|
setIsFlyoutOpen(false);
|
||||||
}, [setIsAddingNewLocation]);
|
}, [setIsFlyoutOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -163,7 +166,7 @@ export const GettingStartedOnPrem = () => {
|
||||||
fill
|
fill
|
||||||
iconType="plusInCircleFilled"
|
iconType="plusInCircleFilled"
|
||||||
data-test-subj="gettingStartedAddLocationButton"
|
data-test-subj="gettingStartedAddLocationButton"
|
||||||
onClick={() => setIsAddingNewLocation(true)}
|
onClick={() => setIsFlyoutOpen(true)}
|
||||||
>
|
>
|
||||||
{CREATE_LOCATION_LABEL}
|
{CREATE_LOCATION_LABEL}
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
@ -173,9 +176,9 @@ export const GettingStartedOnPrem = () => {
|
||||||
footer={<GettingStartedLink />}
|
footer={<GettingStartedLink />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isAddingNewLocation ? (
|
{isPrivateLocationFlyoutVisible ? (
|
||||||
<AddLocationFlyout
|
<AddOrEditLocationFlyout
|
||||||
setIsOpen={setIsAddingNewLocation}
|
onCloseFlyout={() => setIsFlyoutOpen(false)}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
privateLocations={privateLocations}
|
privateLocations={privateLocations}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,9 +16,13 @@ import { ClientPluginsStart } from '../../../../../plugin';
|
||||||
|
|
||||||
interface SpaceSelectorProps {
|
interface SpaceSelectorProps {
|
||||||
helpText: string;
|
helpText: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaceSelector = <T extends FieldValues>({ helpText }: SpaceSelectorProps) => {
|
export const SpaceSelector = <T extends FieldValues>({
|
||||||
|
helpText,
|
||||||
|
isDisabled = false,
|
||||||
|
}: SpaceSelectorProps) => {
|
||||||
const NAMESPACES_NAME = 'spaces' as Path<T>;
|
const NAMESPACES_NAME = 'spaces' as Path<T>;
|
||||||
const { services } = useKibana<ClientPluginsStart>();
|
const { services } = useKibana<ClientPluginsStart>();
|
||||||
const [spacesList, setSpacesList] = React.useState<Array<{ id: string; label: string }>>([]);
|
const [spacesList, setSpacesList] = React.useState<Array<{ id: string; label: string }>>([]);
|
||||||
|
@ -61,6 +65,7 @@ export const SpaceSelector = <T extends FieldValues>({ helpText }: SpaceSelector
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<EuiComboBox
|
<EuiComboBox
|
||||||
|
isDisabled={isDisabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
aria-label={SPACES_LABEL}
|
aria-label={SPACES_LABEL}
|
||||||
placeholder={SPACES_LABEL}
|
placeholder={SPACES_LABEL}
|
||||||
|
|
|
@ -15,10 +15,12 @@ export function TagsField({
|
||||||
tagsList,
|
tagsList,
|
||||||
control,
|
control,
|
||||||
errors,
|
errors,
|
||||||
|
isDisabled,
|
||||||
}: {
|
}: {
|
||||||
tagsList: string[];
|
tagsList: string[];
|
||||||
errors: FieldErrors;
|
errors: FieldErrors;
|
||||||
control: Control<PrivateLocation, any>;
|
control: Control<PrivateLocation, any>;
|
||||||
|
isDisabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<EuiFormRow fullWidth label={TAGS_LABEL}>
|
<EuiFormRow fullWidth label={TAGS_LABEL}>
|
||||||
|
@ -27,6 +29,7 @@ export function TagsField({
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<EuiComboBox
|
<EuiComboBox
|
||||||
|
isDisabled={isDisabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
aria-label={TAGS_LABEL}
|
aria-label={TAGS_LABEL}
|
||||||
placeholder={TAGS_LABEL}
|
placeholder={TAGS_LABEL}
|
||||||
|
|
|
@ -35,20 +35,22 @@ import { selectPrivateLocationsState } from '../../../state/private_locations/se
|
||||||
export type NewLocation = Omit<PrivateLocation, 'id'>;
|
export type NewLocation = Omit<PrivateLocation, 'id'>;
|
||||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||||
|
|
||||||
export const AddLocationFlyout = ({
|
export const AddOrEditLocationFlyout = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
setIsOpen,
|
onCloseFlyout,
|
||||||
privateLocations,
|
privateLocations,
|
||||||
|
privateLocationToEdit,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: (val: NewLocation) => void;
|
onSubmit: (val: NewLocation) => void;
|
||||||
setIsOpen: (val: boolean) => void;
|
onCloseFlyout: () => void;
|
||||||
privateLocations: PrivateLocation[];
|
privateLocations: PrivateLocation[];
|
||||||
|
privateLocationToEdit?: PrivateLocation;
|
||||||
}) => {
|
}) => {
|
||||||
const form = useFormWrapped({
|
const form = useFormWrapped({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
shouldFocusError: true,
|
shouldFocusError: true,
|
||||||
defaultValues: {
|
defaultValues: privateLocationToEdit || {
|
||||||
label: '',
|
label: '',
|
||||||
agentPolicyId: '',
|
agentPolicyId: '',
|
||||||
geo: {
|
geo: {
|
||||||
|
@ -61,7 +63,7 @@ export const AddLocationFlyout = ({
|
||||||
|
|
||||||
const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext();
|
const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext();
|
||||||
|
|
||||||
const { createLoading } = useSelector(selectPrivateLocationsState);
|
const { createLoading, editLoading } = useSelector(selectPrivateLocationsState);
|
||||||
|
|
||||||
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
|
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
|
||||||
|
|
||||||
|
@ -72,33 +74,35 @@ export const AddLocationFlyout = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleSubmit } = form;
|
const { handleSubmit } = form;
|
||||||
const closeFlyout = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextWrapper>
|
<ContextWrapper>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<EuiFlyout onClose={closeFlyout} style={{ width: 540 }}>
|
<EuiFlyout onClose={onCloseFlyout} css={{ width: 540 }}>
|
||||||
<EuiFlyoutHeader hasBorder>
|
<EuiFlyoutHeader hasBorder>
|
||||||
<EuiTitle size="m">
|
<EuiTitle size="m">
|
||||||
<h2>{ADD_PRIVATE_LOCATION}</h2>
|
<h2>
|
||||||
|
{privateLocationToEdit !== undefined ? EDIT_PRIVATE_LOCATION : ADD_PRIVATE_LOCATION}
|
||||||
|
</h2>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
</EuiFlyoutHeader>
|
</EuiFlyoutHeader>
|
||||||
<EuiFlyoutBody>
|
<EuiFlyoutBody>
|
||||||
<ManageEmptyState privateLocations={privateLocations} showEmptyLocations={false}>
|
<ManageEmptyState privateLocations={privateLocations} showEmptyLocations={false}>
|
||||||
<LocationForm privateLocations={privateLocations} />
|
<LocationForm
|
||||||
|
privateLocations={privateLocations}
|
||||||
|
privateLocationToEdit={privateLocationToEdit}
|
||||||
|
/>
|
||||||
</ManageEmptyState>
|
</ManageEmptyState>
|
||||||
</EuiFlyoutBody>
|
</EuiFlyoutBody>
|
||||||
<EuiFlyoutFooter>
|
<EuiFlyoutFooter>
|
||||||
<EuiFlexGroup justifyContent="spaceBetween">
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty
|
||||||
data-test-subj="syntheticsAddLocationFlyoutButton"
|
data-test-subj="syntheticsLocationFlyoutCancelButton"
|
||||||
iconType="cross"
|
iconType="cross"
|
||||||
onClick={closeFlyout}
|
onClick={onCloseFlyout}
|
||||||
flush="left"
|
flush="left"
|
||||||
isLoading={createLoading}
|
isLoading={createLoading || editLoading}
|
||||||
>
|
>
|
||||||
{CANCEL_LABEL}
|
{CANCEL_LABEL}
|
||||||
</EuiButtonEmpty>
|
</EuiButtonEmpty>
|
||||||
|
@ -106,10 +110,10 @@ export const AddLocationFlyout = ({
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<NoPermissionsTooltip canEditSynthetics={canSave}>
|
<NoPermissionsTooltip canEditSynthetics={canSave}>
|
||||||
<EuiButton
|
<EuiButton
|
||||||
data-test-subj="syntheticsAddLocationFlyoutButton"
|
data-test-subj="syntheticsLocationFlyoutSaveButton"
|
||||||
fill
|
fill
|
||||||
onClick={handleSubmit(onSubmit)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
isLoading={createLoading}
|
isLoading={createLoading || editLoading}
|
||||||
isDisabled={!canSave || !canManagePrivateLocations}
|
isDisabled={!canSave || !canManagePrivateLocations}
|
||||||
>
|
>
|
||||||
{SAVE_LABEL}
|
{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', {
|
const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.cancelLabel', {
|
||||||
defaultMessage: 'Cancel',
|
defaultMessage: 'Cancel',
|
||||||
});
|
});
|
|
@ -12,19 +12,19 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { NoPermissionsTooltip } from '../../common/components/permissions';
|
import { NoPermissionsTooltip } from '../../common/components/permissions';
|
||||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||||
import { PRIVATE_LOCATIOSN_ROUTE } from '../../../../../../common/constants';
|
import { PRIVATE_LOCATIONS_ROUTE } from '../../../../../../common/constants';
|
||||||
import {
|
import {
|
||||||
setIsCreatePrivateLocationFlyoutVisible,
|
setIsPrivateLocationFlyoutVisible,
|
||||||
setManageFlyoutOpen,
|
setManageFlyoutOpen,
|
||||||
} from '../../../state/private_locations/actions';
|
} from '../../../state/private_locations/actions';
|
||||||
|
|
||||||
export const EmptyLocations = ({
|
export const EmptyLocations = ({
|
||||||
inFlyout = true,
|
inFlyout = true,
|
||||||
setIsAddingNew,
|
setIsFlyoutOpen,
|
||||||
redirectToSettings,
|
redirectToSettings,
|
||||||
}: {
|
}: {
|
||||||
inFlyout?: boolean;
|
inFlyout?: boolean;
|
||||||
setIsAddingNew?: (val: boolean) => void;
|
setIsFlyoutOpen?: (val: boolean) => void;
|
||||||
redirectToSettings?: boolean;
|
redirectToSettings?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
@ -52,7 +52,7 @@ export const EmptyLocations = ({
|
||||||
fill
|
fill
|
||||||
isDisabled={!canSave}
|
isDisabled={!canSave}
|
||||||
href={history.createHref({
|
href={history.createHref({
|
||||||
pathname: PRIVATE_LOCATIOSN_ROUTE,
|
pathname: PRIVATE_LOCATIONS_ROUTE,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{ADD_LOCATION}
|
{ADD_LOCATION}
|
||||||
|
@ -65,9 +65,9 @@ export const EmptyLocations = ({
|
||||||
color="primary"
|
color="primary"
|
||||||
fill
|
fill
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsAddingNew?.(true);
|
setIsFlyoutOpen?.(true);
|
||||||
dispatch(setManageFlyoutOpen(true));
|
dispatch(setManageFlyoutOpen(true));
|
||||||
dispatch(setIsCreatePrivateLocationFlyoutVisible(true));
|
dispatch(setIsPrivateLocationFlyoutVisible(true));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ADD_LOCATION}
|
{ADD_LOCATION}
|
||||||
|
|
|
@ -7,10 +7,12 @@
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 {
|
import {
|
||||||
createPrivateLocationAction,
|
createPrivateLocationAction,
|
||||||
deletePrivateLocationAction,
|
deletePrivateLocationAction,
|
||||||
|
editPrivateLocationAction,
|
||||||
getPrivateLocationsAction,
|
getPrivateLocationsAction,
|
||||||
} from '../../../../state/private_locations/actions';
|
} from '../../../../state/private_locations/actions';
|
||||||
import { selectPrivateLocationsState } from '../../../../state/private_locations/selectors';
|
import { selectPrivateLocationsState } from '../../../../state/private_locations/selectors';
|
||||||
|
@ -30,17 +32,22 @@ export const usePrivateLocationsAPI = () => {
|
||||||
}
|
}
|
||||||
}, [data, dispatch]);
|
}, [data, dispatch]);
|
||||||
|
|
||||||
const onSubmit = (newLoc: NewLocation) => {
|
const onCreateLocationAPI = (newLoc: NewLocation) => {
|
||||||
dispatch(createPrivateLocationAction.get(newLoc));
|
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));
|
dispatch(deletePrivateLocationAction.get(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onSubmit,
|
onCreateLocationAPI,
|
||||||
onDelete,
|
onEditLocationAPI,
|
||||||
|
onDeleteLocationAPI,
|
||||||
deleteLoading,
|
deleteLoading,
|
||||||
loading,
|
loading,
|
||||||
createLoading,
|
createLoading,
|
||||||
|
|
|
@ -18,7 +18,13 @@ import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||||
import { AgentPolicyNeeded } from './agent_policy_needed';
|
import { AgentPolicyNeeded } from './agent_policy_needed';
|
||||||
import { PolicyHostsField } from './policy_hosts';
|
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 { data } = useSelector(selectAgentPolicies);
|
||||||
const { control, register } = useFormContext<PrivateLocation>();
|
const { control, register } = useFormContext<PrivateLocation>();
|
||||||
const { errors } = useFormState();
|
const { errors } = useFormState();
|
||||||
|
@ -28,6 +34,8 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
|
||||||
return [...acc, ...tags];
|
return [...acc, ...tags];
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
|
|
||||||
|
const isEditingLocation = privateLocationToEdit !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 && <AgentPolicyNeeded />}
|
{data?.length === 0 && <AgentPolicyNeeded />}
|
||||||
|
@ -48,7 +56,8 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
|
||||||
message: NAME_REQUIRED,
|
message: NAME_REQUIRED,
|
||||||
},
|
},
|
||||||
validate: (val: string) => {
|
validate: (val: string) => {
|
||||||
return privateLocations.some((loc) => loc.label === val)
|
return privateLocations.some((loc) => loc.label === val) &&
|
||||||
|
val !== privateLocationToEdit?.label
|
||||||
? NAME_ALREADY_EXISTS
|
? NAME_ALREADY_EXISTS
|
||||||
: undefined;
|
: undefined;
|
||||||
},
|
},
|
||||||
|
@ -56,13 +65,13 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
<PolicyHostsField privateLocations={privateLocations} />
|
<PolicyHostsField privateLocations={privateLocations} isDisabled={isEditingLocation} />
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
<TagsField tagsList={tagsList} control={control} errors={errors} />
|
<TagsField tagsList={tagsList} control={control} errors={errors} />
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
<BrowserMonitorCallout />
|
<BrowserMonitorCallout />
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
<SpaceSelector helpText={LOCATION_HELP_TEXT} />
|
<SpaceSelector helpText={LOCATION_HELP_TEXT} isDisabled={isEditingLocation} />
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
EuiBadge,
|
EuiBadge,
|
||||||
|
EuiBasicTableColumn,
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
@ -31,7 +32,7 @@ import { DeleteLocation } from './delete_location';
|
||||||
import { useLocationMonitors } from './hooks/use_location_monitors';
|
import { useLocationMonitors } from './hooks/use_location_monitors';
|
||||||
import { PolicyName } from './policy_name';
|
import { PolicyName } from './policy_name';
|
||||||
import { LOCATION_NAME_LABEL } from './location_form';
|
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';
|
import { ClientPluginsStart } from '../../../../../plugin';
|
||||||
|
|
||||||
interface ListItem extends PrivateLocation {
|
interface ListItem extends PrivateLocation {
|
||||||
|
@ -41,10 +42,12 @@ interface ListItem extends PrivateLocation {
|
||||||
export const PrivateLocationsTable = ({
|
export const PrivateLocationsTable = ({
|
||||||
deleteLoading,
|
deleteLoading,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onEdit,
|
||||||
privateLocations,
|
privateLocations,
|
||||||
}: {
|
}: {
|
||||||
deleteLoading?: boolean;
|
deleteLoading?: boolean;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
|
onEdit: (privateLocation: PrivateLocation) => void;
|
||||||
privateLocations: PrivateLocation[];
|
privateLocations: PrivateLocation[];
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
@ -65,7 +68,7 @@ export const PrivateLocationsTable = ({
|
||||||
return new Set([...acc, ...tags]);
|
return new Set([...acc, ...tags]);
|
||||||
}, new Set<string>());
|
}, new Set<string>());
|
||||||
|
|
||||||
const columns = [
|
const columns: Array<EuiBasicTableColumn<ListItem>> = [
|
||||||
{
|
{
|
||||||
field: 'label',
|
field: 'label',
|
||||||
name: LOCATION_NAME_LABEL,
|
name: LOCATION_NAME_LABEL,
|
||||||
|
@ -114,6 +117,15 @@ export const PrivateLocationsTable = ({
|
||||||
{
|
{
|
||||||
name: ACTIONS_LABEL,
|
name: ACTIONS_LABEL,
|
||||||
actions: [
|
actions: [
|
||||||
|
{
|
||||||
|
name: EDIT_LOCATION,
|
||||||
|
description: EDIT_LOCATION,
|
||||||
|
isPrimary: true,
|
||||||
|
'data-test-subj': 'action-edit',
|
||||||
|
onClick: onEdit,
|
||||||
|
icon: 'pencil',
|
||||||
|
type: 'icon',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: DELETE_LOCATION,
|
name: DELETE_LOCATION,
|
||||||
description: DELETE_LOCATION,
|
description: DELETE_LOCATION,
|
||||||
|
@ -138,7 +150,7 @@ export const PrivateLocationsTable = ({
|
||||||
monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0,
|
monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setIsAddingNew = (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val));
|
const openFlyout = () => dispatch(setIsPrivateLocationFlyoutVisible(true));
|
||||||
|
|
||||||
const renderToolRight = () => {
|
const renderToolRight = () => {
|
||||||
return [
|
return [
|
||||||
|
@ -152,7 +164,7 @@ export const PrivateLocationsTable = ({
|
||||||
data-test-subj={'addPrivateLocationButton'}
|
data-test-subj={'addPrivateLocationButton'}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
disabled={!canSave || !canManagePrivateLocations}
|
disabled={!canSave || !canManagePrivateLocations}
|
||||||
onClick={() => setIsAddingNew(true)}
|
onClick={openFlyout}
|
||||||
iconType="plusInCircle"
|
iconType="plusInCircle"
|
||||||
>
|
>
|
||||||
{ADD_LABEL}
|
{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', {
|
const ADD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
|
||||||
defaultMessage: 'Create location',
|
defaultMessage: 'Create location',
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,14 +15,14 @@ import { selectAgentPolicies } from '../../../state/agent_policies';
|
||||||
export const ManageEmptyState: FC<
|
export const ManageEmptyState: FC<
|
||||||
PropsWithChildren<{
|
PropsWithChildren<{
|
||||||
privateLocations: PrivateLocation[];
|
privateLocations: PrivateLocation[];
|
||||||
setIsAddingNew?: (val: boolean) => void;
|
setIsFlyoutOpen?: (val: boolean) => void;
|
||||||
showNeedAgentPolicy?: boolean;
|
showNeedAgentPolicy?: boolean;
|
||||||
showEmptyLocations?: boolean;
|
showEmptyLocations?: boolean;
|
||||||
}>
|
}>
|
||||||
> = ({
|
> = ({
|
||||||
children,
|
children,
|
||||||
privateLocations,
|
privateLocations,
|
||||||
setIsAddingNew,
|
setIsFlyoutOpen,
|
||||||
showNeedAgentPolicy = true,
|
showNeedAgentPolicy = true,
|
||||||
showEmptyLocations = true,
|
showEmptyLocations = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -33,7 +33,7 @@ export const ManageEmptyState: FC<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateLocations.length === 0 && showEmptyLocations) {
|
if (privateLocations.length === 0 && showEmptyLocations) {
|
||||||
return <EmptyLocations setIsAddingNew={setIsAddingNew} />;
|
return <EmptyLocations setIsFlyoutOpen={setIsFlyoutOpen} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|
|
@ -28,9 +28,10 @@ describe('<ManagePrivateLocations />', () => {
|
||||||
});
|
});
|
||||||
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
|
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
|
||||||
loading: false,
|
loading: false,
|
||||||
onSubmit: jest.fn(),
|
onCreateLocationAPI: jest.fn(),
|
||||||
|
onEditLocationAPI: jest.fn(),
|
||||||
privateLocations: [],
|
privateLocations: [],
|
||||||
onDelete: jest.fn(),
|
onDeleteLocationAPI: jest.fn(),
|
||||||
deleteLoading: false,
|
deleteLoading: false,
|
||||||
createLoading: false,
|
createLoading: false,
|
||||||
});
|
});
|
||||||
|
@ -60,7 +61,7 @@ describe('<ManagePrivateLocations />', () => {
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: false,
|
isPrivateLocationFlyoutVisible: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -95,7 +96,7 @@ describe('<ManagePrivateLocations />', () => {
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: false,
|
isPrivateLocationFlyoutVisible: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -125,7 +126,8 @@ describe('<ManagePrivateLocations />', () => {
|
||||||
|
|
||||||
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
|
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
|
||||||
loading: false,
|
loading: false,
|
||||||
onSubmit: jest.fn(),
|
onCreateLocationAPI: jest.fn(),
|
||||||
|
onEditLocationAPI: jest.fn(),
|
||||||
privateLocations: [
|
privateLocations: [
|
||||||
{
|
{
|
||||||
label: privateLocationName,
|
label: privateLocationName,
|
||||||
|
@ -134,7 +136,7 @@ describe('<ManagePrivateLocations />', () => {
|
||||||
isServiceManaged: false,
|
isServiceManaged: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onDelete: jest.fn(),
|
onDeleteLocationAPI: jest.fn(),
|
||||||
deleteLoading: false,
|
deleteLoading: false,
|
||||||
createLoading: false,
|
createLoading: false,
|
||||||
});
|
});
|
||||||
|
@ -146,7 +148,7 @@ describe('<ManagePrivateLocations />', () => {
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: false,
|
isPrivateLocationFlyoutVisible: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,15 +8,23 @@ import React, { useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
import { SpacesContextProps } from '@kbn/spaces-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 { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout';
|
||||||
import { PrivateLocationsTable } from './locations_table';
|
import { PrivateLocationsTable } from './locations_table';
|
||||||
import { ManageEmptyState } from './manage_empty_state';
|
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 { 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 { getServiceLocations } from '../../../state';
|
||||||
import { getAgentPoliciesAction } from '../../../state/agent_policies';
|
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';
|
import { ClientPluginsStart } from '../../../../../plugin';
|
||||||
|
|
||||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||||
|
@ -33,23 +41,53 @@ export const ManagePrivateLocations = () => {
|
||||||
[spacesApi]
|
[spacesApi]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAddingNew = useSelector(selectAddingNewPrivateLocation);
|
const isPrivateLocationFlyoutVisible = useSelector(selectPrivateLocationFlyoutVisible);
|
||||||
const setIsAddingNew = useCallback(
|
const privateLocationToEdit = useSelector(selectPrivateLocationToEdit);
|
||||||
(val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
|
const setIsFlyoutOpen = useCallback(
|
||||||
|
(val: boolean) => dispatch(setIsPrivateLocationFlyoutVisible(val)),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = usePrivateLocationsAPI();
|
const {
|
||||||
|
onCreateLocationAPI,
|
||||||
|
onEditLocationAPI,
|
||||||
|
loading,
|
||||||
|
privateLocations,
|
||||||
|
onDeleteLocationAPI,
|
||||||
|
deleteLoading,
|
||||||
|
} = usePrivateLocationsAPI();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAgentPoliciesAction.get());
|
dispatch(getAgentPoliciesAction.get());
|
||||||
dispatch(getServiceLocations());
|
dispatch(getServiceLocations());
|
||||||
// make sure flyout is closed when first visiting the page
|
// make sure flyout is closed when first visiting the page
|
||||||
dispatch(setIsCreatePrivateLocationFlyoutVisible(false));
|
dispatch(setIsPrivateLocationFlyoutVisible(false));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleSubmit = (formData: NewLocation) => {
|
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 (
|
return (
|
||||||
|
@ -57,20 +95,22 @@ export const ManagePrivateLocations = () => {
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
) : (
|
) : (
|
||||||
<ManageEmptyState privateLocations={privateLocations} setIsAddingNew={setIsAddingNew}>
|
<ManageEmptyState privateLocations={privateLocations} setIsFlyoutOpen={setIsFlyoutOpen}>
|
||||||
<PrivateLocationsTable
|
<PrivateLocationsTable
|
||||||
privateLocations={privateLocations}
|
privateLocations={privateLocations}
|
||||||
onDelete={onDelete}
|
onDelete={onDeleteLocationAPI}
|
||||||
|
onEdit={onEditLocation}
|
||||||
deleteLoading={deleteLoading}
|
deleteLoading={deleteLoading}
|
||||||
/>
|
/>
|
||||||
</ManageEmptyState>
|
</ManageEmptyState>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAddingNew ? (
|
{isPrivateLocationFlyoutVisible ? (
|
||||||
<AddLocationFlyout
|
<AddOrEditLocationFlyout
|
||||||
setIsOpen={setIsAddingNew}
|
onCloseFlyout={onCloseFlyout}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
privateLocations={privateLocations}
|
privateLocations={privateLocations}
|
||||||
|
privateLocationToEdit={privateLocationToEdit}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</SpacesContextProvider>
|
</SpacesContextProvider>
|
||||||
|
|
|
@ -29,7 +29,13 @@ import { selectAgentPolicies } from '../../../state/agent_policies';
|
||||||
|
|
||||||
export const AGENT_POLICY_FIELD_NAME = 'agentPolicyId';
|
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 { data } = useSelector(selectAgentPolicies);
|
||||||
const { basePath } = useSyntheticsSettingsContext();
|
const { basePath } = useSyntheticsSettingsContext();
|
||||||
|
|
||||||
|
@ -53,7 +59,7 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
|
||||||
inputDisplay: (
|
inputDisplay: (
|
||||||
<EuiHealth
|
<EuiHealth
|
||||||
color={item.status === 'active' ? 'success' : 'warning'}
|
color={item.status === 'active' ? 'success' : 'warning'}
|
||||||
style={{ lineHeight: 'inherit' }}
|
css={{ lineHeight: 'inherit' }}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
|
@ -74,7 +80,7 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
|
||||||
<>
|
<>
|
||||||
<EuiHealth
|
<EuiHealth
|
||||||
color={item.status === 'active' ? 'success' : 'warning'}
|
color={item.status === 'active' ? 'success' : 'warning'}
|
||||||
style={{ lineHeight: 'inherit' }}
|
css={{ lineHeight: 'inherit' }}
|
||||||
>
|
>
|
||||||
<strong>{item.name}</strong>
|
<strong>{item.name}</strong>
|
||||||
</EuiHealth>
|
</EuiHealth>
|
||||||
|
@ -124,6 +130,7 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SuperSelect
|
<SuperSelect
|
||||||
|
disabled={isDisabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
aria-label={SELECT_POLICY_HOSTS}
|
aria-label={SELECT_POLICY_HOSTS}
|
||||||
placeholder={SELECT_POLICY_HOSTS}
|
placeholder={SELECT_POLICY_HOSTS}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
|
import { NewLocation } from '../../components/settings/private_locations/add_or_edit_location_flyout';
|
||||||
import { AgentPolicyInfo } from '../../../../../common/types';
|
import { AgentPolicyInfo } from '../../../../../common/types';
|
||||||
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||||
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
|
import { NewLocation } from '../../components/settings/private_locations/add_or_edit_location_flyout';
|
||||||
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||||
import { createAsyncAction } from '../utils/actions';
|
import { createAsyncAction } from '../utils/actions';
|
||||||
|
import type { EditPrivateLocationAttributes } from '../../../../../server/routes/settings/private_locations/edit_private_location';
|
||||||
|
|
||||||
export const getPrivateLocationsAction = createAsyncAction<void, SyntheticsPrivateLocations>(
|
export const getPrivateLocationsAction = createAsyncAction<void, SyntheticsPrivateLocations>(
|
||||||
'[PRIVATE LOCATIONS] GET'
|
'[PRIVATE LOCATIONS] GET'
|
||||||
|
@ -18,12 +19,24 @@ export const createPrivateLocationAction = createAsyncAction<NewLocation, Privat
|
||||||
'CREATE PRIVATE LOCATION'
|
'CREATE PRIVATE LOCATION'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const editPrivateLocationAction = createAsyncAction<
|
||||||
|
{
|
||||||
|
locationId: string;
|
||||||
|
newAttributes: EditPrivateLocationAttributes;
|
||||||
|
},
|
||||||
|
PrivateLocation
|
||||||
|
>('EDIT PRIVATE LOCATION');
|
||||||
|
|
||||||
export const deletePrivateLocationAction = createAsyncAction<string, SyntheticsPrivateLocations>(
|
export const deletePrivateLocationAction = createAsyncAction<string, SyntheticsPrivateLocations>(
|
||||||
'DELETE PRIVATE LOCATION'
|
'DELETE PRIVATE LOCATION'
|
||||||
);
|
);
|
||||||
|
|
||||||
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');
|
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');
|
||||||
|
|
||||||
export const setIsCreatePrivateLocationFlyoutVisible = createAction<boolean>(
|
export const setIsPrivateLocationFlyoutVisible = createAction<boolean>(
|
||||||
'SET IS CREATE PRIVATE LOCATION FLYOUT VISIBLE'
|
'SET IS CREATE OR EDIT PRIVATE LOCATION FLYOUT VISIBLE'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setPrivateLocationToEdit = createAction<PrivateLocation | undefined>(
|
||||||
|
'SET PRIVATE LOCATION TO EDIT'
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 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 { AgentPolicyInfo } from '../../../../../common/types';
|
||||||
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||||
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
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<PrivateLocation> => {
|
||||||
|
return apiService.put(
|
||||||
|
`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${locationId}`,
|
||||||
|
newAttributes,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
version: INITIAL_REST_VERSION,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getSyntheticsPrivateLocations = async (): Promise<SyntheticsPrivateLocations> => {
|
export const getSyntheticsPrivateLocations = async (): Promise<SyntheticsPrivateLocations> => {
|
||||||
return await apiService.get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, {
|
return await apiService.get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, {
|
||||||
version: INITIAL_REST_VERSION,
|
version: INITIAL_REST_VERSION,
|
||||||
|
|
|
@ -11,11 +11,13 @@ import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||||
import {
|
import {
|
||||||
createSyntheticsPrivateLocation,
|
createSyntheticsPrivateLocation,
|
||||||
deleteSyntheticsPrivateLocation,
|
deleteSyntheticsPrivateLocation,
|
||||||
|
editSyntheticsPrivateLocation,
|
||||||
getSyntheticsPrivateLocations,
|
getSyntheticsPrivateLocations,
|
||||||
} from './api';
|
} from './api';
|
||||||
import {
|
import {
|
||||||
createPrivateLocationAction,
|
createPrivateLocationAction,
|
||||||
deletePrivateLocationAction,
|
deletePrivateLocationAction,
|
||||||
|
editPrivateLocationAction,
|
||||||
getPrivateLocationsAction,
|
getPrivateLocationsAction,
|
||||||
} from './actions';
|
} 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() {
|
export function* deletePrivateLocationEffect() {
|
||||||
yield takeLeading(
|
yield takeLeading(
|
||||||
deletePrivateLocationAction.get,
|
deletePrivateLocationAction.get,
|
||||||
|
@ -61,5 +80,6 @@ export function* deletePrivateLocationEffect() {
|
||||||
export const privateLocationsEffects = [
|
export const privateLocationsEffects = [
|
||||||
fetchPrivateLocationsEffect,
|
fetchPrivateLocationsEffect,
|
||||||
createPrivateLocationEffect,
|
createPrivateLocationEffect,
|
||||||
|
editPrivateLocationEffect,
|
||||||
deletePrivateLocationEffect,
|
deletePrivateLocationEffect,
|
||||||
];
|
];
|
||||||
|
|
|
@ -7,19 +7,26 @@
|
||||||
|
|
||||||
import { createReducer } from '@reduxjs/toolkit';
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||||
import { createPrivateLocationAction, deletePrivateLocationAction } from './actions';
|
import {
|
||||||
import { setIsCreatePrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
|
createPrivateLocationAction,
|
||||||
|
deletePrivateLocationAction,
|
||||||
|
editPrivateLocationAction,
|
||||||
|
setPrivateLocationToEdit,
|
||||||
|
} from './actions';
|
||||||
|
import { setIsPrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
|
||||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||||
|
|
||||||
export interface PrivateLocationsState {
|
export interface PrivateLocationsState {
|
||||||
data?: SyntheticsPrivateLocations | null;
|
data?: SyntheticsPrivateLocations | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
createLoading?: boolean;
|
createLoading?: boolean;
|
||||||
|
editLoading?: boolean;
|
||||||
deleteLoading?: boolean;
|
deleteLoading?: boolean;
|
||||||
error: IHttpSerializedFetchError | null;
|
error: IHttpSerializedFetchError | null;
|
||||||
isManageFlyoutOpen?: boolean;
|
isManageFlyoutOpen?: boolean;
|
||||||
isCreatePrivateLocationFlyoutVisible?: boolean;
|
isPrivateLocationFlyoutVisible?: boolean;
|
||||||
newLocation?: PrivateLocation;
|
newLocation?: PrivateLocation;
|
||||||
|
privateLocationToEdit?: PrivateLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: PrivateLocationsState = {
|
const initialState: PrivateLocationsState = {
|
||||||
|
@ -27,8 +34,10 @@ const initialState: PrivateLocationsState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isManageFlyoutOpen: false,
|
isManageFlyoutOpen: false,
|
||||||
isCreatePrivateLocationFlyoutVisible: false,
|
isPrivateLocationFlyoutVisible: false,
|
||||||
createLoading: false,
|
createLoading: false,
|
||||||
|
editLoading: false,
|
||||||
|
privateLocationToEdit: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const privateLocationsStateReducer = createReducer(initialState, (builder) => {
|
export const privateLocationsStateReducer = createReducer(initialState, (builder) => {
|
||||||
|
@ -51,12 +60,26 @@ export const privateLocationsStateReducer = createReducer(initialState, (builder
|
||||||
state.newLocation = action.payload;
|
state.newLocation = action.payload;
|
||||||
state.createLoading = false;
|
state.createLoading = false;
|
||||||
state.data = null;
|
state.data = null;
|
||||||
state.isCreatePrivateLocationFlyoutVisible = false;
|
state.isPrivateLocationFlyoutVisible = false;
|
||||||
})
|
})
|
||||||
.addCase(createPrivateLocationAction.fail, (state, action) => {
|
.addCase(createPrivateLocationAction.fail, (state, action) => {
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
state.createLoading = false;
|
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) => {
|
.addCase(deletePrivateLocationAction.get, (state) => {
|
||||||
state.deleteLoading = true;
|
state.deleteLoading = true;
|
||||||
})
|
})
|
||||||
|
@ -68,7 +91,10 @@ export const privateLocationsStateReducer = createReducer(initialState, (builder
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
state.deleteLoading = false;
|
state.deleteLoading = false;
|
||||||
})
|
})
|
||||||
.addCase(setIsCreatePrivateLocationFlyoutVisible, (state, action) => {
|
.addCase(setIsPrivateLocationFlyoutVisible, (state, action) => {
|
||||||
state.isCreatePrivateLocationFlyoutVisible = action.payload;
|
state.isPrivateLocationFlyoutVisible = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(setPrivateLocationToEdit, (state, action) => {
|
||||||
|
state.privateLocationToEdit = action.payload;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,8 +11,11 @@ import { AppState } from '..';
|
||||||
const getState = (appState: AppState) => appState.privateLocations;
|
const getState = (appState: AppState) => appState.privateLocations;
|
||||||
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
||||||
|
|
||||||
export const selectAddingNewPrivateLocation = (state: AppState) =>
|
export const selectPrivateLocationFlyoutVisible = (state: AppState) =>
|
||||||
state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
|
state.privateLocations.isPrivateLocationFlyoutVisible ?? false;
|
||||||
|
|
||||||
|
export const selectPrivateLocationToEdit = (state: AppState) =>
|
||||||
|
state.privateLocations.privateLocationToEdit;
|
||||||
|
|
||||||
export const selectPrivateLocationsLoading = (state: AppState) =>
|
export const selectPrivateLocationsLoading = (state: AppState) =>
|
||||||
state.privateLocations.loading ?? false;
|
state.privateLocations.loading ?? false;
|
||||||
|
|
|
@ -14,7 +14,7 @@ const getState = (appState: AppState) => appState.agentPolicies;
|
||||||
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
||||||
|
|
||||||
export const selectAddingNewPrivateLocation = (state: AppState) =>
|
export const selectAddingNewPrivateLocation = (state: AppState) =>
|
||||||
state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
|
state.privateLocations.isPrivateLocationFlyoutVisible ?? false;
|
||||||
|
|
||||||
export const selectLocationMonitors = (state: AppState) => ({
|
export const selectLocationMonitors = (state: AppState) => ({
|
||||||
locationMonitors: state.dynamicSettings.locationMonitors,
|
locationMonitors: state.dynamicSettings.locationMonitors,
|
||||||
|
|
|
@ -116,7 +116,7 @@ export const mockState: SyntheticsAppState = {
|
||||||
data: null,
|
data: null,
|
||||||
},
|
},
|
||||||
privateLocations: {
|
privateLocations: {
|
||||||
isCreatePrivateLocationFlyoutVisible: false,
|
isPrivateLocationFlyoutVisible: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
data: [],
|
data: [],
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { PrivateLocationAttributes } from '../runtime_types/private_locations';
|
||||||
import { PrivateLocationObject } from '../routes/settings/private_locations/add_private_location';
|
import { PrivateLocationObject } from '../routes/settings/private_locations/add_private_location';
|
||||||
import { RouteContext } from '../routes/types';
|
import { RouteContext } from '../routes/types';
|
||||||
import { privateLocationSavedObjectName } from '../../common/saved_objects/private_locations';
|
import { privateLocationSavedObjectName } from '../../common/saved_objects/private_locations';
|
||||||
|
import { EditPrivateLocationAttributes } from '../routes/settings/private_locations/edit_private_location';
|
||||||
|
|
||||||
export class PrivateLocationRepository {
|
export class PrivateLocationRepository {
|
||||||
internalSOClient: ISavedObjectsRepository;
|
internalSOClient: ISavedObjectsRepository;
|
||||||
|
@ -33,6 +34,26 @@ export class PrivateLocationRepository {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPrivateLocation(locationId: string) {
|
||||||
|
const { savedObjectsClient } = this.routeContext;
|
||||||
|
|
||||||
|
return savedObjectsClient.get<PrivateLocationAttributes>(
|
||||||
|
privateLocationSavedObjectName,
|
||||||
|
locationId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editPrivateLocation(locationId: string, newAttributes: EditPrivateLocationAttributes) {
|
||||||
|
const { savedObjectsClient } = this.routeContext;
|
||||||
|
|
||||||
|
return savedObjectsClient.update<PrivateLocationAttributes>(
|
||||||
|
privateLocationSavedObjectName,
|
||||||
|
locationId,
|
||||||
|
newAttributes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async validatePrivateLocation() {
|
async validatePrivateLocation() {
|
||||||
const { response, request, server } = this.routeContext;
|
const { response, request, server } = this.routeContext;
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ import { getDefaultAlertingRoute } from './default_alerts/get_default_alert';
|
||||||
import { createNetworkEventsRoute } from './network_events';
|
import { createNetworkEventsRoute } from './network_events';
|
||||||
import { addPrivateLocationRoute } from './settings/private_locations/add_private_location';
|
import { addPrivateLocationRoute } from './settings/private_locations/add_private_location';
|
||||||
import { deletePrivateLocationRoute } from './settings/private_locations/delete_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 { getPrivateLocationsRoute } from './settings/private_locations/get_private_locations';
|
||||||
import { getSyntheticsFilters } from './filters/filters';
|
import { getSyntheticsFilters } from './filters/filters';
|
||||||
import { getAllSyntheticsMonitorRoute } from './monitor_cruds/get_monitors_list';
|
import { getAllSyntheticsMonitorRoute } from './monitor_cruds/get_monitors_list';
|
||||||
|
@ -112,6 +113,7 @@ export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] =
|
||||||
deleteSyntheticsParamsRoute,
|
deleteSyntheticsParamsRoute,
|
||||||
addPrivateLocationRoute,
|
addPrivateLocationRoute,
|
||||||
deletePrivateLocationRoute,
|
deletePrivateLocationRoute,
|
||||||
|
editPrivateLocationRoute,
|
||||||
getPrivateLocationsRoute,
|
getPrivateLocationsRoute,
|
||||||
getAllSyntheticsMonitorRoute,
|
getAllSyntheticsMonitorRoute,
|
||||||
getSyntheticsMonitorRoute,
|
getSyntheticsMonitorRoute,
|
||||||
|
|
|
@ -83,7 +83,10 @@ export const syncEditedMonitorBulk = async ({
|
||||||
} as unknown as MonitorFields,
|
} as unknown as MonitorFields,
|
||||||
}));
|
}));
|
||||||
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
|
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 }),
|
syncUpdatedMonitors({ monitorsToUpdate, routeContext, spaceId, privateLocations }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -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<typeof EditPrivateLocationSchema>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const isPrivateLocationLabelChanged = (oldLabel: string, newLabel?: string): newLabel is string => {
|
||||||
|
return typeof newLabel === 'string' && oldLabel !== newLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPrivateLocationChanged = ({
|
||||||
|
privateLocation,
|
||||||
|
newParams,
|
||||||
|
}: {
|
||||||
|
privateLocation: SavedObject<PrivateLocationAttributes>;
|
||||||
|
newParams: TypeOf<typeof EditPrivateLocationSchema>;
|
||||||
|
}) => {
|
||||||
|
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<typeof EditPrivateLocationQuery>,
|
||||||
|
any,
|
||||||
|
TypeOf<typeof EditPrivateLocationSchema>
|
||||||
|
> = () => ({
|
||||||
|
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<ReturnType<typeof repo.editPrivateLocation>> | 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -5,7 +5,20 @@
|
||||||
* 2.0.
|
* 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 = {
|
const testLocations = {
|
||||||
locations: [
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -4,14 +4,24 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import { 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 { AgentPolicyInfo } from '../../../../common/types';
|
||||||
import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
|
import type {
|
||||||
|
SyntheticsMonitor,
|
||||||
|
SyntheticsMonitorWithSecretsAttributes,
|
||||||
|
SyntheticsPrivateLocations,
|
||||||
|
} from '../../../../common/runtime_types';
|
||||||
import type {
|
import type {
|
||||||
SyntheticsPrivateLocationsAttributes,
|
SyntheticsPrivateLocationsAttributes,
|
||||||
PrivateLocationAttributes,
|
PrivateLocationAttributes,
|
||||||
} from '../../../runtime_types/private_locations';
|
} from '../../../runtime_types/private_locations';
|
||||||
import { PrivateLocation } from '../../../../common/runtime_types';
|
import { PrivateLocation } from '../../../../common/runtime_types';
|
||||||
|
import {
|
||||||
|
MonitorConfigUpdate,
|
||||||
|
syncEditedMonitorBulk,
|
||||||
|
} from '../../monitor_cruds/bulk_cruds/edit_monitor_bulk';
|
||||||
|
import { RouteContext } from '../../types';
|
||||||
|
|
||||||
export const toClientContract = (
|
export const toClientContract = (
|
||||||
locationObject: SavedObject<PrivateLocationAttributes>
|
locationObject: SavedObject<PrivateLocationAttributes>
|
||||||
|
@ -60,3 +70,54 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio
|
||||||
spaces: location.spaces,
|
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<SavedObjectsFindResult<SyntheticsMonitorWithSecretsAttributes>>;
|
||||||
|
}) => {
|
||||||
|
const updatedMonitorsPerSpace = monitorsInLocation.reduce<Record<string, MonitorConfigUpdate[]>>(
|
||||||
|
(acc, m) => {
|
||||||
|
const decryptedMonitorsWithNormalizedSecrets: SavedObject<SyntheticsMonitor> =
|
||||||
|
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());
|
||||||
|
};
|
||||||
|
|
|
@ -82,17 +82,20 @@ export class MonitorConfigRepository {
|
||||||
|
|
||||||
async bulkUpdate({
|
async bulkUpdate({
|
||||||
monitors,
|
monitors,
|
||||||
|
namespace,
|
||||||
}: {
|
}: {
|
||||||
monitors: Array<{
|
monitors: Array<{
|
||||||
attributes: MonitorFields;
|
attributes: MonitorFields;
|
||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
|
namespace?: string;
|
||||||
}) {
|
}) {
|
||||||
return await this.soClient.bulkUpdate<MonitorFields>(
|
return this.soClient.bulkUpdate<MonitorFields>(
|
||||||
monitors.map(({ attributes, id }) => ({
|
monitors.map(({ attributes, id }) => ({
|
||||||
type: syntheticsMonitorType,
|
type: syntheticsMonitorType,
|
||||||
id,
|
id,
|
||||||
attributes,
|
attributes,
|
||||||
|
namespace,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export function normalizeMonitorSecretAttributes(
|
||||||
const normalizedMonitorAttributes = {
|
const normalizedMonitorAttributes = {
|
||||||
...defaultFields,
|
...defaultFields,
|
||||||
...monitor,
|
...monitor,
|
||||||
...JSON.parse(monitor.secrets || ''),
|
...JSON.parse(monitor.secrets || '{}'),
|
||||||
};
|
};
|
||||||
delete normalizedMonitorAttributes.secrets;
|
delete normalizedMonitorAttributes.secrets;
|
||||||
return normalizedMonitorAttributes;
|
return normalizedMonitorAttributes;
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const GETTING_STARTED_ROUTE = '/monitors/getting-started';
|
||||||
|
|
||||||
export const SETTINGS_ROUTE = '/settings';
|
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';
|
export const SYNTHETICS_SETTINGS_ROUTE = '/settings/:tabId';
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,10 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
let resp;
|
let resp;
|
||||||
const { statusCodes, SPACE_ID, username, password, writeAccess, readUser } = options;
|
const { statusCodes, SPACE_ID, username, password, writeAccess, readUser } = options;
|
||||||
let tags = !writeAccess ? '[uptime-read]' : options.tags ?? '[uptime-read,uptime-write]';
|
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
|
tags = readUser
|
||||||
? '[private-location-write,uptime-write]'
|
? '[private-location-write,uptime-write]'
|
||||||
: '[uptime-read,private-location-write,uptime-write]';
|
: '[uptime-read,private-location-write,uptime-write]';
|
||||||
|
|
|
@ -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<PrivateLocation | ServiceLocation> = [
|
||||||
|
{
|
||||||
|
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].'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -31,5 +31,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
||||||
loadTestFile(require.resolve('./sync_global_params'));
|
loadTestFile(require.resolve('./sync_global_params'));
|
||||||
loadTestFile(require.resolve('./synthetics_enablement'));
|
loadTestFile(require.resolve('./synthetics_enablement'));
|
||||||
loadTestFile(require.resolve('./test_now_monitor'));
|
loadTestFile(require.resolve('./test_now_monitor'));
|
||||||
|
loadTestFile(require.resolve('./edit_private_location'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue