[Synthetics] Space aware private locations !! (#202634)

## Summary

Fixes https://github.com/elastic/kibana/issues/199976

User can choose which space the location will be visible in while
creating a location !!


### Testing

- [ ] Create location in all spaces and make sure it's visible
everywhere.
- [ ] Creation location in a specific space and make sure it's only
visible in specified space


<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/6aa5cac9-500a-447a-8ef5-bf53e91a16dd"
/>
This commit is contained in:
Shahzad 2025-01-27 19:37:22 +01:00 committed by GitHub
parent a108c632a4
commit e51b2bda27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1039 additions and 604 deletions

View file

@ -37,6 +37,9 @@ The request body should contain the following attributes:
- `lat` (Required, number): The latitude of the location. - `lat` (Required, number): The latitude of the location.
- `lon` (Required, number): The longitude 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]] [[private-location-create-example]]
==== Example ==== Example
@ -53,6 +56,7 @@ POST /api/private_locations
"lat": 40.7128, "lat": 40.7128,
"lon": -74.0060 "lon": -74.0060
} }
"spaces": ["default"]
} }
-------------------------------------------------- --------------------------------------------------

View file

@ -22,6 +22,7 @@ export const PrivateLocationCodec = t.intersection([
lon: t.number, lon: t.number,
}), }),
namespace: t.string, namespace: t.string,
spaces: t.array(t.string),
}), }),
]); ]);

View file

@ -23,7 +23,7 @@ describe('GettingStartedPage', () => {
deleteLoading: false, deleteLoading: false,
onSubmit: jest.fn(), onSubmit: jest.fn(),
onDelete: jest.fn(), onDelete: jest.fn(),
formData: undefined, createLoading: false,
}); });
jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true); jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true);
}); });
@ -81,9 +81,11 @@ describe('GettingStartedPage', () => {
locationsLoaded: true, locationsLoaded: true,
loading: false, loading: false,
}, },
privateLocations: {
isCreatePrivateLocationFlyoutVisible: true,
},
agentPolicies: { agentPolicies: {
data: [], data: [],
isAddingNewPrivateLocation: true,
}, },
}, },
}); });
@ -108,7 +110,9 @@ describe('GettingStartedPage', () => {
}, },
agentPolicies: { agentPolicies: {
data: [{}], data: [{}],
isAddingNewPrivateLocation: true, },
privateLocations: {
isCreatePrivateLocationFlyoutVisible: true,
}, },
}, },
}); });
@ -145,7 +149,9 @@ describe('GettingStartedPage', () => {
}, },
agentPolicies: { agentPolicies: {
data: [{}], data: [{}],
isAddingNewPrivateLocation: true, },
privateLocations: {
isCreatePrivateLocationFlyoutVisible: true,
}, },
}, },
} }

View file

@ -24,18 +24,14 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useBreadcrumbs, useEnablement, useLocations } from '../../hooks'; import { useBreadcrumbs, useEnablement, useLocations } from '../../hooks';
import { usePrivateLocationsAPI } from '../settings/private_locations/hooks/use_locations_api'; import { usePrivateLocationsAPI } from '../settings/private_locations/hooks/use_locations_api';
import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout'; import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout';
import { import { getServiceLocations, cleanMonitorListState } from '../../state';
getServiceLocations,
selectAddingNewPrivateLocation,
setAddingNewPrivateLocation,
getAgentPoliciesAction,
selectAgentPolicies,
cleanMonitorListState,
} from '../../state';
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui'; import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui';
import { SimpleMonitorForm } from './simple_monitor_form'; import { SimpleMonitorForm } from './simple_monitor_form';
import { AddLocationFlyout, NewLocation } from '../settings/private_locations/add_location_flyout'; import { AddLocationFlyout, NewLocation } from '../settings/private_locations/add_location_flyout';
import type { ClientPluginsStart } from '../../../../plugin'; 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 = () => { export const GettingStartedPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -134,11 +130,11 @@ export const GettingStartedOnPrem = () => {
const isAddingNewLocation = useSelector(selectAddingNewPrivateLocation); const isAddingNewLocation = useSelector(selectAddingNewPrivateLocation);
const setIsAddingNewLocation = useCallback( const setIsAddingNewLocation = useCallback(
(val: boolean) => dispatch(setAddingNewPrivateLocation(val)), (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
[dispatch] [dispatch]
); );
const { onSubmit, privateLocations, loading } = usePrivateLocationsAPI(); const { onSubmit, privateLocations } = usePrivateLocationsAPI();
const handleSubmit = (formData: NewLocation) => { const handleSubmit = (formData: NewLocation) => {
onSubmit(formData); onSubmit(formData);
@ -182,7 +178,6 @@ export const GettingStartedOnPrem = () => {
setIsOpen={setIsAddingNewLocation} setIsOpen={setIsAddingNewLocation}
onSubmit={handleSubmit} onSubmit={handleSubmit}
privateLocations={privateLocations} privateLocations={privateLocations}
isLoading={loading}
/> />
) : null} ) : null}
</> </>

View file

@ -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<ClientPluginsStart>();
const [spacesList, setSpacesList] = React.useState<Array<{ id: string; label: string }>>([]);
const data = services.spaces?.ui.useSpaces();
const {
control,
formState: { isSubmitted },
trigger,
} = useFormContext<PrivateLocation>();
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 (
<EuiFormRow
fullWidth
label={SPACES_LABEL}
helpText={HELP_TEXT}
isInvalid={showFieldInvalid}
error={showFieldInvalid ? NAMESPACES_NAME : undefined}
>
<Controller
name={NAMESPACES_NAME}
control={control}
rules={{ required: true }}
render={({ field }) => (
<EuiComboBox
fullWidth
aria-label={SPACES_LABEL}
placeholder={SPACES_LABEL}
isInvalid={showFieldInvalid}
{...field}
onBlur={async () => {
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);
}}
/>
)}
/>
</EuiFormRow>
);
};
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.',
});

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import React from 'react'; import React, { useMemo } from 'react';
import { FormProvider } from 'react-hook-form'; import { FormProvider } from 'react-hook-form';
import { import {
EuiButtonEmpty, EuiButtonEmpty,
@ -19,22 +19,27 @@ import {
EuiButton, EuiButton,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; 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 { NoPermissionsTooltip } from '../../common/components/permissions';
import { useSyntheticsSettingsContext } from '../../../contexts'; import { useSyntheticsSettingsContext } from '../../../contexts';
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped'; import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
import { PrivateLocation } from '../../../../../../common/runtime_types'; import { PrivateLocation } from '../../../../../../common/runtime_types';
import { LocationForm } from './location_form'; import { LocationForm } from './location_form';
import { ManageEmptyState } from './manage_empty_state'; import { ManageEmptyState } from './manage_empty_state';
import { ClientPluginsStart } from '../../../../../plugin';
import { selectPrivateLocationsState } from '../../../state/private_locations/selectors';
export type NewLocation = Omit<PrivateLocation, 'id'>; export type NewLocation = Omit<PrivateLocation, 'id'>;
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const AddLocationFlyout = ({ export const AddLocationFlyout = ({
onSubmit, onSubmit,
setIsOpen, setIsOpen,
privateLocations, privateLocations,
isLoading,
}: { }: {
isLoading: boolean;
onSubmit: (val: NewLocation) => void; onSubmit: (val: NewLocation) => void;
setIsOpen: (val: boolean) => void; setIsOpen: (val: boolean) => void;
privateLocations: PrivateLocation[]; privateLocations: PrivateLocation[];
@ -50,10 +55,21 @@ export const AddLocationFlyout = ({
lat: 0, lat: 0,
lon: 0, lon: 0,
}, },
spaces: [ALL_SPACES_ID],
}, },
}); });
const { canSave } = useSyntheticsSettingsContext(); const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext();
const { createLoading } = useSelector(selectPrivateLocationsState);
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
const { handleSubmit } = form; const { handleSubmit } = form;
const closeFlyout = () => { const closeFlyout = () => {
@ -61,48 +77,50 @@ export const AddLocationFlyout = ({
}; };
return ( return (
<FormProvider {...form}> <ContextWrapper>
<EuiFlyout onClose={closeFlyout} style={{ width: 540 }}> <FormProvider {...form}>
<EuiFlyoutHeader hasBorder> <EuiFlyout onClose={closeFlyout} style={{ width: 540 }}>
<EuiTitle size="m"> <EuiFlyoutHeader hasBorder>
<h2>{ADD_PRIVATE_LOCATION}</h2> <EuiTitle size="m">
</EuiTitle> <h2>{ADD_PRIVATE_LOCATION}</h2>
</EuiFlyoutHeader> </EuiTitle>
<EuiFlyoutBody> </EuiFlyoutHeader>
<ManageEmptyState privateLocations={privateLocations} showEmptyLocations={false}> <EuiFlyoutBody>
<LocationForm privateLocations={privateLocations} /> <ManageEmptyState privateLocations={privateLocations} showEmptyLocations={false}>
</ManageEmptyState> <LocationForm privateLocations={privateLocations} />
</EuiFlyoutBody> </ManageEmptyState>
<EuiFlyoutFooter> </EuiFlyoutBody>
<EuiFlexGroup justifyContent="spaceBetween"> <EuiFlyoutFooter>
<EuiFlexItem grow={false}> <EuiFlexGroup justifyContent="spaceBetween">
<EuiButtonEmpty <EuiFlexItem grow={false}>
data-test-subj="syntheticsAddLocationFlyoutButton" <EuiButtonEmpty
iconType="cross"
onClick={closeFlyout}
flush="left"
isLoading={isLoading}
>
{CANCEL_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<NoPermissionsTooltip canEditSynthetics={canSave}>
<EuiButton
data-test-subj="syntheticsAddLocationFlyoutButton" data-test-subj="syntheticsAddLocationFlyoutButton"
fill iconType="cross"
onClick={handleSubmit(onSubmit)} onClick={closeFlyout}
isLoading={isLoading} flush="left"
isDisabled={!canSave} isLoading={createLoading}
> >
{SAVE_LABEL} {CANCEL_LABEL}
</EuiButton> </EuiButtonEmpty>
</NoPermissionsTooltip> </EuiFlexItem>
</EuiFlexItem> <EuiFlexItem grow={false}>
</EuiFlexGroup> <NoPermissionsTooltip canEditSynthetics={canSave}>
</EuiFlyoutFooter> <EuiButton
</EuiFlyout> data-test-subj="syntheticsAddLocationFlyoutButton"
</FormProvider> fill
onClick={handleSubmit(onSubmit)}
isLoading={createLoading}
isDisabled={!canSave || !canManagePrivateLocations}
>
{SAVE_LABEL}
</EuiButton>
</NoPermissionsTooltip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
</FormProvider>
</ContextWrapper>
); );
}; };

View file

@ -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 (
<EuiCallOut
title={AGENT_MISSING_CALLOUT_TITLE}
size="s"
style={{ textAlign: 'left' }}
color="warning"
>
<p>
{
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentMissingCallout.content"
defaultMessage="You have selected an agent policy that has no agent attached. Make sure that you have at least one agent enrolled in this policy. You can add an agent before or after creating a location. For more information, {link}."
values={{
link: (
<EuiLink
data-test-subj="syntheticsLocationFormReadTheDocsLink"
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/synthetics-private-location.html#synthetics-private-location-fleet-agent"
external
>
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentCallout.link"
defaultMessage="read the docs"
/>
</EuiLink>
),
}}
/>
}
</p>
</EuiCallOut>
);
};

View file

@ -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 (
<EuiCallOut title={AGENT_CALLOUT_TITLE} size="s" style={{ textAlign: 'left' }}>
<p>
{
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentCallout.content"
defaultMessage='To run "Browser" monitors on this private location, make sure that you&apos;re using the {code} Docker container, which contains the dependencies necessary to run these monitors. For more information, {link}.'
values={{
code: <EuiCode>elastic-agent-complete</EuiCode>,
link: (
<EuiLink
data-test-subj="syntheticsLocationFormReadTheDocsLink"
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/uptime-set-up-choose-agent.html#private-locations"
external
>
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentCallout.link"
defaultMessage="read the docs"
/>
</EuiLink>
),
}}
/>
}
</p>
</EuiCallOut>
);
};

View file

@ -13,7 +13,10 @@ import { useDispatch } from 'react-redux';
import { NoPermissionsTooltip } from '../../common/components/permissions'; import { NoPermissionsTooltip } from '../../common/components/permissions';
import { useSyntheticsSettingsContext } from '../../../contexts'; import { useSyntheticsSettingsContext } from '../../../contexts';
import { PRIVATE_LOCATIOSN_ROUTE } from '../../../../../../common/constants'; import { PRIVATE_LOCATIOSN_ROUTE } from '../../../../../../common/constants';
import { setAddingNewPrivateLocation, setManageFlyoutOpen } from '../../../state/private_locations'; import {
setIsCreatePrivateLocationFlyoutVisible,
setManageFlyoutOpen,
} from '../../../state/private_locations/actions';
export const EmptyLocations = ({ export const EmptyLocations = ({
inFlyout = true, inFlyout = true,
@ -64,7 +67,7 @@ export const EmptyLocations = ({
onClick={() => { onClick={() => {
setIsAddingNew?.(true); setIsAddingNew?.(true);
dispatch(setManageFlyoutOpen(true)); dispatch(setManageFlyoutOpen(true));
dispatch(setAddingNewPrivateLocation(true)); dispatch(setIsCreatePrivateLocationFlyoutVisible(true));
}} }}
> >
{ADD_LOCATION} {ADD_LOCATION}

View file

@ -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());
});
});
});

View file

