[Uptime] UI Monitor Management - Add namespace field (#123248)

* Add namespace field to monitor management monitor form

* Fix field id, add namespace validation and uptime-level default namespace constant

* Namespace format validation and unit tests

* Update integration test fixtures with namespace

* Update validation to use util provided by fleet package

* Reference locations from config enum

* Fix namespace data for tests

* [Uptime] Move monitor namespace field to advanced options section

* Add advanced field rendering test

* Add icmp advanced fields tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Emilio Alvarez Piñeiro 2022-01-29 00:06:41 +01:00 committed by GitHub
parent 110dc8b7cb
commit d66a7bf255
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 427 additions and 156 deletions

View file

@ -13,3 +13,4 @@ export * from './settings_defaults';
export { QUERY } from './query';
export * from './ui';
export * from './rest_api';
export const DEFAULT_NAMESPACE_STRING = 'default';

View file

@ -20,6 +20,7 @@ export enum ConfigKey {
METADATA = '__ui',
MONITOR_TYPE = 'type',
NAME = 'name',
NAMESPACE = 'namespace',
LOCATIONS = 'locations',
PARAMS = 'params',
PASSWORD = 'password',

View file

@ -50,6 +50,7 @@ export type ZipUrlTLSFields = t.TypeOf<typeof ZipUrlTLSFieldsCodec>;
// CommonFields
export const CommonFieldsCodec = t.interface({
[ConfigKey.NAME]: t.string,
[ConfigKey.NAMESPACE]: t.string,
[ConfigKey.MONITOR_TYPE]: DataStreamCodec,
[ConfigKey.ENABLED]: t.boolean,
[ConfigKey.SCHEDULE]: Schedule,

View file

@ -8,5 +8,9 @@
import { ConfigKey, MonitorFields } from '../runtime_types';
export type Validator = (config: Partial<MonitorFields>) => boolean;
export type NamespaceValidator = (config: Partial<MonitorFields>) => false | string;
export type Validation = Partial<Record<ConfigKey, Validator>>;
export type ConfigValidation = Omit<Record<ConfigKey, Validator>, ConfigKey.NAMESPACE> &
Record<ConfigKey.NAMESPACE, NamespaceValidator>;
export type Validation = Partial<ConfigValidation>;

View file

@ -37,16 +37,18 @@ describe('<BrowserAdvancedFields />', () => {
defaultValues = defaultConfig,
defaultSimpleFields = defaultBrowserSimpleFields,
validate = defaultValidation,
children,
}: {
defaultValues?: BrowserAdvancedFieldsType;
defaultSimpleFields?: BrowserSimpleFields;
validate?: Validation;
children?: React.ReactNode;
}) => {
return (
<IntlProvider locale="en">
<BrowserSimpleFieldsContextProvider defaultValues={defaultSimpleFields}>
<BrowserAdvancedFieldsContextProvider defaultValues={defaultValues}>
<BrowserAdvancedFields validate={validate} />
<BrowserAdvancedFields validate={validate}>{children}</BrowserAdvancedFields>
</BrowserAdvancedFieldsContextProvider>
</BrowserSimpleFieldsContextProvider>
</IntlProvider>
@ -96,4 +98,12 @@ describe('<BrowserAdvancedFields />', () => {
)
).toBeInTheDocument();
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
expect(upstream).toBeInTheDocument();
});
});

View file

@ -27,9 +27,10 @@ import { ThrottlingFields } from './throttling_fields';
interface Props {
validate: Validation;
children?: React.ReactNode;
}
export const BrowserAdvancedFields = memo<Props>(({ validate }) => {
export const BrowserAdvancedFields = memo<Props>(({ validate, children }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const { fields: simpleFields } = useBrowserSimpleFieldsContext();
@ -213,6 +214,7 @@ export const BrowserAdvancedFields = memo<Props>(({ validate }) => {
</EuiDescribedFormGroup>
<ThrottlingFields validate={validate} />
{children}
</EuiAccordion>
);
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DEFAULT_NAMESPACE_STRING } from '../../../../common/constants';
import { CommonFields, ConfigKey, ScheduleUnit, DataStream } from '../types';
export const defaultValues: CommonFields = {
@ -20,4 +21,5 @@ export const defaultValues: CommonFields = {
[ConfigKey.TIMEOUT]: '16',
[ConfigKey.NAME]: '',
[ConfigKey.LOCATIONS]: [],
[ConfigKey.NAMESPACE]: DEFAULT_NAMESPACE_STRING,
};

View file

@ -23,6 +23,7 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.APM_SERVICE_NAME]: null,
[ConfigKey.TAGS]: (fields) => arrayToJsonFormatter(fields[ConfigKey.TAGS]),
[ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT]),
[ConfigKey.NAMESPACE]: null,
};
export const arrayToJsonFormatter = (value: string[] = []) =>

View file

@ -8,6 +8,7 @@
import { CommonFields, ConfigKey } from '../types';
import { NewPackagePolicyInput } from '../../../../../fleet/common';
import { defaultValues as commonDefaultValues } from './default_values';
import { DEFAULT_NAMESPACE_STRING } from '../../../../common/constants';
// TO DO: create a standard input format that all fields resolve to
export type Normalizer = (fields: NewPackagePolicyInput['vars']) => unknown;
@ -79,4 +80,6 @@ export const commonNormalizers: CommonNormalizerMap = {
[ConfigKey.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKey.APM_SERVICE_NAME),
[ConfigKey.TAGS]: getCommonjsonToJavascriptNormalizer(ConfigKey.TAGS),
[ConfigKey.TIMEOUT]: getCommonCronToSecondsNormalizer(ConfigKey.TIMEOUT),
[ConfigKey.NAMESPACE]: (fields) =>
fields?.[ConfigKey.NAMESPACE]?.value ?? DEFAULT_NAMESPACE_STRING,
};

View file

@ -6,6 +6,7 @@
*/
import React, { createContext, useContext, useMemo, useState } from 'react';
import { DEFAULT_NAMESPACE_STRING } from '../../../../common/constants';
import { ScheduleUnit, ServiceLocations } from '../../../../common/runtime_types';
import { DataStream } from '../types';
@ -15,6 +16,7 @@ interface IPolicyConfigContext {
setLocations: React.Dispatch<React.SetStateAction<ServiceLocations>>;
setIsTLSEnabled: React.Dispatch<React.SetStateAction<boolean>>;
setIsZipUrlTLSEnabled: React.Dispatch<React.SetStateAction<boolean>>;
setNamespace: React.Dispatch<React.SetStateAction<string>>;
monitorType: DataStream;
defaultMonitorType: DataStream;
isTLSEnabled?: boolean;
@ -28,6 +30,8 @@ interface IPolicyConfigContext {
defaultLocations?: ServiceLocations;
locations?: ServiceLocations;
allowedScheduleUnits?: ScheduleUnit[];
defaultNamespace?: string;
namespace?: string;
}
export interface IPolicyConfigContextProvider {
@ -37,6 +41,7 @@ export interface IPolicyConfigContextProvider {
defaultIsZipUrlTLSEnabled?: boolean;
defaultName?: string;
defaultLocations?: ServiceLocations;
defaultNamespace?: string;
isEditable?: boolean;
isZipUrlSourceEnabled?: boolean;
allowedScheduleUnits?: ScheduleUnit[];
@ -62,6 +67,9 @@ const defaultContext: IPolicyConfigContext = {
'setIsZipUrlTLSEnabled was not initialized, set it when you invoke the context'
);
},
setNamespace: (_namespace: React.SetStateAction<string>) => {
throw new Error('setNamespace was not initialized, set it when you invoke the context');
},
monitorType: initialValue, // mutable
defaultMonitorType: initialValue, // immutable,
defaultIsTLSEnabled: false,
@ -71,6 +79,7 @@ const defaultContext: IPolicyConfigContext = {
isEditable: false,
isZipUrlSourceEnabled: true,
allowedScheduleUnits: [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS],
defaultNamespace: DEFAULT_NAMESPACE_STRING,
};
export const PolicyConfigContext = createContext(defaultContext);
@ -82,6 +91,7 @@ export function PolicyConfigContextProvider<ExtraFields = unknown>({
defaultIsZipUrlTLSEnabled = false,
defaultName = '',
defaultLocations = [],
defaultNamespace = DEFAULT_NAMESPACE_STRING,
isEditable = false,
isZipUrlSourceEnabled = true,
allowedScheduleUnits = [ScheduleUnit.MINUTES, ScheduleUnit.SECONDS],
@ -91,6 +101,7 @@ export function PolicyConfigContextProvider<ExtraFields = unknown>({
const [locations, setLocations] = useState<ServiceLocations>(defaultLocations);
const [isTLSEnabled, setIsTLSEnabled] = useState<boolean>(defaultIsTLSEnabled);
const [isZipUrlTLSEnabled, setIsZipUrlTLSEnabled] = useState<boolean>(defaultIsZipUrlTLSEnabled);
const [namespace, setNamespace] = useState<string>(defaultNamespace);
const value = useMemo(() => {
return {
@ -112,6 +123,8 @@ export function PolicyConfigContextProvider<ExtraFields = unknown>({
setLocations,
isZipUrlSourceEnabled,
allowedScheduleUnits,
namespace,
setNamespace,
} as IPolicyConfigContext;
}, [
monitorType,
@ -127,6 +140,7 @@ export function PolicyConfigContextProvider<ExtraFields = unknown>({
locations,
defaultLocations,
allowedScheduleUnits,
namespace,
]);
return <PolicyConfigContext.Provider value={value} children={children} />;

View file

@ -29,11 +29,13 @@ import { TCPAdvancedFields } from './tcp/advanced_fields';
import { ICMPSimpleFields } from './icmp/simple_fields';
import { BrowserSimpleFields } from './browser/simple_fields';
import { BrowserAdvancedFields } from './browser/advanced_fields';
import { ICMPAdvancedFields } from './icmp/advanced_fields';
interface Props {
validate: Validation;
dataStreams?: DataStream[];
children?: React.ReactNode;
appendAdvancedFields?: React.ReactNode;
}
const dataStreamToString = [
@ -51,154 +53,162 @@ const dataStreamToString = [
},
];
export const CustomFields = memo<Props>(({ validate, dataStreams = [], children }) => {
const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } =
usePolicyConfigContext();
export const CustomFields = memo<Props>(
({ validate, dataStreams = [], children, appendAdvancedFields }) => {
const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } =
usePolicyConfigContext();
const isHTTP = monitorType === DataStream.HTTP;
const isTCP = monitorType === DataStream.TCP;
const isBrowser = monitorType === DataStream.BROWSER;
const isHTTP = monitorType === DataStream.HTTP;
const isTCP = monitorType === DataStream.TCP;
const isBrowser = monitorType === DataStream.BROWSER;
const isICMP = monitorType === DataStream.ICMP;
const dataStreamOptions = useMemo(() => {
return dataStreamToString.filter((dataStream) => dataStreams.includes(dataStream.value));
}, [dataStreams]);
const dataStreamOptions = useMemo(() => {
return dataStreamToString.filter((dataStream) => dataStreams.includes(dataStream.value));
}, [dataStreams]);
const renderSimpleFields = (type: DataStream) => {
switch (type) {
case DataStream.HTTP:
return <HTTPSimpleFields validate={validate} />;
case DataStream.ICMP:
return <ICMPSimpleFields validate={validate} />;
case DataStream.TCP:
return <TCPSimpleFields validate={validate} />;
case DataStream.BROWSER:
return <BrowserSimpleFields validate={validate} />;
default:
return null;
}
};
const renderSimpleFields = (type: DataStream) => {
switch (type) {
case DataStream.HTTP:
return <HTTPSimpleFields validate={validate} />;
case DataStream.ICMP:
return <ICMPSimpleFields validate={validate} />;
case DataStream.TCP:
return <TCPSimpleFields validate={validate} />;
case DataStream.BROWSER:
return <BrowserSimpleFields validate={validate} />;
default:
return null;
}
};
const isWithInUptime = window.location.pathname.includes('/app/uptime');
const isWithInUptime = window.location.pathname.includes('/app/uptime');
return (
<EuiForm component="form">
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSectionTitle"
defaultMessage="Monitor settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSectionDescription"
defaultMessage="Configure your monitor with the following options."
/>
}
data-test-subj="monitorSettingsSection"
>
<EuiFlexGroup>
<EuiFlexItem>
{children}
{!isEditable && (
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType"
defaultMessage="Monitor Type"
/>
}
isInvalid={
!!validate[ConfigKey.MONITOR_TYPE]?.({
[ConfigKey.MONITOR_TYPE]: monitorType as DataStream,
})
}
error={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.error"
defaultMessage="Monitor type is required"
/>
}
>
<EuiSelect
options={dataStreamOptions}
value={monitorType}
onChange={(event) => setMonitorType(event.target.value as DataStream)}
data-test-subj="syntheticsMonitorTypeField"
/>
</EuiFormRow>
)}
<EuiSpacer size="s" />
{isBrowser && !isWithInUptime && (
<EuiCallOut
title={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.description"
defaultMessage='To create a "Browser" monitor, please ensure you are using the elastic-agent-complete Docker container, which contains the dependencies to run these monitors. For more information, please visit our {link}.'
values={{
link: (
<EuiLink
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/synthetics-quickstart-fleet.html"
external
>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.link"
defaultMessage="synthetics documentation"
/>
</EuiLink>
),
}}
/>
}
iconType="help"
size="s"
/>
)}
<EuiSpacer size="s" />
{renderSimpleFields(monitorType)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescribedFormGroup>
{(isHTTP || isTCP) && (
return (
<EuiForm component="form">
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.label"
defaultMessage="TLS settings"
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSectionTitle"
defaultMessage="Monitor settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.description"
defaultMessage="Configure TLS options, including verification mode, certificate authorities, and client certificates."
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSectionDescription"
defaultMessage="Configure your monitor with the following options."
/>
}
id="uptimeFleetIsTLSEnabled"
data-test-subj="monitorSettingsSection"
>
<EuiSwitch
id="uptimeFleetIsTLSEnabled"
data-test-subj="syntheticsIsTLSEnabled"
checked={!!isTLSEnabled}
label={
<EuiFlexGroup>
<EuiFlexItem>
{children}
{!isEditable && (
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType"
defaultMessage="Monitor Type"
/>
}
isInvalid={
!!validate[ConfigKey.MONITOR_TYPE]?.({
[ConfigKey.MONITOR_TYPE]: monitorType as DataStream,
})
}
error={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.error"
defaultMessage="Monitor type is required"
/>
}
>
<EuiSelect
options={dataStreamOptions}
value={monitorType}
onChange={(event) => setMonitorType(event.target.value as DataStream)}
data-test-subj="syntheticsMonitorTypeField"
/>
</EuiFormRow>
)}
<EuiSpacer size="s" />
{isBrowser && !isWithInUptime && (
<EuiCallOut
title={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.description"
defaultMessage='To create a "Browser" monitor, please ensure you are using the elastic-agent-complete Docker container, which contains the dependencies to run these monitors. For more information, please visit our {link}.'
values={{
link: (
<EuiLink
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/synthetics-quickstart-fleet.html"
external
>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.monitorType.browser.warning.link"
defaultMessage="synthetics documentation"
/>
</EuiLink>
),
}}
/>
}
iconType="help"
size="s"
/>
)}
<EuiSpacer size="s" />
{renderSimpleFields(monitorType)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescribedFormGroup>
{(isHTTP || isTCP) && (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.label"
defaultMessage="TLS settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.certificateSettings.enableSSLSettings.label"
defaultMessage="Enable TLS configuration"
id="xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.description"
defaultMessage="Configure TLS options, including verification mode, certificate authorities, and client certificates."
/>
}
onChange={(event) => setIsTLSEnabled(event.target.checked)}
/>
<TLSFields />
</EuiDescribedFormGroup>
)}
<EuiSpacer size="m" />
{isHTTP && <HTTPAdvancedFields validate={validate} />}
{isTCP && <TCPAdvancedFields />}
{isBrowser && <BrowserAdvancedFields validate={validate} />}
</EuiForm>
);
});
id="uptimeFleetIsTLSEnabled"
>
<EuiSwitch
id="uptimeFleetIsTLSEnabled"
data-test-subj="syntheticsIsTLSEnabled"
checked={!!isTLSEnabled}
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.certificateSettings.enableSSLSettings.label"
defaultMessage="Enable TLS configuration"
/>
}
onChange={(event) => setIsTLSEnabled(event.target.checked)}
/>
<TLSFields />
</EuiDescribedFormGroup>
)}
<EuiSpacer size="m" />
{isHTTP && (
<HTTPAdvancedFields validate={validate}>{appendAdvancedFields}</HTTPAdvancedFields>
)}
{isTCP && <TCPAdvancedFields>{appendAdvancedFields}</TCPAdvancedFields>}
{isBrowser && (
<BrowserAdvancedFields validate={validate}>{appendAdvancedFields}</BrowserAdvancedFields>
)}
{isICMP && <ICMPAdvancedFields>{appendAdvancedFields}</ICMPAdvancedFields>}
</EuiForm>
);
}
);

View file

@ -60,6 +60,7 @@ export const usePolicy = (fleetPolicyName: string = '') => {
isZipUrlTLSEnabled,
name: monitorName, // the monitor name can come from two different places, either from fleet or from uptime
locations,
namespace,
} = usePolicyConfigContext();
const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext();
const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext();
@ -91,6 +92,7 @@ export const usePolicy = (fleetPolicyName: string = '') => {
},
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as HTTPFields,
[DataStream.TCP]: {
...tcpSimpleFields,
@ -102,11 +104,13 @@ export const usePolicy = (fleetPolicyName: string = '') => {
},
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as TCPFields,
[DataStream.ICMP]: {
...icmpSimpleFields,
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as ICMPFields,
[DataStream.BROWSER]: {
...browserSimpleFields,
@ -117,6 +121,7 @@ export const usePolicy = (fleetPolicyName: string = '') => {
},
[ConfigKey.NAME]: fleetPolicyName || monitorName,
[ConfigKey.LOCATIONS]: locations,
[ConfigKey.NAMESPACE]: namespace,
} as BrowserFields,
}),
[
@ -132,6 +137,7 @@ export const usePolicy = (fleetPolicyName: string = '') => {
fleetPolicyName,
monitorName,
locations,
namespace,
]
);

View file

@ -49,13 +49,15 @@ describe('<HTTPAdvancedFields />', () => {
const WrappedComponent = ({
defaultValues,
validate = defaultValidation,
children,
}: {
defaultValues?: HTTPAdvancedFieldsType;
validate?: Validation;
children?: React.ReactNode;
}) => {
return (
<HTTPAdvancedFieldsContextProvider defaultValues={defaultValues}>
<HTTPAdvancedFields validate={validate} />
<HTTPAdvancedFields validate={validate}>{children}</HTTPAdvancedFields>
</HTTPAdvancedFieldsContextProvider>
);
};
@ -126,4 +128,12 @@ describe('<HTTPAdvancedFields />', () => {
expect(indexResponseBody.checked).toBe(false);
expect(indexResponseHeaders.checked).toBe(false);
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
expect(upstream).toBeInTheDocument();
});
});

View file

@ -32,9 +32,10 @@ import { ComboBox } from '../combo_box';
interface Props {
validate: Validation;
children?: React.ReactNode;
}
export const HTTPAdvancedFields = memo<Props>(({ validate }) => {
export const HTTPAdvancedFields = memo<Props>(({ validate, children }) => {
const { fields, setFields } = useHTTPAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
@ -461,6 +462,7 @@ export const HTTPAdvancedFields = memo<Props>(({ validate }) => {
/>
</EuiFormRow>
</EuiDescribedFormGroup>
{children}
</EuiAccordion>
);
});

View file

@ -0,0 +1,33 @@
/*
* 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 from 'react';
import { render } from '../../../lib/helper/rtl_helpers';
import { ICMPAdvancedFields } from './advanced_fields';
// ensures fields and labels map appropriately
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
describe('<ICMPAdvancedFields />', () => {
const WrappedComponent = ({ children }: { children?: React.ReactNode }) => (
<ICMPAdvancedFields>{children}</ICMPAdvancedFields>
);
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText, getByTestId } = render(
<WrappedComponent>{upstreamFieldsText}</WrappedComponent>
);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
const accordion = getByTestId('syntheticsICMPAdvancedFieldsAccordion') as HTMLInputElement;
expect(upstream).toBeInTheDocument();
expect(accordion).toBeInTheDocument();
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { EuiAccordion, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
export const ICMPAdvancedFields = ({ children }: { children?: React.ReactNode }) => {
if (!!children) {
return (
<EuiAccordion
id="uptimeFleetIcmpAdvancedOptions"
buttonContent={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.icmpAdvancedOptions"
defaultMessage="Advanced ICMP options"
/>
}
data-test-subj="syntheticsICMPAdvancedFieldsAccordion"
>
<EuiSpacer size="xl" />
{children}
</EuiAccordion>
);
}
return <></>;
};

View file

@ -23,12 +23,14 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
describe('<TCPAdvancedFields />', () => {
const WrappedComponent = ({
defaultValues = defaultConfig,
children,
}: {
defaultValues?: TCPAdvancedFieldsType;
children?: React.ReactNode;
}) => {
return (
<TCPAdvancedFieldsContextProvider defaultValues={defaultValues}>
<TCPAdvancedFields />
<TCPAdvancedFields>{children}</TCPAdvancedFields>
</TCPAdvancedFieldsContextProvider>
);
};
@ -68,4 +70,12 @@ describe('<TCPAdvancedFields />', () => {
expect(getByLabelText('Resolve hostnames locally')).toBeInTheDocument();
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);
const upstream = getByText(upstreamFieldsText) as HTMLInputElement;
expect(upstream).toBeInTheDocument();
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiAccordion,
@ -22,7 +22,11 @@ import { ConfigKey } from '../types';
import { OptionalLabel } from '../optional_label';
export const TCPAdvancedFields = () => {
interface Props {
children?: React.ReactNode;
}
export const TCPAdvancedFields = memo<Props>(({ children }) => {
const { fields, setFields } = useTCPAdvancedFieldsContext();
const handleInputChange = useCallback(
@ -176,6 +180,7 @@ export const TCPAdvancedFields = () => {
/>
</EuiFormRow>
</EuiDescribedFormGroup>
{children}
</EuiAccordion>
);
};
});

View file

@ -17,6 +17,7 @@ import { useTrackPageview } from '../../../../observability/public';
import { SyntheticsProviders } from '../fleet_package/contexts';
import { PolicyConfig } from '../fleet_package/types';
import { MonitorConfig } from './monitor_config/monitor_config';
import { DEFAULT_NAMESPACE_STRING } from '../../../common/constants';
interface Props {
monitor: MonitorFields;
@ -74,8 +75,9 @@ export const EditMonitorConfig = ({ monitor }: Props) => {
defaultIsTLSEnabled: isTLSEnabled,
defaultIsZipUrlTLSEnabled: isZipUrlTLSEnabled,
defaultMonitorType: monitorType,
defaultName: defaultConfig?.name || '', // TODO - figure out typing concerns for name
defaultLocations: defaultConfig.locations,
defaultName: defaultConfig?.[ConfigKey.NAME] || '', // TODO - figure out typing concerns for name
defaultNamespace: defaultConfig?.[ConfigKey.NAMESPACE] || DEFAULT_NAMESPACE_STRING,
defaultLocations: defaultConfig[ConfigKey.LOCATIONS],
isEditable: true,
isZipUrlSourceEnabled: false,
allowedScheduleUnits: [ScheduleUnit.MINUTES],

View file

@ -0,0 +1,88 @@
/*
* 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, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiSpacer, EuiDescribedFormGroup, EuiLink, EuiFieldText } from '@elastic/eui';
import type { Validation } from '../../../../common/types/index';
import { ConfigKey } from '../../../../common/runtime_types/monitor_management';
import { usePolicyConfigContext } from '../../fleet_package/contexts';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
interface Props {
validate: Validation;
}
export const MonitorManagementAdvancedFields = memo<Props>(({ validate }) => {
const { namespace, setNamespace } = usePolicyConfigContext();
const namespaceErrorMsg = validate[ConfigKey.NAMESPACE]?.({
[ConfigKey.NAMESPACE]: namespace,
});
const isNamespaceInvalid = !!namespaceErrorMsg;
const { services } = useKibana();
return (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.dataStreamConfiguration.title"
defaultMessage="Data stream settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.dataStreamConfiguration.description"
defaultMessage="Configure additional Data Stream options."
/>
}
data-test-subj="monitorAdvancedFieldsSection"
>
<EuiSpacer size="s" />
<EuiFormRow
isInvalid={isNamespaceInvalid}
error={namespaceErrorMsg}
label={
<FormattedMessage
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.monitorNamespaceFieldLabel"
defaultMessage="Namespace"
/>
}
helpText={
<FormattedMessage
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.namespaceHelpLabel"
defaultMessage="Change the default namespace. This setting changes the name of the monitor's data stream. {learnMore}."
values={{
learnMore: (
<EuiLink
target="_blank"
href={services.docLinks?.links?.fleet?.datastreamsNamingScheme}
external
>
<FormattedMessage
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.namespaceHelpLearnMoreLabel"
defaultMessage="Learn More"
/>
</EuiLink>
),
}}
/>
}
>
<EuiFieldText
defaultValue={namespace}
onChange={(event) => setNamespace(event.target.value)}
required={true}
isInvalid={isNamespaceInvalid}
fullWidth={true}
name="namespace"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
});

View file

@ -13,6 +13,7 @@ import { usePolicyConfigContext } from '../../fleet_package/contexts';
import { CustomFields } from '../../fleet_package/custom_fields';
import { validate } from '../validation';
import { MonitorNameAndLocation } from './monitor_name_location';
import { MonitorManagementAdvancedFields } from './monitor_advanced_fields';
export const MonitorFields = () => {
const { monitorType } = usePolicyConfigContext();
@ -21,6 +22,7 @@ export const MonitorFields = () => {
<CustomFields
validate={validate[monitorType]}
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER]}
appendAdvancedFields={<MonitorManagementAdvancedFields validate={validate[monitorType]} />}
>
<MonitorNameAndLocation validate={validate[monitorType]} />
</CustomFields>

View file

@ -13,7 +13,7 @@ import {
ScheduleUnit,
ServiceLocations,
} from '../../../common/runtime_types';
import { validate } from './validation';
import { validate, validateCommon } from './validation';
describe('[Monitor Management] validation', () => {
const commonPropsValid: Partial<MonitorFields> = {
@ -27,8 +27,29 @@ describe('[Monitor Management] validation', () => {
label: 'EU West',
},
] as ServiceLocations,
[ConfigKey.NAME]: 'test-name',
[ConfigKey.NAMESPACE]: 'namespace',
};
describe('Common monitor fields', () => {
it('should return false for all valid props', () => {
const result = Object.values(validateCommon).map((validator) => {
return validator ? validator(commonPropsValid) : true;
});
expect(result.reduce((previous, current) => previous || current)).toBeFalsy();
});
it('should invalidate on invalid namespace', () => {
const validatorFn = validateCommon[ConfigKey.NAMESPACE];
const result = [undefined, null, '', '*/&<>:', 'A', 'a'.repeat(101)].map((testValue) =>
validatorFn?.({ [ConfigKey.NAMESPACE]: testValue } as Partial<MonitorFields>)
);
expect(result.reduce((previous, current) => previous && current)).toBeTruthy();
});
});
describe('HTTP', () => {
const httpPropsValid: Partial<HTTPFields> = {
...commonPropsValid,

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isValidNamespace } from '../../../../fleet/common';
import {
ConfigKey,
DataStream,
@ -11,13 +12,11 @@ import {
MonitorFields,
isServiceLocationInvalid,
} from '../../../common/runtime_types';
import { Validation, Validator } from '../../../common/types';
import { Validation } from '../../../common/types';
export const digitsOnly = /^[0-9]*$/g;
export const includesValidPort = /[^\:]+:[0-9]{1,5}$/g;
type ValidationLibrary = Record<string, Validator>;
// returns true if invalid
function validateHeaders<T>(headers: T): boolean {
return Object.keys(headers).some((key) => {
@ -56,7 +55,7 @@ const validateTimeout = ({
};
// validation functions return true when invalid
const validateCommon: ValidationLibrary = {
export const validateCommon: Validation = {
[ConfigKey.NAME]: ({ [ConfigKey.NAME]: value }) => {
return !value || typeof value !== 'string';
},
@ -83,9 +82,13 @@ const validateCommon: ValidationLibrary = {
!Array.isArray(locations) || locations.length < 1 || locations.some(isServiceLocationInvalid)
);
},
[ConfigKey.NAMESPACE]: ({ [ConfigKey.NAMESPACE]: value }) => {
const { error = '', valid } = isValidNamespace(value ?? '');
return valid ? false : error;
},
};
const validateHTTP: ValidationLibrary = {
const validateHTTP: Validation = {
[ConfigKey.RESPONSE_STATUS_CHECK]: ({ [ConfigKey.RESPONSE_STATUS_CHECK]: value }) => {
const statusCodes = value as MonitorFields[ConfigKey.RESPONSE_STATUS_CHECK];
return statusCodes.length ? statusCodes.some((code) => !`${code}`.match(digitsOnly)) : false;
@ -105,14 +108,14 @@ const validateHTTP: ValidationLibrary = {
...validateCommon,
};
const validateTCP: Record<string, Validator> = {
const validateTCP: Validation = {
[ConfigKey.HOSTS]: ({ [ConfigKey.HOSTS]: value }) => {
return !value || !`${value}`.match(includesValidPort);
},
...validateCommon,
};
const validateICMP: ValidationLibrary = {
const validateICMP: Validation = {
[ConfigKey.HOSTS]: ({ [ConfigKey.HOSTS]: value }) => !value,
[ConfigKey.WAIT]: ({ [ConfigKey.WAIT]: value }) =>
!!value &&
@ -127,7 +130,7 @@ const validateThrottleValue = (speed: string | undefined, allowZero?: boolean) =
return isNaN(throttleValue) || (allowZero ? throttleValue < 0 : throttleValue <= 0);
};
const validateBrowser: ValidationLibrary = {
const validateBrowser: Validation = {
...validateCommon,
[ConfigKey.SOURCE_ZIP_URL]: ({
[ConfigKey.SOURCE_ZIP_URL]: zipUrl,

View file

@ -24,6 +24,7 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.APM_SERVICE_NAME]: null,
[ConfigKey.TAGS]: (fields) => arrayFormatter(fields[ConfigKey.TAGS]),
[ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT]),
[ConfigKey.NAMESPACE]: null,
};
export const arrayFormatter = (value: string[] = []) => (value.length ? value : null);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DEFAULT_NAMESPACE_STRING } from '../../../../common/constants';
import { DataStream, MonitorFields } from '../../../../common/runtime_types';
interface DataStreamConfig {
@ -33,7 +34,7 @@ export function convertToDataStreamFormat(monitor: Record<string, any>): DataStr
schedule: monitor.schedule,
enabled: monitor.enabled ?? true,
data_stream: {
namespace: monitor.namespace ?? 'default',
namespace: monitor.namespace ?? DEFAULT_NAMESPACE_STRING,
},
streams: [
{

View file

@ -71,6 +71,7 @@ describe('validateMonitor', () => {
url: 'https://test-url.com',
},
],
[ConfigKey.NAMESPACE]: 'testnamespace',
};
testMetaData = {
is_tls_enabled: false,
@ -425,6 +426,7 @@ function getJsonPayload() {
' "TLSv1.2"' +
' ],' +
' "name": "test-monitor-name",' +
' "namespace": "testnamespace",' +
' "locations": [{' +
' "id": "eu-west-01",' +
' "label": "Europe West",' +

View file

@ -37,5 +37,6 @@
"throttling.latency": "20",
"throttling.config": "5d/3u/20l",
"locations": [],
"name": "Test HTTP Monitor 03"
"name": "Test HTTP Monitor 03",
"namespace": "testnamespace"
}

View file

@ -57,5 +57,6 @@
"lon": 73.2342343434
},
"url": "https://example-url.com"
}]
}],
"namespace": "testnamespace"
}

View file

@ -31,5 +31,6 @@
"TLSv1.2",
"TLSv1.3"
],
"name": "Test HTTP Monitor 04"
"name": "Test HTTP Monitor 04",
"namespace": "testnamespace"
}

View file

@ -27,5 +27,6 @@
"TLSv1.1",
"TLSv1.3"
],
"name": "Test HTTP Monitor 04"
"name": "Test HTTP Monitor 04",
"namespace": "testnamespace"
}