Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Synthetics] Implement private location run once #162582

Merged
merged 15 commits into from Jul 31, 2023
Expand Up @@ -13,14 +13,11 @@ import { v4 as uuidv4 } from 'uuid';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { TestNowModeFlyout, TestRun } from '../../test_now_mode/test_now_mode_flyout';
import { format } from './formatter';
import {
Locations,
MonitorFields as MonitorFieldsType,
} from '../../../../../../common/runtime_types';
import { MonitorFields as MonitorFieldsType } from '../../../../../../common/runtime_types';
import { runOnceMonitor } from '../../../state/manual_test_runs/api';

export const RunTestButton = () => {
const { watch, formState, getValues, handleSubmit } = useFormContext();
const { formState, getValues, handleSubmit } = useFormContext();

const [inProgress, setInProgress] = useState(false);
const [testRun, setTestRun] = useState<TestRun>();
Expand Down Expand Up @@ -51,13 +48,7 @@ export const RunTestButton = () => {
}
}, [testRun?.id]);

const locations = watch('locations') as Locations;

const { tooltipContent, isDisabled } = useTooltipContent(
locations,
formState.isValid,
inProgress
);
const { tooltipContent, isDisabled } = useTooltipContent(formState.isValid, inProgress);

return (
<>
Expand Down Expand Up @@ -94,22 +85,12 @@ export const RunTestButton = () => {
);
};

const useTooltipContent = (
locations: Locations,
isValid: boolean,
isTestRunInProgress?: boolean
) => {
const isAnyPublicLocationSelected = locations?.some((loc) => loc.isServiceManaged);
const isOnlyPrivateLocations = (locations?.length ?? 0) > 0 && !isAnyPublicLocationSelected;

let tooltipContent =
isOnlyPrivateLocations || (isValid && !isAnyPublicLocationSelected)
? PRIVATE_AVAILABLE_LABEL
: TEST_NOW_DESCRIPTION;
const useTooltipContent = (isValid: boolean, isTestRunInProgress?: boolean) => {
let tooltipContent = !isValid ? INVALID_DESCRIPTION : TEST_NOW_DESCRIPTION;

tooltipContent = isTestRunInProgress ? TEST_SCHEDULED_LABEL : tooltipContent;

const isDisabled = isTestRunInProgress || !isAnyPublicLocationSelected;
const isDisabled = isTestRunInProgress || !isValid;

return { tooltipContent, isDisabled };
};
Expand All @@ -118,20 +99,17 @@ const TEST_NOW_DESCRIPTION = i18n.translate('xpack.synthetics.testRun.descriptio
defaultMessage: 'Test your monitor and verify the results before saving',
});

const INVALID_DESCRIPTION = i18n.translate('xpack.synthetics.testRun.invalid', {
defaultMessage: 'Monitor has to be valid to run test, please fix above required fields.',
});

export const TEST_SCHEDULED_LABEL = i18n.translate(
'xpack.synthetics.monitorList.testNow.scheduled',
{
defaultMessage: 'Test is already scheduled',
}
);

export const PRIVATE_AVAILABLE_LABEL = i18n.translate(
'xpack.synthetics.app.testNow.available.private',
{
defaultMessage: `You can't manually start tests on a private location.`,
}
);

