Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum SYNTHETICS_API_URLS {
OVERVIEW_STATUS = `/internal/synthetics/overview_status`,
INDEX_SIZE = `/internal/synthetics/index_size`,
PARAMS = `/synthetics/params`,
PRIVATE_LOCATIONS = `/synthetics/private_locations`,
SYNC_GLOBAL_PARAMS = `/synthetics/sync_global_params`,
ENABLE_DEFAULT_ALERTING = `/synthetics/enable_default_alerting`,
JOURNEY = `/internal/synthetics/journey/{checkGroup}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@
* 2.0.
*/

import { renderHook } from '@testing-library/react-hooks';
import { defaultCore, WrappedHelper } from '../../../../utils/testing';

import { renderHook, act } from '@testing-library/react-hooks';
import { WrappedHelper } from '../../../../utils/testing';
import { getServiceLocations } from '../../../../state/service_locations';
import { setAddingNewPrivateLocation } from '../../../../state/private_locations';
import { useLocationsAPI } from './use_locations_api';
import * as locationAPI from '../../../../state/private_locations/api';
import * as reduxHooks from 'react-redux';

describe('useLocationsAPI', () => {
const dispatch = jest.fn();
const addAPI = jest.spyOn(locationAPI, 'addSyntheticsPrivateLocations').mockResolvedValue({
locations: [],
});
const deletedAPI = jest.spyOn(locationAPI, 'deleteSyntheticsPrivateLocations').mockResolvedValue({
locations: [],
});
const getAPI = jest.spyOn(locationAPI, 'getSyntheticsPrivateLocations');
jest.spyOn(reduxHooks, 'useDispatch').mockReturnValue(dispatch);

it('returns expected results', () => {
const { result } = renderHook(() => useLocationsAPI(), {
wrapper: WrappedHelper,
Expand All @@ -22,20 +35,15 @@ describe('useLocationsAPI', () => {
privateLocations: [],
})
);
expect(defaultCore.savedObjects.client.get).toHaveBeenCalledWith(
'synthetics-privates-locations',
'synthetics-privates-locations-singleton'
);
expect(getAPI).toHaveBeenCalledTimes(1);
});
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
attributes: {
locations: [
{
id: 'Test',
agentPolicyId: 'testPolicy',
},
],
},
jest.spyOn(locationAPI, 'getSyntheticsPrivateLocations').mockResolvedValue({
locations: [
{
id: 'Test',
agentPolicyId: 'testPolicy',
} as any,
],
});
it('returns expected results after data', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI(), {
Expand Down Expand Up @@ -71,77 +79,50 @@ describe('useLocationsAPI', () => {

await waitForNextUpdate();

result.current.onSubmit({
id: 'new',
agentPolicyId: 'newPolicy',
label: 'new',
act(() => {
result.current.onSubmit({
id: 'new',
agentPolicyId: 'newPolicy',
label: 'new',
concurrentMonitors: 1,
geo: {
lat: 0,
lon: 0,
},
});
});

await waitForNextUpdate();

expect(addAPI).toHaveBeenCalledWith({
concurrentMonitors: 1,
id: 'newPolicy',
geo: {
lat: 0,
lon: 0,
},
label: 'new',
agentPolicyId: 'newPolicy',
});

await waitForNextUpdate();

expect(defaultCore.savedObjects.client.create).toHaveBeenCalledWith(
'synthetics-privates-locations',
{
locations: [
{ id: 'Test', agentPolicyId: 'testPolicy' },
{
concurrentMonitors: 1,
id: 'newPolicy',
geo: {
lat: 0,
lon: 0,
},
label: 'new',
agentPolicyId: 'newPolicy',
},
],
},
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
);
expect(dispatch).toBeCalledWith(setAddingNewPrivateLocation(false));
expect(dispatch).toBeCalledWith(getServiceLocations());
});

it('deletes location on delete', async () => {
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
attributes: {
locations: [
{
id: 'Test',
agentPolicyId: 'testPolicy',
},
{
id: 'Test1',
agentPolicyId: 'testPolicy1',
},
],
},
});

const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI(), {
wrapper: WrappedHelper,
});

await waitForNextUpdate();

result.current.onDelete('Test');
act(() => {
result.current.onDelete('Test');
});

await waitForNextUpdate();

expect(defaultCore.savedObjects.client.create).toHaveBeenLastCalledWith(
'synthetics-privates-locations',
{
locations: [
{
id: 'Test1',
agentPolicyId: 'testPolicy1',
},
],
},
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
);
expect(deletedAPI).toHaveBeenLastCalledWith('Test');
expect(dispatch).toBeCalledWith(setAddingNewPrivateLocation(false));
expect(dispatch).toBeCalledWith(getServiceLocations());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@

import { useFetcher } from '@kbn/observability-plugin/public';
import { useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useDispatch } from 'react-redux';
import { getServiceLocations } from '../../../../state/service_locations';
import { setAddingNewPrivateLocation } from '../../../../state/private_locations';
import {
addSyntheticsPrivateLocations,
deleteSyntheticsPrivateLocations,
getSyntheticsPrivateLocations,
setSyntheticsPrivateLocations,
} from '../../../../state/private_locations/api';
import { PrivateLocation } from '../../../../../../../common/runtime_types';

Expand All @@ -21,31 +22,29 @@ export const useLocationsAPI = () => {
const [deleteId, setDeleteId] = useState<string>();
const [privateLocations, setPrivateLocations] = useState<PrivateLocation[]>([]);

const { savedObjects } = useKibana().services;

const dispatch = useDispatch();

const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val));

const { loading: fetchLoading } = useFetcher(async () => {
const result = await getSyntheticsPrivateLocations(savedObjects?.client!);
setPrivateLocations(result);
const result = await getSyntheticsPrivateLocations();
setPrivateLocations(result.locations);
return result;
}, []);

const { loading: saveLoading } = useFetcher(async () => {
if (privateLocations && formData) {
const existingLocations = privateLocations.filter((loc) => loc.id !== formData.agentPolicyId);

const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
locations: [...(existingLocations ?? []), { ...formData, id: formData.agentPolicyId }],
if (formData) {
const result = await addSyntheticsPrivateLocations({
...formData,
id: formData.agentPolicyId,
});
setPrivateLocations(result.locations);
setFormData(undefined);
setIsAddingNew(false);
dispatch(getServiceLocations());
return result;
}
}, [formData, privateLocations]);
}, [formData]);

const onSubmit = (data: PrivateLocation) => {
setFormData(data);
Expand All @@ -57,14 +56,13 @@ export const useLocationsAPI = () => {

const { loading: deleteLoading } = useFetcher(async () => {
if (deleteId) {
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
locations: (privateLocations ?? []).filter((loc) => loc.id !== deleteId),
});
const result = await deleteSyntheticsPrivateLocations(deleteId);
setPrivateLocations(result.locations);
setDeleteId(undefined);
dispatch(getServiceLocations());
return result;
}
}, [deleteId, privateLocations]);
}, [deleteId]);

return {
formData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export const LocationForm = ({
hasPermissions: boolean;
}) => {
const { data } = useSelector(selectAgentPolicies);
const { control, register } = useFormContext<PrivateLocation>();
const { control, register, watch } = useFormContext<PrivateLocation>();
const { errors } = useFormState();
const selectedPolicyId = watch('agentPolicyId');
const selectedPolicy = data?.items.find((item) => item.id === selectedPolicyId);

const tagsList = privateLocations.reduce((acc, item) => {
const tags = item.tags || [];
Expand Down Expand Up @@ -97,6 +99,39 @@ export const LocationForm = ({
}
</p>
</EuiCallOut>

<EuiSpacer />
{selectedPolicy?.agents === 0 && (
<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 agents attached. Please ensure you have at least one agent enrolled in this policy. You can add agent before or after creating a location. For more information, {link}."
values={{
link: (
<EuiLink
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>
</>
);
Expand All @@ -109,6 +144,13 @@ export const AGENT_CALLOUT_TITLE = i18n.translate(
}
);

export const AGENT_MISSING_CALLOUT_TITLE = i18n.translate(
'xpack.synthetics.monitorManagement.agentMissingCallout.title',
{
defaultMessage: 'Selected agent policy has no agents',
}
);

export const LOCATION_NAME_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.locationName',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const ManagePrivateLocations = () => {
const dispatch = useDispatch();

const isAddingNew = useSelector(selectAddingNewPrivateLocation);

const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val));

const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = useLocationsAPI();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React from 'react';
import { EuiLink, EuiLoadingSpinner, EuiText, EuiTextColor } from '@elastic/eui';
import { EuiBadge, EuiLink, EuiLoadingSpinner, EuiText, EuiTextColor } from '@elastic/eui';
import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { useSyntheticsSettingsContext } from '../../../contexts';
Expand All @@ -28,7 +28,7 @@ export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => {

return (
<EuiText size="s">
{canReadAgentPolicies && (
{canReadAgentPolicies ? (
<EuiTextColor color="subdued">
{policy ? (
<EuiLink href={`${basePath}/app/fleet/policies/${agentPolicyId}`}>
Expand All @@ -40,11 +40,22 @@ export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => {
</EuiText>
)}
</EuiTextColor>
) : (
agentPolicyId
)}
&nbsp; &nbsp;
<EuiBadge color={policy?.agents === 0 ? 'warning' : 'hollow'}>
{AGENTS_LABEL}
{policy?.agents}
</EuiBadge>
</EuiText>
);
};

const POLICY_IS_DELETED = i18n.translate('xpack.synthetics.monitorManagement.deletedPolicy', {
defaultMessage: 'Policy is deleted',
});

const AGENTS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agents', {
defaultMessage: 'Agents: ',
});
Loading