diff --git a/docs/api/synthetics/private-locations/create-private-location.asciidoc b/docs/api/synthetics/private-locations/create-private-location.asciidoc index 61a71de535b9..040cf26d851e 100644 --- a/docs/api/synthetics/private-locations/create-private-location.asciidoc +++ b/docs/api/synthetics/private-locations/create-private-location.asciidoc @@ -37,6 +37,9 @@ The request body should contain the following attributes: - `lat` (Required, number): The latitude of the location. - `lon` (Required, number): The longitude of the location. +`spaces`:: +(Optional, array of strings) An array of space IDs where the private location is available. If not provided, the private location is available in all spaces. + [[private-location-create-example]] ==== Example @@ -53,6 +56,7 @@ POST /api/private_locations "lat": 40.7128, "lon": -74.0060 } + "spaces": ["default"] } -------------------------------------------------- diff --git a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts index 7e402848a298..ec8db15e5e7d 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts @@ -22,6 +22,7 @@ export const PrivateLocationCodec = t.intersection([ lon: t.number, }), namespace: t.string, + spaces: t.array(t.string), }), ]); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx index 2587fe21fba2..1a35504699cb 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.test.tsx @@ -23,7 +23,7 @@ describe('GettingStartedPage', () => { deleteLoading: false, onSubmit: jest.fn(), onDelete: jest.fn(), - formData: undefined, + createLoading: false, }); jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true); }); @@ -81,9 +81,11 @@ describe('GettingStartedPage', () => { locationsLoaded: true, loading: false, }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: true, + }, agentPolicies: { data: [], - isAddingNewPrivateLocation: true, }, }, }); @@ -108,7 +110,9 @@ describe('GettingStartedPage', () => { }, agentPolicies: { data: [{}], - isAddingNewPrivateLocation: true, + }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: true, }, }, }); @@ -145,7 +149,9 @@ describe('GettingStartedPage', () => { }, agentPolicies: { data: [{}], - isAddingNewPrivateLocation: true, + }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: true, }, }, } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx index eb6172674240..c102536e0454 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/getting_started/getting_started_page.tsx @@ -24,18 +24,14 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useBreadcrumbs, useEnablement, useLocations } from '../../hooks'; import { usePrivateLocationsAPI } from '../settings/private_locations/hooks/use_locations_api'; import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout'; -import { - getServiceLocations, - selectAddingNewPrivateLocation, - setAddingNewPrivateLocation, - getAgentPoliciesAction, - selectAgentPolicies, - cleanMonitorListState, -} from '../../state'; +import { getServiceLocations, cleanMonitorListState } from '../../state'; import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui'; import { SimpleMonitorForm } from './simple_monitor_form'; import { AddLocationFlyout, NewLocation } from '../settings/private_locations/add_location_flyout'; import type { ClientPluginsStart } from '../../../../plugin'; +import { getAgentPoliciesAction, selectAgentPolicies } from '../../state/agent_policies'; +import { selectAddingNewPrivateLocation } from '../../state/settings/selectors'; +import { setIsCreatePrivateLocationFlyoutVisible } from '../../state/private_locations/actions'; export const GettingStartedPage = () => { const dispatch = useDispatch(); @@ -134,11 +130,11 @@ export const GettingStartedOnPrem = () => { const isAddingNewLocation = useSelector(selectAddingNewPrivateLocation); const setIsAddingNewLocation = useCallback( - (val: boolean) => dispatch(setAddingNewPrivateLocation(val)), + (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)), [dispatch] ); - const { onSubmit, privateLocations, loading } = usePrivateLocationsAPI(); + const { onSubmit, privateLocations } = usePrivateLocationsAPI(); const handleSubmit = (formData: NewLocation) => { onSubmit(formData); @@ -182,7 +178,6 @@ export const GettingStartedOnPrem = () => { setIsOpen={setIsAddingNewLocation} onSubmit={handleSubmit} privateLocations={privateLocations} - isLoading={loading} /> ) : null} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx new file mode 100644 index 000000000000..ce002a189c8e --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/components/spaces_select.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { Controller, useFormContext } from 'react-hook-form'; +import { ALL_SPACES_ID } from '@kbn/security-plugin/public'; + +import { ClientPluginsStart } from '../../../../../plugin'; +import { PrivateLocation } from '../../../../../../common/runtime_types'; + +export const NAMESPACES_NAME = 'spaces'; + +export const SpaceSelector: React.FC = () => { + const { services } = useKibana(); + const [spacesList, setSpacesList] = React.useState>([]); + const data = services.spaces?.ui.useSpaces(); + + const { + control, + formState: { isSubmitted }, + trigger, + } = useFormContext(); + const { isTouched, error } = control.getFieldState(NAMESPACES_NAME); + + const showFieldInvalid = (isSubmitted || isTouched) && !!error; + + useEffect(() => { + if (data) { + data.spacesDataPromise.then((spacesData) => { + setSpacesList([ + allSpacesOption, + ...[...spacesData.spacesMap].map(([spaceId, dataS]) => ({ + id: spaceId, + label: dataS.name, + })), + ]); + }); + } + }, [data]); + + return ( + + ( + { + await trigger(); + }} + options={spacesList} + selectedOptions={(field.value ?? []).map((id) => { + const sp = spacesList.find((space) => space.id === id); + if (!sp) { + return { + id, + label: id, + }; + } + return { id: sp.id, label: sp.label }; + })} + isClearable={true} + onChange={(selected) => { + const selectedIds = selected.map((option) => option.id); + + // if last value is not all spaces, remove all spaces value + if ( + selectedIds.length > 0 && + selectedIds[selectedIds.length - 1] !== allSpacesOption.id + ) { + field.onChange(selectedIds.filter((id) => id !== allSpacesOption.id)); + return; + } + + // if last value is all spaces, remove all other values + if ( + selectedIds.length > 0 && + selectedIds[selectedIds.length - 1] === allSpacesOption.id + ) { + field.onChange([allSpacesOption.id]); + return; + } + + field.onChange(selectedIds); + }} + /> + )} + /> + + ); +}; + +export const ALL_SPACES_LABEL = i18n.translate('xpack.synthetics.spaceList.allSpacesLabel', { + defaultMessage: `* All spaces`, +}); + +const allSpacesOption = { + id: ALL_SPACES_ID, + label: ALL_SPACES_LABEL, +}; + +const SPACES_LABEL = i18n.translate('xpack.synthetics.privateLocation.spacesLabel', { + defaultMessage: 'Spaces ', +}); + +const HELP_TEXT = i18n.translate('xpack.synthetics.privateLocation.spacesHelpText', { + defaultMessage: 'Select the spaces where this location will be available.', +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx index 36dc8be1cd0e..25027d5f02e0 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormProvider } from 'react-hook-form'; import { EuiButtonEmpty, @@ -19,22 +19,27 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { SpacesContextProps } from '@kbn/spaces-plugin/public'; +import { ALL_SPACES_ID } from '@kbn/security-plugin/public'; +import { useSelector } from 'react-redux'; import { NoPermissionsTooltip } from '../../common/components/permissions'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { useFormWrapped } from '../../../../../hooks/use_form_wrapped'; import { PrivateLocation } from '../../../../../../common/runtime_types'; import { LocationForm } from './location_form'; import { ManageEmptyState } from './manage_empty_state'; +import { ClientPluginsStart } from '../../../../../plugin'; +import { selectPrivateLocationsState } from '../../../state/private_locations/selectors'; export type NewLocation = Omit; +const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}; export const AddLocationFlyout = ({ onSubmit, setIsOpen, privateLocations, - isLoading, }: { - isLoading: boolean; onSubmit: (val: NewLocation) => void; setIsOpen: (val: boolean) => void; privateLocations: PrivateLocation[]; @@ -50,10 +55,21 @@ export const AddLocationFlyout = ({ lat: 0, lon: 0, }, + spaces: [ALL_SPACES_ID], }, }); - const { canSave } = useSyntheticsSettingsContext(); + const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext(); + + const { createLoading } = useSelector(selectPrivateLocationsState); + + const { spaces: spacesApi } = useKibana().services; + + const ContextWrapper = useMemo( + () => + spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent, + [spacesApi] + ); const { handleSubmit } = form; const closeFlyout = () => { @@ -61,48 +77,50 @@ export const AddLocationFlyout = ({ }; return ( - - - - -

{ADD_PRIVATE_LOCATION}

-
-
- - - - - - - - - - {CANCEL_LABEL} - - - - - + + + + +

{ADD_PRIVATE_LOCATION}

+
+
+ + + + + + + + + - {SAVE_LABEL} -
-
-
-
-
-
-
+ {CANCEL_LABEL} + + + + + + {SAVE_LABEL} + + + + + + + + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_callout.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_callout.tsx new file mode 100644 index 000000000000..84f87bcee4ce --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_callout.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { AGENT_MISSING_CALLOUT_TITLE } from './location_form'; + +export const AgentPolicyCallout: React.FC = () => { + return ( + +

+ { + + + + ), + }} + /> + } +

+
+ ); +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/browser_monitor_callout.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/browser_monitor_callout.tsx new file mode 100644 index 000000000000..17f120e6db00 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/browser_monitor_callout.tsx @@ -0,0 +1,42 @@ +/* + * 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 { EuiCallOut, EuiCode, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { AGENT_CALLOUT_TITLE } from './location_form'; + +export const BrowserMonitorCallout: React.FC = () => { + return ( + +

+ { + elastic-agent-complete, + link: ( + + + + ), + }} + /> + } +

+
+ ); +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx index 8a3af2c27558..9d871c7507ad 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/empty_locations.tsx @@ -13,7 +13,10 @@ import { useDispatch } from 'react-redux'; import { NoPermissionsTooltip } from '../../common/components/permissions'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { PRIVATE_LOCATIOSN_ROUTE } from '../../../../../../common/constants'; -import { setAddingNewPrivateLocation, setManageFlyoutOpen } from '../../../state/private_locations'; +import { + setIsCreatePrivateLocationFlyoutVisible, + setManageFlyoutOpen, +} from '../../../state/private_locations/actions'; export const EmptyLocations = ({ inFlyout = true, @@ -64,7 +67,7 @@ export const EmptyLocations = ({ onClick={() => { setIsAddingNew?.(true); dispatch(setManageFlyoutOpen(true)); - dispatch(setAddingNewPrivateLocation(true)); + dispatch(setIsCreatePrivateLocationFlyoutVisible(true)); }} > {ADD_LOCATION} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.test.tsx deleted file mode 100644 index 279a0b2a57af..000000000000 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.test.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createElement } from 'react'; -import { act, waitFor, renderHook } from '@testing-library/react'; -import { WrappedHelper } from '../../../../utils/testing'; -import { getServiceLocations } from '../../../../state/service_locations'; -import { setAddingNewPrivateLocation } from '../../../../state/private_locations'; -import { usePrivateLocationsAPI } from './use_locations_api'; -import * as locationAPI from '../../../../state/private_locations/api'; -import * as reduxHooks from 'react-redux'; - -describe('usePrivateLocationsAPI', () => { - const dispatch = jest.fn(); - const addAPI = jest.spyOn(locationAPI, 'addSyntheticsPrivateLocations').mockResolvedValue([]); - const deletedAPI = jest - .spyOn(locationAPI, 'deleteSyntheticsPrivateLocations') - .mockResolvedValue([]); - jest.spyOn(locationAPI, 'getSyntheticsPrivateLocations'); - jest.spyOn(reduxHooks, 'useDispatch').mockReturnValue(dispatch); - - it('returns expected results', () => { - const { result } = renderHook(() => usePrivateLocationsAPI(), { - wrapper: ({ children }) => createElement(WrappedHelper, null, children), - }); - - expect(result.current).toEqual( - expect.objectContaining({ - loading: true, - privateLocations: [], - }) - ); - expect(dispatch).toHaveBeenCalledTimes(1); - }); - jest.spyOn(locationAPI, 'getSyntheticsPrivateLocations').mockResolvedValue([ - { - id: 'Test', - agentPolicyId: 'testPolicy', - } as any, - ]); - it('returns expected results after data', async () => { - const { result } = renderHook(() => usePrivateLocationsAPI(), { - wrapper: ({ children }) => createElement(WrappedHelper, null, children), - }); - - expect(result.current).toEqual( - expect.objectContaining({ - loading: true, - privateLocations: [], - }) - ); - - await waitFor(() => - expect(result.current).toEqual( - expect.objectContaining({ - loading: false, - privateLocations: [], - }) - ) - ); - }); - - it('adds location on submit', async () => { - const { result } = renderHook(() => usePrivateLocationsAPI(), { - wrapper: ({ children }) => createElement(WrappedHelper, null, children), - }); - - await waitFor(() => new Promise((resolve) => resolve(null))); - - act(() => { - result.current.onSubmit({ - agentPolicyId: 'newPolicy', - label: 'new', - geo: { - lat: 0, - lon: 0, - }, - }); - }); - - await waitFor(() => { - expect(addAPI).toHaveBeenCalledWith({ - geo: { - lat: 0, - lon: 0, - }, - label: 'new', - agentPolicyId: 'newPolicy', - }); - expect(dispatch).toBeCalledWith(setAddingNewPrivateLocation(false)); - expect(dispatch).toBeCalledWith(getServiceLocations()); - }); - }); - - it('deletes location on delete', async () => { - const { result } = renderHook(() => usePrivateLocationsAPI(), { - wrapper: ({ children }) => createElement(WrappedHelper, null, children), - }); - - await waitFor(() => new Promise((resolve) => resolve(null))); - - act(() => { - result.current.onDelete('Test'); - }); - - await waitFor(() => { - expect(deletedAPI).toHaveBeenLastCalledWith('Test'); - expect(dispatch).toBeCalledWith(setAddingNewPrivateLocation(false)); - expect(dispatch).toBeCalledWith(getServiceLocations()); - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts index 4f3790edddec..162f8ff59f87 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts @@ -5,75 +5,45 @@ * 2.0. */ -import { useFetcher } from '@kbn/observability-shared-plugin/public'; -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { NewLocation } from '../add_location_flyout'; -import { getServiceLocations } from '../../../../state/service_locations'; import { + createPrivateLocationAction, + deletePrivateLocationAction, getPrivateLocationsAction, - selectPrivateLocations, - selectPrivateLocationsLoading, - setAddingNewPrivateLocation, -} from '../../../../state/private_locations'; -import { - addSyntheticsPrivateLocations, - deleteSyntheticsPrivateLocations, -} from '../../../../state/private_locations/api'; +} from '../../../../state/private_locations/actions'; +import { selectPrivateLocationsState } from '../../../../state/private_locations/selectors'; export const usePrivateLocationsAPI = () => { - const [formData, setFormData] = useState(); - const [deleteId, setDeleteId] = useState(); - const dispatch = useDispatch(); - const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val)); - const privateLocations = useSelector(selectPrivateLocations); - const fetchLoading = useSelector(selectPrivateLocationsLoading); + const { loading, createLoading, deleteLoading, data } = useSelector(selectPrivateLocationsState); useEffect(() => { dispatch(getPrivateLocationsAction.get()); }, [dispatch]); - const { loading: saveLoading } = useFetcher(async () => { - if (formData) { - const result = await addSyntheticsPrivateLocations(formData); - setFormData(undefined); - setIsAddingNew(false); - dispatch(getServiceLocations()); + useEffect(() => { + if (data === null) { dispatch(getPrivateLocationsAction.get()); - return result; } - // FIXME: Dario thinks there is a better way to do this but - // he's getting tired and maybe the Synthetics folks can fix it - }, [formData]); + }, [data, dispatch]); - const onSubmit = (data: NewLocation) => { - setFormData(data); + const onSubmit = (newLoc: NewLocation) => { + dispatch(createPrivateLocationAction.get(newLoc)); }; const onDelete = (id: string) => { - setDeleteId(id); + dispatch(deletePrivateLocationAction.get(id)); }; - const { loading: deleteLoading } = useFetcher(async () => { - if (deleteId) { - const result = await deleteSyntheticsPrivateLocations(deleteId); - setDeleteId(undefined); - dispatch(getServiceLocations()); - dispatch(getPrivateLocationsAction.get()); - return result; - } - // FIXME: Dario thinks there is a better way to do this but - // he's getting tired and maybe the Synthetics folks can fix it - }, [deleteId]); - return { - formData, onSubmit, onDelete, - deleteLoading: Boolean(deleteLoading), - loading: Boolean(fetchLoading || saveLoading), - privateLocations, + deleteLoading, + loading, + createLoading, + privateLocations: data ?? [], }; }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx index a0d98ae78b4e..ee58ed2165e4 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx @@ -6,32 +6,22 @@ */ import React, { Ref } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFieldText, - EuiForm, - EuiFormRow, - EuiSpacer, - EuiCallOut, - EuiCode, - EuiLink, - EuiFieldTextProps, -} from '@elastic/eui'; +import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiFieldTextProps } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { useFormContext, useFormState } from 'react-hook-form'; +import { selectAgentPolicies } from '../../../state/agent_policies'; +import { BrowserMonitorCallout } from './browser_monitor_callout'; +import { SpaceSelector } from '../components/spaces_select'; import { TagsField } from '../components/tags_field'; import { PrivateLocation } from '../../../../../../common/runtime_types'; import { AgentPolicyNeeded } from './agent_policy_needed'; -import { PolicyHostsField, AGENT_POLICY_FIELD_NAME } from './policy_hosts'; -import { selectAgentPolicies } from '../../../state/private_locations'; +import { PolicyHostsField } from './policy_hosts'; export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => { const { data } = useSelector(selectAgentPolicies); - const { control, register, getValues } = useFormContext(); + const { control, register } = useFormContext(); const { errors } = useFormState(); - const selectedPolicyId = getValues(AGENT_POLICY_FIELD_NAME); - const selectedPolicy = data?.find((item) => item.id === selectedPolicyId); const tagsList = privateLocations.reduce((acc, item) => { const tags = item.tags || []; @@ -70,66 +60,9 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo - -

- { - elastic-agent-complete, - link: ( - - - - ), - }} - /> - } -

-
- + - {selectedPolicy?.agents === 0 && ( - -

- { - - - - ), - }} - /> - } -

-
- )} + ); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx index ac6b471c9046..af51265feee2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx @@ -18,12 +18,12 @@ import { import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { CopyName } from './copy_name'; import { ViewLocationMonitors } from './view_location_monitors'; import { TableTitle } from '../../common/components/table_title'; import { TAGS_LABEL } from '../components/tags_field'; import { useSyntheticsSettingsContext } from '../../../contexts'; -import { setAddingNewPrivateLocation } from '../../../state/private_locations'; import { PrivateLocationDocsLink, START_ADDING_LOCATIONS_DESCRIPTION } from './empty_locations'; import { PrivateLocation } from '../../../../../../common/runtime_types'; import { NoPermissionsTooltip } from '../../common/components/permissions'; @@ -31,6 +31,8 @@ import { DeleteLocation } from './delete_location'; import { useLocationMonitors } from './hooks/use_location_monitors'; import { PolicyName } from './policy_name'; import { LOCATION_NAME_LABEL } from './location_form'; +import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions'; +import { ClientPluginsStart } from '../../../../../plugin'; interface ListItem extends PrivateLocation { monitors: number; @@ -41,7 +43,7 @@ export const PrivateLocationsTable = ({ onDelete, privateLocations, }: { - deleteLoading: boolean; + deleteLoading?: boolean; onDelete: (id: string) => void; privateLocations: PrivateLocation[]; }) => { @@ -54,6 +56,10 @@ export const PrivateLocationsTable = ({ const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext(); + const { services } = useKibana(); + + const LazySpaceList = services.spaces?.ui.components.getSpaceList ?? (() => null); + const tagsList = privateLocations.reduce((acc, item) => { const tags = item.tags || []; return new Set([...acc, ...tags]); @@ -97,6 +103,14 @@ export const PrivateLocationsTable = ({ ); }, }, + { + name: 'Spaces', + field: 'spaces', + sortable: true, + render: (spaces: string[]) => { + return ; + }, + }, { name: ACTIONS_LABEL, actions: [ @@ -124,7 +138,7 @@ export const PrivateLocationsTable = ({ monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0, })); - const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val)); + const setIsAddingNew = (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)); const renderToolRight = () => { return [ diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx index ff0ffea15bd3..dd0b5d8b9993 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx @@ -10,7 +10,7 @@ import { useSelector } from 'react-redux'; import { PrivateLocation } from '../../../../../../common/runtime_types'; import { AgentPolicyNeeded } from './agent_policy_needed'; import { EmptyLocations } from './empty_locations'; -import { selectAgentPolicies } from '../../../state/private_locations'; +import { selectAgentPolicies } from '../../../state/agent_policies'; export const ManageEmptyState: FC< PropsWithChildren<{ diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx index 5cabd6cf1374..a6d6ccfb1af7 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx @@ -12,7 +12,6 @@ import * as locationHooks from './hooks/use_locations_api'; import * as settingsHooks from '../../../contexts/synthetics_settings_context'; import type { SyntheticsSettingsContextValues } from '../../../contexts'; import { ManagePrivateLocations } from './manage_private_locations'; -import { PrivateLocation } from '../../../../../../common/runtime_types'; import { fireEvent } from '@testing-library/react'; jest.mock('../../../hooks'); @@ -28,12 +27,12 @@ describe('', () => { canCreateAgentPolicies: false, }); jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({ - formData: {} as PrivateLocation, loading: false, onSubmit: jest.fn(), privateLocations: [], onDelete: jest.fn(), deleteLoading: false, + createLoading: false, }); jest.spyOn(permissionsHooks, 'useEnablement').mockReturnValue({ isServiceAllowed: true, @@ -59,8 +58,9 @@ describe('', () => { data: [], loading: false, error: null, - isManageFlyoutOpen: false, - isAddingNewPrivateLocation: false, + }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: false, }, }, }); @@ -93,8 +93,9 @@ describe('', () => { data: [{}], loading: false, error: null, - isManageFlyoutOpen: false, - isAddingNewPrivateLocation: false, + }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: false, }, }, }); @@ -123,7 +124,6 @@ describe('', () => { } as SyntheticsSettingsContextValues); jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({ - formData: {} as PrivateLocation, loading: false, onSubmit: jest.fn(), privateLocations: [ @@ -136,6 +136,7 @@ describe('', () => { ], onDelete: jest.fn(), deleteLoading: false, + createLoading: false, }); const { getByText, getByRole, findByText } = render(, { state: { @@ -143,8 +144,9 @@ describe('', () => { data: [{}], loading: false, error: null, - isManageFlyoutOpen: false, - isAddingNewPrivateLocation: false, + }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: false, }, }, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx index 1f61e88bb762..4478c973f7ea 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx @@ -4,39 +4,48 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { SpacesContextProps } from '@kbn/spaces-plugin/public'; import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout'; import { PrivateLocationsTable } from './locations_table'; import { ManageEmptyState } from './manage_empty_state'; import { AddLocationFlyout, NewLocation } from './add_location_flyout'; import { usePrivateLocationsAPI } from './hooks/use_locations_api'; -import { - getAgentPoliciesAction, - selectAddingNewPrivateLocation, - setAddingNewPrivateLocation, -} from '../../../state/private_locations'; +import { selectAddingNewPrivateLocation } from '../../../state/private_locations/selectors'; import { getServiceLocations } from '../../../state'; +import { getAgentPoliciesAction } from '../../../state/agent_policies'; +import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions'; +import { ClientPluginsStart } from '../../../../../plugin'; + +const getEmptyFunctionComponent: React.FC = ({ children }) => <>{children}; export const ManagePrivateLocations = () => { const dispatch = useDispatch(); + const { services } = useKibana(); + + const spacesApi = services.spaces; + + const SpacesContextProvider = useMemo( + () => + spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent, + [spacesApi] + ); const isAddingNew = useSelector(selectAddingNewPrivateLocation); const setIsAddingNew = useCallback( - (val: boolean) => dispatch(setAddingNewPrivateLocation(val)), + (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)), [dispatch] ); const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = usePrivateLocationsAPI(); - // make sure flyout is closed when first visiting the page - useEffect(() => { - setIsAddingNew(false); - }, [setIsAddingNew]); - useEffect(() => { dispatch(getAgentPoliciesAction.get()); dispatch(getServiceLocations()); + // make sure flyout is closed when first visiting the page + dispatch(setIsCreatePrivateLocationFlyoutVisible(false)); }, [dispatch]); const handleSubmit = (formData: NewLocation) => { @@ -44,7 +53,7 @@ export const ManagePrivateLocations = () => { }; return ( - <> + {loading ? ( ) : ( @@ -62,9 +71,8 @@ export const ManagePrivateLocations = () => { setIsOpen={setIsAddingNew} onSubmit={handleSubmit} privateLocations={privateLocations} - isLoading={loading} /> ) : null} - + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx index 4b0f74120b90..f5743479015b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_hosts.tsx @@ -17,23 +17,33 @@ import { EuiSuperSelect, EuiText, EuiToolTip, + EuiSpacer, + EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useSyntheticsSettingsContext } from '../../../contexts'; +import { AgentPolicyCallout } from './agent_policy_callout'; import { PrivateLocation } from '../../../../../../common/runtime_types'; -import { selectAgentPolicies } from '../../../state/private_locations'; +import { selectAgentPolicies } from '../../../state/agent_policies'; export const AGENT_POLICY_FIELD_NAME = 'agentPolicyId'; export const PolicyHostsField = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => { const { data } = useSelector(selectAgentPolicies); + const { basePath } = useSyntheticsSettingsContext(); + const { control, formState: { isSubmitted }, trigger, + getValues, } = useFormContext(); const { isTouched, error } = control.getFieldState(AGENT_POLICY_FIELD_NAME); const showFieldInvalid = (isSubmitted || isTouched) && !!error; + const selectedPolicyId = getValues(AGENT_POLICY_FIELD_NAME); + + const selectedPolicy = data?.find((item) => item.id === selectedPolicyId); const policyHostsOptions = data?.map((item) => { const hasLocation = privateLocations.find((location) => location.agentPolicyId === item.id); @@ -89,36 +99,47 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva }); return ( - - ( - { - await trigger(); - }} - /> - )} - /> - + <> + + {i18n.translate('xpack.synthetics.policyHostsField.createButtonEmptyLabel', { + defaultMessage: 'Create policy', + })} + + } + helpText={showFieldInvalid ? SELECT_POLICY_HOSTS_HELP_TEXT : undefined} + isInvalid={showFieldInvalid} + error={showFieldInvalid ? SELECT_POLICY_HOSTS : undefined} + > + ( + { + await trigger(); + }} + /> + )} + /> + + + {selectedPolicy?.agents === 0 && } + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx index ea4ac5b75688..37017cbebf63 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/policy_name.tsx @@ -11,7 +11,7 @@ import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { useFleetPermissions } from '../../../hooks'; -import { selectAgentPolicies } from '../../../state/private_locations'; +import { selectAgentPolicies } from '../../../state/agent_policies'; export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => { const { canReadAgentPolicies } = useFleetPermissions(); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/actions.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/actions.ts new file mode 100644 index 000000000000..07f3cd7f8d0b --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/actions.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AgentPolicyInfo } from '../../../../../common/types'; +import { createAsyncAction } from '../utils/actions'; + +export const getAgentPoliciesAction = createAsyncAction( + '[AGENT POLICIES] GET' +); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/api.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/api.ts new file mode 100644 index 000000000000..e83482d544f5 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/api.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { NewLocation } from '../../components/settings/private_locations/add_location_flyout'; +import { AgentPolicyInfo } from '../../../../../common/types'; +import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants'; +import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; +import { apiService } from '../../../../utils/api_service/api_service'; + +export const fetchAgentPolicies = async (): Promise => { + return await apiService.get(SYNTHETICS_API_URLS.AGENT_POLICIES); +}; + +export const addSyntheticsPrivateLocations = async ( + newLocation: NewLocation +): Promise => { + return await apiService.post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, newLocation, undefined, { + version: INITIAL_REST_VERSION, + }); +}; + +export const getSyntheticsPrivateLocations = async (): Promise => { + return await apiService.get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, { + version: INITIAL_REST_VERSION, + }); +}; + +export const deleteSyntheticsPrivateLocations = async ( + locationId: string +): Promise => { + return await apiService.delete(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + `/${locationId}`, { + version: INITIAL_REST_VERSION, + }); +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/effects.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/effects.ts new file mode 100644 index 000000000000..c8889585676b --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/effects.ts @@ -0,0 +1,22 @@ +/* + * 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 { takeLeading } from 'redux-saga/effects'; +import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchAgentPolicies } from './api'; +import { getAgentPoliciesAction } from './actions'; + +export function* fetchAgentPoliciesEffect() { + yield takeLeading( + getAgentPoliciesAction.get, + fetchEffectFactory( + fetchAgentPolicies, + getAgentPoliciesAction.success, + getAgentPoliciesAction.fail + ) + ); +} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/index.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/index.ts new file mode 100644 index 000000000000..0042b9f1a9fb --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/index.ts @@ -0,0 +1,42 @@ +/* + * 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 { createReducer } from '@reduxjs/toolkit'; +import { AgentPolicyInfo } from '../../../../../common/types'; +import { IHttpSerializedFetchError } from '..'; +import { getAgentPoliciesAction } from './actions'; + +export interface AgentPoliciesState { + data: AgentPolicyInfo[] | null; + loading: boolean; + error: IHttpSerializedFetchError | null; +} + +const initialState: AgentPoliciesState = { + data: null, + loading: false, + error: null, +}; + +export const agentPoliciesReducer = createReducer(initialState, (builder) => { + builder + .addCase(getAgentPoliciesAction.get, (state) => { + state.loading = true; + }) + .addCase(getAgentPoliciesAction.success, (state, action) => { + state.data = action.payload; + state.loading = false; + }) + .addCase(getAgentPoliciesAction.fail, (state, action) => { + state.error = action.payload; + state.loading = false; + }); +}); + +export * from './actions'; +export * from './effects'; +export * from './selectors'; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/selectors.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/selectors.ts new file mode 100644 index 000000000000..a5d93d842310 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/agent_policies/selectors.ts @@ -0,0 +1,12 @@ +/* + * 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 { createSelector } from 'reselect'; +import { AppState } from '..'; + +const getState = (appState: AppState) => appState.agentPolicies; +export const selectAgentPolicies = createSelector(getState, (state) => state); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts index 42a464797807..29cf7510111f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/actions.ts @@ -6,18 +6,24 @@ */ import { createAction } from '@reduxjs/toolkit'; -import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; -import { AgentPolicyInfo } from '../../../../../common/types'; +import { NewLocation } from '../../components/settings/private_locations/add_location_flyout'; +import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; import { createAsyncAction } from '../utils/actions'; -export const getAgentPoliciesAction = createAsyncAction( - '[AGENT POLICIES] GET' -); - export const getPrivateLocationsAction = createAsyncAction( '[PRIVATE LOCATIONS] GET' ); +export const createPrivateLocationAction = createAsyncAction( + 'CREATE PRIVATE LOCATION' +); + +export const deletePrivateLocationAction = createAsyncAction( + 'DELETE PRIVATE LOCATION' +); + export const setManageFlyoutOpen = createAction('SET MANAGE FLYOUT OPEN'); -export const setAddingNewPrivateLocation = createAction('SET MANAGE FLYOUT ADDING NEW'); +export const setIsCreatePrivateLocationFlyoutVisible = createAction( + 'SET IS CREATE PRIVATE LOCATION FLYOUT VISIBLE' +); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts index e83482d544f5..afa722302c89 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts @@ -8,16 +8,16 @@ import { NewLocation } from '../../components/settings/private_locations/add_location_flyout'; import { AgentPolicyInfo } from '../../../../../common/types'; import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants'; -import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; +import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; import { apiService } from '../../../../utils/api_service/api_service'; export const fetchAgentPolicies = async (): Promise => { return await apiService.get(SYNTHETICS_API_URLS.AGENT_POLICIES); }; -export const addSyntheticsPrivateLocations = async ( +export const createSyntheticsPrivateLocation = async ( newLocation: NewLocation -): Promise => { +): Promise => { return await apiService.post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, newLocation, undefined, { version: INITIAL_REST_VERSION, }); @@ -29,7 +29,7 @@ export const getSyntheticsPrivateLocations = async (): Promise => { return await apiService.delete(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + `/${locationId}`, { diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts index 7be5abbdc407..0642e1d697ca 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/effects.ts @@ -6,20 +6,18 @@ */ import { takeLeading } from 'redux-saga/effects'; +import { i18n } from '@kbn/i18n'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { fetchAgentPolicies, getSyntheticsPrivateLocations } from './api'; -import { getAgentPoliciesAction, getPrivateLocationsAction } from './actions'; - -export function* fetchAgentPoliciesEffect() { - yield takeLeading( - getAgentPoliciesAction.get, - fetchEffectFactory( - fetchAgentPolicies, - getAgentPoliciesAction.success, - getAgentPoliciesAction.fail - ) - ); -} +import { + createSyntheticsPrivateLocation, + deleteSyntheticsPrivateLocation, + getSyntheticsPrivateLocations, +} from './api'; +import { + createPrivateLocationAction, + deletePrivateLocationAction, + getPrivateLocationsAction, +} from './actions'; export function* fetchPrivateLocationsEffect() { yield takeLeading( @@ -31,3 +29,37 @@ export function* fetchPrivateLocationsEffect() { ) ); } + +export function* createPrivateLocationEffect() { + yield takeLeading( + createPrivateLocationAction.get, + fetchEffectFactory( + createSyntheticsPrivateLocation, + createPrivateLocationAction.success, + createPrivateLocationAction.fail, + i18n.translate('xpack.synthetics.createPrivateLocationSuccess', { + defaultMessage: 'Successfully created private location.', + }), + i18n.translate('xpack.synthetics.createPrivateLocationFailure', { + defaultMessage: 'Failed to create private location.', + }) + ) + ); +} + +export function* deletePrivateLocationEffect() { + yield takeLeading( + deletePrivateLocationAction.get, + fetchEffectFactory( + deleteSyntheticsPrivateLocation, + deletePrivateLocationAction.success, + deletePrivateLocationAction.fail + ) + ); +} + +export const privateLocationsEffects = [ + fetchPrivateLocationsEffect, + createPrivateLocationEffect, + deletePrivateLocationEffect, +]; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts index 96634c74232b..45c9ba4ece57 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/index.ts @@ -6,62 +6,69 @@ */ import { createReducer } from '@reduxjs/toolkit'; -import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; -import { AgentPolicyInfo } from '../../../../../common/types'; -import { IHttpSerializedFetchError } from '..'; -import { - getAgentPoliciesAction, - setAddingNewPrivateLocation, - getPrivateLocationsAction, -} from './actions'; +import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; +import { createPrivateLocationAction, deletePrivateLocationAction } from './actions'; +import { setIsCreatePrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions'; +import { IHttpSerializedFetchError } from '../utils/http_error'; -export interface AgentPoliciesState { - data: AgentPolicyInfo[] | null; - privateLocations?: SyntheticsPrivateLocations | null; +export interface PrivateLocationsState { + data?: SyntheticsPrivateLocations | null; loading: boolean; - fetchLoading?: boolean; + createLoading?: boolean; + deleteLoading?: boolean; error: IHttpSerializedFetchError | null; isManageFlyoutOpen?: boolean; - isAddingNewPrivateLocation?: boolean; + isCreatePrivateLocationFlyoutVisible?: boolean; + newLocation?: PrivateLocation; } -const initialState: AgentPoliciesState = { +const initialState: PrivateLocationsState = { data: null, loading: false, error: null, isManageFlyoutOpen: false, - isAddingNewPrivateLocation: false, + isCreatePrivateLocationFlyoutVisible: false, + createLoading: false, }; -export const agentPoliciesReducer = createReducer(initialState, (builder) => { +export const privateLocationsStateReducer = createReducer(initialState, (builder) => { builder - .addCase(getAgentPoliciesAction.get, (state) => { + .addCase(getPrivateLocationsAction.get, (state) => { state.loading = true; }) - .addCase(getAgentPoliciesAction.success, (state, action) => { + .addCase(getPrivateLocationsAction.success, (state, action) => { state.data = action.payload; state.loading = false; }) - .addCase(getAgentPoliciesAction.fail, (state, action) => { + .addCase(getPrivateLocationsAction.fail, (state, action) => { state.error = action.payload; state.loading = false; }) - .addCase(getPrivateLocationsAction.get, (state) => { - state.fetchLoading = true; + .addCase(createPrivateLocationAction.get, (state) => { + state.createLoading = true; }) - .addCase(getPrivateLocationsAction.success, (state, action) => { - state.privateLocations = action.payload; - state.fetchLoading = false; + .addCase(createPrivateLocationAction.success, (state, action) => { + state.newLocation = action.payload; + state.createLoading = false; + state.data = null; + state.isCreatePrivateLocationFlyoutVisible = false; }) - .addCase(getPrivateLocationsAction.fail, (state, action) => { + .addCase(createPrivateLocationAction.fail, (state, action) => { state.error = action.payload; - state.fetchLoading = false; + state.createLoading = false; }) - .addCase(setAddingNewPrivateLocation, (state, action) => { - state.isAddingNewPrivateLocation = action.payload; + .addCase(deletePrivateLocationAction.get, (state) => { + state.deleteLoading = true; + }) + .addCase(deletePrivateLocationAction.success, (state, action) => { + state.deleteLoading = false; + state.data = null; + }) + .addCase(deletePrivateLocationAction.fail, (state, action) => { + state.error = action.payload; + state.deleteLoading = false; + }) + .addCase(setIsCreatePrivateLocationFlyoutVisible, (state, action) => { + state.isCreatePrivateLocationFlyoutVisible = action.payload; }); }); - -export * from './actions'; -export * from './effects'; -export * from './selectors'; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts index 0f504d189c0c..a9ce77217579 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/private_locations/selectors.ts @@ -8,14 +8,21 @@ import { createSelector } from 'reselect'; import { AppState } from '..'; -const getState = (appState: AppState) => appState.agentPolicies; +const getState = (appState: AppState) => appState.privateLocations; export const selectAgentPolicies = createSelector(getState, (state) => state); export const selectAddingNewPrivateLocation = (state: AppState) => - state.agentPolicies.isAddingNewPrivateLocation ?? false; + state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false; export const selectPrivateLocationsLoading = (state: AppState) => - state.agentPolicies.fetchLoading ?? false; + state.privateLocations.loading ?? false; -export const selectPrivateLocations = (state: AppState) => - state.agentPolicies.privateLocations ?? []; +export const selectPrivateLocationCreating = (state: AppState) => + state.privateLocations.createLoading ?? false; + +export const selectPrivateLocationDeleting = (state: AppState) => + state.privateLocations.deleteLoading ?? false; + +export const selectPrivateLocationsState = (state: AppState) => state.privateLocations; + +export const selectPrivateLocations = (state: AppState) => state.privateLocations.data ?? []; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_effect.ts index e38a1b5ad918..9295e318104a 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_effect.ts @@ -28,7 +28,7 @@ import { setDynamicSettingsEffect, } from './settings/effects'; import { syncGlobalParamsEffect } from './settings'; -import { fetchAgentPoliciesEffect, fetchPrivateLocationsEffect } from './private_locations'; +import { privateLocationsEffects } from './private_locations/effects'; import { fetchNetworkEventsEffect } from './network_events/effects'; import { fetchSyntheticsMonitorEffect } from './monitor_details'; import { fetchSyntheticsEnablementEffect } from './synthetics_enablement'; @@ -44,6 +44,7 @@ import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journe import { fetchOverviewStatusEffect } from './overview_status'; import { fetchMonitorStatusHeatmap, quietFetchMonitorStatusHeatmap } from './status_heatmap'; import { fetchOverviewTrendStats, refreshOverviewTrendStats } from './overview/effects'; +import { fetchAgentPoliciesEffect } from './agent_policies'; export const rootEffect = function* root(): Generator { yield all([ @@ -57,7 +58,6 @@ export const rootEffect = function* root(): Generator { fork(fetchOverviewStatusEffect), fork(fetchNetworkEventsEffect), fork(fetchAgentPoliciesEffect), - fork(fetchPrivateLocationsEffect), fork(fetchDynamicSettingsEffect), fork(fetchLocationMonitorsEffect), fork(setDynamicSettingsEffect), @@ -80,5 +80,6 @@ export const rootEffect = function* root(): Generator { fork(quietFetchMonitorStatusHeatmap), fork(fetchOverviewTrendStats), fork(refreshOverviewTrendStats), + ...privateLocationsEffects.map((effect) => fork(effect)), ]); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts index f8ace41e9319..70dcfb1aed9e 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -21,7 +21,7 @@ import { SettingsState, } from './settings'; import { elasticsearchReducer, QueriesState } from './elasticsearch'; -import { agentPoliciesReducer, AgentPoliciesState } from './private_locations'; +import { PrivateLocationsState, privateLocationsStateReducer } from './private_locations'; import { networkEventsReducer, NetworkEventsState } from './network_events'; import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details'; import { uiReducer, UiState } from './ui'; @@ -31,47 +31,50 @@ import { serviceLocationsReducer, ServiceLocationsState } from './service_locati import { monitorOverviewReducer, MonitorOverviewState } from './overview'; import { BrowserJourneyState } from './browser_journey/models'; import { monitorStatusHeatmapReducer, MonitorStatusHeatmap } from './status_heatmap'; +import { agentPoliciesReducer, AgentPoliciesState } from './agent_policies'; export interface SyntheticsAppState { - ui: UiState; - settings: SettingsState; - elasticsearch: QueriesState; - monitorList: MonitorListState; - overview: MonitorOverviewState; - certificates: CertificatesState; - globalParams: GlobalParamsState; - networkEvents: NetworkEventsState; agentPolicies: AgentPoliciesState; - manualTestRuns: ManualTestRunsState; - monitorDetails: MonitorDetailsState; browserJourney: BrowserJourneyState; + certificates: CertificatesState; certsList: CertsListState; defaultAlerting: DefaultAlertingState; dynamicSettings: DynamicSettingsState; - serviceLocations: ServiceLocationsState; - overviewStatus: OverviewStatusStateReducer; - syntheticsEnablement: SyntheticsEnablementState; + elasticsearch: QueriesState; + globalParams: GlobalParamsState; + manualTestRuns: ManualTestRunsState; + monitorDetails: MonitorDetailsState; + monitorList: MonitorListState; monitorStatusHeatmap: MonitorStatusHeatmap; + networkEvents: NetworkEventsState; + overview: MonitorOverviewState; + overviewStatus: OverviewStatusStateReducer; + privateLocations: PrivateLocationsState; + serviceLocations: ServiceLocationsState; + settings: SettingsState; + syntheticsEnablement: SyntheticsEnablementState; + ui: UiState; } export const rootReducer = combineReducers({ - ui: uiReducer, - settings: settingsReducer, - monitorList: monitorListReducer, - overview: monitorOverviewReducer, - globalParams: globalParamsReducer, - networkEvents: networkEventsReducer, - elasticsearch: elasticsearchReducer, agentPolicies: agentPoliciesReducer, - monitorDetails: monitorDetailsReducer, browserJourney: browserJourneyReducer, - manualTestRuns: manualTestRunsReducer, - overviewStatus: overviewStatusReducer, - defaultAlerting: defaultAlertingReducer, - dynamicSettings: dynamicSettingsReducer, - serviceLocations: serviceLocationsReducer, - syntheticsEnablement: syntheticsEnablementReducer, certificates: certificatesReducer, certsList: certsListReducer, + defaultAlerting: defaultAlertingReducer, + dynamicSettings: dynamicSettingsReducer, + elasticsearch: elasticsearchReducer, + globalParams: globalParamsReducer, + manualTestRuns: manualTestRunsReducer, + monitorDetails: monitorDetailsReducer, + monitorList: monitorListReducer, monitorStatusHeatmap: monitorStatusHeatmapReducer, + networkEvents: networkEventsReducer, + overview: monitorOverviewReducer, + overviewStatus: overviewStatusReducer, + privateLocations: privateLocationsStateReducer, + serviceLocations: serviceLocationsReducer, + settings: settingsReducer, + syntheticsEnablement: syntheticsEnablementReducer, + ui: uiReducer, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts index e73fe77423d4..195c1380ebc9 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/settings/selectors.ts @@ -14,7 +14,7 @@ const getState = (appState: AppState) => appState.agentPolicies; export const selectAgentPolicies = createSelector(getState, (state) => state); export const selectAddingNewPrivateLocation = (state: AppState) => - state.agentPolicies.isAddingNewPrivateLocation ?? false; + state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false; export const selectLocationMonitors = (state: AppState) => ({ locationMonitors: state.dynamicSettings.locationMonitors, diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index fe2ad5f7512c..dc71f90ea9e1 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -114,6 +114,12 @@ export const mockState: SyntheticsAppState = { error: null, data: null, }, + privateLocations: { + isCreatePrivateLocationFlyoutVisible: false, + loading: false, + error: null, + data: [], + }, settings: { loading: false, error: null, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts b/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts new file mode 100644 index 000000000000..f42947b8252c --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/repositories/private_location_repository.ts @@ -0,0 +1,112 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import { isEmpty } from 'lodash'; +import { getAgentPoliciesAsInternalUser } from '../routes/settings/private_locations/get_agent_policies'; +import { PrivateLocationAttributes } from '../runtime_types/private_locations'; +import { PrivateLocationObject } from '../routes/settings/private_locations/add_private_location'; +import { RouteContext } from '../routes/types'; +import { privateLocationSavedObjectName } from '../../common/saved_objects/private_locations'; + +export class PrivateLocationRepository { + internalSOClient: ISavedObjectsRepository; + constructor(private routeContext: RouteContext) { + const { server } = routeContext; + this.internalSOClient = server.coreStart.savedObjects.createInternalRepository(); + } + + async createPrivateLocation(formattedLocation: PrivateLocationAttributes, newId: string) { + const { savedObjectsClient } = this.routeContext; + const { spaces } = formattedLocation; + + return await savedObjectsClient.create( + privateLocationSavedObjectName, + formattedLocation, + { + id: newId, + initialNamespaces: isEmpty(spaces) || spaces?.includes('*') ? ['*'] : spaces, + } + ); + } + async validatePrivateLocation() { + const { response, request, server } = this.routeContext; + + let errorMessages = ''; + + const location = request.body as PrivateLocationObject; + + const { spaces } = location; + + const [data, agentPolicies] = await Promise.all([ + this.internalSOClient.find({ + type: privateLocationSavedObjectName, + perPage: 10000, + namespaces: spaces, + }), + await getAgentPoliciesAsInternalUser({ server }), + ]); + + const locations = data.saved_objects.map((loc) => ({ + ...loc.attributes, + spaces: loc.attributes.spaces || loc.namespaces, + })); + + const locWithAgentPolicyId = locations.find( + (loc) => loc.agentPolicyId === location.agentPolicyId + ); + + if (locWithAgentPolicyId) { + errorMessages = i18n.translate( + 'xpack.synthetics.privateLocations.create.errorMessages.policyExists', + { + defaultMessage: `Private location with agentPolicyId {agentPolicyId} already exists in spaces {spaces}`, + values: { + agentPolicyId: location.agentPolicyId, + spaces: formatSpaces(locWithAgentPolicyId.spaces), + }, + } + ); + } + + // return if name is already taken + const locWithSameLabel = locations.find((loc) => loc.label === location.label); + if (locWithSameLabel) { + errorMessages = i18n.translate( + 'xpack.synthetics.privateLocations.create.errorMessages.labelExists', + { + defaultMessage: `Private location with label {label} already exists in spaces: {spaces}`, + values: { label: location.label, spaces: formatSpaces(locWithSameLabel.spaces) }, + } + ); + } + + const agentPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); + if (!agentPolicy) { + errorMessages = `Agent policy with id ${location.agentPolicyId} does not exist`; + } + if (errorMessages) { + return response.badRequest({ + body: { + message: errorMessages, + }, + }); + } + } +} + +const formatSpaces = (spaces: string[] | undefined) => { + return ( + spaces + ?.map((space) => + space === '*' + ? i18n.translate('xpack.synthetics.formatSpaces.', { defaultMessage: '* All Spaces' }) + : space + ) + .join(', ') ?? 'Unknown' + ); +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/add_private_location.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/add_private_location.ts index fa88de31e3ec..fe4d547ff9f6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/add_private_location.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/add_private_location.ts @@ -6,13 +6,13 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { v4 as uuidV4 } from 'uuid'; +import { PrivateLocationRepository } from '../../../repositories/private_location_repository'; import { PRIVATE_LOCATION_WRITE_API } from '../../../feature'; import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../types'; -import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; -import { privateLocationSavedObjectName } from '../../../../common/saved_objects/private_locations'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; -import { PrivateLocationAttributes } from '../../../runtime_types/private_locations'; import { toClientContract, toSavedObjectContract } from './helpers'; import { PrivateLocation } from '../../../../common/runtime_types'; @@ -26,6 +26,11 @@ export const PrivateLocationSchema = schema.object({ lon: schema.number(), }) ), + spaces: schema.maybe( + schema.arrayOf(schema.string(), { + minSize: 1, + }) + ), }); export type PrivateLocationObject = TypeOf; @@ -41,60 +46,43 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory { - const { response, request, savedObjectsClient, syntheticsMonitorClient, server } = routeContext; + const { response, request, server } = routeContext; const internalSOClient = server.coreStart.savedObjects.createInternalRepository(); - await migrateLegacyPrivateLocations(internalSOClient, server.logger); + const repo = new PrivateLocationRepository(routeContext); + + const invalidError = await repo.validatePrivateLocation(); + if (invalidError) { + return invalidError; + } + const location = request.body as PrivateLocationObject; + const newId = uuidV4(); + const formattedLocation = toSavedObjectContract({ ...location, id: newId }); + const { spaces } = location; - const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( - savedObjectsClient, - syntheticsMonitorClient - ); + try { + const result = await repo.createPrivateLocation(formattedLocation, newId); - if (locations.find((loc) => loc.agentPolicyId === location.agentPolicyId)) { - return response.badRequest({ - body: { - message: `Private location with agentPolicyId ${location.agentPolicyId} already exists`, - }, - }); - } - - // return if name is already taken - if (locations.find((loc) => loc.label === location.label)) { - return response.badRequest({ - body: { - message: `Private location with label ${location.label} already exists`, - }, - }); - } - - const formattedLocation = toSavedObjectContract({ - ...location, - id: location.agentPolicyId, - }); - - const agentPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); - if (!agentPolicy) { - return response.badRequest({ - body: { - message: `Agent policy with id ${location.agentPolicyId} does not exist`, - }, - }); - } - - const soClient = routeContext.server.coreStart.savedObjects.createInternalRepository(); - - const result = await soClient.create( - privateLocationSavedObjectName, - formattedLocation, - { - id: location.agentPolicyId, - initialNamespaces: ['*'], + return toClientContract(result); + } catch (error) { + if (SavedObjectsErrorHelpers.isForbiddenError(error)) { + if (spaces?.includes('*')) { + return response.badRequest({ + body: { + message: `You do not have permission to create a location in all spaces.`, + }, + }); + } + return response.customError({ + statusCode: error.output.statusCode, + body: { + message: error.message, + }, + }); } - ); - - return toClientContract(result.attributes, agentPolicies); + throw error; + } }, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts index 8df065ad3e48..956fdec42335 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { SavedObject } from '@kbn/core/server'; import { AgentPolicyInfo } from '../../../../common/types'; import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types'; import type { @@ -13,18 +14,18 @@ import type { import { PrivateLocation } from '../../../../common/runtime_types'; export const toClientContract = ( - location: PrivateLocationAttributes, - agentPolicies?: AgentPolicyInfo[] + locationObject: SavedObject ): PrivateLocation => { - const agPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); + const location = locationObject.attributes; return { label: location.label, id: location.id, agentPolicyId: location.agentPolicyId, isServiceManaged: false, - isInvalid: !Boolean(agPolicy), + isInvalid: false, tags: location.tags, geo: location.geo, + spaces: locationObject.namespaces, }; }; @@ -42,6 +43,7 @@ export const allLocationsToClientContract = ( isInvalid: !Boolean(agPolicy), tags: location.tags, geo: location.geo, + spaces: location.spaces, }; }); }; @@ -55,5 +57,6 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio isServiceManaged: false, geo: location.geo, namespace: location.namespace, + spaces: location.spaces, }; }; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts index a476df9dfe03..3bfa62988a16 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts @@ -25,22 +25,18 @@ export const getPrivateLocations = async ( client: SavedObjectsClientContract ): Promise => { try { - const finder = client.createPointInTimeFinder({ - type: privateLocationSavedObjectName, - perPage: 1000, - }); + const [results, legacyLocations] = await Promise.all([ + getNewPrivateLocations(client), + getLegacyPrivateLocations(client), + ]); - const results: Array> = []; - - for await (const response of finder.find()) { - results.push(...response.saved_objects); - } - - finder.close().catch((e) => {}); - - const legacyLocations = await getLegacyPrivateLocations(client); - - return uniqBy([...results.map((r) => r.attributes), ...legacyLocations], 'id'); + return uniqBy( + [ + ...results.map((r) => ({ ...r.attributes, spaces: r.namespaces, id: r.id })), + ...legacyLocations, + ], + 'id' + ); } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { return []; @@ -49,6 +45,22 @@ export const getPrivateLocations = async ( } }; +const getNewPrivateLocations = async (client: SavedObjectsClientContract) => { + const finder = client.createPointInTimeFinder({ + type: privateLocationSavedObjectName, + perPage: 1000, + }); + + const results: Array> = []; + + for await (const response of finder.find()) { + results.push(...response.saved_objects); + } + + finder.close().catch((e) => {}); + return results; +}; + const getLegacyPrivateLocations = async (client: SavedObjectsClientContract) => { try { const obj = await client.get( diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index c67e7decbe98..5dc6abe0453b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -217,10 +217,9 @@ export const getMonitorLocations = ({ }) || []; const privateLocs = monitorLocations.privateLocations?.map((locationName) => { + const loc = locationName.toLowerCase(); const locationFound = allPrivateLocations.find( - (location) => - location.label.toLowerCase() === locationName.toLowerCase() || - location.id.toLowerCase() === locationName.toLowerCase() + (location) => location.label.toLowerCase() === loc || location.id.toLowerCase() === loc ); if (locationFound) { return locationFound; diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 5b0c96760163..df35097b9676 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -71,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { it('add a test private location', async () => { pvtLoc = await testPrivateLocations.addPrivateLocation(); - testFleetPolicyID = pvtLoc.id; + testFleetPolicyID = pvtLoc.agentPolicyId; const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); @@ -124,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) { it('adds a monitor in private location', async () => { const newMonitor = httpMonitorJson; - newMonitor.locations.push(pvtLoc); + newMonitor.locations.push(omit(pvtLoc, ['spaces'])); const { body, rawBody } = await addMonitorAPI(newMonitor); @@ -138,8 +138,7 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = apiResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc.id + '-default' ); expect(packagePolicy?.policy_id).eql(testFleetPolicyID); @@ -149,23 +148,33 @@ export default function ({ getService }: FtrProviderContext) { getTestSyntheticsPolicy({ name: httpMonitorJson.name, id: newMonitorId, - location: { id: testFleetPolicyID }, + location: { id: pvtLoc.id }, }) ); }); let testFleetPolicyID2: string; + let pvtLoc2: PrivateLocation; it('edits a monitor with additional private location', async () => { const resPolicy = await testPrivateLocations.addFleetPolicy(testPolicyName + 1); testFleetPolicyID2 = resPolicy.body.item.id; - const pvtLoc2 = await testPrivateLocations.addPrivateLocation({ + pvtLoc2 = await testPrivateLocations.addPrivateLocation({ policyId: testFleetPolicyID2, label: 'Test private location 1', }); - httpMonitorJson.locations.push(pvtLoc2); + httpMonitorJson.locations.push({ + id: pvtLoc2.id, + label: pvtLoc2.label, + isServiceManaged: false, + agentPolicyId: pvtLoc2.agentPolicyId, + geo: { + lat: 0, + lon: 0, + }, + }); const apiResponse = await supertestAPI .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId) @@ -191,8 +200,7 @@ export default function ({ getService }: FtrProviderContext) { ); let packagePolicy = apiResponsePolicy.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc.id + '-default' ); expect(packagePolicy.policy_id).eql(testFleetPolicyID); @@ -202,13 +210,12 @@ export default function ({ getService }: FtrProviderContext) { getTestSyntheticsPolicy({ name: httpMonitorJson.name, id: newMonitorId, - location: { id: testFleetPolicyID }, + location: { id: pvtLoc.id }, }) ); packagePolicy = apiResponsePolicy.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID2 + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc2.id + '-default' ); expect(packagePolicy.policy_id).eql(testFleetPolicyID2); @@ -219,16 +226,14 @@ export default function ({ getService }: FtrProviderContext) { id: newMonitorId, location: { name: 'Test private location 1', - id: testFleetPolicyID2, + id: pvtLoc2.id, }, }) ); }); it('deletes integration for a removed location from monitor', async () => { - httpMonitorJson.locations = httpMonitorJson.locations.filter( - ({ id }) => id !== testFleetPolicyID2 - ); + httpMonitorJson.locations = httpMonitorJson.locations.filter(({ id }) => id !== pvtLoc2.id); await supertestAPI .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?internal=true') @@ -241,8 +246,7 @@ export default function ({ getService }: FtrProviderContext) { ); let packagePolicy = apiResponsePolicy.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc.id + '-default' ); expect(packagePolicy.policy_id).eql(testFleetPolicyID); @@ -252,13 +256,12 @@ export default function ({ getService }: FtrProviderContext) { getTestSyntheticsPolicy({ name: httpMonitorJson.name, id: newMonitorId, - location: { id: testFleetPolicyID }, + location: { id: pvtLoc.id }, }) ); packagePolicy = apiResponsePolicy.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID2 + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc2.id + '-default' ); expect(packagePolicy).eql(undefined); @@ -287,7 +290,7 @@ export default function ({ getService }: FtrProviderContext) { ...httpMonitorJson, name: `Test monitor ${uuidv4()}`, [ConfigKey.NAMESPACE]: 'default', - locations: [pvtLoc], + locations: [omit(pvtLoc, ['spaces'])], }; try { @@ -316,7 +319,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy = policyResponse.body.items.find( (pkgPolicy: PackagePolicy) => - pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-${SPACE_ID}` + pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-${SPACE_ID}` ); expect(packagePolicy.policy_id).eql(testFleetPolicyID); @@ -326,7 +329,7 @@ export default function ({ getService }: FtrProviderContext) { getTestSyntheticsPolicy({ name: monitor.name, id: monitorId, - location: { id: testFleetPolicyID }, + location: { id: pvtLoc.id }, namespace: formatKibanaNamespace(SPACE_ID), spaceId: SPACE_ID, }) @@ -350,7 +353,7 @@ export default function ({ getService }: FtrProviderContext) { ...httpMonitorJson, locations: [ { - id: testFleetPolicyID, + id: pvtLoc.id, label: 'Test private location 0', isServiceManaged: false, }, @@ -374,15 +377,14 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = policyResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default` + (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default` ); comparePolicies( packagePolicy, getTestSyntheticsPolicy({ name: monitor.name, id: monitorId, - location: { id: testFleetPolicyID }, + location: { id: pvtLoc.id }, isTLSEnabled: true, }) ); @@ -398,7 +400,7 @@ export default function ({ getService }: FtrProviderContext) { ...httpMonitorJson, locations: [ { - id: testFleetPolicyID, + id: pvtLoc.id, label: 'Test private location 0', isServiceManaged: false, }, @@ -412,8 +414,9 @@ export default function ({ getService }: FtrProviderContext) { const apiResponse = await supertestAPI .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .set('kbn-xsrf', 'true') - .send(monitor) - .expect(200); + .send(monitor); + + expect(apiResponse.status).eql(200, JSON.stringify(apiResponse.body)); monitorId = apiResponse.body.id; @@ -422,15 +425,14 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = policyResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default` + (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default` ); comparePolicies( packagePolicy, getTestSyntheticsPolicy({ name: monitor.name, id: monitorId, - location: { id: testFleetPolicyID }, + location: { id: pvtLoc.id }, }) ); } finally { @@ -447,7 +449,7 @@ export default function ({ getService }: FtrProviderContext) { [ConfigKey.NAMESPACE]: 'default', locations: [ { - id: testFleetPolicyID, + id: pvtLoc.id, label: 'Test private location 0', isServiceManaged: false, }, @@ -467,8 +469,7 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = policyResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default` + (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default` ); expect(packagePolicy.package.version).eql(INSTALLED_VERSION); @@ -478,8 +479,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics' ); const packagePolicyAfterUpgrade = policyResponseAfterUpgrade.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default` + (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default` ); expect(semver.gte(packagePolicyAfterUpgrade.package.version, INSTALLED_VERSION)).eql(true); } finally { diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index 661fe4af3c87..c7eb8131e9d3 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -46,6 +46,7 @@ export default function ({ getService }: FtrProviderContext) { let icmpProjectMonitors: ProjectMonitorsRequest; let testPolicyId = ''; + let loc: any; const setUniqueIds = (request: ProjectMonitorsRequest) => { return { ...request, @@ -85,8 +86,8 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); await testPrivateLocations.installSyntheticsPackage(); - const loc = await testPrivateLocations.addPrivateLocation(); - testPolicyId = loc.id; + loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.agentPolicyId; await supertest .post(SYNTHETICS_API_URLS.PARAMS) .set('kbn-xsrf', 'true') @@ -644,7 +645,7 @@ export default function ({ getService }: FtrProviderContext) { lat: 0, lon: 0, }, - id: testPolicyId, + id: loc.id, agentPolicyId: testPolicyId, isServiceManaged: false, label: 'Test private location 0', @@ -1443,7 +1444,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${testPolicyId}` + `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${loc.id}` ); expect(packagePolicy.name).eql( `${projectMonitors.monitors[0].id}-${project}-default-Test private location 0` @@ -1511,7 +1512,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${testPolicyId}` + `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${loc.id}` ); expect(packagePolicy.name).eql( `${httpProjectMonitors.monitors[1].id}-${project}-default-Test private location 0` @@ -1530,7 +1531,7 @@ export default function ({ getService }: FtrProviderContext) { configId, projectId: project, locationName: 'Test private location 0', - locationId: testPolicyId, + locationId: loc.id, }) ); } finally { @@ -1575,7 +1576,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${testPolicyId}` + `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${loc.id}` ); expect(packagePolicy.policy_id).eql(testPolicyId); @@ -1597,7 +1598,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy2 = apiResponsePolicy2.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${testPolicyId}` + `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${loc.id}` ); expect(packagePolicy2).eql(undefined); @@ -1637,7 +1638,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${testPolicyId}` + `${monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]}-${loc.id}` ); expect(packagePolicy.policy_id).eql(testPolicyId); @@ -1721,7 +1722,7 @@ export default function ({ getService }: FtrProviderContext) { const configId = monitorsResponse.body.monitors[0].id; const id = monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]; - const policyId = `${id}-${testPolicyId}`; + const policyId = `${id}-${loc.id}`; const packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId @@ -1737,7 +1738,7 @@ export default function ({ getService }: FtrProviderContext) { id, configId, projectId: project, - locationId: testPolicyId, + locationId: loc.id, locationName: 'Test private location 0', }) ); @@ -1764,7 +1765,7 @@ export default function ({ getService }: FtrProviderContext) { const configId2 = monitorsResponse.body.monitors[0].id; const id2 = monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID]; - const policyId2 = `${id}-${testPolicyId}`; + const policyId2 = `${id}-${loc.id}`; const packagePolicy2 = apiResponsePolicy2.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId2 @@ -1778,7 +1779,7 @@ export default function ({ getService }: FtrProviderContext) { id: id2, configId: configId2, projectId: project, - locationId: testPolicyId, + locationId: loc.id, locationName: 'Test private location 0', namespace: 'custom_namespace', }) @@ -1832,7 +1833,7 @@ export default function ({ getService }: FtrProviderContext) { label: 'Test private location 0', isServiceManaged: false, agentPolicyId: testPolicyId, - id: testPolicyId, + id: loc.id, geo: { lat: 0, lon: 0, diff --git a/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts index b240b1ec3d11..88d2e4384e24 100644 --- a/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/delete_monitor_project.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { let projectMonitors: ProjectMonitorsRequest; let testPolicyId = ''; + let loc: any; const testPrivateLocations = new PrivateLocationTestService(getService); const setUniqueIds = (request: ProjectMonitorsRequest) => { @@ -39,8 +40,8 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await testPrivateLocations.installSyntheticsPackage(); - const loc = await testPrivateLocations.addPrivateLocation(); - testPolicyId = loc.id; + loc = await testPrivateLocations.addPrivateLocation(); + testPolicyId = loc.agentPolicyId; }); beforeEach(() => { @@ -404,9 +405,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy = apiResponsePolicy.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + - '-' + - testPolicyId + savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + '-' + loc.id ); expect(packagePolicy.policy_id).to.be(testPolicyId); @@ -438,9 +437,7 @@ export default function ({ getService }: FtrProviderContext) { const packagePolicy2 = apiResponsePolicy2.body.items.find( (pkgPolicy: PackagePolicy) => pkgPolicy.id === - savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + - '-' + - testPolicyId + savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + '-' + loc.id ); expect(packagePolicy2).to.be(undefined); } finally { diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts index aeb0eaa0299b..ef4294a4089d 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts @@ -65,7 +65,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { - await kibanaServer.savedObjects.cleanStandardList(); + // await kibanaServer.savedObjects.cleanStandardList(); }); let monitorId = 'test-id'; @@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { ...updates, revision: 3, url: 'https://www.google.com', - locations: [localLoc, pvtLoc], + locations: [localLoc, omit(pvtLoc, 'spaces')], }) ); @@ -270,7 +270,7 @@ export default function ({ getService }: FtrProviderContext) { ...updates, revision: 4, url: 'https://www.google.com', - locations: [pvtLoc], + locations: [omit(pvtLoc, 'spaces')], }) ); }); @@ -289,7 +289,7 @@ export default function ({ getService }: FtrProviderContext) { ...updates, revision: 5, url: 'https://www.google.com', - locations: [localLoc, pvtLoc], + locations: [localLoc, omit(pvtLoc, 'spaces')], }) ); diff --git a/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts index a4351ede2eda..7f55ef4cf064 100644 --- a/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts +++ b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts @@ -21,6 +21,7 @@ export default function ({ getService }: FtrProviderContext) { describe('PrivateLocationAPI', function () { this.tags('skipCloud'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); const kServer = getService('kibanaServer'); @@ -132,5 +133,64 @@ export default function ({ getService }: FtrProviderContext) { expect(deleteResponse.status).to.be(200); } }); + + it('can create private location in multiple spaces', async () => { + const apiResponse = await testPrivateLocations.addFleetPolicy(); + const agentPolicyId = apiResponse.body.item.id; + + const { SPACE_ID } = await monitorTestService.addsNewSpace([ + 'minimal_all', + 'can_manage_private_locations', + ]); + + const location: Omit = { + label: 'Test private location 11', + agentPolicyId: agentPolicyId!, + geo: { + lat: 0, + lon: 0, + }, + spaces: [SPACE_ID, 'default'], + }; + const response = await supertest + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .set('kbn-xsrf', 'true') + .send(location); + + expect(response.status).to.be(200); + }); + + it('validation errors works in multiple spaces as well', async () => { + const apiResponse = await testPrivateLocations.addFleetPolicy(); + const agentPolicyId = apiResponse.body.item.id; + + const { username, password, SPACE_ID } = await monitorTestService.addsNewSpace([ + 'minimal_all', + 'can_manage_private_locations', + ]); + + const location: Omit = { + label: 'Test private location 12', + agentPolicyId: agentPolicyId!, + geo: { + lat: 0, + lon: 0, + }, + spaces: [SPACE_ID, 'default'], + }; + const response = await supertest + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .set('kbn-xsrf', 'true') + .send(location); + + expect(response.status).to.be(200); + + const response1 = await supertestWithoutAuth + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ ...location, spaces: [SPACE_ID] }); + expect(response1.status).to.be(400); + }); }); } diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index 44cd5b19d669..e519b8a874af 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) { const kServer = getService('kibanaServer'); let testFleetPolicyID: string; + let loc: any; let _browserMonitorJson: HTTPFields; let browserMonitorJson: HTTPFields; @@ -61,8 +62,8 @@ export default function ({ getService }: FtrProviderContext) { }); it('add a test private location', async () => { - const loc = await testPrivateLocations.addPrivateLocation(); - testFleetPolicyID = loc.id; + loc = await testPrivateLocations.addPrivateLocation(); + testFleetPolicyID = loc.agentPolicyId; const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); @@ -86,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) { isInvalid: false, }, { - id: testFleetPolicyID, + id: loc.id, isInvalid: false, isServiceManaged: false, label: 'Test private location 0', @@ -95,6 +96,7 @@ export default function ({ getService }: FtrProviderContext) { lon: 0, }, agentPolicyId: testFleetPolicyID, + spaces: ['*'], }, ]; @@ -105,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) { const newMonitor = browserMonitorJson; const pvtLoc = { - id: testFleetPolicyID, + id: loc.id, agentPolicyId: testFleetPolicyID, label: 'Test private location 0', isServiceManaged: false, @@ -136,8 +138,7 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = apiResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + loc.id + '-default' ); expect(packagePolicy?.policy_id).eql( @@ -193,8 +194,7 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = apiResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + loc.id + '-default' ); expect(packagePolicy.policy_id).eql(testFleetPolicyID); @@ -214,7 +214,7 @@ export default function ({ getService }: FtrProviderContext) { it('add a http monitor using param', async () => { const newMonitor = httpMonitorJson; const pvtLoc = { - id: testFleetPolicyID, + id: loc.id, agentPolicyId: testFleetPolicyID, label: 'Test private location 0', isServiceManaged: false, @@ -246,8 +246,7 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = apiResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newHttpMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newHttpMonitorId + '-' + loc.id + '-default' ); expect(packagePolicy.policy_id).eql(testFleetPolicyID); @@ -257,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { id: newHttpMonitorId, isTLSEnabled: false, namespace: 'testnamespace', - location: { id: testFleetPolicyID }, + location: { id: loc.id }, }); comparePolicies(packagePolicy, pPolicy); @@ -304,8 +303,7 @@ export default function ({ getService }: FtrProviderContext) { ); const packagePolicy = apiResponse.body.items.find( - (pkgPolicy: PackagePolicy) => - pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default' + (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + loc.id + '-default' ); expect(packagePolicy.policy_id).eql(testFleetPolicyID); @@ -316,7 +314,7 @@ export default function ({ getService }: FtrProviderContext) { name: browserMonitorJson.name, id: newMonitorId, isBrowser: true, - location: { id: testFleetPolicyID }, + location: { id: loc.id }, }) ); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts index c140ba7319e2..4a24dfeac6ba 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts @@ -97,6 +97,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { lon: 0, }, agentPolicyId: testFleetPolicyID, + spaces: ['default'], }, ]; @@ -115,6 +116,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { lat: 0, lon: 0, }, + spaces: ['default'], }; newMonitor.name = invalidName; @@ -123,7 +125,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send({ ...newMonitor, locations: [location] }) + .send({ ...newMonitor, locations: [omit(location, 'spaces')] }) .expect(400); expect(apiResponse.body).eql({