export const TEST_NOW_ARIA_LABEL = i18n.translate(
'xpack.synthetics.monitorList.testNow.AriaLabel',
{
Expand Down
Expand Up @@ -9,47 +9,27 @@ import { EuiButton, EuiToolTip } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useDispatch, useSelector } from 'react-redux';
import {
TEST_NOW_ARIA_LABEL,
TEST_SCHEDULED_LABEL,
PRIVATE_AVAILABLE_LABEL,
} from '../monitor_add_edit/form/run_test_btn';
import { TEST_NOW_ARIA_LABEL, TEST_SCHEDULED_LABEL } from '../monitor_add_edit/form/run_test_btn';
import { useSelectedMonitor } from './hooks/use_selected_monitor';
import {
manualTestMonitorAction,
manualTestRunInProgressSelector,
} from '../../state/manual_test_runs';
import { useGetUrlParams } from '../../hooks/use_url_params';

export const RunTestManually = () => {
const dispatch = useDispatch();

const { monitor } = useSelectedMonitor();

const hasPublicLocation = monitor?.locations.some((loc) => loc.isServiceManaged);

const { locationId } = useGetUrlParams();

const isSelectedLocationPrivate = monitor?.locations.some(
(loc) => loc.isServiceManaged === false && loc.id === locationId
);

const testInProgress = useSelector(manualTestRunInProgressSelector(monitor?.config_id));

const content =
!hasPublicLocation || isSelectedLocationPrivate
? PRIVATE_AVAILABLE_LABEL
: testInProgress
? TEST_SCHEDULED_LABEL
: TEST_NOW_ARIA_LABEL;
const content = testInProgress ? TEST_SCHEDULED_LABEL : TEST_NOW_ARIA_LABEL;

return (
<EuiToolTip content={content} key={content}>
<EuiButton
data-test-subj="syntheticsRunTestManuallyButton"
color="success"
iconType="beaker"
isDisabled={!hasPublicLocation || isSelectedLocationPrivate}
isLoading={!Boolean(monitor) || testInProgress}
onClick={() => {
if (monitor) {
Expand Down
Expand Up @@ -14,13 +14,11 @@ import {
EuiPanel,
EuiLoadingSpinner,
EuiContextMenuPanelItemDescriptor,
EuiToolTip,
} from '@elastic/eui';
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { toggleStatusAlert } from '../../../../../../../common/runtime_types/monitor_management/alert_config';
import { PRIVATE_AVAILABLE_LABEL } from '../../../monitor_add_edit/form/run_test_btn';
import {
manualTestMonitorAction,
manualTestRunInProgressSelector,
Expand Down Expand Up @@ -106,8 +104,6 @@ export function ActionsPopover({
const location = useLocationName({ locationId });
const locationName = location?.label || monitor.location.id;

const isPrivateLocation = !Boolean(location?.isServiceManaged);

const detailUrl = useMonitorDetailLocator({
configId: monitor.configId,
locationId: locationId ?? monitor.location.id,
Expand Down Expand Up @@ -176,15 +172,9 @@ export function ActionsPopover({
},
quickInspectPopoverItem,
{
name: isPrivateLocation ? (
<EuiToolTip content={PRIVATE_AVAILABLE_LABEL}>
<span>{runTestManually}</span>
</EuiToolTip>
) : (
runTestManually
),
name: runTestManually,
icon: 'beaker',
disabled: testInProgress || isPrivateLocation,
disabled: testInProgress,
onClick: () => {
dispatch(manualTestMonitorAction.get({ configId: monitor.configId, name: monitor.name }));
dispatch(setFlyoutConfig(null));
Expand Down
Expand Up @@ -14,20 +14,14 @@ export function useRunOnceErrors({
serviceError,
errors,
locations,
showErrors = true,
}: {
showErrors?: boolean;
testRunId: string;
serviceError?: Error;
errors: ServiceLocationErrors;
locations: Locations;
}) {
const [locationErrors, setLocationErrors] = useState<ServiceLocationErrors>([]);
const [runOnceServiceError, setRunOnceServiceError] = useState<Error | undefined | null>(null);
const publicLocations = useMemo(
() => (locations ?? []).filter((loc) => loc.isServiceManaged),
[locations]
);

useEffect(() => {
setLocationErrors([]);
Expand All @@ -49,12 +43,12 @@ export function useRunOnceErrors({
}, [serviceError]);

const locationsById: Record<string, Locations[number]> = useMemo(
() => (publicLocations as Locations).reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}),
[publicLocations]
() => (locations as Locations).reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}),
[locations]
);

const expectPings =
publicLocations.length - (locationErrors ?? []).filter(({ locationId }) => !!locationId).length;
locations.length - (locationErrors ?? []).filter(({ locationId }) => !!locationId).length;

const locationErrorReasons = useMemo(() => {
return (locationErrors ?? [])
Expand All @@ -64,7 +58,7 @@ export function useRunOnceErrors({
}, [locationErrors]);
const hasBlockingError =
!!runOnceServiceError ||
(locationErrors?.length && locationErrors?.length === publicLocations.length);
(locationErrors?.length && locationErrors?.length === locations.length);

const errorMessages = useMemo(() => {
if (hasBlockingError) {
Expand Down
Expand Up @@ -15,14 +15,11 @@ import { Locations } from '../../../../../../common/runtime_types';
export function ManualTestRunMode({
manualTestRun,
onDone,
showErrors,
}: {
showErrors: boolean;
manualTestRun: ManualTestRun;
onDone: (testRunId: string) => void;
}) {
const { expectPings } = useRunOnceErrors({
showErrors,
testRunId: manualTestRun.testRunId!,
locations: (manualTestRun.monitor!.locations ?? []) as Locations,
errors: manualTestRun.errors ?? [],
Expand Down
Expand Up @@ -83,7 +83,6 @@ export function TestNowModeFlyoutContainer() {
key={manualTestRun.testRunId}
manualTestRun={manualTestRun}
onDone={onDone}
showErrors={flyoutOpenTestRun?.testRunId !== manualTestRun.testRunId}
/>
))}
{flyout}
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/synthetics/server/plugin.ts
Expand Up @@ -104,6 +104,7 @@ export class Plugin implements PluginType {

if (this.server) {
this.server.coreStart = coreStart;
this.server.pluginsStart = pluginsStart;
this.server.security = pluginsStart.security;
this.server.fleet = pluginsStart.fleet;
this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects;
Expand Down
Expand Up @@ -174,7 +174,7 @@ export const syncNewMonitor = async ({
routeContext: RouteContext;
privateLocations: PrivateLocationAttributes[];
}) => {
const { savedObjectsClient, server, syntheticsMonitorClient, request, spaceId } = routeContext;
const { savedObjectsClient, server, syntheticsMonitorClient, spaceId } = routeContext;
const newMonitorId = id ?? uuidV4();

let monitorSavedObject: SavedObject<EncryptedSyntheticsMonitorAttributes> | null = null;
Expand All @@ -193,7 +193,6 @@ export const syncNewMonitor = async ({

const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
[{ monitor: monitorWithNamespace as MonitorFields, id: newMonitorId }],
request,
savedObjectsClient,
privateLocations,
spaceId
Expand Down
Expand Up @@ -64,7 +64,7 @@ export const syncNewMonitorBulk = async ({
privateLocations: PrivateLocationAttributes[];
spaceId: string;
}) => {
const { server, savedObjectsClient, syntheticsMonitorClient, request } = routeContext;
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext;
let newMonitors: CreatedMonitors | null = null;

const monitorsToCreate = normalizedMonitors.map((monitor) => {
Expand All @@ -88,7 +88,6 @@ export const syncNewMonitorBulk = async ({
}),
syntheticsMonitorClient.addMonitors(
monitorsToCreate,
request,
savedObjectsClient,
privateLocations,
spaceId
Expand Down
Expand Up @@ -47,7 +47,6 @@ export const deleteMonitorBulk = async ({
...normalizedMonitor.attributes,
id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID],
})) as SyntheticsMonitorWithId[],
request,
savedObjectsClient,
spaceId
);
Expand Down
Expand Up @@ -68,7 +68,7 @@ export const deleteMonitor = async ({
routeContext: RouteContext;
monitorId: string;
}) => {
const { spaceId, savedObjectsClient, server, syntheticsMonitorClient, request } = routeContext;
const { spaceId, savedObjectsClient, server, syntheticsMonitorClient } = routeContext;
const { logger, telemetry, stackVersion } = server;

const { monitor, monitorWithSecret } = await getMonitorToDelete(
Expand All @@ -92,7 +92,6 @@ export const deleteMonitor = async ({
/* Type cast encrypted saved objects to decrypted saved objects for delete flow only.
* Deletion does not require all monitor fields */
] as SyntheticsMonitorWithId[],
request,
savedObjectsClient,
spaceId
);
Expand Down
Expand Up @@ -26,7 +26,6 @@ export const syncParamsSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = ()
const allPrivateLocations = await getPrivateLocations(savedObjectsClient);

await syntheticsMonitorClient.syncGlobalParams({
request,
spaceId,
allPrivateLocations,
encryptedSavedObjects: server.encryptedSavedObjects,
Expand Down
Expand Up @@ -6,6 +6,8 @@
*/
import { schema } from '@kbn/config-schema';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor';
import { SyntheticsRestApiRouteFactory } from '../types';
import { MonitorFields } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
Expand All @@ -20,7 +22,13 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, response, server, syntheticsMonitorClient }): Promise<any> => {
handler: async ({
request,
response,
server,
syntheticsMonitorClient,
savedObjectsClient,
}): Promise<any> => {
const monitor = request.body as MonitorFields;
const { monitorId } = request.params;

Expand All @@ -33,19 +41,22 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}

const { syntheticsService } = syntheticsMonitorClient;
const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
savedObjectsClient,
validationResult.decodedMonitor
);

const paramsBySpace = await syntheticsService.getSyntheticsParams({ spaceId });

const errors = await syntheticsService.runOnceConfigs({
// making it enabled, even if it's disabled in the UI
monitor: { ...validationResult.decodedMonitor, enabled: true },
configId: monitorId,
heartbeatId: monitorId,
runOnce: true,
testRunId: monitorId,
params: paramsBySpace[spaceId],
});
const [, errors] = await syntheticsMonitorClient.testNowConfigs(
{
monitor: { ...validationResult.decodedMonitor, config_id: monitorId } as MonitorFields,
id: monitorId,
testRunId: monitorId,
},
savedObjectsClient,
privateLocations,
spaceId,
true
);

if (errors) {
return { errors };
Expand Down