[Uptime][Monitor Management] Show form validation errors only after user interaction (#126116)(uptime/issues/456)

* Expose `onBlur` and `onFieldBlur` on fleet components.

* Only pass down validators if either field is interacted with or form submit attempt has been made.

uptime/issues/456
This commit is contained in:
Abdul Wahab Zahid 2022-02-23 18:03:29 +01:00 committed by GitHub
parent 4bffe73187
commit 20dc80fc36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1290 additions and 694 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { fireEvent } from '@testing-library/react';
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
@ -38,17 +39,21 @@ describe('<BrowserAdvancedFields />', () => {
defaultSimpleFields = defaultBrowserSimpleFields,
validate = defaultValidation,
children,
onFieldBlur,
}: {
defaultValues?: BrowserAdvancedFieldsType;
defaultSimpleFields?: BrowserSimpleFields;
validate?: Validation;
children?: React.ReactNode;
onFieldBlur?: (field: ConfigKey) => void;
}) => {
return (
<IntlProvider locale="en">
<BrowserSimpleFieldsContextProvider defaultValues={defaultSimpleFields}>
<BrowserAdvancedFieldsContextProvider defaultValues={defaultValues}>
<BrowserAdvancedFields validate={validate}>{children}</BrowserAdvancedFields>
<BrowserAdvancedFields validate={validate} onFieldBlur={onFieldBlur}>
{children}
</BrowserAdvancedFields>
</BrowserAdvancedFieldsContextProvider>
</BrowserSimpleFieldsContextProvider>
</IntlProvider>
@ -72,6 +77,16 @@ describe('<BrowserAdvancedFields />', () => {
userEvent.selectOptions(screenshots, ['off']);
expect(screenshots.value).toEqual('off');
});
it('calls onFieldBlur after change', () => {
const onFieldBlur = jest.fn();
const { getByLabelText } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const screenshots = getByLabelText('Screenshot options') as HTMLInputElement;
userEvent.selectOptions(screenshots, ['off']);
fireEvent.blur(screenshots);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.SCREENSHOTS);
});
});
it('only displayed filter options when zip url is truthy', () => {

View file

@ -29,198 +29,212 @@ interface Props {
validate: Validation;
children?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const BrowserAdvancedFields = memo<Props>(({ validate, children, minColumnWidth }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const { fields: simpleFields } = useBrowserSimpleFieldsContext();
export const BrowserAdvancedFields = memo<Props>(
({ validate, children, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const { fields: simpleFields } = useBrowserSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<EuiAccordion
id="syntheticsIntegrationBrowserAdvancedOptions"
buttonContent="Advanced Browser options"
data-test-subj="syntheticsBrowserAdvancedFieldsAccordion"
>
<EuiSpacer size="m" />
{simpleFields[ConfigKey.SOURCE_ZIP_URL] && (
return (
<EuiAccordion
id="syntheticsIntegrationBrowserAdvancedOptions"
buttonContent="Advanced Browser options"
data-test-subj="syntheticsBrowserAdvancedFieldsAccordion"
>
<EuiSpacer size="m" />
{simpleFields[ConfigKey.SOURCE_ZIP_URL] && (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.filtering.title"
defaultMessage="Selective tests"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.filtering.description"
defaultMessage="Use these options to apply the selected monitor settings to a subset of the tests in your suite. Only the configured subset will be run by this monitor."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersMatch.label"
defaultMessage="Filter match"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersMatch.helpText"
defaultMessage="Run only journeys with a name that matches the provided glob with this monitor."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.JOURNEY_FILTERS_MATCH]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.JOURNEY_FILTERS_MATCH,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.JOURNEY_FILTERS_MATCH)}
data-test-subj="syntheticsBrowserJourneyFiltersMatch"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersTags.label"
defaultMessage="Filter tags"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersTags.helpText"
defaultMessage="Run only journeys with the given tags with this monitor."
/>
}
>
<ComboBox
selectedOptions={fields[ConfigKey.JOURNEY_FILTERS_TAGS]}
onChange={(value) =>
handleInputChange({ value, configKey: ConfigKey.JOURNEY_FILTERS_TAGS })
}
onBlur={() => onFieldBlur?.(ConfigKey.JOURNEY_FILTERS_TAGS)}
data-test-subj="syntheticsBrowserJourneyFiltersTags"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
)}
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.filtering.title"
defaultMessage="Selective tests"
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.title"
defaultMessage="Synthetics agent options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.filtering.description"
defaultMessage="Use these options to apply the selected monitor settings to a subset of the tests in your suite. Only the configured subset will be run by this monitor."
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.description"
defaultMessage="Provide fine-tuned configuration for the synthetics agent."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.ignoreHttpsErrors.helpText"
defaultMessage="Set this option to true to disable TLS/SSL validation in the synthetics browser. This is useful for testing sites that use self-signed certs."
/>
</>
}
data-test-subj="syntheticsBrowserIgnoreHttpsErrors"
>
<EuiCheckbox
id="syntheticsBrowserIgnoreHttpsErrorsCheckbox"
checked={fields[ConfigKey.IGNORE_HTTPS_ERRORS]}
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.ignoreHttpsErrors.label"
defaultMessage="Ignore HTTPS errors"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.IGNORE_HTTPS_ERRORS,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.IGNORE_HTTPS_ERRORS)}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersMatch.label"
defaultMessage="Filter match"
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.screenshots.label"
defaultMessage="Screenshot options"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersMatch.helpText"
defaultMessage="Run only journeys with a name that matches the provided glob with this monitor."
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.screenshots.helpText"
defaultMessage="Set this option to manage the screenshots captured by the synthetics agent."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.JOURNEY_FILTERS_MATCH]}
<EuiSelect
options={requestMethodOptions}
value={fields[ConfigKey.SCREENSHOTS]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.JOURNEY_FILTERS_MATCH,
configKey: ConfigKey.SCREENSHOTS,
})
}
data-test-subj="syntheticsBrowserJourneyFiltersMatch"
onBlur={() => onFieldBlur?.(ConfigKey.SCREENSHOTS)}
data-test-subj="syntheticsBrowserScreenshots"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersTags.label"
defaultMessage="Filter tags"
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.syntheticsArgs.label"
defaultMessage="Synthetics args"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.journeyFiltersTags.helpText"
defaultMessage="Run only journeys with the given tags with this monitor."
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.syntheticsArgs.helpText"
defaultMessage="Extra arguments to pass to the synthetics agent package. Takes a list of strings. This is useful in rare scenarios, and should not ordinarily need to be set."
/>
}
>
<ComboBox
selectedOptions={fields[ConfigKey.JOURNEY_FILTERS_TAGS]}
selectedOptions={fields[ConfigKey.SYNTHETICS_ARGS]}
onChange={(value) =>
handleInputChange({ value, configKey: ConfigKey.JOURNEY_FILTERS_TAGS })
handleInputChange({ value, configKey: ConfigKey.SYNTHETICS_ARGS })
}
data-test-subj="syntheticsBrowserJourneyFiltersTags"
onBlur={() => onFieldBlur?.(ConfigKey.SYNTHETICS_ARGS)}
data-test-subj="syntheticsBrowserSyntheticsArgs"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
)}
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.title"
defaultMessage="Synthetics agent options"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.description"
defaultMessage="Provide fine-tuned configuration for the synthetics agent."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.ignoreHttpsErrors.helpText"
defaultMessage="Set this option to true to disable TLS/SSL validation in the synthetics browser. This is useful for testing sites that use self-signed certs."
/>
</>
}
data-test-subj="syntheticsBrowserIgnoreHttpsErrors"
>
<EuiCheckbox
id="syntheticsBrowserIgnoreHttpsErrorsCheckbox"
checked={fields[ConfigKey.IGNORE_HTTPS_ERRORS]}
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.ignoreHttpsErrors.label"
defaultMessage="Ignore HTTPS errors"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.IGNORE_HTTPS_ERRORS,
})
}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.screenshots.label"
defaultMessage="Screenshot options"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.screenshots.helpText"
defaultMessage="Set this option to manage the screenshots captured by the synthetics agent."
/>
}
>
<EuiSelect
options={requestMethodOptions}
value={fields[ConfigKey.SCREENSHOTS]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.SCREENSHOTS,
})
}
data-test-subj="syntheticsBrowserScreenshots"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.syntheticsArgs.label"
defaultMessage="Synthetics args"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.browserAdvancedSettings.syntheticsArgs.helpText"
defaultMessage="Extra arguments to pass to the synthetics agent package. Takes a list of strings. This is useful in rare scenarios, and should not ordinarily need to be set."
/>
}
>
<ComboBox
selectedOptions={fields[ConfigKey.SYNTHETICS_ARGS]}
onChange={(value) => handleInputChange({ value, configKey: ConfigKey.SYNTHETICS_ARGS })}
data-test-subj="syntheticsBrowserSyntheticsArgs"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<ThrottlingFields validate={validate} minColumnWidth={minColumnWidth} />
{children}
</EuiAccordion>
);
});
<ThrottlingFields
validate={validate}
minColumnWidth={minColumnWidth}
onFieldBlur={onFieldBlur}
/>
{children}
</EuiAccordion>
);
}
);
const requestMethodOptions = Object.values(ScreenshotOption).map((option) => ({
value: option,

View file

@ -17,9 +17,10 @@ import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const BrowserSimpleFields = memo<Props>(({ validate }) => {
export const BrowserSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields, defaultValues } = useBrowserSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
@ -61,7 +62,12 @@ export const BrowserSimpleFields = memo<Props>(({ validate }) => {
);
return (
<SimpleFieldsWrapper fields={fields} validate={validate} onInputChange={handleInputChange}>
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
id="syntheticsFleetScheduleField--number syntheticsFleetScheduleField--unit"
label={
@ -85,6 +91,7 @@ export const BrowserSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
@ -99,6 +106,7 @@ export const BrowserSimpleFields = memo<Props>(({ validate }) => {
>
<SourceField
onChange={onChangeSourceField}
onFieldBlur={onFieldBlur}
defaultConfig={useMemo(
() => ({
zipUrl: defaultValues[ConfigKey.SOURCE_ZIP_URL],

View file

@ -8,6 +8,7 @@ import 'jest-canvas-mock';
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { ConfigKey } from '../../../../common/runtime_types';
import { render } from '../../../lib/helper/rtl_helpers';
import { IPolicyConfigContextProvider } from '../contexts/policy_config_context';
import { SourceField, defaultValues } from './source_field';
@ -42,6 +43,7 @@ jest.mock('../../../../../../../src/plugins/kibana_react/public', () => {
});
const onChange = jest.fn();
const onBlur = jest.fn();
describe('<SourceField />', () => {
const WrappedComponent = ({
@ -50,7 +52,7 @@ describe('<SourceField />', () => {
return (
<PolicyConfigContextProvider isZipUrlSourceEnabled={isZipUrlSourceEnabled}>
<BrowserSimpleFieldsContextProvider>
<SourceField onChange={onChange} />
<SourceField onChange={onChange} onFieldBlur={onBlur} />
</BrowserSimpleFieldsContextProvider>
</PolicyConfigContextProvider>
);
@ -72,6 +74,16 @@ describe('<SourceField />', () => {
});
});
it('calls onBlur', () => {
render(<WrappedComponent />);
const zipUrlField = screen.getByTestId('syntheticsBrowserZipUrl');
fireEvent.click(zipUrlField);
fireEvent.blur(zipUrlField);
expect(onBlur).toBeCalledWith(ConfigKey.SOURCE_ZIP_URL);
});
it('shows ZipUrl source type by default', async () => {
render(<WrappedComponent />);

View file

@ -24,7 +24,7 @@ import { OptionalLabel } from '../optional_label';
import { CodeEditor } from '../code_editor';
import { ScriptRecorderFields } from './script_recorder_fields';
import { ZipUrlTLSFields } from './zip_url_tls_fields';
import { MonacoEditorLangId } from '../types';
import { ConfigKey, MonacoEditorLangId } from '../types';
enum SourceType {
INLINE = 'syntheticsBrowserInlineConfig',
@ -46,6 +46,7 @@ interface SourceConfig {
interface Props {
onChange: (sourceConfig: SourceConfig) => void;
onFieldBlur: (field: ConfigKey) => void;
defaultConfig?: SourceConfig;
}
@ -71,7 +72,7 @@ const getDefaultTab = (defaultConfig: SourceConfig, isZipUrlSourceEnabled = true
return isZipUrlSourceEnabled ? SourceType.ZIP : SourceType.INLINE;
};
export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) => {
export const SourceField = ({ onChange, onFieldBlur, defaultConfig = defaultValues }: Props) => {
const { isZipUrlSourceEnabled } = usePolicyConfigContext();
const [sourceType, setSourceType] = useState<SourceType>(
getDefaultTab(defaultConfig, isZipUrlSourceEnabled)
@ -118,6 +119,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
onChange={({ target: { value } }) =>
setConfig((prevConfig) => ({ ...prevConfig, zipUrl: value }))
}
onBlur={() => onFieldBlur(ConfigKey.SOURCE_ZIP_URL)}
value={config.zipUrl}
data-test-subj="syntheticsBrowserZipUrl"
/>
@ -142,6 +144,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
onChange={({ target: { value } }) =>
setConfig((prevConfig) => ({ ...prevConfig, proxyUrl: value }))
}
onBlur={() => onFieldBlur(ConfigKey.SOURCE_ZIP_PROXY_URL)}
value={config.proxyUrl}
data-test-subj="syntheticsBrowserZipUrlProxy"
/>
@ -165,6 +168,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
onChange={({ target: { value } }) =>
setConfig((prevConfig) => ({ ...prevConfig, folder: value }))
}
onBlur={() => onFieldBlur(ConfigKey.SOURCE_ZIP_FOLDER)}
value={config.folder}
data-test-subj="syntheticsBrowserZipUrlFolder"
/>
@ -193,7 +197,10 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
)}
id="jsonParamsEditor"
languageId={MonacoEditorLangId.JSON}
onChange={(code) => setConfig((prevConfig) => ({ ...prevConfig, params: code }))}
onChange={(code) => {
setConfig((prevConfig) => ({ ...prevConfig, params: code }));
onFieldBlur(ConfigKey.PARAMS);
}}
value={config.params}
data-test-subj="syntheticsBrowserZipUrlParams"
/>
@ -217,6 +224,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
onChange={({ target: { value } }) =>
setConfig((prevConfig) => ({ ...prevConfig, username: value }))
}
onBlur={() => onFieldBlur(ConfigKey.SOURCE_ZIP_USERNAME)}
value={config.username}
data-test-subj="syntheticsBrowserZipUrlUsername"
/>
@ -240,6 +248,7 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
onChange={({ target: { value } }) =>
setConfig((prevConfig) => ({ ...prevConfig, password: value }))
}
onBlur={() => onFieldBlur(ConfigKey.SOURCE_ZIP_PASSWORD)}
value={config.password}
data-test-subj="syntheticsBrowserZipUrlPassword"
/>
@ -281,7 +290,10 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props)
)}
id="javascript"
languageId={MonacoEditorLangId.JAVASCRIPT}
onChange={(code) => setConfig((prevConfig) => ({ ...prevConfig, inlineScript: code }))}
onChange={(code) => {
setConfig((prevConfig) => ({ ...prevConfig, inlineScript: code }));
onFieldBlur(ConfigKey.SOURCE_INLINE);
}}
value={config.inlineScript}
/>
</EuiFormRow>

View file

@ -5,11 +5,18 @@
* 2.0.
*/
import { fireEvent } from '@testing-library/react';
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import { ThrottlingFields } from './throttling_fields';
import { DataStream, BrowserAdvancedFields, BrowserSimpleFields, Validation } from '../types';
import {
DataStream,
BrowserAdvancedFields,
BrowserSimpleFields,
Validation,
ConfigKey,
} from '../types';
import {
BrowserAdvancedFieldsContextProvider,
BrowserSimpleFieldsContextProvider,
@ -31,16 +38,18 @@ describe('<ThrottlingFields />', () => {
defaultValues = defaultConfig,
defaultSimpleFields = defaultBrowserSimpleFields,
validate = defaultValidation,
onFieldBlur,
}: {
defaultValues?: BrowserAdvancedFields;
defaultSimpleFields?: BrowserSimpleFields;
validate?: Validation;
onFieldBlur?: (field: ConfigKey) => void;
}) => {
return (
<IntlProvider locale="en">
<BrowserSimpleFieldsContextProvider defaultValues={defaultSimpleFields}>
<BrowserAdvancedFieldsContextProvider defaultValues={defaultValues}>
<ThrottlingFields validate={validate} />
<ThrottlingFields validate={validate} onFieldBlur={onFieldBlur} />
</BrowserAdvancedFieldsContextProvider>
</BrowserSimpleFieldsContextProvider>
</IntlProvider>
@ -97,6 +106,39 @@ describe('<ThrottlingFields />', () => {
});
});
describe('calls onBlur on fields', () => {
const onFieldBlur = jest.fn();
afterEach(() => {
jest.resetAllMocks();
});
it('for the enable switch', () => {
const { getByTestId } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const enableSwitch = getByTestId('syntheticsBrowserIsThrottlingEnabled');
fireEvent.focus(enableSwitch);
fireEvent.blur(enableSwitch);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.IS_THROTTLING_ENABLED);
});
it('for throttling inputs', () => {
const { getByLabelText } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const downloadSpeed = getByLabelText('Download Speed') as HTMLInputElement;
const uploadSpeed = getByLabelText('Upload Speed') as HTMLInputElement;
const latency = getByLabelText('Latency') as HTMLInputElement;
fireEvent.blur(downloadSpeed);
fireEvent.blur(uploadSpeed);
fireEvent.blur(latency);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.DOWNLOAD_SPEED);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.UPLOAD_SPEED);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.LATENCY);
});
});
describe('validates changing fields', () => {
it('disallows negative/zero download speeds', () => {
const { getByLabelText, queryByText } = render(<WrappedComponent />);

View file

@ -17,6 +17,7 @@ import { Validation, ConfigKey } from '../types';
interface Props {
validate: Validation;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
type ThrottlingConfigs =
@ -25,7 +26,7 @@ type ThrottlingConfigs =
| ConfigKey.UPLOAD_SPEED
| ConfigKey.LATENCY;
export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth }) => {
export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const handleInputChange = useCallback(
@ -64,6 +65,7 @@ export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth }) => {
configKey: ConfigKey.DOWNLOAD_SPEED,
});
}}
onBlur={() => onFieldBlur?.(ConfigKey.DOWNLOAD_SPEED)}
data-test-subj="syntheticsBrowserDownloadSpeed"
append={
<EuiText size="xs">
@ -98,6 +100,7 @@ export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth }) => {
configKey: ConfigKey.UPLOAD_SPEED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.UPLOAD_SPEED)}
data-test-subj="syntheticsBrowserUploadSpeed"
append={
<EuiText size="xs">
@ -131,6 +134,7 @@ export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth }) => {
configKey: ConfigKey.LATENCY,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.LATENCY)}
data-test-subj="syntheticsBrowserLatency"
append={
<EuiText size="xs">
@ -177,6 +181,7 @@ export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth }) => {
configKey: ConfigKey.IS_THROTTLING_ENABLED,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)}
/>
{throttlingInputs}
</DescribedFormGroupWithWrap>

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { render } from '../../lib/helper/rtl_helpers';
import { ComboBox } from './combo_box';
@ -20,4 +21,17 @@ describe('<ComboBox />', () => {
expect(getByTestId('syntheticsFleetComboBox')).toBeInTheDocument();
});
it('calls onBlur', () => {
const onBlur = jest.fn();
const { getByTestId } = render(
<ComboBox selectedOptions={selectedOptions} onChange={onChange} onBlur={onBlur} />
);
const combobox = getByTestId('syntheticsFleetComboBox');
fireEvent.focus(combobox);
fireEvent.blur(combobox);
expect(onBlur).toHaveBeenCalledTimes(1);
});
});

View file

@ -10,10 +10,11 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
export interface Props {
onChange: (value: string[]) => void;
onBlur?: () => void;
selectedOptions: string[];
}
export const ComboBox = ({ onChange, selectedOptions, ...props }: Props) => {
export const ComboBox = ({ onChange, onBlur, selectedOptions, ...props }: Props) => {
const [formattedSelectedOptions, setSelectedOptions] = useState<
Array<EuiComboBoxOptionOption<string>>
>(selectedOptions.map((option) => ({ label: option, key: option })));
@ -64,6 +65,7 @@ export const ComboBox = ({ onChange, selectedOptions, ...props }: Props) => {
selectedOptions={formattedSelectedOptions}
onCreateOption={onCreateOption}
onChange={onOptionsChange}
onBlur={() => onBlur?.()}
onSearchChange={onSearchChange}
isInvalid={isInvalid}
{...props}

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import { EuiFieldNumber, EuiFieldText, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui';
import { Validation, DataStream } from '../types';
import { ConfigKey, CommonFields as CommonFieldsType } from '../types';
import React, { useEffect } from 'react';
import { ComboBox } from '../combo_box';
import { OptionalLabel } from '../optional_label';
import { usePolicyConfigContext } from '../contexts';
import { OptionalLabel } from '../optional_label';
import { CommonFields as CommonFieldsType, ConfigKey, DataStream, Validation } from '../types';
interface Props {
validate: Validation;
@ -24,9 +23,10 @@ interface Props {
value: string | string[] | null;
configKey: ConfigKey;
}) => void;
onFieldBlur?: (field: ConfigKey) => void;
}
export function CommonFields({ fields, onChange, validate }: Props) {
export function CommonFields({ fields, onChange, onFieldBlur, validate }: Props) {
const { monitorType } = usePolicyConfigContext();
const isBrowser = monitorType === DataStream.BROWSER;
@ -65,6 +65,7 @@ export function CommonFields({ fields, onChange, validate }: Props) {
configKey: ConfigKey.APM_SERVICE_NAME,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.APM_SERVICE_NAME)}
data-test-subj="syntheticsAPMServiceName"
/>
</EuiFormRow>
@ -106,6 +107,7 @@ export function CommonFields({ fields, onChange, validate }: Props) {
configKey: ConfigKey.TIMEOUT,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.TIMEOUT)}
step={'any'}
/>
</EuiFormRow>
@ -128,6 +130,7 @@ export function CommonFields({ fields, onChange, validate }: Props) {
<ComboBox
selectedOptions={fields[ConfigKey.TAGS]}
onChange={(value) => onChange({ value, configKey: ConfigKey.TAGS })}
onBlur={() => onFieldBlur?.(ConfigKey.TAGS)}
data-test-subj="syntheticsTags"
/>
</EuiFormRow>

View file

@ -13,9 +13,10 @@ import { ConfigKey, CommonFields } from '../types';
interface Props {
fields: CommonFields;
onChange: ({ value, configKey }: { value: boolean; configKey: ConfigKey }) => void;
onBlur?: () => void;
}
export function Enabled({ fields, onChange }: Props) {
export function Enabled({ fields, onChange, onBlur }: Props) {
return (
<>
<EuiFormRow
@ -41,6 +42,7 @@ export function Enabled({ fields, onChange }: Props) {
configKey: ConfigKey.ENABLED,
})
}
onBlur={() => onBlur?.()}
/>
</EuiFormRow>
</>

View file

@ -6,23 +6,39 @@
*/
import React from 'react';
import { ConfigKey, Validation, CommonFields as CommonFieldsType } from '../types';
import { CommonFields } from '../common/common_fields';
import { Enabled } from '../common/enabled';
import { CommonFields } from './common_fields';
import { Enabled } from './enabled';
import { CommonFields as CommonFieldsType, ConfigKey, Validation } from '../types';
interface Props {
validate: Validation;
onInputChange: ({ value, configKey }: { value: unknown; configKey: ConfigKey }) => void;
onFieldBlur?: (field: ConfigKey) => void;
children: React.ReactNode;
fields: CommonFieldsType;
}
export const SimpleFieldsWrapper = ({ validate, onInputChange, children, fields }: Props) => {
export const SimpleFieldsWrapper = ({
validate,
onInputChange,
onFieldBlur,
children,
fields,
}: Props) => {
return (
<>
<Enabled fields={fields} onChange={onInputChange} />
<Enabled
fields={fields}
onChange={onInputChange}
onBlur={() => onFieldBlur?.(ConfigKey.ENABLED)}
/>
{children}
<CommonFields fields={fields} onChange={onInputChange} validate={validate} />
<CommonFields
fields={fields}
validate={validate}
onChange={onInputChange}
onFieldBlur={onFieldBlur}
/>
</>
);
};

View file

@ -57,10 +57,13 @@ const defaultHTTPConfig = defaultConfig[DataStream.HTTP];
const defaultTCPConfig = defaultConfig[DataStream.TCP];
describe('<CustomFields />', () => {
let onFieldBlurMock: jest.Mock | undefined;
const WrappedComponent = ({
validate = defaultValidation,
isEditable = false,
dataStreams = [DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER],
onFieldBlur = onFieldBlurMock,
}) => {
return (
<HTTPContextProvider>
@ -69,7 +72,11 @@ describe('<CustomFields />', () => {
<BrowserContextProvider>
<ICMPSimpleFieldsContextProvider>
<TLSFieldsContextProvider>
<CustomFields validate={validate} dataStreams={dataStreams} />
<CustomFields
validate={validate}
dataStreams={dataStreams}
onFieldBlur={onFieldBlur}
/>
</TLSFieldsContextProvider>
</ICMPSimpleFieldsContextProvider>
</BrowserContextProvider>
@ -79,8 +86,15 @@ describe('<CustomFields />', () => {
);
};
beforeEach(() => {
onFieldBlurMock = undefined;
jest.resetAllMocks();
});
it('renders CustomFields', async () => {
const { getByText, getByLabelText, queryByLabelText } = render(<WrappedComponent />);
const { getByText, getByLabelText, queryByLabelText } = render(
<WrappedComponent onFieldBlur={undefined} />
);
const monitorType = getByLabelText('Monitor Type') as HTMLInputElement;
const url = getByLabelText('URL') as HTMLInputElement;
const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement;
@ -366,4 +380,19 @@ describe('<CustomFields />', () => {
expect(enabled).not.toBeChecked();
});
});
it('calls onFieldBlur on fields', () => {
onFieldBlurMock = jest.fn();
const { queryByLabelText } = render(
<WrappedComponent
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP]}
onFieldBlur={onFieldBlurMock}
/>
);
const monitorTypeSelect = queryByLabelText('Monitor Type') as HTMLInputElement;
fireEvent.click(monitorTypeSelect);
fireEvent.blur(monitorTypeSelect);
expect(onFieldBlurMock).toHaveBeenCalledWith(ConfigKey.MONITOR_TYPE);
});
});

View file

@ -37,6 +37,7 @@ interface Props {
children?: React.ReactNode;
appendAdvancedFields?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
const dataStreamToString = [
@ -55,7 +56,7 @@ const dataStreamToString = [
];
export const CustomFields = memo<Props>(
({ validate, dataStreams = [], children, appendAdvancedFields, minColumnWidth }) => {
({ validate, dataStreams = [], children, appendAdvancedFields, minColumnWidth, onFieldBlur }) => {
const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } =
usePolicyConfigContext();
@ -71,13 +72,24 @@ export const CustomFields = memo<Props>(
const renderSimpleFields = (type: DataStream) => {
switch (type) {
case DataStream.HTTP:
return <HTTPSimpleFields validate={validate} />;
return (
<HTTPSimpleFields validate={validate} onFieldBlur={(field) => onFieldBlur?.(field)} />
);
case DataStream.ICMP:
return <ICMPSimpleFields validate={validate} />;
return (
<ICMPSimpleFields validate={validate} onFieldBlur={(field) => onFieldBlur?.(field)} />
);
case DataStream.TCP:
return <TCPSimpleFields validate={validate} />;
return (
<TCPSimpleFields validate={validate} onFieldBlur={(field) => onFieldBlur?.(field)} />
);
case DataStream.BROWSER:
return <BrowserSimpleFields validate={validate} />;
return (
<BrowserSimpleFields
validate={validate}
onFieldBlur={(field) => onFieldBlur?.(field)}
/>
);
default:
return null;
}
@ -132,6 +144,7 @@ export const CustomFields = memo<Props>(
options={dataStreamOptions}
value={monitorType}
onChange={(event) => setMonitorType(event.target.value as DataStream)}
onBlur={() => onFieldBlur?.(ConfigKey.MONITOR_TYPE)}
data-test-subj="syntheticsMonitorTypeField"
/>
</EuiFormRow>
@ -204,17 +217,25 @@ export const CustomFields = memo<Props>(
)}
<EuiSpacer size="m" />
{isHTTP && (
<HTTPAdvancedFields validate={validate} minColumnWidth={minColumnWidth}>
<HTTPAdvancedFields
validate={validate}
minColumnWidth={minColumnWidth}
onFieldBlur={onFieldBlur}
>
{appendAdvancedFields}
</HTTPAdvancedFields>
)}
{isTCP && (
<TCPAdvancedFields minColumnWidth={minColumnWidth}>
<TCPAdvancedFields minColumnWidth={minColumnWidth} onFieldBlur={onFieldBlur}>
{appendAdvancedFields}
</TCPAdvancedFields>
)}
{isBrowser && (
<BrowserAdvancedFields validate={validate} minColumnWidth={minColumnWidth}>
<BrowserAdvancedFields
validate={validate}
minColumnWidth={minColumnWidth}
onFieldBlur={onFieldBlur}
>
{appendAdvancedFields}
</BrowserAdvancedFields>
)}

View file

@ -13,8 +13,13 @@ import { Mode } from './types';
describe('<HeaderField />', () => {
const onChange = jest.fn();
const onBlur = jest.fn();
const defaultValue = {};
afterEach(() => {
jest.resetAllMocks();
});
it('renders HeaderField', () => {
const { getByText, getByTestId } = render(
<HeaderField defaultValue={{ sample: 'header' }} onChange={onChange} />
@ -28,6 +33,20 @@ describe('<HeaderField />', () => {
expect(value.value).toEqual('header');
});
it('calls onBlur', () => {
const { getByTestId } = render(
<HeaderField defaultValue={{ sample: 'header' }} onChange={onChange} onBlur={onBlur} />
);
const key = getByTestId('keyValuePairsKey0') as HTMLInputElement;
const value = getByTestId('keyValuePairsValue0') as HTMLInputElement;
fireEvent.blur(key);
fireEvent.blur(value);
expect(onBlur).toHaveBeenCalledTimes(2);
});
it('formats headers and handles onChange', async () => {
const { getByTestId, getByText } = render(
<HeaderField defaultValue={defaultValue} onChange={onChange} />

View file

@ -15,6 +15,7 @@ interface Props {
contentMode?: Mode;
defaultValue: Record<string, string>;
onChange: (value: Record<string, string>) => void;
onBlur?: () => void;
'data-test-subj'?: string;
}
@ -22,6 +23,7 @@ export const HeaderField = ({
contentMode,
defaultValue,
onChange,
onBlur,
'data-test-subj': dataTestSubj,
}: Props) => {
const defaultValueKeys = Object.keys(defaultValue).filter((key) => key !== 'Content-Type'); // Content-Type is a secret header we hide from the user
@ -61,6 +63,7 @@ export const HeaderField = ({
}
defaultPairs={headers}
onChange={setHeaders}
onBlur={() => onBlur?.()}
data-test-subj={dataTestSubj}
/>
);

View file

@ -5,22 +5,22 @@
* 2.0.
*/
import React from 'react';
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { render } from '../../../lib/helper/rtl_helpers';
import { HTTPAdvancedFields } from './advanced_fields';
import {
defaultHTTPAdvancedFields as defaultConfig,
HTTPAdvancedFieldsContextProvider,
} from '../contexts';
import {
ConfigKey,
DataStream,
HTTPMethod,
HTTPAdvancedFields as HTTPAdvancedFieldsType,
HTTPMethod,
Validation,
} from '../types';
import {
HTTPAdvancedFieldsContextProvider,
defaultHTTPAdvancedFields as defaultConfig,
} from '../contexts';
import { validate as centralValidation } from '../validation';
import { HTTPAdvancedFields } from './advanced_fields';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
@ -46,6 +46,8 @@ jest.mock('../../../../../../../src/plugins/kibana_react/public', () => {
const defaultValidation = centralValidation[DataStream.HTTP];
describe('<HTTPAdvancedFields />', () => {
const onFieldBlur = jest.fn();
const WrappedComponent = ({
defaultValues,
validate = defaultValidation,
@ -57,7 +59,9 @@ describe('<HTTPAdvancedFields />', () => {
}) => {
return (
<HTTPAdvancedFieldsContextProvider defaultValues={defaultValues}>
<HTTPAdvancedFields validate={validate}>{children}</HTTPAdvancedFields>
<HTTPAdvancedFields validate={validate} onFieldBlur={onFieldBlur}>
{children}
</HTTPAdvancedFields>
</HTTPAdvancedFieldsContextProvider>
);
};
@ -129,6 +133,20 @@ describe('<HTTPAdvancedFields />', () => {
expect(indexResponseHeaders.checked).toBe(false);
});
it('calls onBlur', () => {
const { getByLabelText } = render(<WrappedComponent />);
const username = getByLabelText('Username') as HTMLInputElement;
const requestMethod = getByLabelText('Request method') as HTMLInputElement;
const indexResponseBody = getByLabelText('Index response body') as HTMLInputElement;
[username, requestMethod, indexResponseBody].forEach((field) => fireEvent.blur(field));
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.USERNAME);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.REQUEST_METHOD_CHECK);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.RESPONSE_BODY_INDEX);
});
it('renders upstream fields', () => {
const upstreamFieldsText = 'Monitor Advanced field section';
const { getByText } = render(<WrappedComponent>{upstreamFieldsText}</WrappedComponent>);

View file

@ -34,442 +34,457 @@ interface Props {
validate: Validation;
children?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const HTTPAdvancedFields = memo<Props>(({ validate, children, minColumnWidth }) => {
const { fields, setFields } = useHTTPAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
export const HTTPAdvancedFields = memo<Props>(
({ validate, children, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useHTTPAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
setFields((prevFields) => ({ ...prevFields, [configKey]: value }));
},
[setFields]
);
return (
<EuiAccordion
id="uptimeFleetHttpAdvancedOptions"
buttonContent={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions"
defaultMessage="Advanced HTTP options"
/>
}
data-test-subj="syntheticsHTTPAdvancedFieldsAccordion"
>
<EuiSpacer size="xl" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.title"
defaultMessage="Request configuration"
/>
</h4>
}
description={
return (
<EuiAccordion
id="uptimeFleetHttpAdvancedOptions"
buttonContent={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.description"
defaultMessage="Configure an optional request to send to the remote host including method, body, and headers."
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions"
defaultMessage="Advanced HTTP options"
/>
}
data-test-subj="httpAdvancedFieldsSection"
data-test-subj="syntheticsHTTPAdvancedFieldsAccordion"
>
<EuiSpacer size="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.username.label"
defaultMessage="Username"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.username.helpText"
defaultMessage="Username for authenticating with the server."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.USERNAME]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.USERNAME,
})
}
data-test-subj="syntheticsUsername"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.password.label"
defaultMessage="Password"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.password.helpText"
defaultMessage="Password for authenticating with the server."
/>
}
>
<EuiFieldPassword
value={fields[ConfigKey.PASSWORD]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PASSWORD,
})
}
data-test-subj="syntheticsPassword"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyURL.http.label"
defaultMessage="Proxy URL"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyUrl.http.helpText"
defaultMessage="HTTP proxy URL."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.PROXY_URL]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PROXY_URL,
})
}
data-test-subj="syntheticsProxyUrl"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestMethod.label"
defaultMessage="Request method"
/>
}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestMethod.helpText"
defaultMessage="The HTTP method to use."
/>
}
>
<EuiSelect
options={requestMethodOptions}
value={fields[ConfigKey.REQUEST_METHOD_CHECK]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.REQUEST_METHOD_CHECK,
})
}
data-test-subj="syntheticsRequestMethod"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestHeaders"
defaultMessage="Request headers"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.REQUEST_HEADERS_CHECK]?.(fields)}
error={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestHeadersField.error"
defaultMessage="Header key must be a valid HTTP token."
/>
}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestHeadersField.helpText"
defaultMessage="A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself."
/>
}
>
<HeaderField
contentMode={
fields[ConfigKey.REQUEST_BODY_CHECK].value
? fields[ConfigKey.REQUEST_BODY_CHECK].type
: undefined
} // only pass contentMode if the request body is truthy
defaultValue={fields[ConfigKey.REQUEST_HEADERS_CHECK]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.REQUEST_HEADERS_CHECK,
}),
[handleInputChange]
)}
data-test-subj="syntheticsRequestHeaders"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestBody"
defaultMessage="Request body"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestBody.helpText"
defaultMessage="Request body content."
/>
}
fullWidth
>
<RequestBodyField
value={fields[ConfigKey.REQUEST_BODY_CHECK].value}
type={fields[ConfigKey.REQUEST_BODY_CHECK].type}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.REQUEST_BODY_CHECK,
}),
[handleInputChange]
)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<EuiSpacer size="xl" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfiguration.title"
defaultMessage="Response configuration"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfiguration.description"
defaultMessage="Control the indexing of the HTTP response contents."
/>
}
>
<EuiSpacer size="s" />
<EuiFormRow
helpText={
<>
<EuiSpacer size="xl" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.indexResponseHeaders.helpText"
defaultMessage="Controls the indexing of the HTTP response headers to "
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.title"
defaultMessage="Request configuration"
/>
<EuiCode>http.response.body.headers</EuiCode>
</>
</h4>
}
data-test-subj="syntheticsIndexResponseHeaders"
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.description"
defaultMessage="Configure an optional request to send to the remote host including method, body, and headers."
/>
}
data-test-subj="httpAdvancedFieldsSection"
>
<EuiCheckbox
id={'uptimeFleetIndexResponseHeaders'}
checked={fields[ConfigKey.RESPONSE_HEADERS_INDEX]}
<EuiSpacer size="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfig.indexResponseHeaders"
defaultMessage="Index response headers"
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.username.label"
defaultMessage="Username"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.RESPONSE_HEADERS_INDEX,
})
}
/>
</EuiFormRow>
<EuiFormRow
helpText={
<>
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.indexResponseBody.helpText"
defaultMessage="Controls the indexing of the HTTP response body contents to "
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.username.helpText"
defaultMessage="Username for authenticating with the server."
/>
<EuiCode>http.response.body.contents</EuiCode>
</>
}
>
<EuiFieldText
value={fields[ConfigKey.USERNAME]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.USERNAME,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.USERNAME)}
data-test-subj="syntheticsUsername"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.password.label"
defaultMessage="Password"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.password.helpText"
defaultMessage="Password for authenticating with the server."
/>
}
>
<EuiFieldPassword
value={fields[ConfigKey.PASSWORD]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PASSWORD,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.PASSWORD)}
data-test-subj="syntheticsPassword"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyURL.http.label"
defaultMessage="Proxy URL"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.proxyUrl.http.helpText"
defaultMessage="HTTP proxy URL."
/>
}
>
<EuiFieldText
value={fields[ConfigKey.PROXY_URL]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.PROXY_URL,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.PROXY_URL)}
data-test-subj="syntheticsProxyUrl"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestMethod.label"
defaultMessage="Request method"
/>
}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestMethod.helpText"
defaultMessage="The HTTP method to use."
/>
}
>
<EuiSelect
options={requestMethodOptions}
value={fields[ConfigKey.REQUEST_METHOD_CHECK]}
onChange={(event) =>
handleInputChange({
value: event.target.value,
configKey: ConfigKey.REQUEST_METHOD_CHECK,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_METHOD_CHECK)}
data-test-subj="syntheticsRequestMethod"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestHeaders"
defaultMessage="Request headers"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.REQUEST_HEADERS_CHECK]?.(fields)}
error={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestHeadersField.error"
defaultMessage="Header key must be a valid HTTP token."
/>
}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestHeadersField.helpText"
defaultMessage="A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself."
/>
}
>
<HeaderField
contentMode={
fields[ConfigKey.REQUEST_BODY_CHECK].value
? fields[ConfigKey.REQUEST_BODY_CHECK].type
: undefined
} // only pass contentMode if the request body is truthy
defaultValue={fields[ConfigKey.REQUEST_HEADERS_CHECK]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.REQUEST_HEADERS_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_HEADERS_CHECK)}
data-test-subj="syntheticsRequestHeaders"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestConfiguration.requestBody"
defaultMessage="Request body"
/>
}
labelAppend={<OptionalLabel />}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.requestBody.helpText"
defaultMessage="Request body content."
/>
}
fullWidth
>
<RequestBodyField
value={fields[ConfigKey.REQUEST_BODY_CHECK].value}
type={fields[ConfigKey.REQUEST_BODY_CHECK].type}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.REQUEST_BODY_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_BODY_CHECK)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<EuiSpacer size="xl" />
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfiguration.title"
defaultMessage="Response configuration"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfiguration.description"
defaultMessage="Control the indexing of the HTTP response contents."
/>
}
>
<ResponseBodyIndexField
defaultValue={fields[ConfigKey.RESPONSE_BODY_INDEX]}
onChange={useCallback(
(policy) =>
handleInputChange({ value: policy, configKey: ConfigKey.RESPONSE_BODY_INDEX }),
[handleInputChange]
<EuiSpacer size="s" />
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.indexResponseHeaders.helpText"
defaultMessage="Controls the indexing of the HTTP response headers to "
/>
<EuiCode>http.response.body.headers</EuiCode>
</>
}
data-test-subj="syntheticsIndexResponseHeaders"
>
<EuiCheckbox
id={'uptimeFleetIndexResponseHeaders'}
checked={fields[ConfigKey.RESPONSE_HEADERS_INDEX]}
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseConfig.indexResponseHeaders"
defaultMessage="Index response headers"
/>
}
onChange={(event) =>
handleInputChange({
value: event.target.checked,
configKey: ConfigKey.RESPONSE_HEADERS_INDEX,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_HEADERS_INDEX)}
/>
</EuiFormRow>
<EuiFormRow
helpText={
<>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.indexResponseBody.helpText"
defaultMessage="Controls the indexing of the HTTP response body contents to "
/>
<EuiCode>http.response.body.contents</EuiCode>
</>
}
>
<ResponseBodyIndexField
defaultValue={fields[ConfigKey.RESPONSE_BODY_INDEX]}
onChange={useCallback(
(policy) =>
handleInputChange({ value: policy, configKey: ConfigKey.RESPONSE_BODY_INDEX }),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_BODY_INDEX)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.title"
defaultMessage="Response checks"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.description"
defaultMessage="Configure the expected HTTP response."
/>
}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.label"
defaultMessage="Check response status equals"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.RESPONSE_STATUS_CHECK]?.(fields)}
error={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.error"
defaultMessage="Status code must contain digits only."
/>
}
helpText={i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.helpText',
{
defaultMessage:
'A list of expected status codes. Press enter to add a new code. 4xx and 5xx codes are considered down by default. Other codes are considered up.',
}
)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.title"
defaultMessage="Response checks"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.description"
defaultMessage="Configure the expected HTTP response."
/>
}
>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.label"
defaultMessage="Check response status equals"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.RESPONSE_STATUS_CHECK]?.(fields)}
error={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.error"
defaultMessage="Status code must contain digits only."
/>
}
helpText={i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.responseStatusCheck.helpText',
{
defaultMessage:
'A list of expected status codes. Press enter to add a new code. 4xx and 5xx codes are considered down by default. Other codes are considered up.',
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_STATUS_CHECK]}
onChange={(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_STATUS_CHECK,
})
}
data-test-subj="syntheticsResponseStatusCheck"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.checkResponseHeadersContain"
defaultMessage="Check response headers contain"
/>
}
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.RESPONSE_HEADERS_CHECK]?.(fields)}
error={[
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseHeadersField.error"
defaultMessage="Header key must be a valid HTTP token."
/>,
]}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseHeadersField.helpText"
defaultMessage="A list of expected response headers."
/>
}
>
<HeaderField
defaultValue={fields[ConfigKey.RESPONSE_HEADERS_CHECK]}
onChange={useCallback(
(value) =>
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_STATUS_CHECK]}
onChange={(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_HEADERS_CHECK,
}),
[handleInputChange]
)}
data-test-subj="syntheticsResponseHeaders"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseCheckPositive.label"
defaultMessage="Check response body contains"
configKey: ConfigKey.RESPONSE_STATUS_CHECK,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_STATUS_CHECK)}
data-test-subj="syntheticsResponseStatusCheck"
/>
}
labelAppend={<OptionalLabel />}
helpText={i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseBodyCheckPositive.helpText',
{
defaultMessage:
'A list of regular expressions to match the body output. Press enter to add a new expression. Only a single expression needs to match.',
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseChecks.checkResponseHeadersContain"
defaultMessage="Check response headers contain"
/>
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_BODY_CHECK_POSITIVE,
}),
[handleInputChange]
)}
data-test-subj="syntheticsResponseBodyCheckPositive"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseCheckNegative.label"
defaultMessage="Check response body does not contain"
labelAppend={<OptionalLabel />}
isInvalid={!!validate[ConfigKey.RESPONSE_HEADERS_CHECK]?.(fields)}
error={[
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseHeadersField.error"
defaultMessage="Header key must be a valid HTTP token."
/>,
]}
helpText={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseHeadersField.helpText"
defaultMessage="A list of expected response headers."
/>
}
>
<HeaderField
defaultValue={fields[ConfigKey.RESPONSE_HEADERS_CHECK]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_HEADERS_CHECK,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_HEADERS_CHECK)}
data-test-subj="syntheticsResponseHeaders"
/>
}
labelAppend={<OptionalLabel />}
helpText={i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseBodyCheckNegative.helpText',
{
defaultMessage:
'A list of regular expressions to match the the body output negatively. Press enter to add a new expression. Return match failed if single expression matches.',
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseCheckPositive.label"
defaultMessage="Check response body contains"
/>
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE,
}),
[handleInputChange]
labelAppend={<OptionalLabel />}
helpText={i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseBodyCheckPositive.helpText',
{
defaultMessage:
'A list of regular expressions to match the body output. Press enter to add a new expression. Only a single expression needs to match.',
}
)}
data-test-subj="syntheticsResponseBodyCheckNegative"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
{children}
</EuiAccordion>
);
});
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_BODY_CHECK_POSITIVE,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_BODY_CHECK_POSITIVE)}
data-test-subj="syntheticsResponseBodyCheckPositive"
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseCheckNegative.label"
defaultMessage="Check response body does not contain"
/>
}
labelAppend={<OptionalLabel />}
helpText={i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.httpAdvancedOptions.responseBodyCheckNegative.helpText',
{
defaultMessage:
'A list of regular expressions to match the the body output negatively. Press enter to add a new expression. Return match failed if single expression matches.',
}
)}
>
<ComboBox
selectedOptions={fields[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]}
onChange={useCallback(
(value) =>
handleInputChange({
value,
configKey: ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE,
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE)}
data-test-subj="syntheticsResponseBodyCheckNegative"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
{children}
</EuiAccordion>
);
}
);
const requestMethodOptions = Object.values(HTTPMethod).map((method) => ({
value: method,

View file

@ -5,20 +5,21 @@
* 2.0.
*/
import React, { memo, useCallback } from 'react';
import { EuiFieldNumber, EuiFieldText, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui';
import { ConfigKey, Validation } from '../types';
import React, { memo, useCallback } from 'react';
import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
import { useHTTPSimpleFieldsContext } from '../contexts';
import { OptionalLabel } from '../optional_label';
import { ScheduleField } from '../schedule_field';
import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
import { ConfigKey, Validation } from '../types';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const HTTPSimpleFields = memo<Props>(({ validate }) => {
export const HTTPSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields } = useHTTPSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
@ -28,7 +29,12 @@ export const HTTPSimpleFields = memo<Props>(({ validate }) => {
);
return (
<SimpleFieldsWrapper fields={fields} validate={validate} onInputChange={handleInputChange}>
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
label={
<FormattedMessage
@ -49,6 +55,7 @@ export const HTTPSimpleFields = memo<Props>(({ validate }) => {
onChange={(event) =>
handleInputChange({ value: event.target.value, configKey: ConfigKey.URLS })
}
onBlur={() => onFieldBlur(ConfigKey.URLS)}
data-test-subj="syntheticsUrlField"
/>
</EuiFormRow>
@ -75,6 +82,7 @@ export const HTTPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
@ -110,6 +118,7 @@ export const HTTPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.MAX_REDIRECTS,
})
}
onBlur={() => onFieldBlur(ConfigKey.MAX_REDIRECTS)}
/>
</EuiFormRow>
</SimpleFieldsWrapper>

View file

@ -16,9 +16,10 @@ import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const ICMPSimpleFields = memo<Props>(({ validate }) => {
export const ICMPSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields } = useICMPSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
@ -28,7 +29,12 @@ export const ICMPSimpleFields = memo<Props>(({ validate }) => {
);
return (
<SimpleFieldsWrapper fields={fields} validate={validate} onInputChange={handleInputChange}>
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
label={
<FormattedMessage
@ -52,6 +58,7 @@ export const ICMPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.HOSTS,
})
}
onBlur={() => onFieldBlur(ConfigKey.HOSTS)}
data-test-subj="syntheticsICMPHostField"
/>
</EuiFormRow>
@ -78,6 +85,7 @@ export const ICMPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>
@ -113,6 +121,7 @@ export const ICMPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.WAIT,
})
}
onBlur={() => onFieldBlur(ConfigKey.WAIT)}
step={'any'}
/>
</EuiFormRow>

View file

@ -14,10 +14,17 @@ import { ResponseBodyIndexPolicy } from './types';
describe('<ResponseBodyIndexField/>', () => {
const defaultDefaultValue = ResponseBodyIndexPolicy.ON_ERROR;
const onChange = jest.fn();
const onBlur = jest.fn();
const WrappedComponent = ({ defaultValue = defaultDefaultValue }) => {
return <ResponseBodyIndexField defaultValue={defaultValue} onChange={onChange} />;
return (
<ResponseBodyIndexField defaultValue={defaultValue} onChange={onChange} onBlur={onBlur} />
);
};
afterEach(() => {
jest.resetAllMocks();
});
it('renders ResponseBodyIndexField', () => {
const { getByText, getByTestId } = render(<WrappedComponent />);
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
@ -41,6 +48,17 @@ describe('<ResponseBodyIndexField/>', () => {
});
});
it('calls onBlur', async () => {
const { getByTestId } = render(<WrappedComponent />);
const select = getByTestId('indexResponseBodyFieldSelect') as HTMLInputElement;
const newPolicy = ResponseBodyIndexPolicy.ALWAYS;
fireEvent.change(select, { target: { value: newPolicy } });
fireEvent.blur(select);
expect(onBlur).toHaveBeenCalledTimes(1);
});
it('handles checkbox change', async () => {
const { getByTestId, getByLabelText } = render(<WrappedComponent />);
const checkbox = getByLabelText('Index response body') as HTMLInputElement;

View file

@ -15,9 +15,10 @@ import { ResponseBodyIndexPolicy } from './types';
interface Props {
defaultValue: ResponseBodyIndexPolicy;
onChange: (responseBodyIndexPolicy: ResponseBodyIndexPolicy) => void;
onBlur?: () => void;
}
export const ResponseBodyIndexField = ({ defaultValue, onChange }: Props) => {
export const ResponseBodyIndexField = ({ defaultValue, onChange, onBlur }: Props) => {
const [policy, setPolicy] = useState<ResponseBodyIndexPolicy>(
defaultValue !== ResponseBodyIndexPolicy.NEVER ? defaultValue : ResponseBodyIndexPolicy.ON_ERROR
);
@ -52,6 +53,7 @@ export const ResponseBodyIndexField = ({ defaultValue, onChange }: Props) => {
const checkedEvent = event.target.checked;
setChecked(checkedEvent);
}}
onBlur={() => onBlur?.()}
/>
</EuiFlexItem>
{checked && (
@ -69,6 +71,7 @@ export const ResponseBodyIndexField = ({ defaultValue, onChange }: Props) => {
onChange={(event) => {
setPolicy(event.target.value as ResponseBodyIndexPolicy);
}}
onBlur={() => onBlur?.()}
/>
</EuiFlexItem>
)}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import userEvent from '@testing-library/user-event';
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/rtl_helpers';
@ -12,6 +13,7 @@ import { KeyValuePairsField, Pair } from './key_value_field';
describe('<KeyValuePairsField />', () => {
const onChange = jest.fn();
const onBlur = jest.fn();
const defaultDefaultValue = [['', '']] as Pair[];
const WrappedComponent = ({
defaultValue = defaultDefaultValue,
@ -21,11 +23,16 @@ describe('<KeyValuePairsField />', () => {
<KeyValuePairsField
defaultPairs={defaultValue}
onChange={onChange}
onBlur={onBlur}
addPairControlLabel={addPairControlLabel}
/>
);
};
afterEach(() => {
jest.resetAllMocks();
});
it('renders KeyValuePairsField', () => {
const { getByText } = render(<WrappedComponent />);
expect(getByText('Key')).toBeInTheDocument();
@ -34,6 +41,21 @@ describe('<KeyValuePairsField />', () => {
expect(getByText('Add pair')).toBeInTheDocument();
});
it('calls onBlur', () => {
const { getByText, getByTestId } = render(<WrappedComponent />);
const addPair = getByText('Add pair');
fireEvent.click(addPair);
const keyInput = getByTestId('keyValuePairsKey0') as HTMLInputElement;
const valueInput = getByTestId('keyValuePairsValue0') as HTMLInputElement;
userEvent.type(keyInput, 'some-key');
userEvent.type(valueInput, 'some-value');
fireEvent.blur(valueInput);
expect(onBlur).toHaveBeenCalledTimes(2);
});
it('handles adding and editing a new row', async () => {
const { getByTestId, queryByTestId, getByText } = render(
<WrappedComponent defaultValue={[]} />

View file

@ -50,6 +50,7 @@ interface Props {
addPairControlLabel: string | React.ReactElement;
defaultPairs: Pair[];
onChange: (pairs: Pair[]) => void;
onBlur?: () => void;
'data-test-subj'?: string;
}
@ -57,6 +58,7 @@ export const KeyValuePairsField = ({
addPairControlLabel,
defaultPairs,
onChange,
onBlur,
'data-test-subj': dataTestSubj,
}: Props) => {
const [pairs, setPairs] = useState<Pair[]>(defaultPairs);
@ -167,6 +169,7 @@ export const KeyValuePairsField = ({
data-test-subj={`keyValuePairsKey${index}`}
value={key}
onChange={(event) => handleOnChange(event, index, true)}
onBlur={() => onBlur?.()}
/>
}
endControl={
@ -177,6 +180,7 @@ export const KeyValuePairsField = ({
data-test-subj={`keyValuePairsValue${index}`}
value={value}
onChange={(event) => handleOnChange(event, index, false)}
onBlur={() => onBlur?.()}
/>
}
delimiter=":"

View file

@ -15,6 +15,7 @@ import { CodeEditor } from './code_editor';
interface Props {
onChange: (requestBody: { type: Mode; value: string }) => void;
onBlur?: () => void;
type: Mode;
value: string;
}
@ -25,7 +26,7 @@ enum ResponseBodyType {
}
// TO DO: Look into whether or not code editor reports errors, in order to prevent form submission on an error
export const RequestBodyField = ({ onChange, type, value }: Props) => {
export const RequestBodyField = ({ onChange, onBlur, type, value }: Props) => {
const [values, setValues] = useState<Record<ResponseBodyType, string>>({
[ResponseBodyType.FORM]: type === Mode.FORM ? value : '',
[ResponseBodyType.CODE]: type !== Mode.FORM ? value : '',
@ -93,9 +94,10 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
)}
id={Mode.PLAINTEXT}
languageId={MonacoEditorLangId.PLAINTEXT}
onChange={(code) =>
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }))
}
onChange={(code) => {
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }));
onBlur?.();
}}
value={values[ResponseBodyType.CODE]}
/>
),
@ -114,9 +116,10 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
)}
id={Mode.JSON}
languageId={MonacoEditorLangId.JSON}
onChange={(code) =>
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }))
}
onChange={(code) => {
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }));
onBlur?.();
}}
value={values[ResponseBodyType.CODE]}
/>
),
@ -135,9 +138,10 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
)}
id={Mode.XML}
languageId={MonacoEditorLangId.XML}
onChange={(code) =>
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }))
}
onChange={(code) => {
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }));
onBlur?.();
}}
value={values[ResponseBodyType.CODE]}
/>
),
@ -156,6 +160,7 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
}
defaultPairs={defaultFormPairs}
onChange={onChangeFormFields}
onBlur={() => onBlur?.()}
/>
),
},

