Skip to content

Commit

Permalink
[Monitor management UI] Validate monitor service locations (#121302) (#…
Browse files Browse the repository at this point in the history
…120798)

* Add validation for monitor service locations - Monitor management UI, Add/Edit monitor form.
  • Loading branch information
awahab07 authored Dec 21, 2021
1 parent 01d74cf commit d69fd17
Show file tree
Hide file tree
Showing 18 changed files with 405 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { isLeft } from 'fp-ts/lib/Either';
import * as t from 'io-ts';

const LocationGeoCodec = t.interface({
Expand All @@ -30,6 +31,9 @@ export const ServiceLocationCodec = t.interface({

export const ServiceLocationsCodec = t.array(ServiceLocationCodec);

export const isServiceLocationInvalid = (location: ServiceLocation) =>
isLeft(ServiceLocationCodec.decode(location));

export const ServiceLocationsApiResponseCodec = t.interface({
locations: ServiceLocationsCodec,
});
Expand Down
31 changes: 3 additions & 28 deletions x-pack/plugins/uptime/common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,6 @@
* 2.0.
*/

import { SimpleSavedObject } from 'kibana/public';
import { SyntheticsMonitor } from '../runtime_types';

/** Represents the average monitor duration ms at a point in time. */
export interface MonitorDurationAveragePoint {
/** The timeseries value for this point. */
x: number;
/** The average duration ms for the monitor. */
y?: number | null;
}

export interface LocationDurationLine {
name: string;

line: MonitorDurationAveragePoint[];
}

/** The data used to populate the monitor charts. */
export interface MonitorDurationResult {
/** The average values for the monitor duration. */
locationDurationLines: LocationDurationLine[];
}

export interface MonitorIdParam {
monitorId: string;
}

export type SyntheticsMonitorSavedObject = SimpleSavedObject<SyntheticsMonitor>;
export * from './monitor_duration';
export * from './synthetics_monitor';
export * from './monitor_validation';
26 changes: 26 additions & 0 deletions x-pack/plugins/uptime/common/types/monitor_duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.
*/

/** Represents the average monitor duration ms at a point in time. */
export interface MonitorDurationAveragePoint {
/** The timeseries value for this point. */
x: number;
/** The average duration ms for the monitor. */
y?: number | null;
}

export interface LocationDurationLine {
name: string;

line: MonitorDurationAveragePoint[];
}

/** The data used to populate the monitor charts. */
export interface MonitorDurationResult {
/** The average values for the monitor duration. */
locationDurationLines: LocationDurationLine[];
}
12 changes: 12 additions & 0 deletions x-pack/plugins/uptime/common/types/monitor_validation.ts
Original file line number Diff line number Diff line change
@@ -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 { ConfigKey, MonitorFields } from '../runtime_types';

export type Validator = (config: Partial<MonitorFields>) => boolean;

export type Validation = Partial<Record<ConfigKey, Validator>>;
15 changes: 15 additions & 0 deletions x-pack/plugins/uptime/common/types/synthetics_monitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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 { SimpleSavedObject } from 'kibana/public';
import { SyntheticsMonitor } from '../runtime_types';

export interface MonitorIdParam {
monitorId: string;
}

export type SyntheticsMonitorSavedObject = SimpleSavedObject<SyntheticsMonitor>;
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {
ConfigKey,
ContentType,
DataStream,
MonitorFields,
Mode,
ThrottlingConfigKey,
ThrottlingSuffix,
ThrottlingSuffixType,
} from '../../../common/runtime_types';
export * from '../../../common/runtime_types/monitor_management';
export * from '../../../common/types/monitor_validation';

export interface PolicyConfig {
[DataStream.HTTP]: HTTPFields;
Expand All @@ -27,10 +27,6 @@ export interface PolicyConfig {
[DataStream.BROWSER]: BrowserFields;
}

export type Validator = (config: Partial<MonitorFields>) => boolean;

export type Validation = Partial<Record<ConfigKey, Validator>>;

export const contentTypesToMode = {
[ContentType.FORM]: Mode.FORM,
[ContentType.JSON]: Mode.JSON,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import * as fetchers from '../../../state/api/monitor_management';
import { DataStream, HTTPFields, ScheduleUnit, SyntheticsMonitor } from '../../fleet_package/types';
import {
DataStream,
HTTPFields,
ScheduleUnit,
SyntheticsMonitor,
} from '../../../../common/runtime_types';
import { ActionBar } from './action_bar';

describe('<ActionBar />', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useCallback, useState, useEffect } from 'react';
import React, { useCallback, useContext, useState, useEffect } from 'react';
import { useParams, Redirect } from 'react-router-dom';
import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
Expand All @@ -14,9 +14,10 @@ import { FETCH_STATUS, useFetcher } from '../../../../../observability/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';

import { MONITOR_MANAGEMENT } from '../../../../common/constants';
import { UptimeSettingsContext } from '../../../contexts';
import { setMonitor } from '../../../state/api';

import { SyntheticsMonitor } from '../../fleet_package/types';
import { SyntheticsMonitor } from '../../../../common/runtime_types';

interface Props {
monitor: SyntheticsMonitor;
Expand All @@ -26,6 +27,7 @@ interface Props {

export const ActionBar = ({ monitor, isValid, onSave }: Props) => {
const { monitorId } = useParams<{ monitorId: string }>();
const { basePath } = useContext(UptimeSettingsContext);

const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
const [isSaving, setIsSaving] = useState(false);
Expand Down Expand Up @@ -87,7 +89,12 @@ export const ActionBar = ({ monitor, isValid, onSave }: Props) => {
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" size="s" iconType="cross">
<EuiButtonEmpty
color="ghost"
size="s"
iconType="cross"
href={`${basePath}/app/uptime/manage-monitors`}
>
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
Expand All @@ -101,7 +108,7 @@ export const ActionBar = ({ monitor, isValid, onSave }: Props) => {
isLoading={isSaving}
disabled={hasBeenSubmitted && !isValid}
>
{monitorId ? EDIT_MONITOR_LABEL : SAVE_MONITOR_LABEL}
{monitorId ? UPDATE_MONITOR_LABEL : SAVE_MONITOR_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -119,8 +126,8 @@ const SAVE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.saveMo
defaultMessage: 'Save monitor',
});

const EDIT_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorLabel', {
defaultMessage: 'Edit monitor',
const UPDATE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.updateMonitorLabel', {
defaultMessage: 'Update monitor',
});

const VALIDATION_ERROR_LABEL = i18n.translate('xpack.uptime.monitorManagement.validationError', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@
*/

import React, { useMemo } from 'react';
import {
ConfigKey,
MonitorFields,
TLSFields,
PolicyConfig,
DataStream,
} from '../fleet_package/types';
import { ConfigKey, MonitorFields, TLSFields, DataStream } from '../../../common/runtime_types';
import { useTrackPageview } from '../../../../observability/public';
import { SyntheticsProviders } from '../fleet_package/contexts';
import { PolicyConfig } from '../fleet_package/types';
import { MonitorConfig } from './monitor_config/monitor_config';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useEffect, useRef, useState } from 'react';
import { ConfigKey, DataStream, Validation, MonitorFields } from '../../fleet_package/types';
import { ConfigKey, DataStream, MonitorFields } from '../../../../common/runtime_types';
import { Validation } from '../../../../common/types';

interface Props {
monitorType: DataStream;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,31 @@ describe('<ActionBar />', () => {
});

it('renders locations', () => {
render(<ServiceLocations selectedLocations={[]} setLocations={setLocations} />, { state });
render(
<ServiceLocations selectedLocations={[]} setLocations={setLocations} isInvalid={false} />,
{ state }
);

expect(screen.getByText(LOCATIONS_LABEL)).toBeInTheDocument();
expect(screen.queryByText('US Central')).not.toBeInTheDocument();
});

it('shows location options when clicked', async () => {
render(<ServiceLocations selectedLocations={[]} setLocations={setLocations} />, { state });
render(
<ServiceLocations selectedLocations={[]} setLocations={setLocations} isInvalid={false} />,
{ state }
);

userEvent.click(screen.getByRole('button'));

expect(screen.getByText('US Central')).toBeInTheDocument();
});

it('prevents bad inputs', async () => {
render(<ServiceLocations selectedLocations={[]} setLocations={setLocations} />, { state });
render(
<ServiceLocations selectedLocations={[]} setLocations={setLocations} isInvalid={false} />,
{ state }
);

userEvent.click(screen.getByRole('button'));
userEvent.type(screen.getByRole('textbox'), 'fake location');
Expand All @@ -75,11 +84,23 @@ describe('<ActionBar />', () => {
});

it('calls setLocations', async () => {
render(<ServiceLocations selectedLocations={[]} setLocations={setLocations} />, { state });
render(
<ServiceLocations selectedLocations={[]} setLocations={setLocations} isInvalid={false} />,
{ state }
);

userEvent.click(screen.getByRole('button'));
userEvent.click(screen.getByText('US Central'));

expect(setLocations).toBeCalledWith([location]);
});

it('shows invalid error', async () => {
render(
<ServiceLocations selectedLocations={[]} setLocations={setLocations} isInvalid={true} />,
{ state }
);

expect(screen.getByText('At least one service location must be specified')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { monitorManagementListSelector } from '../../../state/selectors';
import { ServiceLocation } from '../../../../common/runtime_types/monitor_management';
import { ServiceLocation } from '../../../../common/runtime_types';

interface Props {
selectedLocations: ServiceLocation[];
setLocations: React.Dispatch<React.SetStateAction<ServiceLocation[]>>;
isInvalid: boolean;
}

export const ServiceLocations = ({ selectedLocations, setLocations }: Props) => {
const [locationsInputRef, setLocationsInputRef] = useState<HTMLInputElement | null>(null);
export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid }: Props) => {
const [error, setError] = useState<string | null>(null);
const { locations } = useSelector(monitorManagementListSelector);

Expand All @@ -30,23 +30,25 @@ export const ServiceLocations = ({ selectedLocations, setLocations }: Props) =>
};

const onSearchChange = (value: string, hasMatchingOptions?: boolean) => {
setError(value.length === 0 || hasMatchingOptions ? null : `"${value}" is not a valid option`);
setError(value.length === 0 || hasMatchingOptions ? null : getInvalidOptionError(value));
};

const onBlur = () => {
if (locationsInputRef) {
const { value } = locationsInputRef;
setError(value.length === 0 ? null : `"${value}" is not a valid option`);
const onBlur = (event: unknown) => {
const inputElement = (event as FocusEvent)?.target as HTMLInputElement;
if (inputElement) {
const { value } = inputElement;
setError(value.length === 0 ? null : getInvalidOptionError(value));
}
};

const errorMessage = error ?? (isInvalid ? VALIDATION_ERROR : null);

return (
<EuiFormRow label={LOCATIONS_LABEL} error={error} isInvalid={error !== null}>
<EuiFormRow label={LOCATIONS_LABEL} error={errorMessage} isInvalid={errorMessage !== null}>
<EuiComboBox
placeholder={PLACEHOLDER_LABEL}
options={locations}
selectedOptions={selectedLocations}
inputRef={setLocationsInputRef}
onChange={onLocationChange}
onSearchChange={onSearchChange}
onBlur={onBlur}
Expand All @@ -59,10 +61,25 @@ export const ServiceLocations = ({ selectedLocations, setLocations }: Props) =>
const PLACEHOLDER_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.serviceLocationsPlaceholderLabel',
{
defaultMessage: 'Select one or locations to run your monitor.',
defaultMessage: 'Select one or more locations to run your monitor.',
}
);

const VALIDATION_ERROR = i18n.translate(
'xpack.uptime.monitorManagement.serviceLocationsValidationError',
{
defaultMessage: 'At least one service location must be specified',
}
);

const getInvalidOptionError = (value: string) =>
i18n.translate('xpack.uptime.monitorManagement.serviceLocationsOptionError', {
defaultMessage: '"{value}" is not a valid option',
values: {
value,
},
});

export const LOCATIONS_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.monitorLocationsLabel',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React from 'react';
import { defaultConfig, usePolicyConfigContext } from '../../fleet_package/contexts';

import { usePolicy } from '../../fleet_package/hooks/use_policy';
import { validate } from '../../fleet_package/validation';
import { validate } from '../validation';
import { ActionBar } from '../action_bar/action_bar';
import { useFormatMonitor } from '../hooks/use_format_monitor';
import { MonitorFields } from './monitor_fields';
Expand Down
Loading

0 comments on commit d69fd17

Please sign in to comment.