@ -5,75 +5,45 @@
* 2.0. * 2.0.
*/ */
import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { NewLocation } from '../add_location_flyout'; import { NewLocation } from '../add_location_flyout';
import { getServiceLocations } from '../../../../state/service_locations';
import { import {
createPrivateLocationAction,
deletePrivateLocationAction,
getPrivateLocationsAction, getPrivateLocationsAction,
selectPrivateLocations, } from '../../../../state/private_locations/actions';
selectPrivateLocationsLoading, import { selectPrivateLocationsState } from '../../../../state/private_locations/selectors';
setAddingNewPrivateLocation,
} from '../../../../state/private_locations';
import {
addSyntheticsPrivateLocations,
deleteSyntheticsPrivateLocations,
} from '../../../../state/private_locations/api';
export const usePrivateLocationsAPI = () => { export const usePrivateLocationsAPI = () => {
const [formData, setFormData] = useState<NewLocation>();
const [deleteId, setDeleteId] = useState<string>();
const dispatch = useDispatch(); const dispatch = useDispatch();
const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val)); const { loading, createLoading, deleteLoading, data } = useSelector(selectPrivateLocationsState);
const privateLocations = useSelector(selectPrivateLocations);
const fetchLoading = useSelector(selectPrivateLocationsLoading);
useEffect(() => { useEffect(() => {
dispatch(getPrivateLocationsAction.get()); dispatch(getPrivateLocationsAction.get());
}, [dispatch]); }, [dispatch]);
const { loading: saveLoading } = useFetcher(async () => { useEffect(() => {
if (formData) { if (data === null) {
const result = await addSyntheticsPrivateLocations(formData);
setFormData(undefined);
setIsAddingNew(false);
dispatch(getServiceLocations());
dispatch(getPrivateLocationsAction.get()); dispatch(getPrivateLocationsAction.get());
return result;
} }
// FIXME: Dario thinks there is a better way to do this but }, [data, dispatch]);
// he's getting tired and maybe the Synthetics folks can fix it
}, [formData]);
const onSubmit = (data: NewLocation) => { const onSubmit = (newLoc: NewLocation) => {
setFormData(data); dispatch(createPrivateLocationAction.get(newLoc));
}; };
const onDelete = (id: string) => { 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 { return {
formData,
onSubmit, onSubmit,
onDelete, onDelete,
deleteLoading: Boolean(deleteLoading), deleteLoading,
loading: Boolean(fetchLoading || saveLoading), loading,
privateLocations, createLoading,
privateLocations: data ?? [],
}; };
}; };

View file

@ -6,32 +6,22 @@
*/ */
import React, { Ref } from 'react'; import React, { Ref } from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiFieldTextProps } from '@elastic/eui';
import {
EuiFieldText,
EuiForm,
EuiFormRow,
EuiSpacer,
EuiCallOut,
EuiCode,
EuiLink,
EuiFieldTextProps,
} from '@elastic/eui';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useFormContext, useFormState } from 'react-hook-form'; 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 { TagsField } from '../components/tags_field';
import { PrivateLocation } from '../../../../../../common/runtime_types'; import { PrivateLocation } from '../../../../../../common/runtime_types';
import { AgentPolicyNeeded } from './agent_policy_needed'; import { AgentPolicyNeeded } from './agent_policy_needed';
import { PolicyHostsField, AGENT_POLICY_FIELD_NAME } from './policy_hosts'; import { PolicyHostsField } from './policy_hosts';
import { selectAgentPolicies } from '../../../state/private_locations';
export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => { export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => {
const { data } = useSelector(selectAgentPolicies); const { data } = useSelector(selectAgentPolicies);
const { control, register, getValues } = useFormContext<PrivateLocation>(); const { control, register } = useFormContext<PrivateLocation>();
const { errors } = useFormState(); 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 tagsList = privateLocations.reduce((acc, item) => {
const tags = item.tags || []; const tags = item.tags || [];
@ -70,66 +60,9 @@ export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLo
<EuiSpacer /> <EuiSpacer />
<TagsField tagsList={tagsList} control={control} errors={errors} /> <TagsField tagsList={tagsList} control={control} errors={errors} />
<EuiSpacer /> <EuiSpacer />
<EuiCallOut title={AGENT_CALLOUT_TITLE} size="s" style={{ textAlign: 'left' }}> <BrowserMonitorCallout />
<p>
{
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentCallout.content"
defaultMessage='To run "Browser" monitors on this private location, make sure that you&apos;re using the {code} Docker container, which contains the dependencies necessary to run these monitors. For more information, {link}.'
values={{
code: <EuiCode>elastic-agent-complete</EuiCode>,
link: (
<EuiLink
data-test-subj="syntheticsLocationFormReadTheDocsLink"
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/uptime-set-up-choose-agent.html#private-locations"
external
>
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentCallout.link"
defaultMessage="read the docs"
/>
</EuiLink>
),
}}
/>
}
</p>
</EuiCallOut>
<EuiSpacer /> <EuiSpacer />
{selectedPolicy?.agents === 0 && ( <SpaceSelector />
<EuiCallOut
title={AGENT_MISSING_CALLOUT_TITLE}
size="s"
style={{ textAlign: 'left' }}
color="warning"
>
<p>
{
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentMissingCallout.content"
defaultMessage="You have selected an agent policy that has no agent attached. Make sure that you have at least one agent enrolled in this policy. You can add an agent before or after creating a location. For more information, {link}."
values={{
link: (
<EuiLink
data-test-subj="syntheticsLocationFormReadTheDocsLink"
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/synthetics-private-location.html#synthetics-private-location-fleet-agent"
external
>
<FormattedMessage
id="xpack.synthetics.monitorManagement.agentCallout.link"
defaultMessage="read the docs"
/>
</EuiLink>
),
}}
/>
}
</p>
</EuiCallOut>
)}
</EuiForm> </EuiForm>
</> </>
); );

View file

@ -18,12 +18,12 @@ import {
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; 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 { CopyName } from './copy_name';
import { ViewLocationMonitors } from './view_location_monitors'; import { ViewLocationMonitors } from './view_location_monitors';
import { TableTitle } from '../../common/components/table_title'; import { TableTitle } from '../../common/components/table_title';
import { TAGS_LABEL } from '../components/tags_field'; import { TAGS_LABEL } from '../components/tags_field';
import { useSyntheticsSettingsContext } from '../../../contexts'; import { useSyntheticsSettingsContext } from '../../../contexts';
import { setAddingNewPrivateLocation } from '../../../state/private_locations';
import { PrivateLocationDocsLink, START_ADDING_LOCATIONS_DESCRIPTION } from './empty_locations'; import { PrivateLocationDocsLink, START_ADDING_LOCATIONS_DESCRIPTION } from './empty_locations';
import { PrivateLocation } from '../../../../../../common/runtime_types'; import { PrivateLocation } from '../../../../../../common/runtime_types';
import { NoPermissionsTooltip } from '../../common/components/permissions'; import { NoPermissionsTooltip } from '../../common/components/permissions';
@ -31,6 +31,8 @@ import { DeleteLocation } from './delete_location';
import { useLocationMonitors } from './hooks/use_location_monitors'; import { useLocationMonitors } from './hooks/use_location_monitors';
import { PolicyName } from './policy_name'; import { PolicyName } from './policy_name';
import { LOCATION_NAME_LABEL } from './location_form'; import { LOCATION_NAME_LABEL } from './location_form';
import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
import { ClientPluginsStart } from '../../../../../plugin';
interface ListItem extends PrivateLocation { interface ListItem extends PrivateLocation {
monitors: number; monitors: number;
@ -41,7 +43,7 @@ export const PrivateLocationsTable = ({
onDelete, onDelete,
privateLocations, privateLocations,
}: { }: {
deleteLoading: boolean; deleteLoading?: boolean;
onDelete: (id: string) => void; onDelete: (id: string) => void;
privateLocations: PrivateLocation[]; privateLocations: PrivateLocation[];
}) => { }) => {
@ -54,6 +56,10 @@ export const PrivateLocationsTable = ({
const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext(); const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext();
const { services } = useKibana<ClientPluginsStart>();
const LazySpaceList = services.spaces?.ui.components.getSpaceList ?? (() => null);
const tagsList = privateLocations.reduce((acc, item) => { const tagsList = privateLocations.reduce((acc, item) => {
const tags = item.tags || []; const tags = item.tags || [];
return new Set([...acc, ...tags]); return new Set([...acc, ...tags]);
@ -97,6 +103,14 @@ export const PrivateLocationsTable = ({
); );
}, },
}, },
{
name: 'Spaces',
field: 'spaces',
sortable: true,
render: (spaces: string[]) => {
return <LazySpaceList namespaces={spaces} behaviorContext="outside-space" />;
},
},
{ {
name: ACTIONS_LABEL, name: ACTIONS_LABEL,
actions: [ actions: [
@ -124,7 +138,7 @@ export const PrivateLocationsTable = ({
monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0, monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0,
})); }));
const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val)); const setIsAddingNew = (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val));
const renderToolRight = () => { const renderToolRight = () => {
return [ return [

View file

@ -10,7 +10,7 @@ import { useSelector } from 'react-redux';
import { PrivateLocation } from '../../../../../../common/runtime_types'; import { PrivateLocation } from '../../../../../../common/runtime_types';
import { AgentPolicyNeeded } from './agent_policy_needed'; import { AgentPolicyNeeded } from './agent_policy_needed';
import { EmptyLocations } from './empty_locations'; import { EmptyLocations } from './empty_locations';
import { selectAgentPolicies } from '../../../state/private_locations'; import { selectAgentPolicies } from '../../../state/agent_policies';
export const ManageEmptyState: FC< export const ManageEmptyState: FC<
PropsWithChildren<{ PropsWithChildren<{

View file

@ -12,7 +12,6 @@ import * as locationHooks from './hooks/use_locations_api';
import * as settingsHooks from '../../../contexts/synthetics_settings_context'; import * as settingsHooks from '../../../contexts/synthetics_settings_context';
import type { SyntheticsSettingsContextValues } from '../../../contexts'; import type { SyntheticsSettingsContextValues } from '../../../contexts';
import { ManagePrivateLocations } from './manage_private_locations'; import { ManagePrivateLocations } from './manage_private_locations';
import { PrivateLocation } from '../../../../../../common/runtime_types';
import { fireEvent } from '@testing-library/react'; import { fireEvent } from '@testing-library/react';
jest.mock('../../../hooks'); jest.mock('../../../hooks');
@ -28,12 +27,12 @@ describe('<ManagePrivateLocations />', () => {
canCreateAgentPolicies: false, canCreateAgentPolicies: false,
}); });
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({ jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
formData: {} as PrivateLocation,
loading: false, loading: false,
onSubmit: jest.fn(), onSubmit: jest.fn(),
privateLocations: [], privateLocations: [],
onDelete: jest.fn(), onDelete: jest.fn(),
deleteLoading: false, deleteLoading: false,
createLoading: false,
}); });
jest.spyOn(permissionsHooks, 'useEnablement').mockReturnValue({ jest.spyOn(permissionsHooks, 'useEnablement').mockReturnValue({
isServiceAllowed: true, isServiceAllowed: true,
@ -59,8 +58,9 @@ describe('<ManagePrivateLocations />', () => {
data: [], data: [],
loading: false, loading: false,
error: null, error: null,
isManageFlyoutOpen: false, },
isAddingNewPrivateLocation: false, privateLocations: {
isCreatePrivateLocationFlyoutVisible: false,
}, },
}, },
}); });
@ -93,8 +93,9 @@ describe('<ManagePrivateLocations />', () => {
data: [{}], data: [{}],
loading: false, loading: false,
error: null, error: null,
isManageFlyoutOpen: false, },
isAddingNewPrivateLocation: false, privateLocations: {
isCreatePrivateLocationFlyoutVisible: false,
}, },
}, },
}); });
@ -123,7 +124,6 @@ describe('<ManagePrivateLocations />', () => {
} as SyntheticsSettingsContextValues); } as SyntheticsSettingsContextValues);
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({ jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
formData: {} as PrivateLocation,
loading: false, loading: false,
onSubmit: jest.fn(), onSubmit: jest.fn(),
privateLocations: [ privateLocations: [
@ -136,6 +136,7 @@ describe('<ManagePrivateLocations />', () => {
], ],
onDelete: jest.fn(), onDelete: jest.fn(),
deleteLoading: false, deleteLoading: false,
createLoading: false,
}); });
const { getByText, getByRole, findByText } = render(<ManagePrivateLocations />, { const { getByText, getByRole, findByText } = render(<ManagePrivateLocations />, {
state: { state: {
@ -143,8 +144,9 @@ describe('<ManagePrivateLocations />', () => {
data: [{}], data: [{}],
loading: false, loading: false,
error: null, error: null,
isManageFlyoutOpen: false, },
isAddingNewPrivateLocation: false, privateLocations: {
isCreatePrivateLocationFlyoutVisible: false,
}, },
}, },
}); });

View file