View file

@ -17,6 +17,7 @@ import { ScheduleUnit } from './types';
describe('<ScheduleField/>', () => {
const number = '1';
const unit = ScheduleUnit.MINUTES;
const onBlur = jest.fn();
const WrappedComponent = ({
allowedScheduleUnits,
}: Omit<IPolicyConfigContextProvider, 'children'>) => {
@ -31,11 +32,16 @@ describe('<ScheduleField/>', () => {
number={config.number}
unit={config.unit}
onChange={(value) => setConfig(value)}
onBlur={onBlur}
/>
</PolicyConfigContextProvider>
);
};
afterEach(() => {
jest.resetAllMocks();
});
it('shows all options by default (allowedScheduleUnits is not provided)', () => {
const { getByText } = render(<WrappedComponent />);
expect(getByText('Minutes')).toBeInTheDocument();
@ -110,4 +116,21 @@ describe('<ScheduleField/>', () => {
expect(getByText('Seconds')).toBeInTheDocument();
});
});
it('calls onBlur when changed', () => {
const { getByTestId } = render(
<WrappedComponent allowedScheduleUnits={[ScheduleUnit.SECONDS, ScheduleUnit.MINUTES]} />
);
const input = getByTestId('scheduleFieldInput') as HTMLInputElement;
const select = getByTestId('scheduleFieldSelect') as HTMLInputElement;
userEvent.clear(input);
userEvent.type(input, '2');
userEvent.selectOptions(select, ScheduleUnit.MINUTES);
userEvent.click(input);
expect(onBlur).toHaveBeenCalledTimes(2);
});
});

View file

@ -14,10 +14,11 @@ import { ConfigKey, MonitorFields, ScheduleUnit } from './types';
interface Props {
number: string;
onChange: (schedule: MonitorFields[ConfigKey.SCHEDULE]) => void;
onBlur: () => void;
unit: ScheduleUnit;
}
export const ScheduleField = ({ number, onChange, unit }: Props) => {
export const ScheduleField = ({ number, onChange, onBlur, unit }: Props) => {
const { allowedScheduleUnits } = usePolicyConfigContext();
const options = !allowedScheduleUnits?.length
? allOptions
@ -51,6 +52,8 @@ export const ScheduleField = ({ number, onChange, unit }: Props) => {
const updatedNumber = `${Math.ceil(+event.target.value)}`;
onChange({ number: updatedNumber, unit });
}
onBlur();
}}
/>
</EuiFlexItem>
@ -70,6 +73,7 @@ export const ScheduleField = ({ number, onChange, unit }: Props) => {
const updatedUnit = event.target.value;
onChange({ number, unit: updatedUnit as ScheduleUnit });
}}
onBlur={() => onBlur()}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -24,13 +24,15 @@ describe('<TCPAdvancedFields />', () => {
const WrappedComponent = ({
defaultValues = defaultConfig,
children,
onFieldBlur,
}: {
defaultValues?: TCPAdvancedFieldsType;
children?: React.ReactNode;
onFieldBlur?: (field: ConfigKey) => void;
}) => {
return (
<TCPAdvancedFieldsContextProvider defaultValues={defaultValues}>
<TCPAdvancedFields>{children}</TCPAdvancedFields>
<TCPAdvancedFields onFieldBlur={onFieldBlur}>{children}</TCPAdvancedFields>
</TCPAdvancedFieldsContextProvider>
);
};
@ -59,6 +61,17 @@ describe('<TCPAdvancedFields />', () => {
expect(requestPayload.value).toEqual('success');
});
it('calls onBlur on fields', () => {
const onFieldBlur = jest.fn();
const { getByLabelText } = render(<WrappedComponent onFieldBlur={onFieldBlur} />);
const requestPayload = getByLabelText('Request payload') as HTMLInputElement;
fireEvent.change(requestPayload, { target: { value: 'success' } });
fireEvent.blur(requestPayload);
expect(onFieldBlur).toHaveBeenCalledWith(ConfigKey.REQUEST_SEND_CHECK);
});
it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => {
const { getByLabelText, queryByLabelText } = render(<WrappedComponent />);

View file

@ -19,9 +19,10 @@ import { OptionalLabel } from '../optional_label';
interface Props {
children?: React.ReactNode;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth }) => {
export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth, onFieldBlur }) => {
const { fields, setFields } = useTCPAdvancedFieldsContext();
const handleInputChange = useCallback(
@ -79,6 +80,7 @@ export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth }) => {
configKey: ConfigKey.PROXY_URL,
})
}
onBlur={() => onFieldBlur?.(ConfigKey.PROXY_URL)}
data-test-subj="syntheticsProxyUrl"
/>
</EuiFormRow>
@ -127,6 +129,7 @@ export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth }) => {
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.REQUEST_SEND_CHECK)}
data-test-subj="syntheticsTCPRequestSendCheck"
/>
</EuiFormRow>
@ -173,6 +176,7 @@ export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth }) => {
}),
[handleInputChange]
)}
onBlur={() => onFieldBlur?.(ConfigKey.RESPONSE_RECEIVE_CHECK)}
data-test-subj="syntheticsTCPResponseReceiveCheck"
/>
</EuiFormRow>

