mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Synthetics] Monitor Add/Edit - Fix form error states (#156626)
- Connects form validation states with react-hoom-form's state (which was the reason of the reported bug that the error state was only reflected in euiForm and not the react-hook-form) - Changes `reValidateMode` from `onChange` to `onSubmit` and manually invalidates the form when fields change so that dependent validations can be executed reactively e.g. fixes the following state: <img width="703" alt="Screenshot 2023-05-04 at 00 10 03" src="https://user-images.githubusercontent.com/2748376/236061192-a9417915-1eca-4cff-b871-b680955ec15a.png"> - Extracts validation and dependencies logic into a hook to simplify state management - Fixes the following error where "Response body max bytes" field appear despite "Index response body" flag being unchecked. <img width="1205" alt="Screenshot 2023-05-05 at 19 35 27" src="https://user-images.githubusercontent.com/2748376/236527581-ff62550a-2679-431e-8571-da02a89c0a32.png"> - Fixes a case where updated values from for nested fields (such as "Response body contains JSON") weren't being passed to monitor inspect component ("Inspect configuration" flyout on monitor Add/Edit).
This commit is contained in:
parent
d2ac7033a2
commit
e3cc4a9a22
22 changed files with 993 additions and 331 deletions
|
@ -0,0 +1,447 @@
|
|||
/*
|
||||
* 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 { expect, journey, Page, step } from '@elastic/synthetics';
|
||||
import { FormMonitorType } from '../../../common/runtime_types';
|
||||
import { recordVideo } from '../../helpers/record_video';
|
||||
import { syntheticsAppPageProvider } from '../page_objects/synthetics_app';
|
||||
import {
|
||||
isEuiFormFieldInValid,
|
||||
clearAndType,
|
||||
typeViaKeyboard,
|
||||
clickAndBlur,
|
||||
assertShouldNotExist,
|
||||
} from '../page_objects/utils';
|
||||
|
||||
const customLocation = process.env.SYNTHETICS_TEST_LOCATION;
|
||||
|
||||
const basicMonitorDetails = {
|
||||
location: customLocation || 'US Central',
|
||||
schedule: '3',
|
||||
};
|
||||
const existingMonitorName = 'https://amazon.com';
|
||||
|
||||
const apmServiceName = 'apmServiceName';
|
||||
|
||||
type ValidationAssertionAction =
|
||||
| 'focus'
|
||||
| 'click'
|
||||
| 'assertExists'
|
||||
| 'assertDoesNotExist'
|
||||
| 'assertEuiFormFieldInValid'
|
||||
| 'assertEuiFormFieldValid'
|
||||
| 'clearAndType'
|
||||
| 'typeViaKeyboard'
|
||||
| 'clickAndBlur'
|
||||
| 'waitForTimeout'
|
||||
| 'selectMonitorFrequency';
|
||||
|
||||
interface ValidationAssertionInstruction {
|
||||
action: ValidationAssertionAction;
|
||||
selector?: string;
|
||||
arg?: string | number | object;
|
||||
}
|
||||
|
||||
const configuration: Record<
|
||||
FormMonitorType,
|
||||
{ monitorType: FormMonitorType; validationInstructions: ValidationAssertionInstruction[] }
|
||||
> = {
|
||||
[FormMonitorType.MULTISTEP]: {
|
||||
monitorType: FormMonitorType.MULTISTEP,
|
||||
validationInstructions: [
|
||||
// Select monitor type
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorTypeMultistep]' },
|
||||
|
||||
// 'required' field assertions
|
||||
{ action: 'focus', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigLocations]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
{ action: 'assertExists', selector: 'text=Monitor name is required' },
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigLocations]',
|
||||
},
|
||||
|
||||
// Name duplication assertion
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: existingMonitorName,
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name already exists' },
|
||||
|
||||
// Mmonitor script
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsSourceTab__inline]' },
|
||||
{ action: 'click', selector: '[aria-labelledby=syntheticsBrowserInlineConfig]' },
|
||||
{
|
||||
action: 'typeViaKeyboard',
|
||||
selector: '[aria-labelledby=syntheticsBrowserInlineConfig]',
|
||||
arg: '}',
|
||||
},
|
||||
{
|
||||
action: 'assertExists',
|
||||
selector:
|
||||
'text=Monitor script is invalid. Inline scripts must contain at least one step definition.',
|
||||
},
|
||||
{
|
||||
action: 'click',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigSubmitButton]',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Please address the highlighted errors.' },
|
||||
],
|
||||
},
|
||||
[FormMonitorType.SINGLE]: {
|
||||
monitorType: FormMonitorType.SINGLE,
|
||||
validationInstructions: [
|
||||
// Select monitor type
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorTypeSingle]' },
|
||||
|
||||
// Name duplication assertion
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: existingMonitorName,
|
||||
},
|
||||
{
|
||||
action: 'click',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigSubmitButton]',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name already exists' },
|
||||
{ action: 'assertExists', selector: 'text=Please address the highlighted errors.' },
|
||||
],
|
||||
},
|
||||
[FormMonitorType.HTTP]: {
|
||||
monitorType: FormMonitorType.HTTP,
|
||||
validationInstructions: [
|
||||
// Select monitor type
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorTypeHTTP]' },
|
||||
|
||||
// 'required' field assertions
|
||||
{ action: 'focus', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
{ action: 'focus', selector: '[data-test-subj=syntheticsMonitorConfigURL]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigLocations]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigMaxRedirects]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
{ action: 'assertExists', selector: 'text=Monitor name is required' },
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigLocations]',
|
||||
},
|
||||
|
||||
// Monitor max redirects
|
||||
{
|
||||
action: 'assertEuiFormFieldValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigMaxRedirects]',
|
||||
},
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigMaxRedirects]',
|
||||
arg: '11',
|
||||
},
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigMaxRedirects]',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Max redirects is invalid.' },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigMaxRedirects]',
|
||||
arg: '3',
|
||||
},
|
||||
{ action: 'clickAndBlur', selector: '[data-test-subj=syntheticsMonitorConfigMaxRedirects]' },
|
||||
{ action: 'assertDoesNotExist', selector: 'text=Max redirects is invalid.' },
|
||||
|
||||
// Monitor timeout
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '-1',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Timeout must be greater than or equal to 0.' },
|
||||
{ action: 'selectMonitorFrequency', selector: undefined, arg: { value: 1, unit: 'minute' } },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '61',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Timeout must be less than the monitor frequency.' },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '60',
|
||||
},
|
||||
{
|
||||
action: 'assertDoesNotExist',
|
||||
selector: 'text=Timeout must be less than the monitor frequency.',
|
||||
},
|
||||
|
||||
// Name duplication assertion
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: existingMonitorName,
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name already exists' },
|
||||
|
||||
// Advanced Settings
|
||||
{ action: 'click', selector: 'text=Advanced options' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsHeaderFieldRequestHeaders__button]' },
|
||||
{ action: 'assertExists', selector: '[data-test-subj=keyValuePairsKey0]' },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=keyValuePairsKey0]',
|
||||
arg: 'K e',
|
||||
},
|
||||
{ action: 'clickAndBlur', selector: '[data-test-subj=keyValuePairsKey0]' },
|
||||
{ action: 'assertExists', selector: 'text=Header key must be a valid HTTP token.' },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=keyValuePairsKey0]',
|
||||
arg: 'X-Api-Key',
|
||||
},
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=keyValuePairsValue0]',
|
||||
arg: 'V a l u e',
|
||||
},
|
||||
{
|
||||
action: 'assertDoesNotExist',
|
||||
selector: 'text=Header key must be a valid HTTP token.',
|
||||
},
|
||||
],
|
||||
},
|
||||
[FormMonitorType.TCP]: {
|
||||
monitorType: FormMonitorType.TCP,
|
||||
validationInstructions: [
|
||||
// Select monitor type
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorTypeTCP]' },
|
||||
|
||||
// 'required' field assertions
|
||||
{ action: 'focus', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigHost]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigLocations]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
|
||||
// Enter a duplicate name
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: existingMonitorName,
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name already exists' },
|
||||
|
||||
// Clear name
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: '',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name is required' },
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigLocations]',
|
||||
},
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigHost]',
|
||||
},
|
||||
|
||||
// Monitor timeout
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '-1',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Timeout must be greater than or equal to 0.' },
|
||||
{ action: 'selectMonitorFrequency', selector: undefined, arg: { value: 1, unit: 'minute' } },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '61',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Timeout must be less than the monitor frequency.' },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '60',
|
||||
},
|
||||
{
|
||||
action: 'assertDoesNotExist',
|
||||
selector: 'text=Timeout must be less than the monitor frequency.',
|
||||
},
|
||||
|
||||
// Submit form
|
||||
{
|
||||
action: 'click',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigSubmitButton]',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Please address the highlighted errors.' },
|
||||
],
|
||||
},
|
||||
[FormMonitorType.ICMP]: {
|
||||
monitorType: FormMonitorType.ICMP,
|
||||
validationInstructions: [
|
||||
// Select monitor type
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorTypeICMP]' },
|
||||
|
||||
// 'required' field assertions
|
||||
{ action: 'focus', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
{ action: 'focus', selector: '[data-test-subj=syntheticsMonitorConfigHost]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigLocations]' },
|
||||
{ action: 'click', selector: '[data-test-subj=syntheticsMonitorConfigName]' },
|
||||
|
||||
// Enter a duplicate name
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: existingMonitorName,
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name already exists' },
|
||||
|
||||
// Clear name
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigName]',
|
||||
arg: '',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Monitor name is required' },
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigLocations]',
|
||||
},
|
||||
{
|
||||
action: 'assertEuiFormFieldInValid',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigHost]',
|
||||
},
|
||||
|
||||
// Monitor timeout
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '-1',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Timeout must be greater than or equal to 0.' },
|
||||
{ action: 'selectMonitorFrequency', selector: undefined, arg: { value: 1, unit: 'minute' } },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '61',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Timeout must be less than the monitor frequency.' },
|
||||
{
|
||||
action: 'clearAndType',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigTimeout]',
|
||||
arg: '60',
|
||||
},
|
||||
{
|
||||
action: 'assertDoesNotExist',
|
||||
selector: 'text=Timeout must be less than the monitor frequency.',
|
||||
},
|
||||
|
||||
// Submit form
|
||||
{
|
||||
action: 'click',
|
||||
selector: '[data-test-subj=syntheticsMonitorConfigSubmitButton]',
|
||||
},
|
||||
{ action: 'assertExists', selector: 'text=Please address the highlighted errors.' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const exitingMonitorConfig = {
|
||||
...basicMonitorDetails,
|
||||
name: existingMonitorName,
|
||||
url: existingMonitorName,
|
||||
locations: [basicMonitorDetails.location],
|
||||
apmServiceName,
|
||||
};
|
||||
|
||||
journey(
|
||||
`SyntheticsAddMonitor - Validation Test`,
|
||||
async ({ page, params }: { page: Page; params: any }) => {
|
||||
page.setDefaultTimeout(60 * 1000);
|
||||
recordVideo(page);
|
||||
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
step('Go to monitor management', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement(true);
|
||||
await syntheticsApp.enableMonitorManagement();
|
||||
});
|
||||
|
||||
step('Ensure all monitors are deleted', async () => {
|
||||
await syntheticsApp.waitForLoadingToFinish();
|
||||
const isSuccessful = await syntheticsApp.deleteMonitors();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
});
|
||||
|
||||
step('Create a monitor to validate duplicate name', async () => {
|
||||
await syntheticsApp.navigateToAddMonitor();
|
||||
await syntheticsApp.ensureIsOnMonitorConfigPage();
|
||||
await syntheticsApp.createMonitor({
|
||||
monitorConfig: exitingMonitorConfig,
|
||||
monitorType: FormMonitorType.HTTP,
|
||||
});
|
||||
const isSuccessful = await syntheticsApp.confirmAndSave();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
});
|
||||
|
||||
step(`Goto Add Monitor page`, async () => {
|
||||
await syntheticsApp.navigateToAddMonitor();
|
||||
await syntheticsApp.ensureIsOnMonitorConfigPage();
|
||||
});
|
||||
|
||||
Object.values(configuration).forEach((config) => {
|
||||
const { monitorType, validationInstructions } = config;
|
||||
|
||||
step(`Test form validation for monitor type [${monitorType}]`, async () => {
|
||||
for (const instruction of validationInstructions) {
|
||||
const { action, selector, arg } = instruction;
|
||||
const locator = page.locator(selector ?? '');
|
||||
switch (action) {
|
||||
case 'focus':
|
||||
await locator.focus();
|
||||
break;
|
||||
case 'click':
|
||||
await locator.click();
|
||||
break;
|
||||
case 'assertExists':
|
||||
await locator.waitFor();
|
||||
break;
|
||||
case 'assertDoesNotExist':
|
||||
await assertShouldNotExist(locator);
|
||||
break;
|
||||
case 'assertEuiFormFieldInValid':
|
||||
expect(await isEuiFormFieldInValid(locator)).toEqual(true);
|
||||
break;
|
||||
case 'assertEuiFormFieldValid':
|
||||
expect(await isEuiFormFieldInValid(locator)).toEqual(false);
|
||||
break;
|
||||
case 'clearAndType':
|
||||
await clearAndType(locator, arg as string);
|
||||
break;
|
||||
case 'typeViaKeyboard':
|
||||
await typeViaKeyboard(locator, arg as string);
|
||||
break;
|
||||
case 'clickAndBlur':
|
||||
await clickAndBlur(locator);
|
||||
break;
|
||||
case 'waitForTimeout':
|
||||
await page.waitForTimeout(arg as number);
|
||||
break;
|
||||
case 'selectMonitorFrequency':
|
||||
await syntheticsApp.selectFrequencyAddEdit(arg as { value: number; unit: 'minute' });
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
`Assertion Instruction ${JSON.stringify(instruction)} is not recognizable`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -167,6 +167,23 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
}
|
||||
},
|
||||
|
||||
async selectFrequencyAddEdit({
|
||||
value,
|
||||
unit,
|
||||
}: {
|
||||
value: number;
|
||||
unit: 'minute' | 'minutes' | 'hours';
|
||||
}) {
|
||||
await page.click(this.byTestId('syntheticsMonitorConfigSchedule'));
|
||||
|
||||
const optionLocator = page.locator(`text=Every ${value} ${unit}`);
|
||||
await optionLocator.evaluate((element: HTMLOptionElement) => {
|
||||
if (element && element.parentElement) {
|
||||
(element.parentElement as HTMLSelectElement).selectedIndex = element.index;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async fillFirstMonitorDetails({ url, locations }: { url: string; locations: string[] }) {
|
||||
await this.fillByTestSubj('urls-input', url);
|
||||
await page.click(this.byTestId('comboBoxInput'));
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { expect, Page } from '@elastic/synthetics';
|
||||
|
||||
export async function isEuiFormFieldInValid(locator: ReturnType<Page['locator']>) {
|
||||
const elementHandle = await locator.elementHandle();
|
||||
expect(elementHandle).toBeTruthy();
|
||||
|
||||
const classAttribute = (await elementHandle!.asElement().getAttribute('class')) ?? '';
|
||||
const isAriaInvalid = (await elementHandle!.asElement().getAttribute('aria-invalid')) ?? 'false';
|
||||
|
||||
return classAttribute.indexOf('-isInvalid') > -1 || isAriaInvalid === 'true';
|
||||
}
|
||||
|
||||
export async function clearAndType(locator: ReturnType<Page['locator']>, value: string) {
|
||||
await locator.fill('');
|
||||
await locator.type(value);
|
||||
}
|
||||
|
||||
export async function typeViaKeyboard(locator: ReturnType<Page['locator']>, value: string) {
|
||||
await locator.click();
|
||||
await locator.page().keyboard.type(value);
|
||||
}
|
||||
|
||||
export async function blur(locator: ReturnType<Page['locator']>) {
|
||||
await locator.evaluate((e) => {
|
||||
e.blur();
|
||||
});
|
||||
}
|
||||
|
||||
export async function clickAndBlur(locator: ReturnType<Page['locator']>) {
|
||||
await locator.click();
|
||||
await blur(locator);
|
||||
}
|
||||
|
||||
export async function assertShouldNotExist(locator: ReturnType<Page['locator']>) {
|
||||
await locator.waitFor({ state: 'detached' });
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { enableInspectEsQueries } from '@kbn/observability-plugin/common';
|
||||
import { useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
|
@ -28,10 +27,14 @@ import {
|
|||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout';
|
||||
import { DataStream, SyntheticsMonitor } from '../../../../../../common/runtime_types';
|
||||
import { DataStream, MonitorFields } from '../../../../../../common/runtime_types';
|
||||
import { inspectMonitorAPI, MonitorInspectResponse } from '../../../state/monitor_management/api';
|
||||
|
||||
export const MonitorInspectWrapper = () => {
|
||||
interface InspectorProps {
|
||||
isValid: boolean;
|
||||
monitorFields: MonitorFields;
|
||||
}
|
||||
export const MonitorInspectWrapper = (props: InspectorProps) => {
|
||||
const {
|
||||
services: { uiSettings },
|
||||
} = useKibana<ClientPluginsStart>();
|
||||
|
@ -40,10 +43,10 @@ export const MonitorInspectWrapper = () => {
|
|||
|
||||
const isInspectorEnabled = uiSettings?.get<boolean>(enableInspectEsQueries);
|
||||
|
||||
return isDev || isInspectorEnabled ? <MonitorInspect /> : null;
|
||||
return isDev || isInspectorEnabled ? <MonitorInspect {...props} /> : null;
|
||||
};
|
||||
|
||||
const MonitorInspect = () => {
|
||||
const MonitorInspect = ({ isValid, monitorFields }: InspectorProps) => {
|
||||
const { isDev } = useSyntheticsSettingsContext();
|
||||
|
||||
const [hideParams, setHideParams] = useState(() => !isDev);
|
||||
|
@ -60,13 +63,11 @@ const MonitorInspect = () => {
|
|||
setIsFlyoutVisible(() => !isFlyoutVisible);
|
||||
};
|
||||
|
||||
const { getValues, formState } = useFormContext();
|
||||
|
||||
const { data, loading, error } = useFetcher(() => {
|
||||
if (isInspecting) {
|
||||
return inspectMonitorAPI({
|
||||
hideParams,
|
||||
monitor: getValues() as SyntheticsMonitor,
|
||||
monitor: monitorFields,
|
||||
});
|
||||
}
|
||||
}, [isInspecting, hideParams]);
|
||||
|
@ -121,9 +122,9 @@ const MonitorInspect = () => {
|
|||
}
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip content={formState.isValid ? FORMATTED_CONFIG_DESCRIPTION : VALID_CONFIG_LABEL}>
|
||||
<EuiToolTip content={isValid ? FORMATTED_CONFIG_DESCRIPTION : VALID_CONFIG_LABEL}>
|
||||
<EuiButton
|
||||
disabled={!formState.isValid}
|
||||
disabled={!isValid}
|
||||
data-test-subj="syntheticsMonitorInspectShowFlyoutExampleButton"
|
||||
onClick={onButtonClick}
|
||||
iconType="inspect"
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Controller, FieldErrors, Control } from 'react-hook-form';
|
||||
import { Controller, Control } from 'react-hook-form';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiComboBox, EuiComboBoxProps, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ServiceLocation } from '../../../../../../common/runtime_types';
|
||||
import { formatLocation } from '../../../../../../common/utils/location_formatter';
|
||||
|
@ -19,28 +19,37 @@ import { SimpleFormData } from '../simple_monitor_form';
|
|||
import { ConfigKey } from '../../../../../../common/constants/monitor_management';
|
||||
|
||||
export const ServiceLocationsField = ({
|
||||
errors,
|
||||
control,
|
||||
onChange,
|
||||
}: {
|
||||
errors: FieldErrors;
|
||||
control: Control<SimpleFormData, any>;
|
||||
onChange: (locations: ServiceLocation[]) => void;
|
||||
}) => {
|
||||
const { locations } = useSelector(selectServiceLocationsState);
|
||||
|
||||
const fieldState = control.getFieldState(ConfigKey.LOCATIONS);
|
||||
const showError = fieldState.isTouched || control._formState.isSubmitted;
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={LOCATIONS_LABEL}
|
||||
helpText={!errors?.[ConfigKey.LOCATIONS] ? SELECT_ONE_OR_MORE_LOCATIONS_DETAILS : undefined}
|
||||
isInvalid={!!errors?.[ConfigKey.LOCATIONS]}
|
||||
error={SELECT_ONE_OR_MORE_LOCATIONS}
|
||||
helpText={showError && fieldState.invalid ? undefined : SELECT_ONE_OR_MORE_LOCATIONS_DETAILS}
|
||||
isInvalid={showError && fieldState.invalid}
|
||||
error={showError ? SELECT_ONE_OR_MORE_LOCATIONS : undefined}
|
||||
>
|
||||
<Controller
|
||||
name={ConfigKey.LOCATIONS}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
rules={{
|
||||
validate: {
|
||||
notEmpty: (value: ServiceLocation[]) => {
|
||||
return value?.length > 0 ? true : SELECT_ONE_OR_MORE_LOCATIONS;
|
||||
},
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<EuiComboBox
|
||||
<ComboBoxWithRef
|
||||
fullWidth
|
||||
aria-label={SELECT_ONE_OR_MORE_LOCATIONS}
|
||||
options={locations.map((location) => ({
|
||||
|
@ -51,10 +60,13 @@ export const ServiceLocationsField = ({
|
|||
isClearable={true}
|
||||
data-test-subj="syntheticsServiceLocations"
|
||||
{...field}
|
||||
onChange={(selectedOptions) =>
|
||||
field.onChange(selectedOptions.map((loc) => formatLocation(loc as ServiceLocation)))
|
||||
}
|
||||
isInvalid={!!errors?.[ConfigKey.LOCATIONS]}
|
||||
onChange={async (selectedOptions) => {
|
||||
const updatedLocations = selectedOptions.map((loc) =>
|
||||
formatLocation(loc as ServiceLocation)
|
||||
);
|
||||
field.onChange(updatedLocations);
|
||||
onChange(updatedLocations as ServiceLocation[]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -62,6 +74,23 @@ export const ServiceLocationsField = ({
|
|||
);
|
||||
};
|
||||
|
||||
const ComboBoxWithRef = React.forwardRef<HTMLInputElement, EuiComboBoxProps<ServiceLocation>>(
|
||||
(props, ref) => (
|
||||
<EuiComboBox
|
||||
{...props}
|
||||
inputRef={(element) => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(element);
|
||||
} else {
|
||||
ref.current = element;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
const SELECT_ONE_OR_MORE_LOCATIONS = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.selectOneOrMoreLocations',
|
||||
{
|
||||
|
|
|
@ -18,7 +18,7 @@ import React, { useState } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { useSimpleMonitor } from './use_simple_monitor';
|
||||
import { ServiceLocationsField } from './form_fields/service_locations';
|
||||
import { ConfigKey, ServiceLocations } from '../../../../../common/runtime_types';
|
||||
import { ConfigKey, ServiceLocation, ServiceLocations } from '../../../../../common/runtime_types';
|
||||
import { useCanEditSynthetics } from '../../../../hooks/use_capabilities';
|
||||
import { useFormWrapped } from '../../../../hooks/use_form_wrapped';
|
||||
import { NoPermissionsTooltip } from '../common/components/permissions';
|
||||
|
@ -33,10 +33,12 @@ export const SimpleMonitorForm = () => {
|
|||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isValid, isSubmitted },
|
||||
formState: { isValid, isSubmitted },
|
||||
getFieldState,
|
||||
trigger,
|
||||
} = useFormWrapped({
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
reValidateMode: 'onSubmit',
|
||||
shouldFocusError: true,
|
||||
defaultValues: { urls: '', locations: [] as ServiceLocations },
|
||||
});
|
||||
|
@ -51,7 +53,8 @@ export const SimpleMonitorForm = () => {
|
|||
|
||||
const canEditSynthetics = useCanEditSynthetics();
|
||||
|
||||
const hasURLError = !!errors?.[ConfigKey.URLS];
|
||||
const urlFieldState = getFieldState(ConfigKey.URLS);
|
||||
const urlError = isSubmitted || urlFieldState.isTouched ? urlFieldState.error : undefined;
|
||||
|
||||
return (
|
||||
<EuiForm
|
||||
|
@ -63,20 +66,29 @@ export const SimpleMonitorForm = () => {
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
label={WEBSITE_URL_LABEL}
|
||||
helpText={!hasURLError ? WEBSITE_URL_HELP_TEXT : ''}
|
||||
isInvalid={!!errors?.[ConfigKey.URLS]}
|
||||
error={hasURLError ? URL_REQUIRED_LABEL : undefined}
|
||||
helpText={urlError ? undefined : WEBSITE_URL_HELP_TEXT}
|
||||
isInvalid={!!urlError}
|
||||
error={urlError?.message}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
{...register(ConfigKey.URLS, { required: true })}
|
||||
isInvalid={!!errors?.[ConfigKey.URLS]}
|
||||
{...register(ConfigKey.URLS, {
|
||||
validate: {
|
||||
notEmpty: (value: string) => (!Boolean(value.trim()) ? URL_REQUIRED_LABEL : true),
|
||||
},
|
||||
})}
|
||||
isInvalid={!!urlError}
|
||||
data-test-subj={`${ConfigKey.URLS}-input`}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<ServiceLocationsField errors={errors} control={control} />
|
||||
<ServiceLocationsField
|
||||
control={control}
|
||||
onChange={async (_locations: ServiceLocation[]) => {
|
||||
await trigger?.();
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -8,17 +8,14 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiAccordion, EuiDescribedFormGroup, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { useFormContext, FieldError } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import styled from 'styled-components';
|
||||
import { FORM_CONFIG } from '../form/form_config';
|
||||
import { Field } from '../form/field';
|
||||
import { ConfigKey, FormMonitorType } from '../types';
|
||||
|
||||
export const AdvancedConfig = ({ readOnly }: { readOnly: boolean }) => {
|
||||
const {
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const { watch } = useFormContext();
|
||||
const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]);
|
||||
|
||||
const formConfig = useMemo(() => {
|
||||
|
@ -36,7 +33,7 @@ export const AdvancedConfig = ({ readOnly }: { readOnly: boolean }) => {
|
|||
<EuiSpacer />
|
||||
{formConfig.advanced?.map((configGroup) => {
|
||||
return (
|
||||
<DescripedFormGroup
|
||||
<DescribedFormGroup
|
||||
description={configGroup.description}
|
||||
title={<h4>{configGroup.title}</h4>}
|
||||
fullWidth
|
||||
|
@ -46,15 +43,9 @@ export const AdvancedConfig = ({ readOnly }: { readOnly: boolean }) => {
|
|||
style={{ flexWrap: 'wrap' }}
|
||||
>
|
||||
{configGroup.components.map((field) => {
|
||||
return (
|
||||
<Field
|
||||
{...field}
|
||||
key={field.fieldKey}
|
||||
fieldError={errors[field.fieldKey] as FieldError}
|
||||
/>
|
||||
);
|
||||
return <Field {...field} key={field.fieldKey} />;
|
||||
})}
|
||||
</DescripedFormGroup>
|
||||
</DescribedFormGroup>
|
||||
);
|
||||
})}
|
||||
</EuiAccordion>
|
||||
|
@ -62,7 +53,7 @@ export const AdvancedConfig = ({ readOnly }: { readOnly: boolean }) => {
|
|||
) : null;
|
||||
};
|
||||
|
||||
const DescripedFormGroup = styled(EuiDescribedFormGroup)`
|
||||
const DescribedFormGroup = styled(EuiDescribedFormGroup)`
|
||||
> div.euiFlexGroup {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
@ -25,15 +25,15 @@ export const ResponseBodyIndexField = ({
|
|||
onBlur,
|
||||
readOnly,
|
||||
}: ResponseBodyIndexFieldProps) => {
|
||||
const [policy, setPolicy] = useState<ResponseBodyIndexPolicy>(
|
||||
defaultValue !== ResponseBodyIndexPolicy.NEVER ? defaultValue : ResponseBodyIndexPolicy.ON_ERROR
|
||||
);
|
||||
const [policy, setPolicy] = useState<ResponseBodyIndexPolicy>(defaultValue);
|
||||
const [checked, setChecked] = useState<boolean>(defaultValue !== ResponseBodyIndexPolicy.NEVER);
|
||||
|
||||
useEffect(() => {
|
||||
if (checked) {
|
||||
setPolicy(policy);
|
||||
onChange(policy);
|
||||
const defaultOrSelected =
|
||||
policy === ResponseBodyIndexPolicy.NEVER ? ResponseBodyIndexPolicy.ON_ERROR : policy;
|
||||
setPolicy(defaultOrSelected);
|
||||
onChange(defaultOrSelected);
|
||||
} else {
|
||||
onChange(ResponseBodyIndexPolicy.NEVER);
|
||||
}
|
||||
|
@ -88,15 +88,6 @@ export const ResponseBodyIndexField = ({
|
|||
};
|
||||
|
||||
const responseBodyIndexPolicyOptions = [
|
||||
{
|
||||
value: ResponseBodyIndexPolicy.ALWAYS,
|
||||
text: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.responseBodyIndex.always',
|
||||
{
|
||||
defaultMessage: 'Always',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ResponseBodyIndexPolicy.ON_ERROR,
|
||||
text: i18n.translate(
|
||||
|
@ -106,4 +97,13 @@ const responseBodyIndexPolicyOptions = [
|
|||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: ResponseBodyIndexPolicy.ALWAYS,
|
||||
text: i18n.translate(
|
||||
'xpack.synthetics.createPackagePolicy.stepConfigure.responseBodyIndex.always',
|
||||
{
|
||||
defaultMessage: 'Always',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -4,15 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { EuiFormRow, EuiFormRowProps } from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
UseFormReturn,
|
||||
ControllerRenderProps,
|
||||
ControllerFieldState,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { ControllerRenderProps, ControllerFieldState, useFormContext } from 'react-hook-form';
|
||||
import { useKibanaSpace, useIsEditFlow } from '../hooks';
|
||||
import { selectServiceLocationsState } from '../../../state';
|
||||
import { FieldMeta, FormConfig } from '../types';
|
||||
|
@ -25,43 +21,60 @@ type Props<TFieldKey extends keyof FormConfig = any> = FieldMeta<TFieldKey> & {
|
|||
error: React.ReactNode;
|
||||
dependenciesValues: unknown[];
|
||||
dependenciesFieldMeta: Record<string, ControllerFieldState>;
|
||||
isInvalid: boolean;
|
||||
};
|
||||
|
||||
const setFieldValue =
|
||||
(key: keyof FormConfig, setValue: UseFormReturn<FormConfig>['setValue']) => (value: any) => {
|
||||
setValue(key, value);
|
||||
};
|
||||
|
||||
export const ControlledField = <TFieldKey extends keyof FormConfig>({
|
||||
component: FieldComponent,
|
||||
props,
|
||||
fieldKey,
|
||||
shouldUseSetValue,
|
||||
field,
|
||||
formRowProps,
|
||||
fieldState,
|
||||
customHook,
|
||||
error,
|
||||
dependenciesValues,
|
||||
dependenciesFieldMeta,
|
||||
isInvalid,
|
||||
}: Props<TFieldKey>) => {
|
||||
const { setValue, reset, formState, setError, clearErrors } = useFormContext<FormConfig>();
|
||||
const noop = () => {};
|
||||
let hook: Function = noop;
|
||||
let hookProps;
|
||||
const { setValue, getFieldState, reset, formState, trigger } = useFormContext<FormConfig>();
|
||||
|
||||
const { locations } = useSelector(selectServiceLocationsState);
|
||||
const { space } = useKibanaSpace();
|
||||
const isEdit = useIsEditFlow();
|
||||
if (customHook) {
|
||||
hookProps = customHook(field.value);
|
||||
hook = hookProps.func;
|
||||
}
|
||||
const { [hookProps?.fieldKey as string]: hookResult } = hook(hookProps?.params) || {};
|
||||
const onChange = shouldUseSetValue ? setFieldValue(fieldKey, setValue) : field.onChange;
|
||||
|
||||
const [onChangeArgs, setOnChangeArgs] = useState<Parameters<typeof field.onChange> | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
async () => {
|
||||
if (onChangeArgs !== undefined) {
|
||||
await trigger?.(); // Manually invalidate whole form to make dependency validations reactive
|
||||
}
|
||||
},
|
||||
500,
|
||||
[onChangeArgs]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
async (...event: any[]) => {
|
||||
if (typeof event?.[0] === 'string' && !getFieldState(fieldKey).isTouched) {
|
||||
// This is needed for composite fields like code editors
|
||||
setValue(fieldKey, event[0], { shouldTouch: true });
|
||||
}
|
||||
|
||||
field.onChange(...event);
|
||||
setOnChangeArgs(event);
|
||||
},
|
||||
// Do not depend on `field`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[setOnChangeArgs]
|
||||
);
|
||||
|
||||
const generatedProps = props
|
||||
? props({
|
||||
field,
|
||||
setValue,
|
||||
trigger,
|
||||
reset,
|
||||
locations: locations.map((location) => ({ ...location, key: location.id })),
|
||||
dependencies: dependenciesValues,
|
||||
|
@ -71,33 +84,14 @@ export const ControlledField = <TFieldKey extends keyof FormConfig>({
|
|||
formState,
|
||||
})
|
||||
: {};
|
||||
const isInvalid = hookResult || Boolean(fieldState.error);
|
||||
const hookErrorContent = hookProps?.error;
|
||||
const hookError = hookResult ? hookProps?.error : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!customHook) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hookResult && !fieldState.error) {
|
||||
setError(fieldKey, { type: 'custom', message: hookErrorContent as string });
|
||||
} else if (!hookResult && fieldState.error?.message === hookErrorContent) {
|
||||
clearErrors(fieldKey);
|
||||
}
|
||||
}, [setError, fieldKey, clearErrors, fieldState, customHook, hookResult, hookErrorContent]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
{...formRowProps}
|
||||
isInvalid={isInvalid}
|
||||
error={isInvalid ? hookError || fieldState.error?.message || error : undefined}
|
||||
>
|
||||
<EuiFormRow {...formRowProps} isInvalid={isInvalid} error={error}>
|
||||
<FieldComponent
|
||||
{...field}
|
||||
checked={field.value || false}
|
||||
defaultValue={field.value}
|
||||
onChange={onChange}
|
||||
onChange={handleChange}
|
||||
{...generatedProps}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Controller, useFormContext, FieldError, ControllerFieldState } from 'react-hook-form';
|
||||
import { Controller, useFormContext, FieldError } from 'react-hook-form';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { selectServiceLocationsState } from '../../../state';
|
||||
import { useKibanaSpace, useIsEditFlow } from '../hooks';
|
||||
import { useKibanaSpace, useIsEditFlow, useValidateField } from '../hooks';
|
||||
import { ControlledField } from './controlled_field';
|
||||
import { FormConfig, FieldMeta } from '../types';
|
||||
|
||||
|
@ -24,39 +24,29 @@ export const Field = memo<Props>(
|
|||
props,
|
||||
fieldKey,
|
||||
controlled,
|
||||
shouldUseSetValue,
|
||||
required,
|
||||
validation,
|
||||
error,
|
||||
error: validationError,
|
||||
fieldError,
|
||||
dependencies,
|
||||
customHook,
|
||||
hidden,
|
||||
}: Props) => {
|
||||
const { register, watch, control, setValue, reset, getFieldState, formState } =
|
||||
useFormContext<FormConfig>();
|
||||
const { register, control, setValue, reset, formState, trigger } = useFormContext<FormConfig>();
|
||||
const { locations } = useSelector(selectServiceLocationsState);
|
||||
const { space } = useKibanaSpace();
|
||||
const isEdit = useIsEditFlow();
|
||||
const [dependenciesFieldMeta, setDependenciesFieldMeta] = useState<
|
||||
Record<string, ControllerFieldState>
|
||||
>({});
|
||||
let dependenciesValues: unknown[] = [];
|
||||
if (dependencies) {
|
||||
dependenciesValues = watch(dependencies);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (dependencies) {
|
||||
dependencies.forEach((dependency) => {
|
||||
setDependenciesFieldMeta((prevState) => ({
|
||||
...prevState,
|
||||
[dependency]: getFieldState(dependency),
|
||||
}));
|
||||
});
|
||||
|
||||
const { dependenciesValues, dependenciesFieldMeta, error, isInvalid, rules } = useValidateField(
|
||||
{
|
||||
fieldKey,
|
||||
validation,
|
||||
dependencies,
|
||||
required: required ?? false,
|
||||
customHook,
|
||||
validationError,
|
||||
}
|
||||
// run effect when dependencies values change, to get the most up to date meta state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(dependenciesValues || []), dependencies, getFieldState]);
|
||||
);
|
||||
|
||||
if (hidden && hidden(dependenciesValues)) {
|
||||
return null;
|
||||
|
@ -73,22 +63,18 @@ export const Field = memo<Props>(
|
|||
<Controller<FormConfig, keyof FormConfig>
|
||||
control={control}
|
||||
name={fieldKey}
|
||||
rules={{
|
||||
required,
|
||||
...(validation ? validation(dependenciesValues) : {}),
|
||||
}}
|
||||
rules={rules}
|
||||
render={({ field, fieldState: fieldStateT }) => {
|
||||
return (
|
||||
<ControlledField
|
||||
field={field}
|
||||
component={Component}
|
||||
props={props}
|
||||
shouldUseSetValue={shouldUseSetValue}
|
||||
fieldKey={fieldKey}
|
||||
customHook={customHook}
|
||||
formRowProps={formRowProps}
|
||||
fieldState={fieldStateT}
|
||||
error={error}
|
||||
isInvalid={isInvalid}
|
||||
dependenciesValues={dependenciesValues}
|
||||
dependenciesFieldMeta={dependenciesFieldMeta}
|
||||
/>
|
||||
|
@ -102,15 +88,13 @@ export const Field = memo<Props>(
|
|||
error={fieldError?.message || error}
|
||||
>
|
||||
<Component
|
||||
{...register(fieldKey, {
|
||||
required,
|
||||
...(validation ? validation(dependenciesValues) : {}),
|
||||
})}
|
||||
{...register(fieldKey, rules)}
|
||||
{...(props
|
||||
? props({
|
||||
field: undefined,
|
||||
formState,
|
||||
setValue,
|
||||
trigger,
|
||||
reset,
|
||||
locations: locations.map((location) => ({ ...location, key: location.id })),
|
||||
dependencies: dependenciesValues,
|
||||
|
@ -119,7 +103,7 @@ export const Field = memo<Props>(
|
|||
isEdit,
|
||||
})
|
||||
: {})}
|
||||
isInvalid={Boolean(fieldError)}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -242,18 +242,17 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }): EuiFieldTextProps => {
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, trigger }): EuiFieldTextProps => {
|
||||
return {
|
||||
'data-test-subj': 'syntheticsMonitorConfigURL',
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.URLS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
onChange: async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.URLS, event.target.value, { shouldTouch: true });
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
await trigger();
|
||||
},
|
||||
readOnly,
|
||||
};
|
||||
|
@ -271,16 +270,15 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }): EuiFieldTextProps => {
|
||||
props: ({ setValue, trigger, dependenciesFieldMeta, isEdit }): EuiFieldTextProps => {
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.URLS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
onChange: async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.URLS, event.target.value, { shouldTouch: true });
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
shouldTouch: true,
|
||||
});
|
||||
await trigger();
|
||||
}
|
||||
},
|
||||
'data-test-subj': 'syntheticsMonitorConfigURL',
|
||||
|
@ -297,17 +295,14 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }): EuiFieldTextProps => {
|
||||
props: ({ setValue, trigger, dependenciesFieldMeta, isEdit }): EuiFieldTextProps => {
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.HOSTS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
onChange: async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.HOSTS, event.target.value, { shouldTouch: true });
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
setValue(ConfigKey.NAME, event.target.value, { shouldTouch: true });
|
||||
}
|
||||
await trigger();
|
||||
},
|
||||
'data-test-subj': 'syntheticsMonitorConfigHost',
|
||||
readOnly,
|
||||
|
@ -323,17 +318,14 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.NAME],
|
||||
props: ({ setValue, dependenciesFieldMeta, isEdit, formState }): EuiFieldTextProps => {
|
||||
props: ({ setValue, trigger, dependenciesFieldMeta, isEdit }): EuiFieldTextProps => {
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.HOSTS, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
onChange: async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(ConfigKey.HOSTS, event.target.value, { shouldTouch: true });
|
||||
if (!dependenciesFieldMeta[ConfigKey.NAME].isDirty && !isEdit) {
|
||||
setValue(ConfigKey.NAME, event.target.value, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
setValue(ConfigKey.NAME, event.target.value, { shouldTouch: true });
|
||||
}
|
||||
await trigger();
|
||||
},
|
||||
'data-test-subj': 'syntheticsMonitorConfigHost',
|
||||
readOnly,
|
||||
|
@ -362,7 +354,12 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
validation: () => ({
|
||||
validate: {
|
||||
notEmpty: (value) => Boolean(value.trim()),
|
||||
notEmpty: (value) =>
|
||||
!Boolean(value.trim())
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.name.error', {
|
||||
defaultMessage: 'Monitor name is required',
|
||||
})
|
||||
: true,
|
||||
},
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.name.error', {
|
||||
|
@ -404,7 +401,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
defaultMessage:
|
||||
'Where do you want to run this test from? Additional locations will increase your total cost.',
|
||||
}),
|
||||
props: ({ field, setValue, locations, formState }) => {
|
||||
props: ({ field, setValue, locations, trigger }) => {
|
||||
return {
|
||||
options: Object.values(locations).map((location) => ({
|
||||
label: locations?.find((loc) => location.id === loc.id)?.label || '',
|
||||
|
@ -425,15 +422,14 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
isServiceManaged: location.isServiceManaged || false,
|
||||
})),
|
||||
'data-test-subj': 'syntheticsMonitorConfigLocations',
|
||||
onChange: (updatedValues: FormLocation[]) => {
|
||||
onChange: async (updatedValues: FormLocation[]) => {
|
||||
const valuesToSave = updatedValues.map(({ id, label, isServiceManaged }) => ({
|
||||
id,
|
||||
label,
|
||||
isServiceManaged,
|
||||
}));
|
||||
setValue(ConfigKey.LOCATIONS, valuesToSave, {
|
||||
shouldValidate: Boolean(formState.submitCount > 0),
|
||||
});
|
||||
setValue(ConfigKey.LOCATIONS, valuesToSave);
|
||||
await trigger(ConfigKey.LOCATIONS);
|
||||
},
|
||||
isDisabled: readOnly,
|
||||
renderOption: (option: FormLocation, searchValue: string) => {
|
||||
|
@ -487,14 +483,15 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
helpText: i18n.translate('xpack.synthetics.monitorConfig.edit.enabled.label', {
|
||||
defaultMessage: `When disabled, the monitor doesn't run any tests. You can enable it at any time.`,
|
||||
}),
|
||||
props: ({ setValue, field }): EuiSwitchProps => ({
|
||||
props: ({ setValue, field, trigger }): EuiSwitchProps => ({
|
||||
id: 'syntheticsMontiorConfigIsEnabled',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.enabled.label', {
|
||||
defaultMessage: 'Enable Monitor',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: (event) => {
|
||||
onChange: async (event) => {
|
||||
setValue(ConfigKey.ENABLED, !!event.target.checked);
|
||||
await trigger(ConfigKey.ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsEnableSwitch',
|
||||
// enabled is an allowed field for read only
|
||||
|
@ -505,7 +502,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
fieldKey: AlertConfigKey.STATUS_ENABLED,
|
||||
component: Switch,
|
||||
controlled: true,
|
||||
props: ({ setValue, field }): EuiSwitchProps => ({
|
||||
props: ({ setValue, field, trigger }): EuiSwitchProps => ({
|
||||
id: 'syntheticsMonitorConfigIsAlertEnabled',
|
||||
label: field?.value
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.enabledAlerting.label', {
|
||||
|
@ -515,8 +512,9 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
defaultMessage: 'Enable status alerts on this monitor',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: (event) => {
|
||||
onChange: async (event) => {
|
||||
setValue(AlertConfigKey.STATUS_ENABLED, !!event.target.checked);
|
||||
await trigger(AlertConfigKey.STATUS_ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsAlertStatusSwitch',
|
||||
// alert config is an allowed field for read only
|
||||
|
@ -527,7 +525,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
fieldKey: AlertConfigKey.TLS_ENABLED,
|
||||
component: Switch,
|
||||
controlled: true,
|
||||
props: ({ setValue, field }): EuiSwitchProps => ({
|
||||
props: ({ setValue, field, trigger }): EuiSwitchProps => ({
|
||||
id: 'syntheticsMonitorConfigIsTlsAlertEnabled',
|
||||
label: field?.value
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.edit.alertTlsEnabled.label', {
|
||||
|
@ -537,8 +535,9 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
defaultMessage: 'Enable TLS alerts on this monitor.',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: (event) => {
|
||||
onChange: async (event) => {
|
||||
setValue(AlertConfigKey.TLS_ENABLED, !!event.target.checked);
|
||||
await trigger(AlertConfigKey.TLS_ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsAlertStatusSwitch',
|
||||
// alert config is an allowed field for read only
|
||||
|
@ -573,14 +572,15 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
defaultMessage: 'The total time allowed for testing the connection and exchanging data.',
|
||||
}),
|
||||
props: (): EuiFieldNumberProps => ({
|
||||
'data-test-subj': 'syntheticsMonitorConfigTimeout',
|
||||
min: 1,
|
||||
step: 'any',
|
||||
readOnly,
|
||||
}),
|
||||
dependencies: [ConfigKey.SCHEDULE],
|
||||
validation: ([schedule]) => {
|
||||
return {
|
||||
validate: (value) => {
|
||||
validation: ([schedule]) => ({
|
||||
validate: {
|
||||
validTimeout: (value) => {
|
||||
switch (true) {
|
||||
case value < 0:
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.timeout.greaterThan0Error', {
|
||||
|
@ -588,7 +588,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
});
|
||||
case value > parseFloat((schedule as MonitorFields[ConfigKey.SCHEDULE]).number) * 60:
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.timeout.scheduleError', {
|
||||
defaultMessage: 'Timemout must be less than the monitor frequency.',
|
||||
defaultMessage: 'Timeout must be less than the monitor frequency.',
|
||||
});
|
||||
case !Boolean(`${value}`.match(FLOATS_ONLY)):
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.timeout.formatError', {
|
||||
|
@ -598,8 +598,8 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ConfigKey.APM_SERVICE_NAME]: {
|
||||
fieldKey: ConfigKey.APM_SERVICE_NAME,
|
||||
|
@ -645,7 +645,9 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
readOnly,
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: (namespace) => isValidNamespace(namespace).error,
|
||||
validate: {
|
||||
validNamespace: (namespace) => isValidNamespace(namespace).error,
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ConfigKey.MAX_REDIRECTS]: {
|
||||
|
@ -658,6 +660,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
defaultMessage: 'The total number of redirects to follow.',
|
||||
}),
|
||||
props: (): EuiFieldNumberProps => ({
|
||||
'data-test-subj': 'syntheticsMonitorConfigMaxRedirects',
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
|
@ -665,6 +668,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
validation: () => ({
|
||||
min: 0,
|
||||
max: 10,
|
||||
pattern: WHOLE_NUMBERS_ONLY,
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.maxRedirects.error', {
|
||||
|
@ -682,6 +686,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
'The duration to wait before emitting another ICMP Echo Request if no response is received.',
|
||||
}),
|
||||
props: (): EuiFieldNumberProps => ({
|
||||
'data-test-subj': 'syntheticsMonitorConfigWait',
|
||||
min: 1,
|
||||
step: 1,
|
||||
readOnly,
|
||||
|
@ -762,18 +767,26 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
validation: () => ({
|
||||
validate: (headers) => !validateHeaders(headers),
|
||||
validate: {
|
||||
validHeaders: (headers) =>
|
||||
validateHeaders(headers)
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.error', {
|
||||
defaultMessage: 'Header key must be a valid HTTP token.',
|
||||
})
|
||||
: true,
|
||||
},
|
||||
}),
|
||||
dependencies: [ConfigKey.REQUEST_BODY_CHECK],
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.requestHeaders.error', {
|
||||
defaultMessage: 'Header key must be a valid HTTP token.',
|
||||
}),
|
||||
// contentMode is optional for other implementations, but required for this implemention of this field
|
||||
// contentMode is optional for other implementations, but required for this implementation of this field
|
||||
props: ({
|
||||
dependencies,
|
||||
}): HeaderFieldProps & { contentMode: HeaderFieldProps['contentMode'] } => {
|
||||
const [requestBody] = dependencies;
|
||||
return {
|
||||
'data-test-subj': 'syntheticsHeaderFieldRequestHeaders',
|
||||
readOnly,
|
||||
contentMode: (requestBody as RequestBodyCheck).type,
|
||||
};
|
||||
|
@ -847,13 +860,15 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
isDisabled: readOnly,
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: (value) => {
|
||||
const validateFn = validate[DataStream.HTTP][ConfigKey.RESPONSE_STATUS_CHECK];
|
||||
if (validateFn) {
|
||||
return !validateFn({
|
||||
[ConfigKey.RESPONSE_STATUS_CHECK]: value,
|
||||
});
|
||||
}
|
||||
validate: {
|
||||
validResponseStatusCheck: (value) => {
|
||||
const validateFn = validate[DataStream.HTTP][ConfigKey.RESPONSE_STATUS_CHECK];
|
||||
if (validateFn) {
|
||||
return !validateFn({
|
||||
[ConfigKey.RESPONSE_STATUS_CHECK]: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.responseStatusCheck.error', {
|
||||
|
@ -871,12 +886,17 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
validation: () => ({
|
||||
validate: (headers) => !validateHeaders(headers),
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.error', {
|
||||
defaultMessage: 'Header key must be a valid HTTP token.',
|
||||
validate: {
|
||||
validHeaders: (headers) =>
|
||||
validateHeaders(headers)
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.responseHeadersCheck.error', {
|
||||
defaultMessage: 'Header key must be a valid HTTP token.',
|
||||
})
|
||||
: true,
|
||||
},
|
||||
}),
|
||||
props: (): HeaderFieldProps => ({
|
||||
'data-test-subj': 'syntheticsHeaderFieldResponseHeaders',
|
||||
readOnly,
|
||||
}),
|
||||
},
|
||||
|
@ -964,31 +984,36 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
isEditFlow: isEdit,
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: (value) => {
|
||||
// return false if script contains import or require statement
|
||||
if (
|
||||
value.script?.includes('import ') ||
|
||||
value.script?.includes('require(') ||
|
||||
value.script?.includes('journey(')
|
||||
) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.monitorScript.invalid', {
|
||||
defaultMessage:
|
||||
'Monitor script is invalid. Inline scripts cannot be full journey scripts, they may only contain step definitions.',
|
||||
});
|
||||
}
|
||||
// should contain at least one step
|
||||
if (value.script && !value.script?.includes('step(')) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.monitorScript.invalid.oneStep', {
|
||||
defaultMessage:
|
||||
'Monitor script is invalid. Inline scripts must contain at least one step definition.',
|
||||
});
|
||||
}
|
||||
return Boolean(value.script);
|
||||
validate: {
|
||||
validScript: (value) => {
|
||||
if (!value.script) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.monitorScript.error', {
|
||||
defaultMessage: 'Monitor script is required',
|
||||
});
|
||||
}
|
||||
|
||||
// return false if script contains import or require statement
|
||||
if (
|
||||
value.script?.includes('import ') ||
|
||||
value.script?.includes('require(') ||
|
||||
value.script?.includes('journey(')
|
||||
) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.monitorScript.invalid', {
|
||||
defaultMessage:
|
||||
'Monitor script is invalid. Inline scripts cannot be full journey scripts, they may only contain step definitions.',
|
||||
});
|
||||
}
|
||||
// should contain at least one step
|
||||
if (value.script && !value.script?.includes('step(')) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.monitorScript.invalid.oneStep', {
|
||||
defaultMessage:
|
||||
'Monitor script is invalid. Inline scripts must contain at least one step definition.',
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.monitorScript.error', {
|
||||
defaultMessage: 'Monitor script is required',
|
||||
}),
|
||||
},
|
||||
[ConfigKey.PARAMS]: {
|
||||
fieldKey: ConfigKey.PARAMS,
|
||||
|
@ -1005,9 +1030,6 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
readOnly,
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.params.error', {
|
||||
defaultMessage: 'Invalid JSON format',
|
||||
}),
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorConfig.params.helpText"
|
||||
|
@ -1018,13 +1040,21 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
/>
|
||||
),
|
||||
validation: () => ({
|
||||
validate: (value) => {
|
||||
const validateFn = validate[DataStream.BROWSER][ConfigKey.PARAMS];
|
||||
if (validateFn) {
|
||||
return !validateFn({
|
||||
[ConfigKey.PARAMS]: value,
|
||||
});
|
||||
}
|
||||
validate: {
|
||||
validParams: (value) => {
|
||||
const validateFn = validate[DataStream.BROWSER][ConfigKey.PARAMS];
|
||||
if (validateFn) {
|
||||
return validateFn({
|
||||
[ConfigKey.PARAMS]: value,
|
||||
})
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.params.error', {
|
||||
defaultMessage: 'Invalid JSON format',
|
||||
})
|
||||
: true;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
@ -1081,7 +1111,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
return !Boolean(isTLSEnabled);
|
||||
},
|
||||
dependencies: ['isTLSEnabled'],
|
||||
props: ({ field, setValue }): EuiComboBoxProps<TLSVersion> => {
|
||||
props: ({ field, setValue, trigger }): EuiComboBoxProps<TLSVersion> => {
|
||||
return {
|
||||
options: Object.values(TLSVersion).map((version) => ({
|
||||
label: version,
|
||||
|
@ -1089,11 +1119,12 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
selectedOptions: Object.values(field?.value || []).map((version) => ({
|
||||
label: version as TLSVersion,
|
||||
})),
|
||||
onChange: (updatedValues: Array<EuiComboBoxOptionOption<TLSVersion>>) => {
|
||||
onChange: async (updatedValues: Array<EuiComboBoxOptionOption<TLSVersion>>) => {
|
||||
setValue(
|
||||
ConfigKey.TLS_VERSION,
|
||||
updatedValues.map((option) => option.label as TLSVersion)
|
||||
);
|
||||
await trigger(ConfigKey.TLS_VERSION);
|
||||
},
|
||||
isDisabled: readOnly,
|
||||
};
|
||||
|
@ -1276,9 +1307,6 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
</EuiLink>
|
||||
</span>
|
||||
),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.playwrightOptions.error', {
|
||||
defaultMessage: 'Invalid JSON format',
|
||||
}),
|
||||
ariaLabel: i18n.translate(
|
||||
'xpack.synthetics.monitorConfig.playwrightOptions.codeEditor.json.ariaLabel',
|
||||
{
|
||||
|
@ -1298,13 +1326,21 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
id: 'syntheticsPlaywrightOptionsJSONCodeEditor',
|
||||
}),
|
||||
validation: () => ({
|
||||
validate: (value) => {
|
||||
const validateFn = validate[DataStream.BROWSER][ConfigKey.PLAYWRIGHT_OPTIONS];
|
||||
if (validateFn) {
|
||||
return !validateFn({
|
||||
[ConfigKey.PLAYWRIGHT_OPTIONS]: value,
|
||||
});
|
||||
}
|
||||
validate: {
|
||||
validPlaywrightOptions: (value) => {
|
||||
const validateFn = validate[DataStream.BROWSER][ConfigKey.PLAYWRIGHT_OPTIONS];
|
||||
if (validateFn) {
|
||||
return validateFn({
|
||||
[ConfigKey.PLAYWRIGHT_OPTIONS]: value,
|
||||
})
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.playwrightOptions.error', {
|
||||
defaultMessage: 'Invalid JSON format',
|
||||
})
|
||||
: true;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
@ -1346,16 +1382,17 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
})}
|
||||
</span>
|
||||
),
|
||||
props: ({ setValue, field }): EuiComboBoxProps<string> => ({
|
||||
props: ({ setValue, field, trigger }): EuiComboBoxProps<string> => ({
|
||||
id: 'syntheticsMontiorConfigSyntheticsArgs',
|
||||
selectedOptions: Object.values(field?.value || []).map((arg) => ({
|
||||
label: arg,
|
||||
})),
|
||||
onChange: (updatedValues: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
onChange: async (updatedValues: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setValue(
|
||||
ConfigKey.SYNTHETICS_ARGS,
|
||||
updatedValues.map((option) => option.label)
|
||||
);
|
||||
await trigger(ConfigKey.SYNTHETICS_ARGS);
|
||||
},
|
||||
onCreateOption: (newValue: string) => {
|
||||
setValue(ConfigKey.SYNTHETICS_ARGS, [...(field?.value || []), newValue]);
|
||||
|
@ -1400,7 +1437,12 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
const [responseBodyIndex] = dependencies || [];
|
||||
return responseBodyIndex === ResponseBodyIndexPolicy.NEVER;
|
||||
},
|
||||
props: (): EuiFieldNumberProps => ({ min: 1, step: 'any', readOnly }),
|
||||
props: (): EuiFieldNumberProps => ({
|
||||
'data-test-subj': 'syntheticsMonitorConfigMaxBytes',
|
||||
min: 1,
|
||||
step: 'any',
|
||||
readOnly,
|
||||
}),
|
||||
dependencies: [ConfigKey.RESPONSE_BODY_INDEX],
|
||||
},
|
||||
[ConfigKey.IPV4]: {
|
||||
|
@ -1414,7 +1456,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
dependencies: [ConfigKey.IPV6],
|
||||
props: ({ field, setValue, dependencies }): EuiComboBoxProps<string> => {
|
||||
props: ({ field, setValue, trigger, dependencies }): EuiComboBoxProps<string> => {
|
||||
const [ipv6] = dependencies;
|
||||
const ipv4 = field?.value;
|
||||
const values: string[] = [];
|
||||
|
@ -1436,7 +1478,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
selectedOptions: values.map((version) => ({
|
||||
label: version,
|
||||
})),
|
||||
onChange: (updatedValues: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
onChange: async (updatedValues: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setValue(
|
||||
ConfigKey.IPV4,
|
||||
updatedValues.some((value) => value.label === 'IPv4')
|
||||
|
@ -1445,6 +1487,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
ConfigKey.IPV6,
|
||||
updatedValues.some((value) => value.label === 'IPv6')
|
||||
);
|
||||
await trigger([ConfigKey.IPV4, ConfigKey.IPV4]);
|
||||
},
|
||||
isDisabled: readOnly,
|
||||
};
|
||||
|
@ -1461,12 +1504,17 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
}),
|
||||
controlled: true,
|
||||
validation: () => ({
|
||||
validate: (headers) => !validateHeaders(headers),
|
||||
}),
|
||||
error: i18n.translate('xpack.synthetics.monitorConfig.proxyHeaders.error', {
|
||||
defaultMessage: 'The header key must be a valid HTTP token.',
|
||||
validate: {
|
||||
validHeaders: (headers) =>
|
||||
validateHeaders(headers)
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.proxyHeaders.error', {
|
||||
defaultMessage: 'The header key must be a valid HTTP token.',
|
||||
})
|
||||
: true,
|
||||
},
|
||||
}),
|
||||
props: (): HeaderFieldProps => ({
|
||||
'data-test-subj': 'syntheticsHeaderFieldProxyHeaders',
|
||||
readOnly,
|
||||
}),
|
||||
},
|
||||
|
@ -1481,7 +1529,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
'A list of expressions executed against the body when parsed as JSON. The body size must be less than or equal to 100 MiB.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field, setValue }): KeyValuePairsFieldProps => ({
|
||||
props: ({ field, setValue, trigger }): KeyValuePairsFieldProps => ({
|
||||
readOnly,
|
||||
keyLabel: i18n.translate('xpack.synthetics.monitorConfig.responseJSON.key.label', {
|
||||
defaultMessage: 'Description',
|
||||
|
@ -1495,7 +1543,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
defaultMessage: 'Add expression',
|
||||
}
|
||||
),
|
||||
onChange: (pairs) => {
|
||||
onChange: async (pairs) => {
|
||||
const value: ResponseCheckJSON[] = pairs
|
||||
.map((pair) => {
|
||||
const [description, expression] = pair;
|
||||
|
@ -1507,21 +1555,23 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
.filter((pair) => pair.description || pair.expression);
|
||||
if (!isEqual(value, field?.value)) {
|
||||
setValue(ConfigKey.RESPONSE_JSON_CHECK, value);
|
||||
await trigger(ConfigKey.RESPONSE_JSON_CHECK);
|
||||
}
|
||||
},
|
||||
defaultPairs: field?.value.map((check) => [check.description, check.expression]) || [],
|
||||
}),
|
||||
validation: () => {
|
||||
return {
|
||||
validate: (value: ResponseCheckJSON[]) => {
|
||||
validation: () => ({
|
||||
validate: {
|
||||
validBodyJSON: (value: ResponseCheckJSON[]) => {
|
||||
if (value.some((check) => !check.expression || !check.description)) {
|
||||
return i18n.translate('xpack.synthetics.monitorConfig.responseJSON.error', {
|
||||
defaultMessage:
|
||||
"This JSON expression isn't valid. Make sure that both the label and expression are defined.",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import { get, pick } from 'lodash';
|
|||
import { ConfigKey, DataStream, FormMonitorType, MonitorFields } from '../types';
|
||||
import { DEFAULT_FIELDS } from '../constants';
|
||||
|
||||
export const formatter = (fields: Record<string, any>) => {
|
||||
export const serializeNestedFormField = (fields: Record<string, any>) => {
|
||||
const monitorType = fields[ConfigKey.MONITOR_TYPE] as DataStream;
|
||||
const monitorFields: Record<string, any> = {};
|
||||
const defaults = DEFAULT_FIELDS[monitorType] as MonitorFields;
|
||||
|
@ -23,7 +23,7 @@ export const formatter = (fields: Record<string, any>) => {
|
|||
export const ALLOWED_FIELDS = [ConfigKey.ENABLED, ConfigKey.ALERT_CONFIG];
|
||||
|
||||
export const format = (fields: Record<string, unknown>, readOnly: boolean = false) => {
|
||||
const formattedFields = formatter(fields) as MonitorFields;
|
||||
const formattedFields = serializeNestedFormField(fields) as MonitorFields;
|
||||
const textAssertion = formattedFields[ConfigKey.TEXT_ASSERTION]
|
||||
? `
|
||||
await page.getByText('${formattedFields[ConfigKey.TEXT_ASSERTION]}').first().waitFor();`
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { EuiForm, EuiSpacer } from '@elastic/eui';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { useFormWrapped } from '../hooks/use_form_wrapped';
|
||||
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
|
||||
import { FormMonitorType, SyntheticsMonitor } from '../types';
|
||||
import { getDefaultFormFields, formatDefaultFormValues } from './defaults';
|
||||
import { ActionBar } from './submit';
|
||||
|
@ -21,7 +21,7 @@ export const MonitorForm: React.FC<{
|
|||
}> = ({ children, defaultValues, space, readOnly = false }) => {
|
||||
const methods = useFormWrapped({
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues:
|
||||
formatDefaultFormValues(defaultValues as SyntheticsMonitor) ||
|
||||
getDefaultFormFields(space)[FormMonitorType.MULTISTEP],
|
||||
|
|
|
@ -26,9 +26,7 @@ export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => {
|
|||
const history = useHistory();
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors, defaultValues },
|
||||
getValues,
|
||||
getFieldState,
|
||||
formState: { defaultValues, isValid },
|
||||
} = useFormContext();
|
||||
|
||||
const [monitorPendingDeletion, setMonitorPendingDeletion] = useState<SyntheticsMonitor | null>(
|
||||
|
@ -42,12 +40,7 @@ export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => {
|
|||
const canEditSynthetics = useCanEditSynthetics();
|
||||
|
||||
const formSubmitter = (formData: Record<string, any>) => {
|
||||
// An additional invalid field check to account for customHook managed validation
|
||||
const isAnyFieldInvalid = Object.keys(getValues()).some(
|
||||
(fieldKey) => getFieldState(fieldKey).invalid
|
||||
);
|
||||
|
||||
if (!Object.keys(errors).length && !isAnyFieldInvalid) {
|
||||
if (isValid) {
|
||||
setMonitorData(format(formData, readOnly));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,4 +6,5 @@
|
|||
*/
|
||||
|
||||
export * from './use_is_edit_flow';
|
||||
export * from './use_validate_field';
|
||||
export { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { FieldValues, useForm, UseFormProps } from 'react-hook-form';
|
||||
|
||||
export function useFormWrapped<TFieldValues extends FieldValues = FieldValues, TContext = any>(
|
||||
props?: UseFormProps<TFieldValues, TContext>
|
||||
) {
|
||||
const { register, ...restOfForm } = useForm(props);
|
||||
|
||||
const euiRegister = useCallback(
|
||||
(name, ...registerArgs) => {
|
||||
const { ref, ...restOfRegister } = register(name, ...registerArgs);
|
||||
|
||||
return {
|
||||
inputRef: ref,
|
||||
ref,
|
||||
...restOfRegister,
|
||||
};
|
||||
},
|
||||
[register]
|
||||
);
|
||||
|
||||
return {
|
||||
register: euiRegister,
|
||||
...restOfForm,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { useEffect, useState, ComponentProps } from 'react';
|
||||
import { Controller, ControllerFieldState, useFormContext } from 'react-hook-form';
|
||||
import { FieldMeta, FormConfig } from '../types';
|
||||
|
||||
export function useValidateField<TFieldKey extends keyof FormConfig>({
|
||||
fieldKey,
|
||||
validation,
|
||||
validationError,
|
||||
required,
|
||||
dependencies,
|
||||
customHook,
|
||||
}: {
|
||||
fieldKey: FieldMeta<TFieldKey>['fieldKey'];
|
||||
validation: FieldMeta<TFieldKey>['validation'];
|
||||
validationError: FieldMeta<TFieldKey>['error'];
|
||||
required: boolean;
|
||||
dependencies: FieldMeta<TFieldKey>['dependencies'];
|
||||
customHook: FieldMeta<TFieldKey>['customHook'];
|
||||
}) {
|
||||
const { getValues, formState, getFieldState, watch } = useFormContext<FormConfig>();
|
||||
const fieldState = getFieldState(fieldKey, formState);
|
||||
const fieldValue = getValues(fieldKey);
|
||||
const fieldError = fieldState.error;
|
||||
const isFieldTouched = fieldState.isTouched;
|
||||
const isFieldInvalid = fieldState.invalid;
|
||||
|
||||
const [dependenciesFieldMeta, setDependenciesFieldMeta] = useState<
|
||||
Record<string, ControllerFieldState>
|
||||
>({});
|
||||
let dependenciesValues: unknown[] = [];
|
||||
|
||||
if (dependencies) {
|
||||
dependenciesValues = watch(dependencies);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (dependencies) {
|
||||
dependencies.forEach((dependency) => {
|
||||
setDependenciesFieldMeta((prevState) => ({
|
||||
...prevState,
|
||||
[dependency]: getFieldState(dependency),
|
||||
}));
|
||||
});
|
||||
}
|
||||
// run effect when dependencies values change, to get the most up-to-date meta state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(dependenciesValues || []), dependencies, getFieldState]);
|
||||
|
||||
let hookFn: Function = () => {};
|
||||
let hookProps;
|
||||
|
||||
if (customHook) {
|
||||
hookProps = customHook(fieldValue);
|
||||
hookFn = hookProps.func;
|
||||
}
|
||||
const { [hookProps?.fieldKey as string]: hookResult } = hookFn(hookProps?.params) || {};
|
||||
const hookErrorContent = hookProps?.error;
|
||||
const hookError = hookResult ? hookProps?.error : undefined;
|
||||
|
||||
const { validate: fieldValidator, ...fieldRules } = validation?.(dependenciesValues) ?? {};
|
||||
const validatorsWithHook = {
|
||||
validHook: () => (hookError ? hookErrorContent : true),
|
||||
...(fieldValidator ?? {}),
|
||||
};
|
||||
|
||||
const showFieldAsInvalid = isFieldInvalid && (isFieldTouched || formState.isSubmitted);
|
||||
|
||||
return {
|
||||
dependenciesValues,
|
||||
dependenciesFieldMeta,
|
||||
isInvalid: showFieldAsInvalid,
|
||||
error: showFieldAsInvalid ? fieldError?.message || validationError : undefined,
|
||||
rules: {
|
||||
required,
|
||||
...(fieldRules ?? {}),
|
||||
validate: validatorsWithHook,
|
||||
} as ComponentProps<typeof Controller>['rules'],
|
||||
};
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { mockGlobals } from '../../utils/testing';
|
||||
import { render } from '../../utils/testing/rtl_helpers';
|
||||
import { MonitorEditPage } from './monitor_edit_page';
|
||||
|
@ -200,7 +202,7 @@ describe('MonitorEditPage', () => {
|
|||
|
||||
it.each([true, false])(
|
||||
'shows duplicate error when "nameAlreadyExists" is %s',
|
||||
(nameAlreadyExists) => {
|
||||
async (nameAlreadyExists) => {
|
||||
(useMonitorName as jest.Mock).mockReturnValue({ nameAlreadyExists });
|
||||
|
||||
jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({
|
||||
|
@ -216,7 +218,7 @@ describe('MonitorEditPage', () => {
|
|||
refetch: () => null,
|
||||
loading: false,
|
||||
});
|
||||
const { getByText, queryByText } = render(<MonitorEditPage />, {
|
||||
const { getByText, queryByText, getByTestId } = render(<MonitorEditPage />, {
|
||||
state: {
|
||||
serviceLocations: {
|
||||
locations: [
|
||||
|
@ -235,8 +237,13 @@ describe('MonitorEditPage', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const inputField = getByTestId('syntheticsMonitorConfigName');
|
||||
fireEvent.focus(inputField);
|
||||
userEvent.type(inputField, 'any value'); // Hook is made to return duplicate error as true
|
||||
fireEvent.blur(inputField);
|
||||
|
||||
if (nameAlreadyExists) {
|
||||
expect(getByText('Monitor name already exists')).toBeInTheDocument();
|
||||
await waitFor(() => getByText('Monitor name already exists'));
|
||||
} else {
|
||||
expect(queryByText('Monitor name already exists')).not.toBeInTheDocument();
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiSteps, EuiPanel, EuiText, EuiSpacer } from '@elastic/eui';
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { InspectMonitorPortal } from './inspect_monitor_portal';
|
||||
import { ConfigKey, FormMonitorType, StepMap } from '../types';
|
||||
import { serializeNestedFormField } from '../form/formatter';
|
||||
import { AdvancedConfig } from '../advanced';
|
||||
import { MonitorTypePortal } from './monitor_type_portal';
|
||||
import { ReadOnlyCallout } from './read_only_callout';
|
||||
|
@ -25,7 +26,7 @@ export const MonitorSteps = ({
|
|||
isEditFlow?: boolean;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
const { watch } = useFormContext();
|
||||
const { watch, formState } = useFormContext();
|
||||
const [type]: [FormMonitorType] = watch([ConfigKey.FORM_MONITOR_TYPE]);
|
||||
const steps = stepMap[type];
|
||||
|
||||
|
@ -55,7 +56,10 @@ export const MonitorSteps = ({
|
|||
)}
|
||||
<AdvancedConfig readOnly={readOnly} />
|
||||
<MonitorTypePortal monitorType={type} />
|
||||
<InspectMonitorPortal />
|
||||
<InspectMonitorPortal
|
||||
isValid={formState.isValid}
|
||||
monitorFields={serializeNestedFormField(watch())}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,13 +7,20 @@
|
|||
|
||||
import React from 'react';
|
||||
import { InPortal } from 'react-reverse-portal';
|
||||
import { MonitorFields } from '../../../../../../common/runtime_types';
|
||||
import { MonitorInspectWrapper } from '../../common/components/monitor_inspect';
|
||||
import { InspectMonitorPortalNode } from '../portals';
|
||||
|
||||
export const InspectMonitorPortal = () => {
|
||||
export const InspectMonitorPortal = ({
|
||||
isValid,
|
||||
monitorFields,
|
||||
}: {
|
||||
isValid: boolean;
|
||||
monitorFields: MonitorFields;
|
||||
}) => {
|
||||
return (
|
||||
<InPortal node={InspectMonitorPortalNode}>
|
||||
<MonitorInspectWrapper />
|
||||
<MonitorInspectWrapper isValid={isValid} monitorFields={monitorFields} />
|
||||
</InPortal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
ControllerRenderProps,
|
||||
ControllerFieldState,
|
||||
FormState,
|
||||
UseControllerProps,
|
||||
FieldValues,
|
||||
} from 'react-hook-form';
|
||||
import {
|
||||
ConfigKey,
|
||||
|
@ -81,6 +83,7 @@ export interface FieldMeta<TFieldKey extends keyof FormConfig> {
|
|||
field?: ControllerRenderProps<FormConfig, TFieldKey>;
|
||||
formState: FormState<FormConfig>;
|
||||
setValue: UseFormReturn<FormConfig>['setValue'];
|
||||
trigger: UseFormReturn<FormConfig>['trigger'];
|
||||
reset: UseFormReturn<FormConfig>['reset'];
|
||||
locations: Array<ServiceLocation & { key: string }>;
|
||||
dependencies: unknown[];
|
||||
|
@ -90,7 +93,6 @@ export interface FieldMeta<TFieldKey extends keyof FormConfig> {
|
|||
}) => Record<string, any>;
|
||||
controlled?: boolean;
|
||||
required?: boolean;
|
||||
shouldUseSetValue?: boolean;
|
||||
customHook?: (value: unknown) => {
|
||||
// custom hooks are only supported for controlled components and only supported for determining error validation
|
||||
func: Function;
|
||||
|
@ -102,9 +104,9 @@ export interface FieldMeta<TFieldKey extends keyof FormConfig> {
|
|||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
formOnChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
) => void;
|
||||
validation?: (dependencies: unknown[]) => Parameters<UseFormReturn['register']>[1];
|
||||
validation?: (dependencies: unknown[]) => UseControllerProps<FieldValues, TFieldKey>['rules'];
|
||||
error?: React.ReactNode;
|
||||
dependencies?: Array<keyof FormConfig>; // fields that another field may depend for or validation. Values are passed to the validation function
|
||||
dependencies?: Array<keyof FormConfig>; // fields that another field may depend on or for validation. Values are passed to the validation function
|
||||
}
|
||||
|
||||
export interface FieldMap {
|
||||
|
|
|
@ -5,30 +5,58 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { FieldValues, useForm, UseFormProps } from 'react-hook-form';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { FieldValues, useForm, UseFormProps, ChangeHandler } from 'react-hook-form';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
|
||||
export function useFormWrapped<TFieldValues extends FieldValues = FieldValues, TContext = any>(
|
||||
props?: UseFormProps<TFieldValues, TContext>
|
||||
) {
|
||||
const form = useForm<TFieldValues>(props);
|
||||
const { register, trigger, ...restOfForm } = useForm(props);
|
||||
|
||||
const [changed, setChanged] = useState<boolean>(false);
|
||||
useDebounce(
|
||||
async () => {
|
||||
if (changed) {
|
||||
await trigger?.(); // Manually invalidate whole form to make dependency validations reactive
|
||||
}
|
||||
},
|
||||
500,
|
||||
[changed]
|
||||
);
|
||||
|
||||
// Wrap `onChange` to validate form to trigger validations
|
||||
const euiOnChange = useCallback(
|
||||
(onChange: ChangeHandler) => {
|
||||
return async (event: Parameters<ChangeHandler>[0]) => {
|
||||
setChanged(false);
|
||||
const onChangeResult = await onChange(event);
|
||||
setChanged(true);
|
||||
|
||||
return onChangeResult;
|
||||
};
|
||||
},
|
||||
[setChanged]
|
||||
);
|
||||
|
||||
// Wrap react-hook-form register method to wire `onChange` and `inputRef`
|
||||
const euiRegister = useCallback(
|
||||
(name, ...registerArgs) => {
|
||||
const { ref, ...restOfRegister } = form.register(name, ...registerArgs);
|
||||
const { ref, onChange, ...restOfRegister } = register(name, ...registerArgs);
|
||||
|
||||
return {
|
||||
inputRef: ref,
|
||||
ref,
|
||||
onChange: euiOnChange(onChange),
|
||||
...restOfRegister,
|
||||
};
|
||||
},
|
||||
[form]
|
||||
[register, euiOnChange]
|
||||
);
|
||||
const formState = form.formState;
|
||||
|
||||
return useMemo(
|
||||
() => ({ ...form, register: euiRegister, formState }),
|
||||
[euiRegister, form, formState]
|
||||
);
|
||||
return {
|
||||
register: euiRegister,
|
||||
trigger,
|
||||
...restOfForm,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue