Grafana-UI: Update React Hook Form to v7 (#33328)

* Update hook form

* Update Form component

* Update ChangePassword.tsx

* Update custom types

* Update SaveDashboardForm

* Update form story

* Update FieldArray.story.tsx

* Bump hook form version

* Update typescript to v4.2.4

* Update ForgottenPassword.tsx

* Update LoginForm.tsx

* Update SignupPage.tsx

* Update VerifyEmail.tsx

* Update AdminEditOrgPage.tsx

* Update UserCreatePage.tsx

* Update BasicSettings.tsx

* Update NotificationChannelForm.tsx

* Update NotificationChannelOptions.tsx

* Update NotificationSettings.tsx

* Update OptionElement.tsx

* Update AlertRuleForm.tsx

* Update AlertTypeStep.tsx

* Update AnnotationsField.tsx

* Update ConditionField.tsx

* Update ConditionsStep.tsx

* Update GroupAndNamespaceFields.tsx

* Update LabelsField.tsx

* Update QueryStep.tsx

* Update RowOptionsForm.tsx

* Update SaveDashboardAsForm.tsx

* Update NewDashboardsFolder.tsx

* Update ImportDashboardForm.tsx

* Update DashboardImportPage.tsx

* Update NewOrgPage.tsx

* Update OrgProfile.tsx

* Update UserInviteForm.tsx

* Update PlaylistForm.tsx

* Update ChangePasswordForm.tsx

* Update UserProfileEditForm.tsx

* Update TeamSettings.tsx

* Update SignupInvited.tsx

* Expose setValue from the Form

* Update typescript to v4.2.4

* Remove ref from field props

* Fix tests

* Revert TS update

* Use exact version

* Update latest batch of changes

* Reduce the number of strict TS errors

* Fix defaults

* more type error fixes

* Update CreateTeam

* fix folder picker in rule form

* fixes for hook form 7

* Update docs

Co-authored-by: Domas <domasx2@gmail.com>
This commit is contained in:
Alex Khomenko 2021-04-29 16:54:38 +03:00 committed by GitHub
parent 9de2f1bb8f
commit 3b515e650c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 471 additions and 433 deletions

View File

@ -71,7 +71,7 @@
"react-custom-scrollbars": "4.2.1", "react-custom-scrollbars": "4.2.1",
"react-dom": "17.0.1", "react-dom": "17.0.1",
"react-highlight-words": "0.16.0", "react-highlight-words": "0.16.0",
"react-hook-form": "5.1.3", "react-hook-form": "7.2.3",
"react-popper": "2.2.4", "react-popper": "2.2.4",
"react-storybook-addon-props-combinations": "1.1.0", "react-storybook-addon-props-combinations": "1.1.0",
"react-table": "7.0.0", "react-table": "7.0.0",

View File

@ -17,7 +17,7 @@ export default {
}; };
export const simple = () => { export const simple = () => {
const defaultValues = { const defaultValues: any = {
people: [{ firstName: 'Janis', lastName: 'Joplin' }], people: [{ firstName: 'Janis', lastName: 'Joplin' }],
}; };
return ( return (
@ -30,8 +30,16 @@ export const simple = () => {
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem' }}>
{fields.map((field, index) => ( {fields.map((field, index) => (
<HorizontalGroup key={field.id}> <HorizontalGroup key={field.id}>
<Input ref={register()} name={`people[${index}].firstName`} value={field.firstName} /> <Input
<Input ref={register()} name={`people[${index}].lastName`} value={field.lastName} /> key={field.id}
{...register(`people.${index}.firstName` as const)}
defaultValue={field.firstName}
/>
<Input
key={field.id}
{...register(`people.${index}.lastName` as const)}
defaultValue={field.lastName}
/>
</HorizontalGroup> </HorizontalGroup>
))} ))}
</div> </div>

View File

@ -1,4 +1,4 @@
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks"; import { Meta, Props } from "@storybook/addon-docs/blocks";
import { Form } from "./Form"; import { Form } from "./Form";
<Meta title="MDX|Form" component={Form} /> <Meta title="MDX|Form" component={Form} />
@ -29,8 +29,8 @@ const defaultUser: Partial<UserDTO> = {
>{({register, errors}) => { >{({register, errors}) => {
return ( return (
<Field> <Field>
<Input name="name" ref={register}/> <Input {...register("name")}/>
<Input type="email" name="email" ref={register({required: true})}/> <Input {...register("email", {required: true})} type="email" />
<Button type="submit">Create User</Button> <Button type="submit">Create User</Button>
</Field> </Field>
) )
@ -43,18 +43,17 @@ const defaultUser: Partial<UserDTO> = {
#### `register` #### `register`
`register` allows to register form elements(inputs, selects, radios, etc) in the form. In order to do that you need to pass `register` as a `ref` property to the form input. For example: `register` allows registering form elements (inputs, selects, radios, etc) in the form. In order to do that you need to invoke the function itself and spread the props into the input. For example:
```jsx ```jsx
<Input name="inputName" ref={register} /> <Input {...register("inputName")} />
``` ```
Register accepts an object which describes validation rules for a given input: The first argument for `register` is the field name. It also accepts an object, which describes validation rules for a given input:
```jsx ```jsx
<Input <Input
name="inputName" {...register("inputName", {
ref={register({
required: true, required: true,
minLength: 10, minLength: 10,
validate: v => { // custom validation rule } validate: v => { // custom validation rule }
@ -70,7 +69,7 @@ See [Validation](#validation) for examples on validation and validation rules.
```jsx ```jsx
<Field label="Name" invalid={!!errors.name} error="Name is required"> <Field label="Name" invalid={!!errors.name} error="Name is required">
<Input name="name" ref={register({ required: true })} /> <Input {...register('name', { required: true })} />
</Field> </Field>
``` ```
@ -89,22 +88,20 @@ import { Form, Field, InputControl } from '@grafana/ui';
<Field label="RadioButtonExample"> <Field label="RadioButtonExample">
<InputControl <InputControl
{/* Render InputControl as controlled input (RadioButtonGroup) */} {/* Render InputControl as controlled input (RadioButtonGroup) */}
as={RadioButtonGroup} render={({field}) => <RadioButtonGroup {...field} options={...} />}
{/* Pass control exposed from Form render prop */} {/* Pass control exposed from Form render prop */}
control={control} control={control}
name="radio" name="radio"
options={...}
/> />
</Field> </Field>
<Field label="SelectExample"> <Field label="SelectExample">
<InputControl <InputControl
{/* Render InputControl as controlled input (Select) */} {/* Render InputControl as controlled input (Select) */}
as={Select} render={({field}) => <Select {...field} options={...} />}
{/* Pass control exposed from Form render prop */} {/* Pass control exposed from Form render prop */}
control={control} control={control}
name="select" name="select"
options={...}
/> />
</Field> </Field>
</> </>
@ -112,32 +109,30 @@ import { Form, Field, InputControl } from '@grafana/ui';
</Form> </Form>
``` ```
Note that when using `InputControl`, it expects the name of the prop that handles input change to be called `onChange`. In case we want to modify the selected value before passing it to the form, we can use the `onChange` callback from the render's `field` argument:
If the property is named differently for any specific component, additional `onChangeName` prop has to be provided, specifying the name.
Additionally, the `onChange` arguments passed as an array. Check [react-hook-form docs](https://react-hook-form.com/api/#Controller)
for more prop options.
```jsx ```jsx
{/* DashboardPicker has onSelected prop instead of onChange */}
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
{/* In case of Select the value has to be returned as an object with a `value` key for the value to be saved to form data */}
const onSelectChange = ([value]) => {
// ...
return { value };
}
<Field label="SelectExample"> <Field label="SelectExample">
<InputControl <InputControl
as={DashboardPicker} {/* Here `value` has a nested `value` property, which we want to save onto the form. */}
render={(field: {onChange, ...field}) => <Select {...field} onChange={(value) => onChange(value.value)}/>}
control={control} control={control}
name="select" name="select"
onSelected={onSelectChange}
{/* Pass the name of the onChange handler */}
onChangeName='onSelected'
/> />
</Field> </Field>
```
Note that `field` also contains `ref` prop, which is passed down to the rendered component by default. In case if that component doesn't support this prop, it will need to be removed before spreading the `field`.
```jsx
<Field label="SelectExample">
<InputControl
{/*Remove `ref` prop, so it doesn't get passed down to the component that doesn't support it. */}
render={(field: {onChange, ref, ...field}) => <Select {...field} onChange={(value) => onChange(value.value)}/>}
control={control}
name="select"
/>
</Field>
``` ```
### Default values ### Default values
@ -179,7 +174,7 @@ const defaultValues: FormDto {
<Form ...>{ <Form ...>{
({register}) => ( ({register}) => (
<> <>
<Input defaultValue={default.name} name="name" ref={register} /> <Input {...register("name")} defaultValue={default.name} />
</> </>
)} )}
</Form> </Form>
@ -197,9 +192,8 @@ Validation can be performed either synchronously or asynchronously. What's impor
<> <>
<Field invalid={!!errors.name} error={errors.name && 'Name is required'} <Field invalid={!!errors.name} error={errors.name && 'Name is required'}
<Input <Input
{...register("name", { required: true })}
defaultValue={default.name} defaultValue={default.name}
name="name"
ref={register({ required: true })}
/> />
</> </>
)} )}
@ -217,8 +211,7 @@ One important thing to note is that if you want to provide different error messa
<Field invalid={!!errors.name} error={errors.name?.message } <Field invalid={!!errors.name} error={errors.name?.message }
<Input <Input
defaultValue={default.name} defaultValue={default.name}
name="name" {...register("name", {
ref={register({
required: 'Name is required', required: 'Name is required',
validation: v => { validation: v => {
return v !== 'John' && 'Name must be John' return v !== 'John' && 'Name must be John'
@ -258,8 +251,7 @@ validateAsync = (newValue: string) => {
<Field invalid={!!errors.name} error={errors.name?.message} <Field invalid={!!errors.name} error={errors.name?.message}
<Input <Input
defaultValue={default.name} defaultValue={default.name}
name="name" {...register("name", {
ref={register({
required: 'Name is required', required: 'Name is required',
validation: async v => { validation: async v => {
return await validateAsync(v); return await validateAsync(v);
@ -271,6 +263,26 @@ validateAsync = (newValue: string) => {
</Form> </Form>
``` ```
### Upgrading to v8
Version 8 of Grafana-UI is using version 7 of `react-hook-form` (previously version 5 was used), which introduced a few breaking changes to the `Form` API. The detailed list of changes can be found in the library's migration guides:
- [Migration guide v5 to v6](https://react-hook-form.com/migrate-v5-to-v6/)
- [Migration guide v6 to v7](https://react-hook-form.com/migrate-v6-to-v7/)
In a nutshell, the two most important changes are:
- register method is no longer passed as a `ref`, but instead its result is spread onto the input component:
```jsx
- <input ref={register({ required: true })} name="test" />
+ <input {...register('test', { required: true })} />
```
- `InputControl`'s `as` prop has been replaced with `render`, which has `field` and `fieldState` objects as arguments. `onChange`, `onBlur`, `value`, `name`, and `ref` are parts of `field`.
```jsx
- <Controller as={<input />} />
+ <Controller render={({ field }) => <input {...field} />}
// or
+ <Controller render={({ field, fieldState }) => <input {...field} />} />
```
### Props ### Props
<Props of={Form} /> <Props of={Form} />

View File

@ -1,8 +1,4 @@
import React from 'react'; import React from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
import mdx from './Form.mdx';
import { ValidateResult } from 'react-hook-form'; import { ValidateResult } from 'react-hook-form';
import { Story } from '@storybook/react'; import { Story } from '@storybook/react';
import { import {
@ -18,9 +14,12 @@ import {
TextArea, TextArea,
RadioButtonGroup, RadioButtonGroup,
} from '@grafana/ui'; } from '@grafana/ui';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
import mdx from './Form.mdx';
export default { export default {
title: 'Forms/Example forms', title: 'Forms/Form',
decorators: [withStoryContainer, withCenteredStory], decorators: [withStoryContainer, withCenteredStory],
parameters: { parameters: {
docs: { docs: {
@ -48,20 +47,20 @@ const selectOptions = [
]; ];
interface FormDTO { interface FormDTO {
name: string; name?: string;
email: string; email?: string;
username: string; username?: string;
checkbox: boolean; checkbox?: boolean;
switch: boolean; switch: boolean;
radio: string; radio: string;
select: string; select: string;
text: string; text?: string;
nested: { nested: {
path: string; path: string;
}; };
} }
const renderForm = (defaultValues?: Partial<FormDTO>) => ( const renderForm = (defaultValues?: FormDTO) => (
<Form <Form
defaultValues={defaultValues} defaultValues={defaultValues}
onSubmit={(data: FormDTO) => { onSubmit={(data: FormDTO) => {
@ -74,34 +73,38 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => (
<Legend>Edit user</Legend> <Legend>Edit user</Legend>
<Field label="Name" invalid={!!errors.name} error="Name is required"> <Field label="Name" invalid={!!errors.name} error="Name is required">
<Input name="name" placeholder="Roger Waters" ref={register({ required: true })} /> <Input {...register('name', { required: true })} placeholder="Roger Waters" />
</Field> </Field>
<Field label="Email" invalid={!!errors.email} error="E-mail is required"> <Field label="Email" invalid={!!errors.email} error="E-mail is required">
<Input id="email" name="email" placeholder="roger.waters@grafana.com" ref={register({ required: true })} /> <Input {...register('email', { required: true })} id="email" placeholder="roger.waters@grafana.com" />
</Field> </Field>
<Field label="Username"> <Field label="Username">
<Input name="username" placeholder="mr.waters" ref={register} /> <Input {...register('username')} placeholder="mr.waters" />
</Field> </Field>
<Field label="Nested object"> <Field label="Nested object">
<Input name="nested.path" placeholder="Nested path" ref={register} /> <Input {...register('nested.path')} placeholder="Nested path" />
</Field> </Field>
<Field label="Textarea" invalid={!!errors.text} error="Text is required"> <Field label="Textarea" invalid={!!errors.text} error="Text is required">
<TextArea name="text" placeholder="Long text" ref={register({ required: true })} /> <TextArea {...register('text', { required: true })} placeholder="Long text" />
</Field> </Field>
<Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent"> <Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent">
<Checkbox name="checkbox" label="Do you consent?" ref={register({ required: true })} /> <Checkbox {...register('checkbox', { required: true })} label="Do you consent?" />
</Field> </Field>
<Field label="Switch"> <Field label="Switch">
<Switch name="switch" ref={register} /> <Switch name="switch" {...register} />
</Field> </Field>
<Field label="RadioButton"> <Field label="RadioButton">
<InputControl name="radio" control={control} options={selectOptions} as={RadioButtonGroup} /> <InputControl
name="radio"
control={control}
render={({ field }) => <RadioButtonGroup {...field} options={selectOptions} />}
/>
</Field> </Field>
<Field label="Select" invalid={!!errors.select} error="Select is required"> <Field label="Select" invalid={!!errors.select} error="Select is required">
@ -111,8 +114,7 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => (
rules={{ rules={{
required: true, required: true,
}} }}
options={selectOptions} render={({ field }) => <Select {...field} options={selectOptions} />}
as={Select}
/> />
</Field> </Field>
@ -158,9 +160,8 @@ export const AsyncValidation: Story = ({ passAsyncValidation }) => {
<Field label="Name" invalid={!!errors.name} error="Username is already taken"> <Field label="Name" invalid={!!errors.name} error="Username is already taken">
<Input <Input
name="name"
placeholder="Roger Waters" placeholder="Roger Waters"
ref={register({ validate: validateAsync(passAsyncValidation) })} {...register('name', { validate: validateAsync(passAsyncValidation) })}
/> />
</Field> </Field>

View File

@ -1,14 +1,14 @@
import React, { HTMLProps, useEffect } from 'react'; import React, { HTMLProps, useEffect } from 'react';
import { useForm, Mode, OnSubmit, DeepPartial } from 'react-hook-form'; import { useForm, Mode, DeepPartial, UnpackNestedValue, SubmitHandler } from 'react-hook-form';
import { FormAPI } from '../../types'; import { FormAPI } from '../../types';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
interface FormProps<T> extends Omit<HTMLProps<HTMLFormElement>, 'onSubmit'> { interface FormProps<T> extends Omit<HTMLProps<HTMLFormElement>, 'onSubmit'> {
validateOn?: Mode; validateOn?: Mode;
validateOnMount?: boolean; validateOnMount?: boolean;
validateFieldsOnMount?: string[]; validateFieldsOnMount?: string | string[];
defaultValues?: DeepPartial<T>; defaultValues?: UnpackNestedValue<DeepPartial<T>>;
onSubmit: OnSubmit<T>; onSubmit: SubmitHandler<T>;
children: (api: FormAPI<T>) => React.ReactNode; children: (api: FormAPI<T>) => React.ReactNode;
/** Sets max-width for container. Use it instead of setting individual widths on inputs.*/ /** Sets max-width for container. Use it instead of setting individual widths on inputs.*/
maxWidth?: number | 'none'; maxWidth?: number | 'none';
@ -24,16 +24,17 @@ export function Form<T>({
maxWidth = 600, maxWidth = 600,
...htmlProps ...htmlProps
}: FormProps<T>) { }: FormProps<T>) {
const { handleSubmit, register, errors, control, triggerValidation, getValues, formState, watch } = useForm<T>({ const { handleSubmit, register, control, trigger, getValues, formState, watch, setValue } = useForm<T>({
mode: validateOn, mode: validateOn,
defaultValues, defaultValues,
}); });
useEffect(() => { useEffect(() => {
if (validateOnMount) { if (validateOnMount) {
triggerValidation(validateFieldsOnMount); //@ts-expect-error
trigger(validateFieldsOnMount);
} }
}, [triggerValidation, validateFieldsOnMount, validateOnMount]); }, [trigger, validateFieldsOnMount, validateOnMount]);
return ( return (
<form <form
@ -44,7 +45,7 @@ export function Form<T>({
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
{...htmlProps} {...htmlProps}
> >
{children({ register, errors, control, getValues, formState, watch })} {children({ register, errors: formState.errors, control, getValues, formState, watch, setValue })}
</form> </form>
); );
} }

View File

@ -1,15 +1,17 @@
import { FormContextValues, FieldValues, ArrayField } from 'react-hook-form'; import { UseFormReturn, FieldValues, FieldErrors } from 'react-hook-form';
export { OnSubmit as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form'; export { SubmitHandler as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form';
export type FormAPI<T> = Pick< export type FormAPI<T> = Pick<
FormContextValues<T>, UseFormReturn<T>,
'register' | 'errors' | 'control' | 'formState' | 'getValues' | 'watch' 'register' | 'control' | 'formState' | 'getValues' | 'watch' | 'setValue'
>; > & {
errors: FieldErrors<T>;
};
type FieldArrayValue = Partial<FieldValues> | Array<Partial<FieldValues>>; type FieldArrayValue = Partial<FieldValues> | Array<Partial<FieldValues>>;
export interface FieldArrayApi { export interface FieldArrayApi {
fields: Array<Partial<ArrayField<FieldValues, 'id'>>>; fields: Array<Record<string, any>>;
append: (value: FieldArrayValue) => void; append: (value: FieldArrayValue) => void;
prepend: (value: FieldArrayValue) => void; prepend: (value: FieldArrayValue) => void;
remove: (index?: number | number[]) => void; remove: (index?: number | number[]) => void;

View File

@ -24,8 +24,7 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
<Input <Input
autoFocus autoFocus
type="password" type="password"
name="newPassword" {...register('newPassword', {
ref={register({
required: 'New password required', required: 'New password required',
})} })}
/> />
@ -33,10 +32,9 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}> <Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
<Input <Input
type="password" type="password"
name="confirmNew" {...register('confirmNew', {
ref={register({
required: 'Confirmed password is required', required: 'Confirmed password is required',
validate: (v) => v === getValues().newPassword || 'Passwords must match!', validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
})} })}
/> />
</Field> </Field>

View File

@ -51,7 +51,7 @@ export const ForgottenPassword: FC = () => {
invalid={!!errors.userOrEmail} invalid={!!errors.userOrEmail}
error={errors?.userOrEmail?.message} error={errors?.userOrEmail?.message}
> >
<Input placeholder="Email or username" name="userOrEmail" ref={register({ required: true })} /> <Input placeholder="Email or username" {...register('userOrEmail', { required: true })} />
</Field> </Field>
<HorizontalGroup> <HorizontalGroup>
<Button>Send reset email</Button> <Button>Send reset email</Button>

View File

@ -31,20 +31,18 @@ export const LoginForm: FC<Props> = ({ children, onSubmit, isLoggingIn, password
<> <>
<Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}> <Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}>
<Input <Input
{...register('user', { required: 'Email or username is required' })}
autoFocus autoFocus
name="user"
autoCapitalize="none" autoCapitalize="none"
ref={register({ required: 'Email or username is required' })}
placeholder={loginHint} placeholder={loginHint}
aria-label={selectors.pages.Login.username} aria-label={selectors.pages.Login.username}
/> />
</Field> </Field>
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}> <Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
<Input <Input
name="password" {...register('password', { required: 'Password is required' })}
type="password" type="password"
placeholder={passwordHint} placeholder={passwordHint}
ref={register({ required: 'Password is required' })}
aria-label={selectors.pages.Login.password} aria-label={selectors.pages.Login.password}
/> />
</Field> </Field>

View File

@ -63,50 +63,47 @@ export const SignupPage: FC<Props> = (props) => {
{({ errors, register, getValues }) => ( {({ errors, register, getValues }) => (
<> <>
<Field label="Your name"> <Field label="Your name">
<Input name="name" placeholder="(optional)" ref={register} /> <Input {...register('name')} placeholder="(optional)" />
</Field> </Field>
<Field label="Email" invalid={!!errors.email} error={errors.email?.message}> <Field label="Email" invalid={!!errors.email} error={errors.email?.message}>
<Input <Input
name="email" {...register('email', {
type="email"
placeholder="Email"
ref={register({
required: 'Email is required', required: 'Email is required',
pattern: { pattern: {
value: /^\S+@\S+$/, value: /^\S+@\S+$/,
message: 'Email is invalid', message: 'Email is invalid',
}, },
})} })}
type="email"
placeholder="Email"
/> />
</Field> </Field>
{!getConfig().autoAssignOrg && ( {!getConfig().autoAssignOrg && (
<Field label="Org. name"> <Field label="Org. name">
<Input name="orgName" placeholder="Org. name" ref={register} /> <Input {...register('orgName')} placeholder="Org. name" />
</Field> </Field>
)} )}
{getConfig().verifyEmailEnabled && ( {getConfig().verifyEmailEnabled && (
<Field label="Email verification code (sent to your email)"> <Field label="Email verification code (sent to your email)">
<Input name="code" ref={register} placeholder="Code" /> <Input {...register('code')} placeholder="Code" />
</Field> </Field>
)} )}
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}> <Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
<Input <Input
autoFocus {...register('password', {
type="password"
name="password"
ref={register({
required: 'Password is required', required: 'Password is required',
})} })}
autoFocus
type="password"
/> />
</Field> </Field>
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}> <Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
<Input <Input
type="password" {...register('confirm', {
name="confirm"
ref={register({
required: 'Confirmed password is required', required: 'Confirmed password is required',
validate: (v) => v === getValues().password || 'Passwords must match!', validate: (v) => v === getValues().password || 'Passwords must match!',
})} })}
type="password"
/> />
</Field> </Field>

View File

@ -47,7 +47,7 @@ export const VerifyEmail: FC = () => {
invalid={!!(errors as any).email} invalid={!!(errors as any).email}
error={(errors as any).email?.message} error={(errors as any).email?.message}
> >
<Input placeholder="Email" name="email" ref={register({ required: true })} /> <Input {...register('email', { required: true })} placeholder="Email" />
</Field> </Field>
<HorizontalGroup> <HorizontalGroup>
<Button>Send verification email</Button> <Button>Send verification email</Button>

View File

@ -66,7 +66,7 @@ export const AdminEditOrgPage: FC<Props> = ({ match }) => {
{({ register, errors }) => ( {({ register, errors }) => (
<> <>
<Field label="Name" invalid={!!errors.orgName} error="Name is required"> <Field label="Name" invalid={!!errors.orgName} error="Name is required">
<Input name="orgName" ref={register({ required: true })} /> <Input {...register('orgName', { required: true })} />
</Field> </Field>
<Button>Update</Button> <Button>Update</Button>
</> </>

View File

@ -46,15 +46,15 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
invalid={!!errors.name} invalid={!!errors.name}
error={errors.name ? 'Name is required' : undefined} error={errors.name ? 'Name is required' : undefined}
> >
<Input name="name" ref={register({ required: true })} /> <Input {...register('name', { required: true })} />
</Field> </Field>
<Field label="Email"> <Field label="Email">
<Input name="email" ref={register} /> <Input {...register('email')} />
</Field> </Field>
<Field label="Username"> <Field label="Username">
<Input name="login" ref={register} /> <Input {...register('login')} />
</Field> </Field>
<Field <Field
label="Password" label="Password"
@ -63,11 +63,10 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined} error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined}
> >
<Input <Input
type="password" {...register('password', {
name="password"
ref={register({
validate: (value) => value.trim() !== '' && value.length >= 4, validate: (value) => value.trim() !== '' && value.length >= 4,
})} })}
type="password"
/> />
</Field> </Field>
<Button type="submit">Create user</Button> <Button type="submit">Create user</Button>

View File

@ -25,10 +25,15 @@ export const BasicSettings: FC<Props> = ({
return ( return (
<> <>
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}> <Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
<Input name="name" ref={register({ required: 'Name is required' })} /> <Input {...register('name', { required: 'Name is required' })} />
</Field> </Field>
<Field label="Type"> <Field label="Type">
<InputControl name="type" as={Select} options={channels} control={control} rules={{ required: true }} /> <InputControl
name="type"
render={({ field: { ref, ...field } }) => <Select {...field} options={channels} />}
control={control}
rules={{ required: true }}
/>
</Field> </Field>
<NotificationChannelOptions <NotificationChannelOptions
selectedChannelOptions={selectedChannel.options.filter((o) => o.required)} selectedChannelOptions={selectedChannel.options.filter((o) => o.required)}

View File

@ -9,7 +9,7 @@ import { ChannelSettings } from './ChannelSettings';
import config from 'app/core/config'; import config from 'app/core/config';
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState'> { interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'setValue'> {
selectableChannels: Array<SelectableValue<string>>; selectableChannels: Array<SelectableValue<string>>;
selectedChannel?: NotificationChannelType; selectedChannel?: NotificationChannelType;
imageRendererAvailable: boolean; imageRendererAvailable: boolean;
@ -19,7 +19,7 @@ interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState'> {
} }
export interface NotificationSettingsProps export interface NotificationSettingsProps
extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues'> { extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues' | 'setValue'> {
currentFormValues: NotificationChannelDTO; currentFormValues: NotificationChannelDTO;
} }
@ -100,7 +100,7 @@ export const NotificationChannelForm: FC<Props> = ({
<div className={styles.formButtons}> <div className={styles.formButtons}>
<HorizontalGroup> <HorizontalGroup>
<Button type="submit">Save</Button> <Button type="submit">Save</Button>
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}> <Button type="button" variant="secondary" onClick={() => onTestChannel(getValues())}>
Test Test
</Button> </Button>
<a href={`${config.appSubUrl}/alerting/notifications`}> <a href={`${config.appSubUrl}/alerting/notifications`}>

View File

@ -4,7 +4,7 @@ import { Button, Checkbox, Field, FormAPI, Input } from '@grafana/ui';
import { OptionElement } from './OptionElement'; import { OptionElement } from './OptionElement';
import { NotificationChannelDTO, NotificationChannelOption, NotificationChannelSecureFields } from '../../../types'; import { NotificationChannelDTO, NotificationChannelOption, NotificationChannelSecureFields } from '../../../types';
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch'> { interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch' | 'setValue'> {
selectedChannelOptions: NotificationChannelOption[]; selectedChannelOptions: NotificationChannelOption[];
currentFormValues: NotificationChannelDTO; currentFormValues: NotificationChannelDTO;
secureFields: NotificationChannelSecureFields; secureFields: NotificationChannelSecureFields;
@ -39,8 +39,9 @@ export const NotificationChannelOptions: FC<Props> = ({
return ( return (
<Field key={key}> <Field key={key}>
<Checkbox <Checkbox
name={option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`} {...register(
ref={register} option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`
)}
label={option.label} label={option.label}
description={option.description} description={option.description}
/> />

View File

@ -10,12 +10,11 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
return ( return (
<CollapsableSection label="Notification settings" isOpen={false}> <CollapsableSection label="Notification settings" isOpen={false}>
<Field> <Field>
<Checkbox name="isDefault" ref={register} label="Default" description="Use this notification for all alerts" /> <Checkbox {...register('isDefault')} label="Default" description="Use this notification for all alerts" />
</Field> </Field>
<Field> <Field>
<Checkbox <Checkbox
name="settings.uploadImage" {...register('settings.uploadImage')}
ref={register}
label="Include image" label="Include image"
description="Captures an image and include it in the notification" description="Captures an image and include it in the notification"
/> />
@ -28,16 +27,14 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
)} )}
<Field> <Field>
<Checkbox <Checkbox
name="disableResolveMessage" {...register('disableResolveMessage')}
ref={register}
label="Disable Resolve Message" label="Disable Resolve Message"
description="Disable the resolve message [OK] that is sent when alerting state returns to false" description="Disable the resolve message [OK] that is sent when alerting state returns to false"
/> />
</Field> </Field>
<Field> <Field>
<Checkbox <Checkbox
name="sendReminder" {...register('sendReminder')}
ref={register}
label="Send reminders" label="Send reminders"
description="Send additional notifications for triggered alerts" description="Send additional notifications for triggered alerts"
/> />
@ -50,7 +47,7 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
Alert reminders are sent after rules are evaluated. A reminder can never be sent more frequently Alert reminders are sent after rules are evaluated. A reminder can never be sent more frequently
than a configured alert rule evaluation interval." than a configured alert rule evaluation interval."
> >
<Input name="frequency" ref={register} width={8} /> <Input {...register('frequency')} width={8} />
</Field> </Field>
</> </>
)} )}

View File

@ -13,13 +13,12 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
case 'input': case 'input':
return ( return (
<Input <Input
invalid={invalid} {...register(`${modelValue}`, {
type={option.inputType}
name={`${modelValue}`}
ref={register({
required: option.required ? 'Required' : false, required: option.required ? 'Required' : false,
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
})} })}
invalid={invalid}
type={option.inputType}
placeholder={option.placeholder} placeholder={option.placeholder}
/> />
); );
@ -27,11 +26,11 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
case 'select': case 'select':
return ( return (
<InputControl <InputControl
as={Select}
options={option.selectOptions}
control={control} control={control}
name={`${modelValue}`} name={`${modelValue}`}
invalid={invalid} render={({ field: { ref, ...field } }) => (
<Select {...field} options={option.selectOptions} invalid={invalid} />
)}
/> />
); );
@ -39,8 +38,7 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
return ( return (
<TextArea <TextArea
invalid={invalid} invalid={invalid}
name={`${modelValue}`} {...register(`${modelValue}`, {
ref={register({
required: option.required ? 'Required' : false, required: option.required ? 'Required' : false,
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
})} })}

View File

@ -75,12 +75,16 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
); );
}; };
const { handleSubmit, register, errors } = useForm<Values>({ const {
handleSubmit,
register,
formState: { errors },
} = useForm<Values>({
mode: 'onSubmit', mode: 'onSubmit',
defaultValues: existing ?? defaults, defaultValues: existing ?? defaults,
}); });
const validateNameIsUnique: Validate = (name: string) => { const validateNameIsUnique: Validate<string> = (name: string) => {
return !config.template_files[name] || existing?.name === name return !config.template_files[name] || existing?.name === name
? true ? true
: 'Another template with this name already exists.'; : 'Another template with this name already exists.';
@ -96,13 +100,12 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
)} )}
<Field label="Template name" error={errors?.name?.message} invalid={!!errors.name?.message}> <Field label="Template name" error={errors?.name?.message} invalid={!!errors.name?.message}>
<Input <Input
width={42} {...register('name', {
autoFocus={true}
ref={register({
required: { value: true, message: 'Required.' }, required: { value: true, message: 'Required.' },
validate: { nameIsUnique: validateNameIsUnique }, validate: { nameIsUnique: validateNameIsUnique },
})} })}
name="name" width={42}
autoFocus={true}
/> />
</Field> </Field>
<Field <Field
@ -133,9 +136,8 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
invalid={!!errors.content?.message} invalid={!!errors.content?.message}
> >
<TextArea <TextArea
{...register('content', { required: { value: true, message: 'Required.' } })}
className={styles.textarea} className={styles.textarea}
ref={register({ required: { value: true, message: 'Required.' } })}
name="content"
rows={12} rows={12}
/> />
</Field> </Field>

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Button, Checkbox, Field, Input } from '@grafana/ui'; import { Button, Checkbox, Field, Input } from '@grafana/ui';
import { OptionElement } from './OptionElement'; import { OptionElement } from './OptionElement';
import { ChannelValues, ReceiverFormValues } from '../../../types/receiver-form'; import { ChannelValues } from '../../../types/receiver-form';
import { useFormContext, FieldError, NestDataObject } from 'react-hook-form'; import { useFormContext, FieldError, FieldErrors } from 'react-hook-form';
import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types'; import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
export interface Props<R extends ChannelValues> { export interface Props<R extends ChannelValues> {
@ -10,7 +10,7 @@ export interface Props<R extends ChannelValues> {
secureFields: NotificationChannelSecureFields; secureFields: NotificationChannelSecureFields;
onResetSecureField: (key: string) => void; onResetSecureField: (key: string) => void;
errors?: NestDataObject<R, FieldError>; errors?: FieldErrors<R>;
pathPrefix?: string; pathPrefix?: string;
} }
@ -21,7 +21,7 @@ export function ChannelOptions<R extends ChannelValues>({
errors, errors,
pathPrefix = '', pathPrefix = '',
}: Props<R>): JSX.Element { }: Props<R>): JSX.Element {
const { register, watch } = useFormContext<ReceiverFormValues<R>>(); const { register, watch } = useFormContext();
const currentFormValues = watch() as Record<string, any>; // react hook form types ARE LYING! const currentFormValues = watch() as Record<string, any>; // react hook form types ARE LYING!
return ( return (
<> <>
@ -41,12 +41,11 @@ export function ChannelOptions<R extends ChannelValues>({
return ( return (
<Field key={key}> <Field key={key}>
<Checkbox <Checkbox
name={ {...register(
option.secure option.secure
? `${pathPrefix}secureSettings.${option.propertyName}` ? `${pathPrefix}secureSettings.${option.propertyName}`
: `${pathPrefix}settings.${option.propertyName}` : `${pathPrefix}settings.${option.propertyName}`
} )}
ref={register()}
label={option.label} label={option.label}
description={option.description} description={option.description}
/> />

View File

@ -1,25 +1,27 @@
import { GrafanaThemeV2, SelectableValue } from '@grafana/data'; import { GrafanaThemeV2, SelectableValue } from '@grafana/data';
import { NotifierDTO } from 'app/types'; import { NotifierDTO } from 'app/types';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Alert, Button, Field, InputControl, Select, useStyles2 } from '@grafana/ui'; import { Alert, Button, Field, InputControl, Select, useStyles2 } from '@grafana/ui';
import { useFormContext, FieldError, NestDataObject } from 'react-hook-form'; import { useFormContext, FieldErrors } from 'react-hook-form';
import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form'; import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form';
import { ChannelOptions } from './ChannelOptions'; import { ChannelOptions } from './ChannelOptions';
import { CollapsibleSection } from './CollapsibleSection'; import { CollapsibleSection } from './CollapsibleSection';
interface Props<R> { interface Props<R> {
defaultValues: R;
pathPrefix: string; pathPrefix: string;
notifiers: NotifierDTO[]; notifiers: NotifierDTO[];
onDuplicate: () => void; onDuplicate: () => void;
commonSettingsComponent: CommonSettingsComponentType; commonSettingsComponent: CommonSettingsComponentType;
secureFields?: Record<string, boolean>; secureFields?: Record<string, boolean>;
errors?: NestDataObject<R, FieldError>; errors?: FieldErrors<R>;
onDelete?: () => void; onDelete?: () => void;
} }
export function ChannelSubForm<R extends ChannelValues>({ export function ChannelSubForm<R extends ChannelValues>({
defaultValues,
pathPrefix, pathPrefix,
onDuplicate, onDuplicate,
onDelete, onDelete,
@ -30,16 +32,8 @@ export function ChannelSubForm<R extends ChannelValues>({
}: Props<R>): JSX.Element { }: Props<R>): JSX.Element {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const name = (fieldName: string) => `${pathPrefix}${fieldName}`; const name = (fieldName: string) => `${pathPrefix}${fieldName}`;
const { control, watch, register, unregister } = useFormContext(); const { control, watch } = useFormContext();
const selectedType = watch(name('type')); const selectedType = watch(name('type')) ?? defaultValues.type; // nope, setting "default" does not work at all.
// keep the __id field registered so it's always passed to submit
useEffect(() => {
register({ name: `${pathPrefix}__id` });
return () => {
unregister(`${pathPrefix}__id`);
};
});
const [_secureFields, setSecureFields] = useState(secureFields ?? {}); const [_secureFields, setSecureFields] = useState(secureFields ?? {});
@ -70,15 +64,21 @@ export function ChannelSubForm<R extends ChannelValues>({
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.topRow}> <div className={styles.topRow}>
<div> <div>
<InputControl
name={name('__id')}
render={({ field }) => <input type="hidden" {...field} />}
defaultValue={defaultValues.__id}
control={control}
/>
<Field label="Contact point type"> <Field label="Contact point type">
<InputControl <InputControl
name={name('type')} name={name('type')}
as={Select} defaultValue={defaultValues.type}
width={37} render={({ field: { ref, onChange, ...field } }) => (
options={typeOptions} <Select {...field} width={37} options={typeOptions} onChange={(value) => onChange(value?.value)} />
)}
control={control} control={control}
rules={{ required: true }} rules={{ required: true }}
onChange={(values) => values[0]?.value}
/> />
</Field> </Field>
</div> </div>

View File

@ -9,16 +9,14 @@ export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({
<div className={className}> <div className={className}>
<Field> <Field>
<Checkbox <Checkbox
name={`${pathPrefix}disableResolveMessage`} {...register(`${pathPrefix}disableResolveMessage`)}
ref={register()}
label="Disable resolved message" label="Disable resolved message"
description="Disable the resolve message [OK] that is sent when alerting state returns to false" description="Disable the resolve message [OK] that is sent when alerting state returns to false"
/> />
</Field> </Field>
<Field> <Field>
<Checkbox <Checkbox
name={`${pathPrefix}sendReminder`} {...register(`${pathPrefix}sendReminder`)}
ref={register()}
label="Send reminders" label="Send reminders"
description="Send additional notifications for triggered alerts" description="Send additional notifications for triggered alerts"
/> />

View File

@ -1,4 +1,4 @@
import React, { FC } from 'react'; import React, { FC, useEffect } from 'react';
import { Input, InputControl, Select, TextArea } from '@grafana/ui'; import { Input, InputControl, Select, TextArea } from '@grafana/ui';
import { NotificationChannelOption } from 'app/types'; import { NotificationChannelOption } from 'app/types';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
@ -10,18 +10,26 @@ interface Props {
} }
export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) => { export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) => {
const { control, register } = useFormContext(); const { control, register, unregister } = useFormContext();
const modelValue = option.secure const modelValue = option.secure
? `${pathPrefix}secureSettings.${option.propertyName}` ? `${pathPrefix}secureSettings.${option.propertyName}`
: `${pathPrefix}settings.${option.propertyName}`; : `${pathPrefix}settings.${option.propertyName}`;
// workaround for https://github.com/react-hook-form/react-hook-form/issues/4993#issuecomment-829012506
useEffect(
() => () => {
unregister(modelValue);
},
[unregister, modelValue]
);
switch (option.element) { switch (option.element) {
case 'input': case 'input':
return ( return (
<Input <Input
invalid={invalid} invalid={invalid}
type={option.inputType} type={option.inputType}
name={`${modelValue}`} {...register(`${modelValue}`, {
ref={register({
required: option.required ? 'Required' : false, required: option.required ? 'Required' : false,
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
})} })}
@ -32,24 +40,27 @@ export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) =
case 'select': case 'select':
return ( return (
<InputControl <InputControl
as={Select} render={({ field: { onChange, ref, ...field } }) => (
options={option.selectOptions} <Select
{...field}
options={option.selectOptions}
invalid={invalid}
onChange={(value) => onChange(value.value)}
/>
)}
control={control} control={control}
name={`${modelValue}`} name={`${modelValue}`}
invalid={invalid}
onChange={(values) => values[0].value}
/> />
); );
case 'textarea': case 'textarea':
return ( return (
<TextArea <TextArea
invalid={invalid} {...register(`${modelValue}`, {
name={`${modelValue}`}
ref={register({
required: option.required ? 'Required' : false, required: option.required ? 'Required' : false,
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
})} })}
invalid={invalid}
/> />
); );

View File

@ -4,8 +4,7 @@ import { Alert, Button, Field, Input, LinkButton, useStyles2 } from '@grafana/ui
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
import { NotifierDTO } from 'app/types'; import { NotifierDTO } from 'app/types';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useForm, FormContext, NestDataObject, FieldError, Validate } from 'react-hook-form'; import { useForm, FormProvider, FieldErrors, Validate, useFieldArray } from 'react-hook-form';
import { useControlledFieldArray } from '../../../hooks/useControlledFieldArray';
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form'; import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form';
import { makeAMLink } from '../../../utils/misc'; import { makeAMLink } from '../../../utils/misc';
@ -50,11 +49,20 @@ export function ReceiverForm<R extends ChannelValues>({
const { loading, error } = useUnifiedAlertingSelector((state) => state.saveAMConfig); const { loading, error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const { handleSubmit, register, errors, getValues } = formAPI; const {
handleSubmit,
register,
formState: { errors },
getValues,
control,
} = formAPI;
const { items, append, remove } = useControlledFieldArray<R>('items', formAPI); const { fields, append, remove } = useFieldArray({
control,
name: 'items' as any, // bug in types
});
const validateNameIsAvailable: Validate = useCallback( const validateNameIsAvailable: Validate<string> = useCallback(
(name: string) => (name: string) =>
takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase()) takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
? 'Another receiver with this name already exists.' ? 'Another receiver with this name already exists.'
@ -63,7 +71,7 @@ export function ReceiverForm<R extends ChannelValues>({
); );
return ( return (
<FormContext {...formAPI}> <FormProvider {...formAPI}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<h4 className={styles.heading}>{initialValues ? 'Update contact point' : 'Create contact point'}</h4> <h4 className={styles.heading}>{initialValues ? 'Update contact point' : 'Create contact point'}</h4>
{error && ( {error && (
@ -73,25 +81,28 @@ export function ReceiverForm<R extends ChannelValues>({
)} )}
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}> <Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
<Input <Input
{...register('name', {
required: 'Name is required',
validate: { nameIsAvailable: validateNameIsAvailable },
})}
width={39} width={39}
name="name"
ref={register({ required: 'Name is required', validate: { nameIsAvailable: validateNameIsAvailable } })}
/> />
</Field> </Field>
{items.map((item, index) => { {fields.map((field: R & { id: string }, index) => {
const initialItem = initialValues?.items.find(({ __id }) => __id === item.__id); const initialItem = initialValues?.items.find(({ __id }) => __id === field.__id);
return ( return (
<ChannelSubForm<R> <ChannelSubForm<R>
key={item.__id} defaultValues={field}
key={field.id}
onDuplicate={() => { onDuplicate={() => {
const currentValues = getValues({ nest: true }).items[index]; const currentValues: R = getValues().items[index];
append({ ...currentValues, __id: String(Math.random()) }); append({ ...currentValues, __id: String(Math.random()) });
}} }}
onDelete={() => remove(index)} onDelete={() => remove(index)}
pathPrefix={`items.${index}.`} pathPrefix={`items.${index}.`}
notifiers={notifiers} notifiers={notifiers}
secureFields={initialItem?.secureFields} secureFields={initialItem?.secureFields}
errors={errors?.items?.[index] as NestDataObject<R, FieldError>} errors={errors?.items?.[index] as FieldErrors<R>}
commonSettingsComponent={commonSettingsComponent} commonSettingsComponent={commonSettingsComponent}
/> />
); );
@ -115,7 +126,7 @@ export function ReceiverForm<R extends ChannelValues>({
</LinkButton> </LinkButton>
</div> </div>
</form> </form>
</FormContext> </FormProvider>
); );
} }

View File

@ -7,7 +7,7 @@ import { AlertTypeStep } from './AlertTypeStep';
import { ConditionsStep } from './ConditionsStep'; import { ConditionsStep } from './ConditionsStep';
import { DetailsStep } from './DetailsStep'; import { DetailsStep } from './DetailsStep';
import { QueryStep } from './QueryStep'; import { QueryStep } from './QueryStep';
import { useForm, FormContext } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
@ -39,7 +39,11 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
defaultValues, defaultValues,
}); });
const { handleSubmit, watch, errors } = formAPI; const {
handleSubmit,
watch,
formState: { errors },
} = formAPI;
const hasErrors = !!Object.values(errors).filter((x) => !!x).length; const hasErrors = !!Object.values(errors).filter((x) => !!x).length;
@ -52,7 +56,6 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
useCleanup((state) => state.unifiedAlerting.ruleForm.saveRule); useCleanup((state) => state.unifiedAlerting.ruleForm.saveRule);
const submit = (values: RuleFormValues, exitOnSave: boolean) => { const submit = (values: RuleFormValues, exitOnSave: boolean) => {
console.log('submit', values);
dispatch( dispatch(
saveRuleFormAction({ saveRuleFormAction({
values: { values: {
@ -68,7 +71,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
}; };
return ( return (
<FormContext {...formAPI}> <FormProvider {...formAPI}>
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}> <form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
<PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}> <PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}>
<Link to="/alerting/list"> <Link to="/alerting/list">
@ -121,7 +124,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
</CustomScrollbar> </CustomScrollbar>
</div> </div>
</form> </form>
</FormContext> </FormProvider>
); );
}; };

View File

@ -6,7 +6,7 @@ import { css } from '@emotion/css';
import { RuleEditorSection } from './RuleEditorSection'; import { RuleEditorSection } from './RuleEditorSection';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { DataSourcePicker, DataSourcePickerProps } from '@grafana/runtime'; import { DataSourcePicker } from '@grafana/runtime';
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler'; import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
import { RuleFolderPicker } from './RuleFolderPicker'; import { RuleFolderPicker } from './RuleFolderPicker';
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
@ -31,7 +31,13 @@ interface Props {
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => { export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const { register, control, watch, errors, setValue } = useFormContext<RuleFormValues>(); const {
register,
control,
watch,
formState: { errors },
setValue,
} = useFormContext<RuleFormValues & { location?: string }>();
const ruleFormType = watch('type'); const ruleFormType = watch('type');
const dataSourceName = watch('dataSourceName'); const dataSourceName = watch('dataSourceName');
@ -61,9 +67,8 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.name?.message} invalid={!!errors.name?.message}
> >
<Input <Input
{...register('name', { required: { value: true, message: 'Must enter an alert name' } })}
autoFocus={true} autoFocus={true}
ref={register({ required: { value: true, message: 'Must enter an alert name' } })}
name="name"
/> />
</Field> </Field>
<div className={styles.flexRow}> <div className={styles.flexRow}>
@ -75,25 +80,29 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.type?.message} invalid={!!errors.type?.message}
> >
<InputControl <InputControl
as={Select} render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
options={alertTypeOptions}
onChange={(v: SelectableValue) => {
const value = v?.value;
// when switching to system alerts, null out data source selection if it's not a rules source with ruler
if (
value === RuleFormType.system &&
dataSourceName &&
!rulesSourcesWithRuler.find(({ name }) => name === dataSourceName)
) {
setValue('dataSourceName', null);
}
onChange(value);
}}
/>
)}
name="type" name="type"
options={alertTypeOptions}
control={control} control={control}
rules={{ rules={{
required: { value: true, message: 'Please select alert type' }, required: { value: true, message: 'Please select alert type' },
}} }}
onChange={(values: SelectableValue[]) => {
const value = values[0]?.value;
// when switching to system alerts, null out data source selection if it's not a rules source with ruler
if (
value === RuleFormType.system &&
dataSourceName &&
!rulesSourcesWithRuler.find(({ name }) => name === dataSourceName)
) {
setValue('dataSourceName', null);
}
return value;
}}
/> />
</Field> </Field>
{ruleFormType === RuleFormType.system && ( {ruleFormType === RuleFormType.system && (
@ -104,21 +113,25 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.dataSourceName?.message} invalid={!!errors.dataSourceName?.message}
> >
<InputControl <InputControl
as={(DataSourcePicker as unknown) as React.ComponentType<Omit<DataSourcePickerProps, 'current'>>} render={({ field: { onChange, ref, value, ...field } }) => (
valueName="current" <DataSourcePicker
filter={dataSourceFilter} {...field}
current={value}
filter={dataSourceFilter}
noDefault
alerting
onChange={(ds: DataSourceInstanceSettings) => {
// reset location if switching data sources, as different rules source will have different groups and namespaces
setValue('location', undefined);
onChange(ds?.name ?? null);
}}
/>
)}
name="dataSourceName" name="dataSourceName"
noDefault={true}
control={control} control={control}
alerting={true}
rules={{ rules={{
required: { value: true, message: 'Please select a data source' }, required: { value: true, message: 'Please select a data source' },
}} }}
onChange={(ds: DataSourceInstanceSettings[]) => {
// reset location if switching data sources, as differnet rules source will have different groups and namespaces
setValue('location', undefined);
return ds[0]?.name ?? null;
}}
/> />
</Field> </Field>
)} )}
@ -134,10 +147,10 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.folder?.message} invalid={!!errors.folder?.message}
> >
<InputControl <InputControl
as={RuleFolderPicker} render={({ field: { ref, ...field } }) => (
<RuleFolderPicker {...field} enableCreateNew={true} enableReset={true} />
)}
name="folder" name="folder"
enableCreateNew={true}
enableReset={true}
rules={{ rules={{
required: { value: true, message: 'Please select a folder' }, required: { value: true, message: 'Please select a folder' },
}} }}

View File

@ -8,11 +8,16 @@ import { AnnotationKeyInput } from './AnnotationKeyInput';
const AnnotationsField: FC = () => { const AnnotationsField: FC = () => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const { control, register, watch, errors } = useFormContext<RuleFormValues>(); const {
const annotations = watch('annotations'); control,
register,
watch,
formState: { errors },
} = useFormContext();
const annotations = watch('annotations') as RuleFormValues['annotations'];
const existingKeys = useCallback( const existingKeys = useCallback(
(index: number): string[] => annotations.filter((_, idx) => idx !== index).map(({ key }) => key), (index: number): string[] => annotations.filter((_, idx: number) => idx !== index).map(({ key }) => key),
[annotations] [annotations]
); );
@ -31,10 +36,10 @@ const AnnotationsField: FC = () => {
error={errors.annotations?.[index]?.key?.message} error={errors.annotations?.[index]?.key?.message}
> >
<InputControl <InputControl
as={AnnotationKeyInput}
width={15}
name={`annotations[${index}].key`} name={`annotations[${index}].key`}
existingKeys={existingKeys(index)} render={({ field: { ref, ...field } }) => (
<AnnotationKeyInput {...field} existingKeys={existingKeys(index)} width={15} />
)}
control={control} control={control}
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }} rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
/> />
@ -45,9 +50,10 @@ const AnnotationsField: FC = () => {
error={errors.annotations?.[index]?.value?.message} error={errors.annotations?.[index]?.value?.message}
> >
<TextArea <TextArea
name={`annotations[${index}].value`}
className={styles.annotationTextArea} className={styles.annotationTextArea}
ref={register({ required: { value: !!annotations[index]?.key, message: 'Required.' } })} {...register(`annotations[${index}].value`, {
required: { value: !!annotations[index]?.key, message: 'Required.' },
})}
placeholder={`value`} placeholder={`value`}
defaultValue={field.value} defaultValue={field.value}
/> />

View File

@ -5,7 +5,11 @@ import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form'; import { RuleFormValues } from '../../types/rule-form';
export const ConditionField: FC = () => { export const ConditionField: FC = () => {
const { watch, setValue, errors } = useFormContext<RuleFormValues>(); const {
watch,
setValue,
formState: { errors },
} = useFormContext<RuleFormValues>();
const queries = watch('queries'); const queries = watch('queries');
const condition = watch('condition'); const condition = watch('condition');
@ -36,18 +40,22 @@ export const ConditionField: FC = () => {
invalid={!!errors.condition?.message} invalid={!!errors.condition?.message}
> >
<InputControl <InputControl
width={42}
name="condition" name="condition"
as={Select} render={({ field: { onChange, ref, ...field } }) => (
onChange={(values: SelectableValue[]) => values[0]?.value ?? null} <Select
options={options} {...field}
width={42}
options={options}
onChange={(v: SelectableValue) => onChange(v?.value ?? null)}
noOptionsMessage="No queries defined"
/>
)}
rules={{ rules={{
required: { required: {
value: true, value: true,
message: 'Please select the condition to alert on', message: 'Please select the condition to alert on',
}, },
}} }}
noOptionsMessage="No queries defined"
/> />
</Field> </Field>
); );

View File

@ -3,12 +3,12 @@ import { Field, Input, Select, useStyles, InputControl, InlineLabel, Switch } fr
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { RuleEditorSection } from './RuleEditorSection'; import { RuleEditorSection } from './RuleEditorSection';
import { useFormContext, ValidationOptions } from 'react-hook-form'; import { useFormContext, RegisterOptions } from 'react-hook-form';
import { RuleFormType, RuleFormValues, TimeOptions } from '../../types/rule-form'; import { RuleFormType, RuleFormValues, TimeOptions } from '../../types/rule-form';
import { ConditionField } from './ConditionField'; import { ConditionField } from './ConditionField';
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker'; import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
const timeRangeValidationOptions: ValidationOptions = { const timeRangeValidationOptions: RegisterOptions = {
required: { required: {
value: true, value: true,
message: 'Required.', message: 'Required.',
@ -29,7 +29,12 @@ const timeOptions = Object.entries(TimeOptions).map(([key, value]) => ({
export const ConditionsStep: FC = () => { export const ConditionsStep: FC = () => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false); const [showErrorHandling, setShowErrorHandling] = useState(false);
const { register, control, watch, errors } = useFormContext<RuleFormValues>(); const {
register,
control,
watch,
formState: { errors },
} = useFormContext<RuleFormValues>();
const type = watch('type'); const type = watch('type');
@ -48,7 +53,7 @@ export const ConditionsStep: FC = () => {
error={errors.evaluateEvery?.message} error={errors.evaluateEvery?.message}
invalid={!!errors.evaluateEvery?.message} invalid={!!errors.evaluateEvery?.message}
> >
<Input width={8} ref={register(timeRangeValidationOptions)} name="evaluateEvery" /> <Input width={8} {...register('evaluateEvery', timeRangeValidationOptions)} />
</Field> </Field>
<InlineLabel <InlineLabel
width={7} width={7}
@ -61,7 +66,7 @@ export const ConditionsStep: FC = () => {
error={errors.evaluateFor?.message} error={errors.evaluateFor?.message}
invalid={!!errors.evaluateFor?.message} invalid={!!errors.evaluateFor?.message}
> >
<Input width={8} ref={register(timeRangeValidationOptions)} name="evaluateFor" /> <Input width={8} {...register('evaluateFor', timeRangeValidationOptions)} />
</Field> </Field>
</div> </div>
</Field> </Field>
@ -72,18 +77,18 @@ export const ConditionsStep: FC = () => {
<> <>
<Field label="Alert state if no data or all values are null"> <Field label="Alert state if no data or all values are null">
<InputControl <InputControl
as={GrafanaAlertStatePicker} render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker {...field} width={42} onChange={(value) => onChange(value?.value)} />
)}
name="noDataState" name="noDataState"
width={42}
onChange={(values) => values[0]?.value}
/> />
</Field> </Field>
<Field label="Alert state if execution error or timeout"> <Field label="Alert state if execution error or timeout">
<InputControl <InputControl
as={GrafanaAlertStatePicker} render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker {...field} width={42} onChange={(value) => onChange(value?.value)} />
)}
name="execErrState" name="execErrState"
width={42}
onChange={(values) => values[0]?.value}
/> />
</Field> </Field>
</> </>
@ -96,19 +101,22 @@ export const ConditionsStep: FC = () => {
<div className={styles.flexRow}> <div className={styles.flexRow}>
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}> <Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
<Input <Input
ref={register({ pattern: { value: /^\d+$/, message: 'Must be a postive integer.' } })} {...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a postive integer.' } })}
name="forTime"
width={8} width={8}
/> />
</Field> </Field>
<InputControl <InputControl
name="forTimeUnit" name="forTimeUnit"
as={Select} render={({ field: { onChange, ref, ...field } }) => (
options={timeOptions} <Select
{...field}
options={timeOptions}
onChange={(value) => onChange(value?.value)}
width={15}
className={styles.timeUnit}
/>
)}
control={control} control={control}
width={15}
className={styles.timeUnit}
onChange={(values) => values[0]?.value}
/> />
</div> </div>
</Field> </Field>
@ -125,7 +133,6 @@ const getStyles = (theme: GrafanaTheme) => ({
flexRow: css` flexRow: css`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-end;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
`, `,

View File

@ -14,7 +14,12 @@ interface Props {
} }
export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => { export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
const { control, watch, errors, setValue } = useFormContext<RuleFormValues>(); const {
control,
watch,
formState: { errors },
setValue,
} = useFormContext<RuleFormValues>();
const [customGroup, setCustomGroup] = useState(false); const [customGroup, setCustomGroup] = useState(false);
@ -44,32 +49,34 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
<> <>
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}> <Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
<InputControl <InputControl
as={SelectWithAdd} render={({ field: { onChange, ref, ...field } }) => (
className={inputStyle} <SelectWithAdd
{...field}
className={inputStyle}
onChange={(value) => {
setValue('group', ''); //reset if namespace changes
onChange(value);
}}
onCustomChange={(custom: boolean) => {
custom && setCustomGroup(true);
}}
options={namespaceOptions}
width={42}
/>
)}
name="namespace" name="namespace"
options={namespaceOptions}
control={control} control={control}
width={42}
rules={{ rules={{
required: { value: true, message: 'Required.' }, required: { value: true, message: 'Required.' },
}} }}
onChange={(values) => {
setValue('group', ''); //reset if namespace changes
return values[0];
}}
onCustomChange={(custom: boolean) => {
custom && setCustomGroup(true);
}}
/> />
</Field> </Field>
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}> <Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
<InputControl <InputControl
as={SelectWithAdd} render={({ field: { ref, ...field } }) => (
<SelectWithAdd {...field} options={groupOptions} width={42} custom={customGroup} className={inputStyle} />
)}
name="group" name="group"
className={inputStyle}
options={groupOptions}
width={42}
custom={customGroup}
control={control} control={control}
rules={{ rules={{
required: { value: true, message: 'Required.' }, required: { value: true, message: 'Required.' },

View File

@ -3,7 +3,6 @@ import { Button, Field, FieldArray, Input, InlineLabel, Label, useStyles } from
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form';
interface Props { interface Props {
className?: string; className?: string;
@ -11,7 +10,12 @@ interface Props {
const LabelsField: FC<Props> = ({ className }) => { const LabelsField: FC<Props> = ({ className }) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const { register, control, watch, errors } = useFormContext<RuleFormValues>(); const {
register,
control,
watch,
formState: { errors },
} = useFormContext();
const labels = watch('labels'); const labels = watch('labels');
return ( return (
<div className={cx(className, styles.wrapper)}> <div className={cx(className, styles.wrapper)}>
@ -33,8 +37,9 @@ const LabelsField: FC<Props> = ({ className }) => {
error={errors.labels?.[index]?.key?.message} error={errors.labels?.[index]?.key?.message}
> >
<Input <Input
ref={register({ required: { value: !!labels[index]?.value, message: 'Required.' } })} {...register(`labels[${index}].key`, {
name={`labels[${index}].key`} required: { value: !!labels[index]?.value, message: 'Required.' },
})}
placeholder="key" placeholder="key"
defaultValue={field.key} defaultValue={field.key}
/> />
@ -46,8 +51,9 @@ const LabelsField: FC<Props> = ({ className }) => {
error={errors.labels?.[index]?.value?.message} error={errors.labels?.[index]?.value?.message}
> >
<Input <Input
ref={register({ required: { value: !!labels[index]?.key, message: 'Required.' } })} {...register(`labels[${index}].value`, {
name={`labels[${index}].value`} required: { value: !!labels[index]?.key, message: 'Required.' },
})}
placeholder="value" placeholder="value"
defaultValue={field.value} defaultValue={field.value}
/> />

View File

@ -7,7 +7,11 @@ import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { AlertingQueryEditor } from '../../../components/AlertingQueryEditor'; import { AlertingQueryEditor } from '../../../components/AlertingQueryEditor';
export const QueryStep: FC = () => { export const QueryStep: FC = () => {
const { control, watch, errors } = useFormContext<RuleFormValues>(); const {
control,
watch,
formState: { errors },
} = useFormContext<RuleFormValues>();
const type = watch('type'); const type = watch('type');
const dataSourceName = watch('dataSourceName'); const dataSourceName = watch('dataSourceName');
return ( return (
@ -16,8 +20,7 @@ export const QueryStep: FC = () => {
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}> <Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
<InputControl <InputControl
name="expression" name="expression"
dataSourceName={dataSourceName} render={({ field: { ref, ...field } }) => <ExpressionEditor {...field} dataSourceName={dataSourceName} />}
as={ExpressionEditor}
control={control} control={control}
rules={{ rules={{
required: { value: true, message: 'A valid expression is required' }, required: { value: true, message: 'A valid expression is required' },
@ -32,7 +35,7 @@ export const QueryStep: FC = () => {
> >
<InputControl <InputControl
name="queries" name="queries"
as={AlertingQueryEditor} render={({ field: { ref, ...field } }) => <AlertingQueryEditor {...field} />}
control={control} control={control}
rules={{ rules={{
validate: (queries) => Array.isArray(queries) && !!queries.length, validate: (queries) => Array.isArray(queries) && !!queries.length,

View File

@ -6,7 +6,7 @@ export interface Folder {
id: number; id: number;
} }
export interface Props extends Omit<FolderPickerProps, 'initiailTitle' | 'initialFolderId'> { export interface Props extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
value?: Folder; value?: Folder;
} }

View File

@ -1,38 +0,0 @@
import { useCallback } from 'react';
import { FormContextValues } from 'react-hook-form';
/*
* react-hook-form's own useFieldArray is uncontrolled and super buggy.
* this is a simple controlled version. It's dead simple and more robust at the cost of re-rendering the form
* on every change to the sub forms in the array.
* Warning: you'll have to take care of your own unique identiifer to use as `key` for the ReactNode array.
* Using index will cause problems.
*/
export function useControlledFieldArray<R>(name: string, formAPI: FormContextValues<any>) {
const { watch, getValues, reset } = formAPI;
const items: R[] = watch(name);
return {
items,
append: useCallback(
(values: R) => {
const existingValues = getValues({ nest: true });
reset({
...existingValues,
[name]: [...(existingValues[name] ?? []), values],
});
},
[getValues, reset, name]
),
remove: useCallback(
(index: number) => {
const values = getValues({ nest: true });
const items = values[name] ?? [];
items.splice(index, 1);
reset({ ...values, [name]: items });
},
[getValues, reset, name]
),
};
}

View File

@ -137,7 +137,7 @@ function formChannelValuesToGrafanaChannelConfig(
): GrafanaManagedReceiverConfig { ): GrafanaManagedReceiverConfig {
const channel: GrafanaManagedReceiverConfig = { const channel: GrafanaManagedReceiverConfig = {
settings: { settings: {
...(existing?.settings ?? {}), ...(existing && existing.type === values.type ? existing.settings ?? {} : {}),
...(values.settings ?? {}), ...(values.settings ?? {}),
}, },
secureSettings: values.secureSettings ?? {}, secureSettings: values.secureSettings ?? {},

View File

@ -26,7 +26,7 @@ export const RowOptionsForm: FC<Props> = ({ repeat, title, onUpdate, onCancel })
{({ register }) => ( {({ register }) => (
<> <>
<Field label="Title"> <Field label="Title">
<Input name="title" ref={register} type="text" /> <Input {...register('title')} type="text" />
</Field> </Field>
<Field label="Repeat for"> <Field label="Repeat for">

View File

@ -92,8 +92,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
<> <>
<Field label="Dashboard name" invalid={!!errors.title} error={errors.title?.message}> <Field label="Dashboard name" invalid={!!errors.title} error={errors.title?.message}>
<Input <Input
name="title" {...register('title', {
ref={register({
validate: validateDashboardName(getValues), validate: validateDashboardName(getValues),
})} })}
aria-label="Save dashboard title field" aria-label="Save dashboard title field"
@ -102,17 +101,21 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
</Field> </Field>
<Field label="Folder"> <Field label="Folder">
<InputControl <InputControl
as={FolderPicker} render={({ field: { ref, ...field } }) => (
<FolderPicker
{...field}
dashboardId={dashboard.id}
initialFolderId={dashboard.meta.folderId}
initialTitle={dashboard.meta.folderTitle}
enableCreateNew
/>
)}
control={control} control={control}
name="$folder" name="$folder"
dashboardId={dashboard.id}
initialFolderId={dashboard.meta.folderId}
initialTitle={dashboard.meta.folderTitle}
enableCreateNew
/> />
</Field> </Field>
<Field label="Copy tags"> <Field label="Copy tags">
<Switch name="copyTags" ref={register} /> <Switch {...register('copyTags')} />
</Field> </Field>
<Modal.ButtonRow> <Modal.ButtonRow>
<Button variant="secondary" onClick={onCancel} fill="outline"> <Button variant="secondary" onClick={onCancel} fill="outline">

View File

@ -39,23 +39,21 @@ export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard,
<div> <div>
{hasTimeChanged && ( {hasTimeChanged && (
<Checkbox <Checkbox
{...register('saveTimerange')}
label="Save current time range as dashboard default" label="Save current time range as dashboard default"
name="saveTimerange"
ref={register}
aria-label={selectors.pages.SaveDashboardModal.saveTimerange} aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
/> />
)} )}
{hasVariableChanged && ( {hasVariableChanged && (
<Checkbox <Checkbox
{...register('saveVariables')}
label="Save current variable values as dashboard default" label="Save current variable values as dashboard default"
name="saveVariables"
ref={register}
aria-label={selectors.pages.SaveDashboardModal.saveVariables} aria-label={selectors.pages.SaveDashboardModal.saveVariables}
/> />
)} )}
{(hasVariableChanged || hasTimeChanged) && <div className="gf-form-group" />} {(hasVariableChanged || hasTimeChanged) && <div className="gf-form-group" />}
<TextArea name="message" ref={register} placeholder="Add a note to describe your changes." autoFocus /> <TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus />
</div> </div>
<Modal.ButtonRow> <Modal.ButtonRow>

View File

@ -56,8 +56,7 @@ export class NewDashboardsFolder extends PureComponent<Props> {
error={errors.folderName && errors.folderName.message} error={errors.folderName && errors.folderName.message}
> >
<Input <Input
name="folderName" {...register('folderName', {
ref={register({
required: 'Folder name is required.', required: 'Folder name is required.',
validate: async (v) => await this.validateFolderName(v), validate: async (v) => await this.validateFolderName(v),
})} })}

View File

@ -98,10 +98,9 @@ class UnthemedDashboardImport extends PureComponent<Props> {
{({ register, errors }) => ( {({ register, errors }) => (
<Field invalid={!!errors.gcomDashboard} error={errors.gcomDashboard && errors.gcomDashboard.message}> <Field invalid={!!errors.gcomDashboard} error={errors.gcomDashboard && errors.gcomDashboard.message}>
<Input <Input
name="gcomDashboard"
placeholder="Grafana.com dashboard URL or ID" placeholder="Grafana.com dashboard URL or ID"
type="text" type="text"
ref={register({ {...register('gcomDashboard', {
required: 'A Grafana dashboard URL or ID is required', required: 'A Grafana dashboard URL or ID is required',
validate: validateGcomDashboard, validate: validateGcomDashboard,
})} })}
@ -118,8 +117,7 @@ class UnthemedDashboardImport extends PureComponent<Props> {
<> <>
<Field invalid={!!errors.dashboardJson} error={errors.dashboardJson && errors.dashboardJson.message}> <Field invalid={!!errors.dashboardJson} error={errors.dashboardJson && errors.dashboardJson.message}>
<TextArea <TextArea
name="dashboardJson" {...register('dashboardJson', {
ref={register({
required: 'Need a dashboard JSON model', required: 'Need a dashboard JSON model',
validate: validateDashboardJson, validate: validateDashboardJson,
})} })}

View File

@ -15,7 +15,7 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers'; import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
import { validateTitle, validateUid } from '../utils/validation'; import { validateTitle, validateUid } from '../utils/validation';
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> { interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'setValue'> {
uidReset: boolean; uidReset: boolean;
inputs: DashboardInputs; inputs: DashboardInputs;
initialFolderId: number; initialFolderId: number;
@ -47,7 +47,7 @@ export const ImportDashboardForm: FC<Props> = ({
*/ */
useEffect(() => { useEffect(() => {
if (isSubmitted && (errors.title || errors.uid)) { if (isSubmitted && (errors.title || errors.uid)) {
onSubmit(getValues({ nest: true }), {} as any); onSubmit(getValues(), {} as any);
} }
}, [errors, getValues, isSubmitted, onSubmit]); }, [errors, getValues, isSubmitted, onSubmit]);
@ -56,20 +56,19 @@ export const ImportDashboardForm: FC<Props> = ({
<Legend>Options</Legend> <Legend>Options</Legend>
<Field label="Name" invalid={!!errors.title} error={errors.title && errors.title.message}> <Field label="Name" invalid={!!errors.title} error={errors.title && errors.title.message}>
<Input <Input
name="title" {...register('title', {
type="text"
ref={register({
required: 'Name is required', required: 'Name is required',
validate: async (v: string) => await validateTitle(v, getValues().folder.id), validate: async (v: string) => await validateTitle(v, getValues().folder.id),
})} })}
type="text"
/> />
</Field> </Field>
<Field label="Folder"> <Field label="Folder">
<InputControl <InputControl
as={FolderPicker} render={({ field: { ref, ...field } }) => (
<FolderPicker {...field} enableCreateNew initialFolderId={initialFolderId} />
)}
name="folder" name="folder"
enableCreateNew
initialFolderId={initialFolderId}
control={control} control={control}
/> />
</Field> </Field>
@ -84,13 +83,12 @@ export const ImportDashboardForm: FC<Props> = ({
<> <>
{!uidReset ? ( {!uidReset ? (
<Input <Input
name="uid"
disabled disabled
ref={register({ validate: async (v: string) => await validateUid(v) })} {...register('uid', { validate: async (v: string) => await validateUid(v) })}
addonAfter={!uidReset && <Button onClick={onUidReset}>Change uid</Button>} addonAfter={!uidReset && <Button onClick={onUidReset}>Change uid</Button>}
/> />
) : ( ) : (
<Input name="uid" ref={register({ required: true, validate: async (v: string) => await validateUid(v) })} /> <Input {...register('uid', { required: true, validate: async (v: string) => await validateUid(v) })} />
)} )}
</> </>
</Field> </Field>
@ -106,13 +104,17 @@ export const ImportDashboardForm: FC<Props> = ({
error={errors.dataSources && errors.dataSources[index] && 'A data source is required'} error={errors.dataSources && errors.dataSources[index] && 'A data source is required'}
> >
<InputControl <InputControl
as={DataSourcePicker} name={dataSourceOption as any}
noDefault={true} render={({ field: { ref, ...field } }) => (
pluginId={input.pluginId} <DataSourcePicker
name={`${dataSourceOption}`} {...field}
current={current[index]?.name} noDefault={true}
placeholder={input.info}
pluginId={input.pluginId}
current={current[index]?.name}
/>
)}
control={control} control={control}
placeholder={input.info}
rules={{ required: true }} rules={{ required: true }}
/> />
</Field> </Field>
@ -128,7 +130,7 @@ export const ImportDashboardForm: FC<Props> = ({
invalid={errors.constants && !!errors.constants[index]} invalid={errors.constants && !!errors.constants[index]}
key={constantIndex} key={constantIndex}
> >
<Input ref={register({ required: true })} name={`${constantIndex}`} defaultValue={input.value} /> <Input {...register(constantIndex as any, { required: true })} defaultValue={input.value} />
</Field> </Field>
); );
})} })}

View File

@ -56,8 +56,7 @@ export const NewOrgPage: FC<PropsWithState> = ({ navModel }) => {
<Field label="Organization name" invalid={!!errors.name} error={errors.name && errors.name.message}> <Field label="Organization name" invalid={!!errors.name} error={errors.name && errors.name.message}>
<Input <Input
placeholder="Org name" placeholder="Org name"
name="name" {...register('name', {
ref={register({
required: 'Organization name is required', required: 'Organization name is required',
validate: async (orgName) => await validateOrg(orgName), validate: async (orgName) => await validateOrg(orgName),
})} })}

View File

@ -16,7 +16,7 @@ const OrgProfile: FC<Props> = ({ onSubmit, orgName }) => {
{({ register }) => ( {({ register }) => (
<FieldSet label="Organization profile"> <FieldSet label="Organization profile">
<Field label="Organization name"> <Field label="Organization name">
<Input name="orgName" type="text" ref={register({ required: true })} /> <Input type="text" {...register('orgName', { required: true })} />
</Field> </Field>
<Button type="submit">Update organization name</Button> <Button type="submit">Update organization name</Button>

View File

@ -59,16 +59,20 @@ export const UserInviteForm: FC<Props> = ({}) => {
error={!!errors.loginOrEmail ? 'Email or username is required' : undefined} error={!!errors.loginOrEmail ? 'Email or username is required' : undefined}
label="Email or username" label="Email or username"
> >
<Input name="loginOrEmail" placeholder="email@example.com" ref={register({ required: true })} /> <Input {...register('loginOrEmail', { required: true })} placeholder="email@example.com" />
</Field> </Field>
<Field invalid={!!errors.name} label="Name"> <Field invalid={!!errors.name} label="Name">
<Input name="name" placeholder="(optional)" ref={register} /> <Input {...register('name')} placeholder="(optional)" />
</Field> </Field>
<Field invalid={!!errors.role} label="Role"> <Field invalid={!!errors.role} label="Role">
<InputControl as={RadioButtonGroup} control={control} options={roles} name="role" /> <InputControl
render={({ field: { ref, ...field } }) => <RadioButtonGroup {...field} options={roles} />}
control={control}
name="role"
/>
</Field> </Field>
<Field label="Send invite email"> <Field label="Send invite email">
<Switch name="sendEmail" ref={register} /> <Switch {...register('sendEmail')} />
</Field> </Field>
<HorizontalGroup> <HorizontalGroup>
<Button type="submit">Submit</Button> <Button type="submit">Submit</Button>

View File

@ -29,8 +29,8 @@ class MetricsPanelCtrl extends PanelCtrl {
intervalMs: any; intervalMs: any;
resolution: any; resolution: any;
timeInfo?: string; timeInfo?: string;
skipDataOnInit: boolean; skipDataOnInit = false;
dataList: LegacyResponseData[]; dataList: LegacyResponseData[] = [];
querySubscription?: Unsubscribable | null; querySubscription?: Unsubscribable | null;
useDataFrames = false; useDataFrames = false;
panelData?: PanelData; panelData?: PanelData;

View File

@ -16,19 +16,19 @@ export class PanelCtrl {
panel: any; panel: any;
error: any; error: any;
dashboard: DashboardModel; dashboard: DashboardModel;
pluginName: string; pluginName = '';
pluginId: string; pluginId = '';
editorTabs: any; editorTabs: any;
$scope: any; $scope: any;
$injector: auto.IInjectorService; $injector: auto.IInjectorService;
$location: any; $location: any;
$timeout: any; $timeout: any;
editModeInitiated: boolean; editModeInitiated = false;
height: number; height: number;
width: number; width: number;
containerHeight: any; containerHeight: any;
events: EventBusExtended; events: EventBusExtended;
loading: boolean; loading = false;
timing: any; timing: any;
constructor($scope: any, $injector: auto.IInjectorService) { constructor($scope: any, $injector: auto.IInjectorService) {

View File

@ -30,8 +30,7 @@ export const PlaylistForm: FC<PlaylistFormProps> = ({ onSubmit, playlist }) => {
<Field label="Name" invalid={!!errors.name} error={errors?.name?.message}> <Field label="Name" invalid={!!errors.name} error={errors?.name?.message}>
<Input <Input
type="text" type="text"
name="name" {...register('name', { required: 'Name is required' })}
ref={register({ required: 'Name is required' })}
placeholder="Name" placeholder="Name"
defaultValue={name} defaultValue={name}
aria-label={selectors.pages.PlaylistForm.name} aria-label={selectors.pages.PlaylistForm.name}
@ -40,8 +39,7 @@ export const PlaylistForm: FC<PlaylistFormProps> = ({ onSubmit, playlist }) => {
<Field label="Interval" invalid={!!errors.interval} error={errors?.interval?.message}> <Field label="Interval" invalid={!!errors.interval} error={errors?.interval?.message}>
<Input <Input
type="text" type="text"
name="interval" {...register('interval', { required: 'Interval is required' })}
ref={register({ required: 'Interval is required' })}
placeholder="5m" placeholder="5m"
defaultValue={interval ?? '5m'} defaultValue={interval ?? '5m'}
aria-label={selectors.pages.PlaylistForm.interval} aria-label={selectors.pages.PlaylistForm.interval}

View File

@ -33,14 +33,13 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
return ( return (
<> <>
<Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}> <Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}>
<Input type="password" name="oldPassword" ref={register({ required: 'Old password is required' })} /> <Input type="password" {...register('oldPassword', { required: 'Old password is required' })} />
</Field> </Field>
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}> <Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
<Input <Input
type="password" type="password"
name="newPassword" {...register('newPassword', {
ref={register({
required: 'New password is required', required: 'New password is required',
validate: { validate: {
confirm: (v) => v === getValues().confirmNew || 'Passwords must match', confirm: (v) => v === getValues().confirmNew || 'Passwords must match',
@ -53,8 +52,7 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
<Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}> <Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
<Input <Input
type="password" type="password"
name="confirmNew" {...register('confirmNew', {
ref={register({
required: 'New password confirmation is required', required: 'New password confirmation is required',
validate: (v) => v === getValues().newPassword || 'Passwords must match', validate: (v) => v === getValues().newPassword || 'Passwords must match',
})} })}

View File

@ -24,8 +24,7 @@ export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfi
<FieldSet label="Edit profile"> <FieldSet label="Edit profile">
<Field label="Name" invalid={!!errors.name} error="Name is required" disabled={disableLoginForm}> <Field label="Name" invalid={!!errors.name} error="Name is required" disabled={disableLoginForm}>
<Input <Input
name="name" {...register('name', { required: true })}
ref={register({ required: true })}
placeholder="Name" placeholder="Name"
defaultValue={user.name} defaultValue={user.name}
suffix={<InputSuffix />} suffix={<InputSuffix />}
@ -33,21 +32,14 @@ export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfi
</Field> </Field>
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}> <Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
<Input <Input
name="email" {...register('email', { required: true })}
ref={register({ required: true })}
placeholder="Email" placeholder="Email"
defaultValue={user.email} defaultValue={user.email}
suffix={<InputSuffix />} suffix={<InputSuffix />}
/> />
</Field> </Field>
<Field label="Username" disabled={disableLoginForm}> <Field label="Username" disabled={disableLoginForm}>
<Input <Input {...register('login')} defaultValue={user.login} placeholder="Username" suffix={<InputSuffix />} />
name="login"
ref={register}
defaultValue={user.login}
placeholder="Username"
suffix={<InputSuffix />}
/>
</Field> </Field>
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<Button variant="primary" disabled={isSavingUser}> <Button variant="primary" disabled={isSavingUser}>

View File

@ -34,7 +34,7 @@ export class CreateTeam extends PureComponent<Props> {
{({ register }) => ( {({ register }) => (
<FieldSet label="New Team"> <FieldSet label="New Team">
<Field label="Name"> <Field label="Name">
<Input name="name" ref={register({ required: true })} width={60} /> <Input {...register('name', { required: true })} width={60} />
</Field> </Field>
<Field <Field
label={ label={
@ -46,7 +46,7 @@ export class CreateTeam extends PureComponent<Props> {
</Label> </Label>
} }
> >
<Input type="email" name="email" ref={register()} placeholder="email@test.com" width={60} /> <Input {...register('email')} type="email" placeholder="email@test.com" width={60} />
</Field> </Field>
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<Button type="submit" variant="primary"> <Button type="submit" variant="primary">

View File

@ -24,14 +24,14 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
{({ register }) => ( {({ register }) => (
<> <>
<Field label="Name"> <Field label="Name">
<Input name="name" ref={register({ required: true })} /> <Input {...register('name', { required: true })} />
</Field> </Field>
<Field <Field
label="Email" label="Email"
description="This is optional and is primarily used to set the team profile avatar (via gravatar service)." description="This is optional and is primarily used to set the team profile avatar (via gravatar service)."
> >
<Input placeholder="team@email.com" type="email" name="email" ref={register} /> <Input {...register('email')} placeholder="team@email.com" type="email" />
</Field> </Field>
<Button type="submit">Update</Button> <Button type="submit">Update</Button>
</> </>

View File

@ -73,8 +73,7 @@ export const SignupInvitedPage: FC<Props> = ({ match }) => {
<Field invalid={!!errors.email} error={errors.email && errors.email.message} label="Email"> <Field invalid={!!errors.email} error={errors.email && errors.email.message} label="Email">
<Input <Input
placeholder="email@example.com" placeholder="email@example.com"
name="email" {...register('email', {
ref={register({
required: 'Email is required', required: 'Email is required',
pattern: { pattern: {
value: /^\S+@\S+$/, value: /^\S+@\S+$/,
@ -84,17 +83,16 @@ export const SignupInvitedPage: FC<Props> = ({ match }) => {
/> />
</Field> </Field>
<Field invalid={!!errors.name} error={errors.name && errors.name.message} label="Name"> <Field invalid={!!errors.name} error={errors.name && errors.name.message} label="Name">
<Input placeholder="Name (optional)" name="name" ref={register} /> <Input placeholder="Name (optional)" {...register('name')} />
</Field> </Field>
<Field invalid={!!errors.username} error={errors.username && errors.username.message} label="Username"> <Field invalid={!!errors.username} error={errors.username && errors.username.message} label="Username">
<Input placeholder="Username" name="username" ref={register({ required: 'Username is required' })} /> <Input {...register('username', { required: 'Username is required' })} placeholder="Username" />
</Field> </Field>
<Field invalid={!!errors.password} error={errors.password && errors.password.message} label="Password"> <Field invalid={!!errors.password} error={errors.password && errors.password.message} label="Password">
<Input <Input
{...register('password', { required: 'Password is required' })}
type="password" type="password"
placeholder="Password" placeholder="Password"
name="password"
ref={register({ required: 'Password is required' })}
/> />
</Field> </Field>

View File

@ -29,7 +29,7 @@ function getLabelFromTrace(trace: TraceResponse): string {
} }
export class JaegerQueryField extends React.PureComponent<Props, State> { export class JaegerQueryField extends React.PureComponent<Props, State> {
private _isMounted: boolean; private _isMounted = false;
constructor(props: Props, context: React.Context<any>) { constructor(props: Props, context: React.Context<any>) {
super(props, context); super(props, context);

View File

@ -69,8 +69,8 @@ export class PromQueryEditor extends PureComponent<Props, State> {
this.setState({ formatOption: option }, this.onRunQuery); this.setState({ formatOption: option }, this.onRunQuery);
}; };
onInstantChange = (e: React.ChangeEvent<HTMLInputElement>) => { onInstantChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const instant = e.target.checked; const instant = (e.target as HTMLInputElement).checked;
this.query.instant = instant; this.query.instant = instant;
this.setState({ instant }, this.onRunQuery); this.setState({ instant }, this.onRunQuery);
}; };

View File

@ -35,27 +35,25 @@ export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
return ( return (
<Form onSubmit={addPoint} maxWidth="none"> <Form onSubmit={addPoint} maxWidth="none">
{({ register, control, watch }) => { {({ register, control, watch, setValue }) => {
const selectedPoint = watch('selectedPoint') as SelectableValue; const selectedPoint = watch('selectedPoint' as any) as SelectableValue;
return ( return (
<InlineFieldRow> <InlineFieldRow>
<InlineField label="New value" labelWidth={14}> <InlineField label="New value" labelWidth={14}>
<Input <Input
{...register('newPointValue')}
width={32} width={32}
type="number" type="number"
placeholder="value" placeholder="value"
id={`newPointValue-${query.refId}`} id={`newPointValue-${query.refId}`}
name="newPointValue"
ref={register}
/> />
</InlineField> </InlineField>
<InlineField label="Time" labelWidth={14}> <InlineField label="Time" labelWidth={14}>
<Input <Input
{...register('newPointTime')}
width={32} width={32}
id={`newPointTime-${query.refId}`} id={`newPointTime-${query.refId}`}
placeholder="time" placeholder="time"
name="newPointTime"
ref={register}
defaultValue={dateTime().format()} defaultValue={dateTime().format()}
/> />
</InlineField> </InlineField>
@ -64,13 +62,11 @@ export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
</InlineField> </InlineField>
<InlineField label="All values"> <InlineField label="All values">
<InputControl <InputControl
name={'selectedPoint' as any}
control={control} control={control}
as={Select} render={({ field: { ref, ...field } }) => (
options={pointOptions} <Select {...field} options={pointOptions} width={32} placeholder="Select point" />
width={32} )}
name="selectedPoint"
onChange={(value) => value[0]}
placeholder="Select point"
/> />
</InlineField> </InlineField>
@ -80,7 +76,7 @@ export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
type="button" type="button"
variant="destructive" variant="destructive"
onClick={() => { onClick={() => {
control.setValue('selectedPoint', [{ value: undefined, label: 'Select value' }]); setValue('selectedPoint' as any, [{ value: undefined, label: 'Select value' }]);
deletePoint(selectedPoint); deletePoint(selectedPoint);
}} }}
> >

View File

@ -20755,10 +20755,10 @@ react-highlight-words@0.16.0:
memoize-one "^4.0.0" memoize-one "^4.0.0"
prop-types "^15.5.8" prop-types "^15.5.8"
react-hook-form@5.1.3: react-hook-form@7.2.3:
version "5.1.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.1.3.tgz#24610e11878c6bd143569ce203320f7367893e75" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.2.3.tgz#a4be9214cab3a6e6358f95d342da2e7ded37e3f0"
integrity sha512-6+6wSge72A2Y7WGqMke4afOz0uDJ3gOPSysmYKkjJszSbmw8X8at7tJPzifnZ+cwLDR88b4on/D+jfH5azWbIw== integrity sha512-ki83pkQH/NK6HbSWb4zHLD78s8nh6OW2j4GC5kAjhB2C3yiiVGvNAvybgAfnsXBbx+xb9mPgSpRRVOQUbss+JQ==
react-hot-loader@4.8.0: react-hot-loader@4.8.0:
version "4.8.0" version "4.8.0"