View file

@ -15,9 +15,10 @@ import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper';
interface Props {
validate: Validation;
onFieldBlur: (field: ConfigKey) => void; // To propagate blurred state up to parents
}
export const TCPSimpleFields = memo<Props>(({ validate }) => {
export const TCPSimpleFields = memo<Props>(({ validate, onFieldBlur }) => {
const { fields, setFields } = useTCPSimpleFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
@ -27,7 +28,12 @@ export const TCPSimpleFields = memo<Props>(({ validate }) => {
);
return (
<SimpleFieldsWrapper fields={fields} validate={validate} onInputChange={handleInputChange}>
<SimpleFieldsWrapper
fields={fields}
validate={validate}
onInputChange={handleInputChange}
onFieldBlur={onFieldBlur}
>
<EuiFormRow
label={
<FormattedMessage
@ -51,6 +57,7 @@ export const TCPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.HOSTS,
})
}
onBlur={() => onFieldBlur(ConfigKey.HOSTS)}
data-test-subj="syntheticsTCPHostField"
/>
</EuiFormRow>
@ -78,6 +85,7 @@ export const TCPSimpleFields = memo<Props>(({ validate }) => {
configKey: ConfigKey.SCHEDULE,
})
}
onBlur={() => onFieldBlur(ConfigKey.SCHEDULE)}
number={fields[ConfigKey.SCHEDULE].number}
unit={fields[ConfigKey.SCHEDULE].unit}
/>

View file

@ -6,11 +6,11 @@
*/
import React from 'react';
import { screen } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import { render } from '../../../lib/helper/rtl_helpers';
import { ServiceLocations } from './locations';
describe('<ActionBar />', () => {
describe('<ServiceLocations />', () => {
const setLocations = jest.fn();
const location = {
label: 'US Central',
@ -21,6 +21,7 @@ describe('<ActionBar />', () => {
},
url: 'url',
};
const locationTestSubId = `syntheticsServiceLocation--${location.id}`;
const state = {
monitorManagementList: {
locations: [location],
@ -62,4 +63,35 @@ describe('<ActionBar />', () => {
expect(screen.getByText('At least one service location must be specified')).toBeInTheDocument();
});
it('checks unchecks location', () => {
const { getByTestId } = render(
<ServiceLocations selectedLocations={[]} setLocations={setLocations} isInvalid={true} />,
{ state }
);
const checkbox = getByTestId(locationTestSubId) as HTMLInputElement;
expect(checkbox.checked).toEqual(false);
fireEvent.click(checkbox);
expect(setLocations).toHaveBeenCalled();
});
it('calls onBlur', () => {
const onBlur = jest.fn();
const { getByTestId } = render(
<ServiceLocations
selectedLocations={[]}
setLocations={setLocations}
isInvalid={true}
onBlur={onBlur}
/>,
{ state }
);
const checkbox = getByTestId(locationTestSubId) as HTMLInputElement;
fireEvent.click(checkbox);
fireEvent.blur(checkbox);
expect(onBlur).toHaveBeenCalledTimes(1);
});
});

View file

@ -16,9 +16,10 @@ interface Props {
selectedLocations: ServiceLocation[];
setLocations: React.Dispatch<React.SetStateAction<ServiceLocation[]>>;
isInvalid: boolean;
onBlur?: () => void;
}
export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid }: Props) => {
export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid, onBlur }: Props) => {
const [error, setError] = useState<string | null>(null);
const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState<Record<string, boolean>>(
{}
@ -58,6 +59,7 @@ export const ServiceLocations = ({ selectedLocations, setLocations, isInvalid }:
}))}
idToSelectedMap={checkboxIdToSelectedMap}
onChange={(id) => onLocationChange(id)}
onBlur={() => onBlur?.()}
/>
</EuiFormRow>
);

View file

@ -4,88 +4,92 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo } from 'react';
import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiSpacer, EuiLink, EuiFieldText } from '@elastic/eui';
import type { Validation } from '../../../../common/types';
import React, { memo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ConfigKey } from '../../../../common/runtime_types';
import type { Validation } from '../../../../common/types';
import { DescribedFormGroupWithWrap } from '../../fleet_package/common/described_form_group_with_wrap';
import { usePolicyConfigContext } from '../../fleet_package/contexts';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
interface Props {
validate: Validation;
minColumnWidth?: string;
onFieldBlur?: (field: ConfigKey) => void;
}
export const MonitorManagementAdvancedFields = memo<Props>(({ validate, minColumnWidth }) => {
const { namespace, setNamespace } = usePolicyConfigContext();
export const MonitorManagementAdvancedFields = memo<Props>(
({ validate, minColumnWidth, onFieldBlur }) => {
const { namespace, setNamespace } = usePolicyConfigContext();
const namespaceErrorMsg = validate[ConfigKey.NAMESPACE]?.({
[ConfigKey.NAMESPACE]: namespace,
});
const isNamespaceInvalid = !!namespaceErrorMsg;
const { services } = useKibana();
const namespaceErrorMsg = validate[ConfigKey.NAMESPACE]?.({
[ConfigKey.NAMESPACE]: namespace,
});
const isNamespaceInvalid = !!namespaceErrorMsg;
const { services } = useKibana();
return (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
return (
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.dataStreamConfiguration.title"
defaultMessage="Data stream settings"
/>
</h4>
}
description={
<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>
),
}}
id="xpack.uptime.monitorManagement.monitorAdvancedOptions.dataStreamConfiguration.description"
defaultMessage="Configure additional Data Stream options."
/>
}
data-test-subj="monitorAdvancedFieldsSection"
>
<EuiFieldText
defaultValue={namespace}
onChange={(event) => setNamespace(event.target.value)}
required={true}
<EuiSpacer size="s" />
<EuiFormRow
isInvalid={isNamespaceInvalid}
fullWidth={true}
name="namespace"
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
);
});
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"
onBlur={() => onFieldBlur?.(ConfigKey.NAMESPACE)}
/>
</EuiFormRow>
</DescribedFormGroupWithWrap>
);
}
);