@ -4,39 +4,48 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout'; import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout';
import { PrivateLocationsTable } from './locations_table'; import { PrivateLocationsTable } from './locations_table';
import { ManageEmptyState } from './manage_empty_state'; import { ManageEmptyState } from './manage_empty_state';
import { AddLocationFlyout, NewLocation } from './add_location_flyout'; import { AddLocationFlyout, NewLocation } from './add_location_flyout';
import { usePrivateLocationsAPI } from './hooks/use_locations_api'; import { usePrivateLocationsAPI } from './hooks/use_locations_api';
import { import { selectAddingNewPrivateLocation } from '../../../state/private_locations/selectors';
getAgentPoliciesAction,
selectAddingNewPrivateLocation,
setAddingNewPrivateLocation,
} from '../../../state/private_locations';
import { getServiceLocations } from '../../../state'; 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<SpacesContextProps> = ({ children }) => <>{children}</>;
export const ManagePrivateLocations = () => { export const ManagePrivateLocations = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { services } = useKibana<ClientPluginsStart>();
const spacesApi = services.spaces;
const SpacesContextProvider = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
const isAddingNew = useSelector(selectAddingNewPrivateLocation); const isAddingNew = useSelector(selectAddingNewPrivateLocation);
const setIsAddingNew = useCallback( const setIsAddingNew = useCallback(
(val: boolean) => dispatch(setAddingNewPrivateLocation(val)), (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
[dispatch] [dispatch]
); );
const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = usePrivateLocationsAPI(); const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = usePrivateLocationsAPI();
// make sure flyout is closed when first visiting the page
useEffect(() => {
setIsAddingNew(false);
}, [setIsAddingNew]);
useEffect(() => { useEffect(() => {
dispatch(getAgentPoliciesAction.get()); dispatch(getAgentPoliciesAction.get());
dispatch(getServiceLocations()); dispatch(getServiceLocations());
// make sure flyout is closed when first visiting the page
dispatch(setIsCreatePrivateLocationFlyoutVisible(false));
}, [dispatch]); }, [dispatch]);
const handleSubmit = (formData: NewLocation) => { const handleSubmit = (formData: NewLocation) => {
@ -44,7 +53,7 @@ export const ManagePrivateLocations = () => {
}; };
return ( return (
<> <SpacesContextProvider>
{loading ? ( {loading ? (
<LoadingState /> <LoadingState />
) : ( ) : (
@ -62,9 +71,8 @@ export const ManagePrivateLocations = () => {
setIsOpen={setIsAddingNew} setIsOpen={setIsAddingNew}
onSubmit={handleSubmit} onSubmit={handleSubmit}
privateLocations={privateLocations} privateLocations={privateLocations}
isLoading={loading}
/> />
) : null} ) : null}
</> </SpacesContextProvider>
); );
}; };

View file

@ -17,23 +17,33 @@ import {
EuiSuperSelect, EuiSuperSelect,
EuiText, EuiText,
EuiToolTip, EuiToolTip,
EuiSpacer,
EuiButtonEmpty,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useSyntheticsSettingsContext } from '../../../contexts';
import { AgentPolicyCallout } from './agent_policy_callout';
import { PrivateLocation } from '../../../../../../common/runtime_types'; 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 AGENT_POLICY_FIELD_NAME = 'agentPolicyId';
export const PolicyHostsField = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => { export const PolicyHostsField = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => {
const { data } = useSelector(selectAgentPolicies); const { data } = useSelector(selectAgentPolicies);
const { basePath } = useSyntheticsSettingsContext();
const { const {
control, control,
formState: { isSubmitted }, formState: { isSubmitted },
trigger, trigger,
getValues,
} = useFormContext<PrivateLocation>(); } = useFormContext<PrivateLocation>();
const { isTouched, error } = control.getFieldState(AGENT_POLICY_FIELD_NAME); const { isTouched, error } = control.getFieldState(AGENT_POLICY_FIELD_NAME);
const showFieldInvalid = (isSubmitted || isTouched) && !!error; 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 policyHostsOptions = data?.map((item) => {
const hasLocation = privateLocations.find((location) => location.agentPolicyId === item.id); const hasLocation = privateLocations.find((location) => location.agentPolicyId === item.id);
@ -89,36 +99,47 @@ export const PolicyHostsField = ({ privateLocations }: { privateLocations: Priva
}); });
return ( return (
<EuiFormRow <>
fullWidth <EuiFormRow
label={POLICY_HOST_LABEL} fullWidth
helpText={showFieldInvalid ? SELECT_POLICY_HOSTS_HELP_TEXT : undefined} label={POLICY_HOST_LABEL}
isInvalid={showFieldInvalid} labelAppend={
error={showFieldInvalid ? SELECT_POLICY_HOSTS : undefined} <EuiButtonEmpty size="xs" href={basePath + '/app/fleet/policies?create'}>
> {i18n.translate('xpack.synthetics.policyHostsField.createButtonEmptyLabel', {
<Controller defaultMessage: 'Create policy',
name={AGENT_POLICY_FIELD_NAME} })}
control={control} </EuiButtonEmpty>
rules={{ required: true }} }
render={({ field }) => ( helpText={showFieldInvalid ? SELECT_POLICY_HOSTS_HELP_TEXT : undefined}
<SuperSelect isInvalid={showFieldInvalid}
fullWidth error={showFieldInvalid ? SELECT_POLICY_HOSTS : undefined}
aria-label={SELECT_POLICY_HOSTS} >
placeholder={SELECT_POLICY_HOSTS} <Controller
valueOfSelected={field.value} name={AGENT_POLICY_FIELD_NAME}
itemLayoutAlign="top" control={control}
popoverProps={{ repositionOnScroll: true }} rules={{ required: true }}
hasDividers render={({ field }) => (
isInvalid={showFieldInvalid} <SuperSelect
options={policyHostsOptions ?? []} fullWidth
{...field} aria-label={SELECT_POLICY_HOSTS}
onBlur={async () => { placeholder={SELECT_POLICY_HOSTS}
await trigger(); valueOfSelected={field.value}
}} itemLayoutAlign="top"
/> popoverProps={{ repositionOnScroll: true }}
)} hasDividers
/> isInvalid={showFieldInvalid}
</EuiFormRow> options={policyHostsOptions ?? []}
{...field}
onBlur={async () => {
await trigger();
}}
/>
)}
/>
</EuiFormRow>
<EuiSpacer />
{selectedPolicy?.agents === 0 && <AgentPolicyCallout />}
</>
); );
}; };

View file

@ -11,7 +11,7 @@ import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useSyntheticsSettingsContext } from '../../../contexts'; import { useSyntheticsSettingsContext } from '../../../contexts';
import { useFleetPermissions } from '../../../hooks'; import { useFleetPermissions } from '../../../hooks';
import { selectAgentPolicies } from '../../../state/private_locations'; import { selectAgentPolicies } from '../../../state/agent_policies';
export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => { export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => {
const { canReadAgentPolicies } = useFleetPermissions(); const { canReadAgentPolicies } = useFleetPermissions();

View file

@ -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<void, AgentPolicyInfo[]>(
'[AGENT POLICIES] GET'
);

View file

@ -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<AgentPolicyInfo[]> => {
return await apiService.get(SYNTHETICS_API_URLS.AGENT_POLICIES);
};
export const addSyntheticsPrivateLocations = async (
newLocation: NewLocation
): Promise<SyntheticsPrivateLocations> => {
return await apiService.post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, newLocation, undefined, {
version: INITIAL_REST_VERSION,
});
};
export const getSyntheticsPrivateLocations = async (): Promise<SyntheticsPrivateLocations> => {
return await apiService.get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, {
version: INITIAL_REST_VERSION,
});
};
export const deleteSyntheticsPrivateLocations = async (
locationId: string
): Promise<SyntheticsPrivateLocations> => {
return await apiService.delete(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + `/${locationId}`, {
version: INITIAL_REST_VERSION,
});
};

View file

@ -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
)
);
}

View file

@ -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';

View file

@ -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);

View file

@ -6,18 +6,24 @@
*/ */
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
import { AgentPolicyInfo } from '../../../../../common/types'; import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions'; import { createAsyncAction } from '../utils/actions';
export const getAgentPoliciesAction = createAsyncAction<void, AgentPolicyInfo[]>(
'[AGENT POLICIES] GET'
);
export const getPrivateLocationsAction = createAsyncAction<void, SyntheticsPrivateLocations>( export const getPrivateLocationsAction = createAsyncAction<void, SyntheticsPrivateLocations>(
'[PRIVATE LOCATIONS] GET' '[PRIVATE LOCATIONS] GET'
); );
export const createPrivateLocationAction = createAsyncAction<NewLocation, PrivateLocation>(
'CREATE PRIVATE LOCATION'
);
export const deletePrivateLocationAction = createAsyncAction<string, SyntheticsPrivateLocations>(
'DELETE PRIVATE LOCATION'
);
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN'); export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');
export const setAddingNewPrivateLocation = createAction<boolean>('SET MANAGE FLYOUT ADDING NEW'); export const setIsCreatePrivateLocationFlyoutVisible = createAction<boolean>(
'SET IS CREATE PRIVATE LOCATION FLYOUT VISIBLE'
);

View file

@ -8,16 +8,16 @@
import { NewLocation } from '../../components/settings/private_locations/add_location_flyout'; import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
import { AgentPolicyInfo } from '../../../../../common/types'; import { AgentPolicyInfo } from '../../../../../common/types';
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants'; import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
import { apiService } from '../../../../utils/api_service/api_service'; import { apiService } from '../../../../utils/api_service/api_service';
export const fetchAgentPolicies = async (): Promise<AgentPolicyInfo[]> => { export const fetchAgentPolicies = async (): Promise<AgentPolicyInfo[]> => {
return await apiService.get(SYNTHETICS_API_URLS.AGENT_POLICIES); return await apiService.get(SYNTHETICS_API_URLS.AGENT_POLICIES);
}; };
export const addSyntheticsPrivateLocations = async ( export const createSyntheticsPrivateLocation = async (
newLocation: NewLocation newLocation: NewLocation
): Promise<SyntheticsPrivateLocations> => { ): Promise<PrivateLocation> => {
return await apiService.post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, newLocation, undefined, { return await apiService.post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, newLocation, undefined, {
version: INITIAL_REST_VERSION, version: INITIAL_REST_VERSION,
}); });
@ -29,7 +29,7 @@ export const getSyntheticsPrivateLocations = async (): Promise<SyntheticsPrivate
}); });
}; };
export const deleteSyntheticsPrivateLocations = async ( export const deleteSyntheticsPrivateLocation = async (
locationId: string locationId: string
): Promise<SyntheticsPrivateLocations> => { ): Promise<SyntheticsPrivateLocations> => {
return await apiService.delete(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + `/${locationId}`, { return await apiService.delete(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + `/${locationId}`, {

View file

@ -6,20 +6,18 @@
*/ */
import { takeLeading } from 'redux-saga/effects'; import { takeLeading } from 'redux-saga/effects';
import { i18n } from '@kbn/i18n';
import { fetchEffectFactory } from '../utils/fetch_effect'; import { fetchEffectFactory } from '../utils/fetch_effect';
import { fetchAgentPolicies, getSyntheticsPrivateLocations } from './api'; import {
import { getAgentPoliciesAction, getPrivateLocationsAction } from './actions'; createSyntheticsPrivateLocation,
deleteSyntheticsPrivateLocation,
export function* fetchAgentPoliciesEffect() { getSyntheticsPrivateLocations,
yield takeLeading( } from './api';
getAgentPoliciesAction.get, import {
fetchEffectFactory( createPrivateLocationAction,
fetchAgentPolicies, deletePrivateLocationAction,
getAgentPoliciesAction.success, getPrivateLocationsAction,
getAgentPoliciesAction.fail } from './actions';
)
);
}
export function* fetchPrivateLocationsEffect() { export function* fetchPrivateLocationsEffect() {
yield takeLeading( 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,
];

View file

@ -6,62 +6,69 @@
*/ */
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types'; import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
import { AgentPolicyInfo } from '../../../../../common/types'; import { createPrivateLocationAction, deletePrivateLocationAction } from './actions';
import { IHttpSerializedFetchError } from '..'; import { setIsCreatePrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
import { import { IHttpSerializedFetchError } from '../utils/http_error';
getAgentPoliciesAction,
setAddingNewPrivateLocation,
getPrivateLocationsAction,
} from './actions';
export interface AgentPoliciesState { export interface PrivateLocationsState {
data: AgentPolicyInfo[] | null; data?: SyntheticsPrivateLocations | null;
privateLocations?: SyntheticsPrivateLocations | null;
loading: boolean; loading: boolean;
fetchLoading?: boolean; createLoading?: boolean;
deleteLoading?: boolean;
error: IHttpSerializedFetchError | null; error: IHttpSerializedFetchError | null;
isManageFlyoutOpen?: boolean; isManageFlyoutOpen?: boolean;
isAddingNewPrivateLocation?: boolean; isCreatePrivateLocationFlyoutVisible?: boolean;
newLocation?: PrivateLocation;
} }
const initialState: AgentPoliciesState = { const initialState: PrivateLocationsState = {
data: null, data: null,
loading: false, loading: false,
error: null, error: null,
isManageFlyoutOpen: false, isManageFlyoutOpen: false,
isAddingNewPrivateLocation: false, isCreatePrivateLocationFlyoutVisible: false,
createLoading: false,
}; };
export const agentPoliciesReducer = createReducer(initialState, (builder) => { export const privateLocationsStateReducer = createReducer(initialState, (builder) => {
builder builder
.addCase(getAgentPoliciesAction.get, (state) => { .addCase(getPrivateLocationsAction.get, (state) => {
state.loading = true; state.loading = true;
}) })
.addCase(getAgentPoliciesAction.success, (state, action) => { .addCase(getPrivateLocationsAction.success, (state, action) => {
state.data = action.payload; state.data = action.payload;
state.loading = false; state.loading = false;
}) })
.addCase(getAgentPoliciesAction.fail, (state, action) => { .addCase(getPrivateLocationsAction.fail, (state, action) => {
state.error = action.payload; state.error = action.payload;
state.loading = false; state.loading = false;
}) })
.addCase(getPrivateLocationsAction.get, (state) => { .addCase(createPrivateLocationAction.get, (state) => {
state.fetchLoading = true; state.createLoading = true;
}) })
.addCase(getPrivateLocationsAction.success, (state, action) => { .addCase(createPrivateLocationAction.success, (state, action) => {
state.privateLocations = action.payload; state.newLocation = action.payload;
state.fetchLoading = false; state.createLoading = false;
state.data = null;
state.isCreatePrivateLocationFlyoutVisible = false;
}) })
.addCase(getPrivateLocationsAction.fail, (state, action) => { .addCase(createPrivateLocationAction.fail, (state, action) => {
state.error = action.payload; state.error = action.payload;
state.fetchLoading = false; state.createLoading = false;
}) })
.addCase(setAddingNewPrivateLocation, (state, action) => { .addCase(deletePrivateLocationAction.get, (state) => {
state.isAddingNewPrivateLocation = action.payload; 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';

View file

@ -8,14 +8,21 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { AppState } from '..'; import { AppState } from '..';
const getState = (appState: AppState) => appState.agentPolicies; const getState = (appState: AppState) => appState.privateLocations;
export const selectAgentPolicies = createSelector(getState, (state) => state); export const selectAgentPolicies = createSelector(getState, (state) => state);
export const selectAddingNewPrivateLocation = (state: AppState) => export const selectAddingNewPrivateLocation = (state: AppState) =>
state.agentPolicies.isAddingNewPrivateLocation ?? false; state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
export const selectPrivateLocationsLoading = (state: AppState) => export const selectPrivateLocationsLoading = (state: AppState) =>
state.agentPolicies.fetchLoading ?? false; state.privateLocations.loading ?? false;
export const selectPrivateLocations = (state: AppState) => export const selectPrivateLocationCreating = (state: AppState) =>
state.agentPolicies.privateLocations ?? []; 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 ?? [];

View file

@ -28,7 +28,7 @@ import {
setDynamicSettingsEffect, setDynamicSettingsEffect,
} from './settings/effects'; } from './settings/effects';
import { syncGlobalParamsEffect } from './settings'; import { syncGlobalParamsEffect } from './settings';
import { fetchAgentPoliciesEffect, fetchPrivateLocationsEffect } from './private_locations'; import { privateLocationsEffects } from './private_locations/effects';
import { fetchNetworkEventsEffect } from './network_events/effects'; import { fetchNetworkEventsEffect } from './network_events/effects';
import { fetchSyntheticsMonitorEffect } from './monitor_details'; import { fetchSyntheticsMonitorEffect } from './monitor_details';
import { fetchSyntheticsEnablementEffect } from './synthetics_enablement'; import { fetchSyntheticsEnablementEffect } from './synthetics_enablement';
@ -44,6 +44,7 @@ import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journe
import { fetchOverviewStatusEffect } from './overview_status'; import { fetchOverviewStatusEffect } from './overview_status';
import { fetchMonitorStatusHeatmap, quietFetchMonitorStatusHeatmap } from './status_heatmap'; import { fetchMonitorStatusHeatmap, quietFetchMonitorStatusHeatmap } from './status_heatmap';
import { fetchOverviewTrendStats, refreshOverviewTrendStats } from './overview/effects'; import { fetchOverviewTrendStats, refreshOverviewTrendStats } from './overview/effects';
import { fetchAgentPoliciesEffect } from './agent_policies';
export const rootEffect = function* root(): Generator { export const rootEffect = function* root(): Generator {
yield all([ yield all([
@ -57,7 +58,6 @@ export const rootEffect = function* root(): Generator {
fork(fetchOverviewStatusEffect), fork(fetchOverviewStatusEffect),
fork(fetchNetworkEventsEffect), fork(fetchNetworkEventsEffect),
fork(fetchAgentPoliciesEffect), fork(fetchAgentPoliciesEffect),
fork(fetchPrivateLocationsEffect),
fork(fetchDynamicSettingsEffect), fork(fetchDynamicSettingsEffect),
fork(fetchLocationMonitorsEffect), fork(fetchLocationMonitorsEffect),
fork(setDynamicSettingsEffect), fork(setDynamicSettingsEffect),
@ -80,5 +80,6 @@ export const rootEffect = function* root(): Generator {
fork(quietFetchMonitorStatusHeatmap), fork(quietFetchMonitorStatusHeatmap),
fork(fetchOverviewTrendStats), fork(fetchOverviewTrendStats),
fork(refreshOverviewTrendStats), fork(refreshOverviewTrendStats),
...privateLocationsEffects.map((effect) => fork(effect)),
]); ]);
}; };

View file

@ -21,7 +21,7 @@ import {
SettingsState, SettingsState,
} from './settings'; } from './settings';
import { elasticsearchReducer, QueriesState } from './elasticsearch'; import { elasticsearchReducer, QueriesState } from './elasticsearch';
import { agentPoliciesReducer, AgentPoliciesState } from './private_locations'; import { PrivateLocationsState, privateLocationsStateReducer } from './private_locations';
import { networkEventsReducer, NetworkEventsState } from './network_events'; import { networkEventsReducer, NetworkEventsState } from './network_events';
import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details'; import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details';
import { uiReducer, UiState } from './ui'; import { uiReducer, UiState } from './ui';
@ -31,47 +31,50 @@ import { serviceLocationsReducer, ServiceLocationsState } from './service_locati
import { monitorOverviewReducer, MonitorOverviewState } from './overview'; import { monitorOverviewReducer, MonitorOverviewState } from './overview';
import { BrowserJourneyState } from './browser_journey/models'; import { BrowserJourneyState } from './browser_journey/models';
import { monitorStatusHeatmapReducer, MonitorStatusHeatmap } from './status_heatmap'; import { monitorStatusHeatmapReducer, MonitorStatusHeatmap } from './status_heatmap';
import { agentPoliciesReducer, AgentPoliciesState } from './agent_policies';
export interface SyntheticsAppState { export interface SyntheticsAppState {
ui: UiState;
settings: SettingsState;
elasticsearch: QueriesState;
monitorList: MonitorListState;
overview: MonitorOverviewState;
certificates: CertificatesState;
globalParams: GlobalParamsState;
networkEvents: NetworkEventsState;
agentPolicies: AgentPoliciesState; agentPolicies: AgentPoliciesState;
manualTestRuns: ManualTestRunsState;
monitorDetails: MonitorDetailsState;
browserJourney: BrowserJourneyState; browserJourney: BrowserJourneyState;
certificates: CertificatesState;
certsList: CertsListState; certsList: CertsListState;
defaultAlerting: DefaultAlertingState; defaultAlerting: DefaultAlertingState;
dynamicSettings: DynamicSettingsState; dynamicSettings: DynamicSettingsState;
serviceLocations: ServiceLocationsState; elasticsearch: QueriesState;
overviewStatus: OverviewStatusStateReducer; globalParams: GlobalParamsState;
syntheticsEnablement: SyntheticsEnablementState; manualTestRuns: ManualTestRunsState;
monitorDetails: MonitorDetailsState;
monitorList: MonitorListState;
monitorStatusHeatmap: MonitorStatusHeatmap; monitorStatusHeatmap: MonitorStatusHeatmap;
networkEvents: NetworkEventsState;
overview: MonitorOverviewState;
overviewStatus: OverviewStatusStateReducer;
privateLocations: PrivateLocationsState;
serviceLocations: ServiceLocationsState;
settings: SettingsState;
syntheticsEnablement: SyntheticsEnablementState;
ui: UiState;
} }
export const rootReducer = combineReducers<SyntheticsAppState>({ export const rootReducer = combineReducers<SyntheticsAppState>({
ui: uiReducer,
settings: settingsReducer,
monitorList: monitorListReducer,
overview: monitorOverviewReducer,
globalParams: globalParamsReducer,
networkEvents: networkEventsReducer,
elasticsearch: elasticsearchReducer,
agentPolicies: agentPoliciesReducer, agentPolicies: agentPoliciesReducer,
monitorDetails: monitorDetailsReducer,
browserJourney: browserJourneyReducer, browserJourney: browserJourneyReducer,
manualTestRuns: manualTestRunsReducer,
overviewStatus: overviewStatusReducer,
defaultAlerting: defaultAlertingReducer,
dynamicSettings: dynamicSettingsReducer,
serviceLocations: serviceLocationsReducer,
syntheticsEnablement: syntheticsEnablementReducer,
certificates: certificatesReducer, certificates: certificatesReducer,
certsList: certsListReducer, certsList: certsListReducer,
defaultAlerting: defaultAlertingReducer,
dynamicSettings: dynamicSettingsReducer,
elasticsearch: elasticsearchReducer,
globalParams: globalParamsReducer,
manualTestRuns: manualTestRunsReducer,
monitorDetails: monitorDetailsReducer,
monitorList: monitorListReducer,
monitorStatusHeatmap: monitorStatusHeatmapReducer, monitorStatusHeatmap: monitorStatusHeatmapReducer,
networkEvents: networkEventsReducer,
overview: monitorOverviewReducer,
overviewStatus: overviewStatusReducer,
privateLocations: privateLocationsStateReducer,
serviceLocations: serviceLocationsReducer,
settings: settingsReducer,
syntheticsEnablement: syntheticsEnablementReducer,
ui: uiReducer,
}); });

View file

@ -14,7 +14,7 @@ const getState = (appState: AppState) => appState.agentPolicies;
export const selectAgentPolicies = createSelector(getState, (state) => state); export const selectAgentPolicies = createSelector(getState, (state) => state);
export const selectAddingNewPrivateLocation = (state: AppState) => export const selectAddingNewPrivateLocation = (state: AppState) =>
state.agentPolicies.isAddingNewPrivateLocation ?? false; state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
export const selectLocationMonitors = (state: AppState) => ({ export const selectLocationMonitors = (state: AppState) => ({
locationMonitors: state.dynamicSettings.locationMonitors, locationMonitors: state.dynamicSettings.locationMonitors,

View file

@ -114,6 +114,12 @@ export const mockState: SyntheticsAppState = {
error: null, error: null,
data: null, data: null,
}, },
privateLocations: {
isCreatePrivateLocationFlyoutVisible: false,
loading: false,
error: null,
data: [],
},
settings: { settings: {
loading: false, loading: false,
error: null, error: null,

View file

@ -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<PrivateLocationAttributes>(
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<PrivateLocationAttributes>({
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'
);
};

View file

@ -6,13 +6,13 @@
*/ */
import { schema, TypeOf } from '@kbn/config-schema'; 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 { PRIVATE_LOCATION_WRITE_API } from '../../../feature';
import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations';
import { SyntheticsRestApiRouteFactory } from '../../types'; 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 { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
import { toClientContract, toSavedObjectContract } from './helpers'; import { toClientContract, toSavedObjectContract } from './helpers';
import { PrivateLocation } from '../../../../common/runtime_types'; import { PrivateLocation } from '../../../../common/runtime_types';
@ -26,6 +26,11 @@ export const PrivateLocationSchema = schema.object({
lon: schema.number(), lon: schema.number(),
}) })
), ),
spaces: schema.maybe(
schema.arrayOf(schema.string(), {
minSize: 1,
})
),
}); });
export type PrivateLocationObject = TypeOf<typeof PrivateLocationSchema>; export type PrivateLocationObject = TypeOf<typeof PrivateLocationSchema>;
@ -41,60 +46,43 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory<PrivateLocat
}, },
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API], requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
handler: async (routeContext) => { handler: async (routeContext) => {
const { response, request, savedObjectsClient, syntheticsMonitorClient, server } = routeContext; const { response, request, server } = routeContext;
const internalSOClient = server.coreStart.savedObjects.createInternalRepository(); const internalSOClient = server.coreStart.savedObjects.createInternalRepository();
await migrateLegacyPrivateLocations(internalSOClient, server.logger); 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 location = request.body as PrivateLocationObject;
const newId = uuidV4();
const formattedLocation = toSavedObjectContract({ ...location, id: newId });
const { spaces } = location;
const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( try {
savedObjectsClient, const result = await repo.createPrivateLocation(formattedLocation, newId);
syntheticsMonitorClient
);
if (locations.find((loc) => loc.agentPolicyId === location.agentPolicyId)) { return toClientContract(result);
return response.badRequest({ } catch (error) {
body: { if (SavedObjectsErrorHelpers.isForbiddenError(error)) {
message: `Private location with agentPolicyId ${location.agentPolicyId} already exists`, if (spaces?.includes('*')) {
}, return response.badRequest({
}); body: {
} message: `You do not have permission to create a location in all spaces.`,
},
// return if name is already taken });
if (locations.find((loc) => loc.label === location.label)) { }
return response.badRequest({ return response.customError({
body: { statusCode: error.output.statusCode,
message: `Private location with label ${location.label} already exists`, body: {
}, message: error.message,
}); },
} });
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<PrivateLocationAttributes>(
privateLocationSavedObjectName,
formattedLocation,
{
id: location.agentPolicyId,
initialNamespaces: ['*'],
} }
); throw error;
}
return toClientContract(result.attributes, agentPolicies);
}, },
}); });

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { SavedObject } from '@kbn/core/server';
import { AgentPolicyInfo } from '../../../../common/types'; import { AgentPolicyInfo } from '../../../../common/types';
import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types'; import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
import type { import type {
@ -13,18 +14,18 @@ import type {
import { PrivateLocation } from '../../../../common/runtime_types'; import { PrivateLocation } from '../../../../common/runtime_types';
export const toClientContract = ( export const toClientContract = (
location: PrivateLocationAttributes, locationObject: SavedObject<PrivateLocationAttributes>
agentPolicies?: AgentPolicyInfo[]
): PrivateLocation => { ): PrivateLocation => {
const agPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId); const location = locationObject.attributes;
return { return {
label: location.label, label: location.label,
id: location.id, id: location.id,
agentPolicyId: location.agentPolicyId, agentPolicyId: location.agentPolicyId,
isServiceManaged: false, isServiceManaged: false,
isInvalid: !Boolean(agPolicy), isInvalid: false,
tags: location.tags, tags: location.tags,
geo: location.geo, geo: location.geo,
spaces: locationObject.namespaces,
}; };
}; };
@ -42,6 +43,7 @@ export const allLocationsToClientContract = (
isInvalid: !Boolean(agPolicy), isInvalid: !Boolean(agPolicy),
tags: location.tags, tags: location.tags,
geo: location.geo, geo: location.geo,
spaces: location.spaces,
}; };
}); });
}; };
@ -55,5 +57,6 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio
isServiceManaged: false, isServiceManaged: false,
geo: location.geo, geo: location.geo,
namespace: location.namespace, namespace: location.namespace,
spaces: location.spaces,
}; };
}; };

View file

@ -25,22 +25,18 @@ export const getPrivateLocations = async (
client: SavedObjectsClientContract client: SavedObjectsClientContract
): Promise<SyntheticsPrivateLocationsAttributes['locations']> => { ): Promise<SyntheticsPrivateLocationsAttributes['locations']> => {
try { try {
const finder = client.createPointInTimeFinder<PrivateLocationAttributes>({ const [results, legacyLocations] = await Promise.all([
type: privateLocationSavedObjectName, getNewPrivateLocations(client),
perPage: 1000, getLegacyPrivateLocations(client),
}); ]);
const results: Array<SavedObject<PrivateLocationAttributes>> = []; return uniqBy(
[
for await (const response of finder.find()) { ...results.map((r) => ({ ...r.attributes, spaces: r.namespaces, id: r.id })),
results.push(...response.saved_objects); ...legacyLocations,
} ],
'id'
finder.close().catch((e) => {}); );
const legacyLocations = await getLegacyPrivateLocations(client);
return uniqBy([...results.map((r) => r.attributes), ...legacyLocations], 'id');
} catch (getErr) { } catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
return []; return [];
@ -49,6 +45,22 @@ export const getPrivateLocations = async (
} }
}; };
const getNewPrivateLocations = async (client: SavedObjectsClientContract) => {
const finder = client.createPointInTimeFinder<PrivateLocationAttributes>({
type: privateLocationSavedObjectName,
perPage: 1000,
});
const results: Array<SavedObject<PrivateLocationAttributes>> = [];
for await (const response of finder.find()) {
results.push(...response.saved_objects);
}
finder.close().catch((e) => {});
return results;
};
const getLegacyPrivateLocations = async (client: SavedObjectsClientContract) => { const getLegacyPrivateLocations = async (client: SavedObjectsClientContract) => {
try { try {
const obj = await client.get<SyntheticsPrivateLocationsAttributes>( const obj = await client.get<SyntheticsPrivateLocationsAttributes>(

View file

@ -217,10 +217,9 @@ export const getMonitorLocations = ({
}) || []; }) || [];
const privateLocs = const privateLocs =
monitorLocations.privateLocations?.map((locationName) => { monitorLocations.privateLocations?.map((locationName) => {
const loc = locationName.toLowerCase();
const locationFound = allPrivateLocations.find( const locationFound = allPrivateLocations.find(
(location) => (location) => location.label.toLowerCase() === loc || location.id.toLowerCase() === loc
location.label.toLowerCase() === locationName.toLowerCase() ||
location.id.toLowerCase() === locationName.toLowerCase()
); );
if (locationFound) { if (locationFound) {
return locationFound; return locationFound;

View file

@ -71,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) {
it('add a test private location', async () => { it('add a test private location', async () => {
pvtLoc = await testPrivateLocations.addPrivateLocation(); pvtLoc = await testPrivateLocations.addPrivateLocation();
testFleetPolicyID = pvtLoc.id; testFleetPolicyID = pvtLoc.agentPolicyId;
const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); 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 () => { it('adds a monitor in private location', async () => {
const newMonitor = httpMonitorJson; const newMonitor = httpMonitorJson;
newMonitor.locations.push(pvtLoc); newMonitor.locations.push(omit(pvtLoc, ['spaces']));
const { body, rawBody } = await addMonitorAPI(newMonitor); const { body, rawBody } = await addMonitorAPI(newMonitor);
@ -138,8 +138,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = apiResponse.body.items.find( const packagePolicy = apiResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy?.policy_id).eql(testFleetPolicyID); expect(packagePolicy?.policy_id).eql(testFleetPolicyID);
@ -149,23 +148,33 @@ export default function ({ getService }: FtrProviderContext) {
getTestSyntheticsPolicy({ getTestSyntheticsPolicy({
name: httpMonitorJson.name, name: httpMonitorJson.name,
id: newMonitorId, id: newMonitorId,
location: { id: testFleetPolicyID }, location: { id: pvtLoc.id },
}) })
); );
}); });
let testFleetPolicyID2: string; let testFleetPolicyID2: string;
let pvtLoc2: PrivateLocation;
it('edits a monitor with additional private location', async () => { it('edits a monitor with additional private location', async () => {
const resPolicy = await testPrivateLocations.addFleetPolicy(testPolicyName + 1); const resPolicy = await testPrivateLocations.addFleetPolicy(testPolicyName + 1);
testFleetPolicyID2 = resPolicy.body.item.id; testFleetPolicyID2 = resPolicy.body.item.id;
const pvtLoc2 = await testPrivateLocations.addPrivateLocation({ pvtLoc2 = await testPrivateLocations.addPrivateLocation({
policyId: testFleetPolicyID2, policyId: testFleetPolicyID2,
label: 'Test private location 1', 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 const apiResponse = await supertestAPI
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId) .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId)
@ -191,8 +200,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
let packagePolicy = apiResponsePolicy.body.items.find( let packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.policy_id).eql(testFleetPolicyID);
@ -202,13 +210,12 @@ export default function ({ getService }: FtrProviderContext) {
getTestSyntheticsPolicy({ getTestSyntheticsPolicy({
name: httpMonitorJson.name, name: httpMonitorJson.name,
id: newMonitorId, id: newMonitorId,
location: { id: testFleetPolicyID }, location: { id: pvtLoc.id },
}) })
); );
packagePolicy = apiResponsePolicy.body.items.find( packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc2.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID2 + '-default'
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID2); expect(packagePolicy.policy_id).eql(testFleetPolicyID2);
@ -219,16 +226,14 @@ export default function ({ getService }: FtrProviderContext) {
id: newMonitorId, id: newMonitorId,
location: { location: {
name: 'Test private location 1', name: 'Test private location 1',
id: testFleetPolicyID2, id: pvtLoc2.id,
}, },
}) })
); );
}); });
it('deletes integration for a removed location from monitor', async () => { it('deletes integration for a removed location from monitor', async () => {
httpMonitorJson.locations = httpMonitorJson.locations.filter( httpMonitorJson.locations = httpMonitorJson.locations.filter(({ id }) => id !== pvtLoc2.id);
({ id }) => id !== testFleetPolicyID2
);
await supertestAPI await supertestAPI
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?internal=true') .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?internal=true')
@ -241,8 +246,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
let packagePolicy = apiResponsePolicy.body.items.find( let packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.policy_id).eql(testFleetPolicyID);
@ -252,13 +256,12 @@ export default function ({ getService }: FtrProviderContext) {
getTestSyntheticsPolicy({ getTestSyntheticsPolicy({
name: httpMonitorJson.name, name: httpMonitorJson.name,
id: newMonitorId, id: newMonitorId,
location: { id: testFleetPolicyID }, location: { id: pvtLoc.id },
}) })
); );
packagePolicy = apiResponsePolicy.body.items.find( packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + pvtLoc2.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID2 + '-default'
); );
expect(packagePolicy).eql(undefined); expect(packagePolicy).eql(undefined);
@ -287,7 +290,7 @@ export default function ({ getService }: FtrProviderContext) {
...httpMonitorJson, ...httpMonitorJson,
name: `Test monitor ${uuidv4()}`, name: `Test monitor ${uuidv4()}`,
[ConfigKey.NAMESPACE]: 'default', [ConfigKey.NAMESPACE]: 'default',
locations: [pvtLoc], locations: [omit(pvtLoc, ['spaces'])],
}; };
try { try {
@ -316,7 +319,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = policyResponse.body.items.find( const packagePolicy = policyResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-${SPACE_ID}` pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-${SPACE_ID}`
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.policy_id).eql(testFleetPolicyID);
@ -326,7 +329,7 @@ export default function ({ getService }: FtrProviderContext) {
getTestSyntheticsPolicy({ getTestSyntheticsPolicy({
name: monitor.name, name: monitor.name,
id: monitorId, id: monitorId,
location: { id: testFleetPolicyID }, location: { id: pvtLoc.id },
namespace: formatKibanaNamespace(SPACE_ID), namespace: formatKibanaNamespace(SPACE_ID),
spaceId: SPACE_ID, spaceId: SPACE_ID,
}) })
@ -350,7 +353,7 @@ export default function ({ getService }: FtrProviderContext) {
...httpMonitorJson, ...httpMonitorJson,
locations: [ locations: [
{ {
id: testFleetPolicyID, id: pvtLoc.id,
label: 'Test private location 0', label: 'Test private location 0',
isServiceManaged: false, isServiceManaged: false,
}, },
@ -374,15 +377,14 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = policyResponse.body.items.find( const packagePolicy = policyResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default`
pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default`
); );
comparePolicies( comparePolicies(
packagePolicy, packagePolicy,
getTestSyntheticsPolicy({ getTestSyntheticsPolicy({
name: monitor.name, name: monitor.name,
id: monitorId, id: monitorId,
location: { id: testFleetPolicyID }, location: { id: pvtLoc.id },
isTLSEnabled: true, isTLSEnabled: true,
}) })
); );
@ -398,7 +400,7 @@ export default function ({ getService }: FtrProviderContext) {
...httpMonitorJson, ...httpMonitorJson,
locations: [ locations: [
{ {
id: testFleetPolicyID, id: pvtLoc.id,
label: 'Test private location 0', label: 'Test private location 0',
isServiceManaged: false, isServiceManaged: false,
}, },
@ -412,8 +414,9 @@ export default function ({ getService }: FtrProviderContext) {
const apiResponse = await supertestAPI const apiResponse = await supertestAPI
.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true') .set('kbn-xsrf', 'true')
.send(monitor) .send(monitor);
.expect(200);
expect(apiResponse.status).eql(200, JSON.stringify(apiResponse.body));
monitorId = apiResponse.body.id; monitorId = apiResponse.body.id;
@ -422,15 +425,14 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = policyResponse.body.items.find( const packagePolicy = policyResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default`
pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default`
); );
comparePolicies( comparePolicies(
packagePolicy, packagePolicy,
getTestSyntheticsPolicy({ getTestSyntheticsPolicy({
name: monitor.name, name: monitor.name,
id: monitorId, id: monitorId,
location: { id: testFleetPolicyID }, location: { id: pvtLoc.id },
}) })
); );
} finally { } finally {
@ -447,7 +449,7 @@ export default function ({ getService }: FtrProviderContext) {
[ConfigKey.NAMESPACE]: 'default', [ConfigKey.NAMESPACE]: 'default',
locations: [ locations: [
{ {
id: testFleetPolicyID, id: pvtLoc.id,
label: 'Test private location 0', label: 'Test private location 0',
isServiceManaged: false, isServiceManaged: false,
}, },
@ -467,8 +469,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = policyResponse.body.items.find( const packagePolicy = policyResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default`
pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default`
); );
expect(packagePolicy.package.version).eql(INSTALLED_VERSION); 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' '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
); );
const packagePolicyAfterUpgrade = policyResponseAfterUpgrade.body.items.find( const packagePolicyAfterUpgrade = policyResponseAfterUpgrade.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === monitorId + '-' + pvtLoc.id + `-default`
pkgPolicy.id === monitorId + '-' + testFleetPolicyID + `-default`
); );
expect(semver.gte(packagePolicyAfterUpgrade.package.version, INSTALLED_VERSION)).eql(true); expect(semver.gte(packagePolicyAfterUpgrade.package.version, INSTALLED_VERSION)).eql(true);
} finally { } finally {

View file

@ -46,6 +46,7 @@ export default function ({ getService }: FtrProviderContext) {
let icmpProjectMonitors: ProjectMonitorsRequest; let icmpProjectMonitors: ProjectMonitorsRequest;
let testPolicyId = ''; let testPolicyId = '';
let loc: any;
const setUniqueIds = (request: ProjectMonitorsRequest) => { const setUniqueIds = (request: ProjectMonitorsRequest) => {
return { return {
...request, ...request,
@ -85,8 +86,8 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200); .expect(200);
await testPrivateLocations.installSyntheticsPackage(); await testPrivateLocations.installSyntheticsPackage();
const loc = await testPrivateLocations.addPrivateLocation(); loc = await testPrivateLocations.addPrivateLocation();
testPolicyId = loc.id; testPolicyId = loc.agentPolicyId;
await supertest await supertest
.post(SYNTHETICS_API_URLS.PARAMS) .post(SYNTHETICS_API_URLS.PARAMS)
.set('kbn-xsrf', 'true') .set('kbn-xsrf', 'true')
@ -644,7 +645,7 @@ export default function ({ getService }: FtrProviderContext) {
lat: 0, lat: 0,
lon: 0, lon: 0,
}, },
id: testPolicyId, id: loc.id,
agentPolicyId: testPolicyId, agentPolicyId: testPolicyId,
isServiceManaged: false, isServiceManaged: false,
label: 'Test private location 0', label: 'Test private location 0',
@ -1443,7 +1444,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = apiResponsePolicy.body.items.find( const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === 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( expect(packagePolicy.name).eql(
`${projectMonitors.monitors[0].id}-${project}-default-Test private location 0` `${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( const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === 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( expect(packagePolicy.name).eql(
`${httpProjectMonitors.monitors[1].id}-${project}-default-Test private location 0` `${httpProjectMonitors.monitors[1].id}-${project}-default-Test private location 0`
@ -1530,7 +1531,7 @@ export default function ({ getService }: FtrProviderContext) {
configId, configId,
projectId: project, projectId: project,
locationName: 'Test private location 0', locationName: 'Test private location 0',
locationId: testPolicyId, locationId: loc.id,
}) })
); );
} finally { } finally {
@ -1575,7 +1576,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = apiResponsePolicy.body.items.find( const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === 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); expect(packagePolicy.policy_id).eql(testPolicyId);
@ -1597,7 +1598,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy2 = apiResponsePolicy2.body.items.find( const packagePolicy2 = apiResponsePolicy2.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === 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); expect(packagePolicy2).eql(undefined);
@ -1637,7 +1638,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = apiResponsePolicy.body.items.find( const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === 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); expect(packagePolicy.policy_id).eql(testPolicyId);
@ -1721,7 +1722,7 @@ export default function ({ getService }: FtrProviderContext) {
const configId = monitorsResponse.body.monitors[0].id; const configId = monitorsResponse.body.monitors[0].id;
const id = monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_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( const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId (pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId
@ -1737,7 +1738,7 @@ export default function ({ getService }: FtrProviderContext) {
id, id,
configId, configId,
projectId: project, projectId: project,
locationId: testPolicyId, locationId: loc.id,
locationName: 'Test private location 0', locationName: 'Test private location 0',
}) })
); );
@ -1764,7 +1765,7 @@ export default function ({ getService }: FtrProviderContext) {
const configId2 = monitorsResponse.body.monitors[0].id; const configId2 = monitorsResponse.body.monitors[0].id;
const id2 = monitorsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_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( const packagePolicy2 = apiResponsePolicy2.body.items.find(
(pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId2 (pkgPolicy: PackagePolicy) => pkgPolicy.id === policyId2
@ -1778,7 +1779,7 @@ export default function ({ getService }: FtrProviderContext) {
id: id2, id: id2,
configId: configId2, configId: configId2,
projectId: project, projectId: project,
locationId: testPolicyId, locationId: loc.id,
locationName: 'Test private location 0', locationName: 'Test private location 0',
namespace: 'custom_namespace', namespace: 'custom_namespace',
}) })
@ -1832,7 +1833,7 @@ export default function ({ getService }: FtrProviderContext) {
label: 'Test private location 0', label: 'Test private location 0',
isServiceManaged: false, isServiceManaged: false,
agentPolicyId: testPolicyId, agentPolicyId: testPolicyId,
id: testPolicyId, id: loc.id,
geo: { geo: {
lat: 0, lat: 0,
lon: 0, lon: 0,

View file

@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) {
let projectMonitors: ProjectMonitorsRequest; let projectMonitors: ProjectMonitorsRequest;
let testPolicyId = ''; let testPolicyId = '';
let loc: any;
const testPrivateLocations = new PrivateLocationTestService(getService); const testPrivateLocations = new PrivateLocationTestService(getService);
const setUniqueIds = (request: ProjectMonitorsRequest) => { const setUniqueIds = (request: ProjectMonitorsRequest) => {
@ -39,8 +40,8 @@ export default function ({ getService }: FtrProviderContext) {
before(async () => { before(async () => {
await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.savedObjects.cleanStandardList();
await testPrivateLocations.installSyntheticsPackage(); await testPrivateLocations.installSyntheticsPackage();
const loc = await testPrivateLocations.addPrivateLocation(); loc = await testPrivateLocations.addPrivateLocation();
testPolicyId = loc.id; testPolicyId = loc.agentPolicyId;
}); });
beforeEach(() => { beforeEach(() => {
@ -404,9 +405,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = apiResponsePolicy.body.items.find( const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === pkgPolicy.id ===
savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + '-' + loc.id
'-' +
testPolicyId
); );
expect(packagePolicy.policy_id).to.be(testPolicyId); expect(packagePolicy.policy_id).to.be(testPolicyId);
@ -438,9 +437,7 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy2 = apiResponsePolicy2.body.items.find( const packagePolicy2 = apiResponsePolicy2.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) =>
pkgPolicy.id === pkgPolicy.id ===
savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + savedObjectsResponse.body.monitors[0][ConfigKey.CUSTOM_HEARTBEAT_ID] + '-' + loc.id
'-' +
testPolicyId
); );
expect(packagePolicy2).to.be(undefined); expect(packagePolicy2).to.be(undefined);
} finally { } finally {

View file

@ -65,7 +65,7 @@ export default function ({ getService }: FtrProviderContext) {
}); });
after(async () => { after(async () => {
await kibanaServer.savedObjects.cleanStandardList(); // await kibanaServer.savedObjects.cleanStandardList();
}); });
let monitorId = 'test-id'; let monitorId = 'test-id';
@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) {
...updates, ...updates,
revision: 3, revision: 3,
url: 'https://www.google.com', url: 'https://www.google.com',
locations: [localLoc, pvtLoc], locations: [localLoc, omit(pvtLoc, 'spaces')],
}) })
); );
@ -270,7 +270,7 @@ export default function ({ getService }: FtrProviderContext) {
...updates, ...updates,
revision: 4, revision: 4,
url: 'https://www.google.com', url: 'https://www.google.com',
locations: [pvtLoc], locations: [omit(pvtLoc, 'spaces')],
}) })
); );
}); });
@ -289,7 +289,7 @@ export default function ({ getService }: FtrProviderContext) {
...updates, ...updates,
revision: 5, revision: 5,
url: 'https://www.google.com', url: 'https://www.google.com',
locations: [localLoc, pvtLoc], locations: [localLoc, omit(pvtLoc, 'spaces')],
}) })
); );

View file

@ -21,6 +21,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('PrivateLocationAPI', function () { describe('PrivateLocationAPI', function () {
this.tags('skipCloud'); this.tags('skipCloud');
const supertestWithoutAuth = getService('supertestWithoutAuth'); const supertestWithoutAuth = getService('supertestWithoutAuth');
const supertest = getService('supertest');
const kServer = getService('kibanaServer'); const kServer = getService('kibanaServer');
@ -132,5 +133,64 @@ export default function ({ getService }: FtrProviderContext) {
expect(deleteResponse.status).to.be(200); 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<PrivateLocation, 'id'> = {
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<PrivateLocation, 'id'> = {
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);
});
}); });
} }

View file

@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) {
const kServer = getService('kibanaServer'); const kServer = getService('kibanaServer');
let testFleetPolicyID: string; let testFleetPolicyID: string;
let loc: any;
let _browserMonitorJson: HTTPFields; let _browserMonitorJson: HTTPFields;
let browserMonitorJson: HTTPFields; let browserMonitorJson: HTTPFields;
@ -61,8 +62,8 @@ export default function ({ getService }: FtrProviderContext) {
}); });
it('add a test private location', async () => { it('add a test private location', async () => {
const loc = await testPrivateLocations.addPrivateLocation(); loc = await testPrivateLocations.addPrivateLocation();
testFleetPolicyID = loc.id; testFleetPolicyID = loc.agentPolicyId;
const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS); const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS);
@ -86,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) {
isInvalid: false, isInvalid: false,
}, },
{ {
id: testFleetPolicyID, id: loc.id,
isInvalid: false, isInvalid: false,
isServiceManaged: false, isServiceManaged: false,
label: 'Test private location 0', label: 'Test private location 0',
@ -95,6 +96,7 @@ export default function ({ getService }: FtrProviderContext) {
lon: 0, lon: 0,
}, },
agentPolicyId: testFleetPolicyID, agentPolicyId: testFleetPolicyID,
spaces: ['*'],
}, },
]; ];
@ -105,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) {
const newMonitor = browserMonitorJson; const newMonitor = browserMonitorJson;
const pvtLoc = { const pvtLoc = {
id: testFleetPolicyID, id: loc.id,
agentPolicyId: testFleetPolicyID, agentPolicyId: testFleetPolicyID,
label: 'Test private location 0', label: 'Test private location 0',
isServiceManaged: false, isServiceManaged: false,
@ -136,8 +138,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = apiResponse.body.items.find( const packagePolicy = apiResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + loc.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy?.policy_id).eql( expect(packagePolicy?.policy_id).eql(
@ -193,8 +194,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = apiResponse.body.items.find( const packagePolicy = apiResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + loc.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.policy_id).eql(testFleetPolicyID);
@ -214,7 +214,7 @@ export default function ({ getService }: FtrProviderContext) {
it('add a http monitor using param', async () => { it('add a http monitor using param', async () => {
const newMonitor = httpMonitorJson; const newMonitor = httpMonitorJson;
const pvtLoc = { const pvtLoc = {
id: testFleetPolicyID, id: loc.id,
agentPolicyId: testFleetPolicyID, agentPolicyId: testFleetPolicyID,
label: 'Test private location 0', label: 'Test private location 0',
isServiceManaged: false, isServiceManaged: false,
@ -246,8 +246,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = apiResponse.body.items.find( const packagePolicy = apiResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newHttpMonitorId + '-' + loc.id + '-default'
pkgPolicy.id === newHttpMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.policy_id).eql(testFleetPolicyID);
@ -257,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) {
id: newHttpMonitorId, id: newHttpMonitorId,
isTLSEnabled: false, isTLSEnabled: false,
namespace: 'testnamespace', namespace: 'testnamespace',
location: { id: testFleetPolicyID }, location: { id: loc.id },
}); });
comparePolicies(packagePolicy, pPolicy); comparePolicies(packagePolicy, pPolicy);
@ -304,8 +303,7 @@ export default function ({ getService }: FtrProviderContext) {
); );
const packagePolicy = apiResponse.body.items.find( const packagePolicy = apiResponse.body.items.find(
(pkgPolicy: PackagePolicy) => (pkgPolicy: PackagePolicy) => pkgPolicy.id === newMonitorId + '-' + loc.id + '-default'
pkgPolicy.id === newMonitorId + '-' + testFleetPolicyID + '-default'
); );
expect(packagePolicy.policy_id).eql(testFleetPolicyID); expect(packagePolicy.policy_id).eql(testFleetPolicyID);
@ -316,7 +314,7 @@ export default function ({ getService }: FtrProviderContext) {
name: browserMonitorJson.name, name: browserMonitorJson.name,
id: newMonitorId, id: newMonitorId,
isBrowser: true, isBrowser: true,
location: { id: testFleetPolicyID }, location: { id: loc.id },
}) })
); );
}); });

View file

@ -97,6 +97,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
lon: 0, lon: 0,
}, },
agentPolicyId: testFleetPolicyID, agentPolicyId: testFleetPolicyID,
spaces: ['default'],
}, },
]; ];
@ -115,6 +116,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
lat: 0, lat: 0,
lon: 0, lon: 0,
}, },
spaces: ['default'],
}; };
newMonitor.name = invalidName; newMonitor.name = invalidName;
@ -123,7 +125,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
.set(editorUser.apiKeyHeader) .set(editorUser.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader()) .set(samlAuth.getInternalRequestHeader())
.send({ ...newMonitor, locations: [location] }) .send({ ...newMonitor, locations: [omit(location, 'spaces')] })
.expect(400); .expect(400);
expect(apiResponse.body).eql({ expect(apiResponse.body).eql({