View file

@ -0,0 +1,61 @@
/*
* 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 { fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { render } from '../../../lib/helper/rtl_helpers';
import {
BrowserContextProvider,
HTTPContextProvider,
ICMPSimpleFieldsContextProvider,
PolicyConfigContextProvider,
TCPContextProvider,
TLSFieldsContextProvider,
} from '../../fleet_package/contexts';
import { MonitorConfig } from './monitor_config';
describe('<MonitorConfig />', () => {
const WrappedComponent = ({ isEditable = true, isEdit = false }) => {
return (
<HTTPContextProvider>
<PolicyConfigContextProvider isEditable={isEditable}>
<TCPContextProvider>
<BrowserContextProvider>
<ICMPSimpleFieldsContextProvider>
<TLSFieldsContextProvider>
<MonitorConfig isEdit={isEdit} />
</TLSFieldsContextProvider>
</ICMPSimpleFieldsContextProvider>
</BrowserContextProvider>
</TCPContextProvider>
</PolicyConfigContextProvider>
</HTTPContextProvider>
);
};
beforeEach(() => {
jest.resetAllMocks();
});
it('renders MonitorConfig', async () => {
const { getByLabelText } = render(<WrappedComponent />);
const monitorName = getByLabelText('Monitor name') as HTMLInputElement;
expect(monitorName).toBeInTheDocument();
});
it('only shows validation errors when field is interacted with', async () => {
const { getByLabelText, queryByText } = render(<WrappedComponent />);
const monitorName = getByLabelText('Monitor name') as HTMLInputElement;
expect(monitorName).toBeInTheDocument();
userEvent.clear(monitorName);
expect(queryByText('Monitor name is required')).toBeNull();
fireEvent.blur(monitorName);
expect(queryByText('Monitor name is required')).not.toBeNull();
});
});

View file

@ -44,10 +44,15 @@ export const MonitorConfig = ({ isEdit = false }: { isEdit: boolean }) => {
defaultConfig: defaultConfig[monitorType],
});
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
const [testRun, setTestRun] = useState<TestRun>();
const [isTestRunInProgress, setIsTestRunInProgress] = useState<boolean>(false);
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
const handleFormSubmit = () => {
setHasBeenSubmitted(true);
};
const handleTestNow = () => {
if (config) {
setTestRun({ id: uuidv4(), monitor: config as MonitorFieldsType });
@ -90,7 +95,7 @@ export const MonitorConfig = ({ isEdit = false }: { isEdit: boolean }) => {
return (
<>
<MonitorFields />
<MonitorFields isFormSubmitted={hasBeenSubmitted} />
{flyout}
@ -100,6 +105,7 @@ export const MonitorConfig = ({ isEdit = false }: { isEdit: boolean }) => {
onTestNow={handleTestNow}
testRun={testRun}
isTestRunInProgress={isTestRunInProgress}
onSave={handleFormSubmit}
/>
</>
);

View file

@ -0,0 +1,94 @@
/*
* 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 { fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ConfigKey, DataStream, HTTPFields } from '../../../../common/runtime_types';
import { render } from '../../../lib/helper/rtl_helpers';
import {
BrowserContextProvider,
HTTPContextProvider,
ICMPSimpleFieldsContextProvider,
PolicyConfigContextProvider,
TCPContextProvider,
TLSFieldsContextProvider,
} from '../../fleet_package/contexts';
import { defaultConfig } from '../../fleet_package/synthetics_policy_create_extension';
import { MonitorFields } from './monitor_fields';
const defaultHTTPConfig = defaultConfig[DataStream.HTTP] as HTTPFields;
describe('<MonitorFields />', () => {
const WrappedComponent = ({
isEditable = true,
isFormSubmitted = false,
defaultSimpleHttpFields = defaultHTTPConfig,
}: {
isEditable?: boolean;
isFormSubmitted?: boolean;
defaultSimpleHttpFields?: HTTPFields;
}) => {
return (
<HTTPContextProvider defaultValues={defaultSimpleHttpFields}>
<PolicyConfigContextProvider isEditable={isEditable}>
<TCPContextProvider>
<BrowserContextProvider>
<ICMPSimpleFieldsContextProvider>
<TLSFieldsContextProvider>
<MonitorFields isFormSubmitted={isFormSubmitted} />
</TLSFieldsContextProvider>
</ICMPSimpleFieldsContextProvider>
</BrowserContextProvider>
</TCPContextProvider>
</PolicyConfigContextProvider>
</HTTPContextProvider>
);
};
beforeEach(() => {
jest.resetAllMocks();
});
it('renders MonitorFields', async () => {
const { getByLabelText } = render(<WrappedComponent />);
const monitorName = getByLabelText('URL') as HTMLInputElement;
expect(monitorName).toBeInTheDocument();
});
it('only shows validation errors when field has been interacted with', async () => {
const { getByLabelText, queryByText } = render(<WrappedComponent />);
const monitorName = getByLabelText('Monitor name') as HTMLInputElement;
expect(monitorName).toBeInTheDocument();
userEvent.clear(monitorName);
expect(queryByText('Monitor name is required')).toBeNull();
fireEvent.blur(monitorName);
expect(queryByText('Monitor name is required')).not.toBeNull();
});
it('shows all validations errors when form is submitted', async () => {
const httpInvalidValues = { ...defaultHTTPConfig, [ConfigKey.NAME]: '', [ConfigKey.URLS]: '' };
const { queryByText } = render(
<WrappedComponent isFormSubmitted={true} defaultSimpleHttpFields={httpInvalidValues} />
);
expect(queryByText('Monitor name is required')).not.toBeNull();
expect(queryByText('URL is required')).not.toBeNull();
});
it('does not show validation errors initially', async () => {
const httpInvalidValues = { ...defaultHTTPConfig, [ConfigKey.NAME]: '', [ConfigKey.URLS]: '' };
const { queryByText } = render(
<WrappedComponent isFormSubmitted={false} defaultSimpleHttpFields={httpInvalidValues} />
);
expect(queryByText('Monitor name is required')).toBeNull();
expect(queryByText('URL is required')).toBeNull();
});
});

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo, useState } from 'react';
import { EuiForm } from '@elastic/eui';
import { DataStream } from '../../../../common/runtime_types';
import { ConfigKey, DataStream } from '../../../../common/runtime_types';
import { usePolicyConfigContext } from '../../fleet_package/contexts';
import { CustomFields } from '../../fleet_package/custom_fields';
@ -17,22 +17,44 @@ import { MonitorManagementAdvancedFields } from './monitor_advanced_fields';
const MIN_COLUMN_WRAP_WIDTH = '360px';
export const MonitorFields = () => {
export const MonitorFields = ({ isFormSubmitted = false }: { isFormSubmitted?: boolean }) => {
const { monitorType } = usePolicyConfigContext();
const [touchedFieldsHash, setTouchedFieldsHash] = useState<Record<string, boolean>>({});
const fieldValidation = useMemo(() => {
const validatorsHash = { ...validate[monitorType] };
if (!isFormSubmitted) {
Object.keys(validatorsHash).map((key) => {
if (!touchedFieldsHash[key]) {
validatorsHash[key as ConfigKey] = undefined;
}
});
}
return validatorsHash;
}, [isFormSubmitted, monitorType, touchedFieldsHash]);
const handleFieldBlur = (field: ConfigKey) => {
setTouchedFieldsHash((hash) => ({ ...hash, [field]: true }));
};
return (
<EuiForm id="syntheticsServiceCreateMonitorForm" component="form">
<CustomFields
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
validate={validate[monitorType]}
validate={fieldValidation}
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER]}
appendAdvancedFields={
<MonitorManagementAdvancedFields
validate={validate[monitorType]}
validate={fieldValidation}
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
onFieldBlur={handleFieldBlur}
/>
}
onFieldBlur={handleFieldBlur}
>
<MonitorNameAndLocation validate={validate[monitorType]} />
<MonitorNameAndLocation validate={fieldValidation} onFieldBlur={handleFieldBlur} />
</CustomFields>
</EuiForm>
);

View file

@ -17,9 +17,10 @@ import { useMonitorName } from './use_monitor_name';
interface Props {
validate: Validation;
onFieldBlur?: (field: ConfigKey) => void;
}
export const MonitorNameAndLocation = ({ validate }: Props) => {
export const MonitorNameAndLocation = ({ validate, onFieldBlur }: Props) => {
const { name, setName, locations = [], setLocations } = usePolicyConfigContext();
const isNameInvalid = !!validate[ConfigKey.NAME]?.({ [ConfigKey.NAME]: name });
const isLocationsInvalid = !!validate[ConfigKey.LOCATIONS]?.({
@ -64,6 +65,7 @@ export const MonitorNameAndLocation = ({ validate }: Props) => {
fullWidth={true}
name="name"
onChange={(event) => setLocalName(event.target.value)}
onBlur={() => onFieldBlur?.(ConfigKey.NAME)}
data-test-subj="monitorManagementMonitorName"
/>
</EuiFormRow>
@ -71,6 +73,7 @@ export const MonitorNameAndLocation = ({ validate }: Props) => {
setLocations={setLocations}
selectedLocations={locations}
isInvalid={isLocationsInvalid}
onBlur={() => onFieldBlur?.(ConfigKey.LOCATIONS)}
/>
</>
);