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-dom": "17.0.1",
"react-highlight-words": "0.16.0",
"react-hook-form": "5.1.3",
"react-hook-form": "7.2.3",
"react-popper": "2.2.4",
"react-storybook-addon-props-combinations": "1.1.0",
"react-table": "7.0.0",

View File

@ -17,7 +17,7 @@ export default {
};
export const simple = () => {
const defaultValues = {
const defaultValues: any = {
people: [{ firstName: 'Janis', lastName: 'Joplin' }],
};
return (
@ -30,8 +30,16 @@ export const simple = () => {
<div style={{ marginBottom: '1rem' }}>
{fields.map((field, index) => (
<HorizontalGroup key={field.id}>
<Input ref={register()} name={`people[${index}].firstName`} value={field.firstName} />
<Input ref={register()} name={`people[${index}].lastName`} value={field.lastName} />
<Input
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>
))}
</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";
<Meta title="MDX|Form" component={Form} />
@ -29,8 +29,8 @@ const defaultUser: Partial<UserDTO> = {
>{({register, errors}) => {
return (
<Field>
<Input name="name" ref={register}/>
<Input type="email" name="email" ref={register({required: true})}/>
<Input {...register("name")}/>
<Input {...register("email", {required: true})} type="email" />
<Button type="submit">Create User</Button>
</Field>
)
@ -43,18 +43,17 @@ const defaultUser: Partial<UserDTO> = {
#### `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
<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
<Input
name="inputName"
ref={register({
{...register("inputName", {
required: true,
minLength: 10,
validate: v => { // custom validation rule }
@ -70,7 +69,7 @@ See [Validation](#validation) for examples on validation and validation rules.
```jsx
<Field label="Name" invalid={!!errors.name} error="Name is required">
<Input name="name" ref={register({ required: true })} />
<Input {...register('name', { required: true })} />
</Field>
```
@ -89,22 +88,20 @@ import { Form, Field, InputControl } from '@grafana/ui';
<Field label="RadioButtonExample">
<InputControl
{/* Render InputControl as controlled input (RadioButtonGroup) */}
as={RadioButtonGroup}
render={({field}) => <RadioButtonGroup {...field} options={...} />}
{/* Pass control exposed from Form render prop */}
control={control}
name="radio"
options={...}
/>
</Field>
<Field label="SelectExample">
<InputControl
{/* Render InputControl as controlled input (Select) */}
as={Select}
render={({field}) => <Select {...field} options={...} />}
{/* Pass control exposed from Form render prop */}
control={control}
name="select"
options={...}
/>
</Field>
</>
@ -112,32 +109,30 @@ import { Form, Field, InputControl } from '@grafana/ui';
</Form>
```
Note that when using `InputControl`, it expects the name of the prop that handles input change to be called `onChange`.
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.
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:
```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">
<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}
name="select"
onSelected={onSelectChange}
{/* Pass the name of the onChange handler */}
onChangeName='onSelected'
/>
</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
@ -179,7 +174,7 @@ const defaultValues: FormDto {
<Form ...>{
({register}) => (
<>
<Input defaultValue={default.name} name="name" ref={register} />
<Input {...register("name")} defaultValue={default.name} />
</>
)}
</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'}
<Input
{...register("name", { required: true })}
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 }
<Input
defaultValue={default.name}
name="name"
ref={register({
{...register("name", {
required: 'Name is required',
validation: v => {
return v !== 'John' && 'Name must be John'
@ -258,8 +251,7 @@ validateAsync = (newValue: string) => {
<Field invalid={!!errors.name} error={errors.name?.message}
<Input
defaultValue={default.name}
name="name"
ref={register({
{...register("name", {
required: 'Name is required',
validation: async v => {
return await validateAsync(v);
@ -271,6 +263,26 @@ validateAsync = (newValue: string) => {
</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 of={Form} />

View File

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

View File

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

View File

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

View File

@ -24,8 +24,7 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
<Input
autoFocus
type="password"
name="newPassword"
ref={register({
{...register('newPassword', {
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}>
<Input
type="password"
name="confirmNew"
ref={register({
{...register('confirmNew', {
required: 'Confirmed password is required',
validate: (v) => v === getValues().newPassword || 'Passwords must match!',
validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
})}
/>
</Field>

View File

@ -51,7 +51,7 @@ export const ForgottenPassword: FC = () => {
invalid={!!errors.userOrEmail}
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>
<HorizontalGroup>
<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}>
<Input
{...register('user', { required: 'Email or username is required' })}
autoFocus
name="user"
autoCapitalize="none"
ref={register({ required: 'Email or username is required' })}
placeholder={loginHint}
aria-label={selectors.pages.Login.username}
/>
</Field>
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
<Input
name="password"
{...register('password', { required: 'Password is required' })}
type="password"
placeholder={passwordHint}
ref={register({ required: 'Password is required' })}
aria-label={selectors.pages.Login.password}
/>
</Field>

View File

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

View File

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

View File

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

View File

@ -46,15 +46,15 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
invalid={!!errors.name}
error={errors.name ? 'Name is required' : undefined}
>
<Input name="name" ref={register({ required: true })} />
<Input {...register('name', { required: true })} />
</Field>
<Field label="Email">
<Input name="email" ref={register} />
<Input {...register('email')} />
</Field>
<Field label="Username">
<Input name="login" ref={register} />
<Input {...register('login')} />
</Field>
<Field
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}
>
<Input
type="password"
name="password"
ref={register({
{...register('password', {
validate: (value) => value.trim() !== '' && value.length >= 4,
})}
type="password"
/>
</Field>
<Button type="submit">Create user</Button>

View File

@ -25,10 +25,15 @@ export const BasicSettings: FC<Props> = ({
return (
<>
<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 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>
<NotificationChannelOptions
selectedChannelOptions={selectedChannel.options.filter((o) => o.required)}

View File

@ -9,7 +9,7 @@ import { ChannelSettings } from './ChannelSettings';
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>>;
selectedChannel?: NotificationChannelType;
imageRendererAvailable: boolean;
@ -19,7 +19,7 @@ interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState'> {
}
export interface NotificationSettingsProps
extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues'> {
extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues' | 'setValue'> {
currentFormValues: NotificationChannelDTO;
}
@ -100,7 +100,7 @@ export const NotificationChannelForm: FC<Props> = ({
<div className={styles.formButtons}>
<HorizontalGroup>
<Button type="submit">Save</Button>
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues())}>
Test
</Button>
<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 { 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[];
currentFormValues: NotificationChannelDTO;
secureFields: NotificationChannelSecureFields;
@ -39,8 +39,9 @@ export const NotificationChannelOptions: FC<Props> = ({
return (
<Field key={key}>
<Checkbox
name={option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`}
ref={register}
{...register(
option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`
)}
label={option.label}
description={option.description}
/>

View File

@ -10,12 +10,11 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
return (
<CollapsableSection label="Notification settings" isOpen={false}>
<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>
<Checkbox
name="settings.uploadImage"
ref={register}
{...register('settings.uploadImage')}
label="Include image"
description="Captures an image and include it in the notification"
/>
@ -28,16 +27,14 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
)}
<Field>
<Checkbox
name="disableResolveMessage"
ref={register}
{...register('disableResolveMessage')}
label="Disable Resolve Message"
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
/>
</Field>
<Field>
<Checkbox
name="sendReminder"
ref={register}
{...register('sendReminder')}
label="Send reminders"
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
than a configured alert rule evaluation interval."
>
<Input name="frequency" ref={register} width={8} />
<Input {...register('frequency')} width={8} />
</Field>
</>
)}

View File

@ -13,13 +13,12 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
case 'input':
return (
<Input
invalid={invalid}
type={option.inputType}
name={`${modelValue}`}
ref={register({
{...register(`${modelValue}`, {
required: option.required ? 'Required' : false,
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
})}
invalid={invalid}
type={option.inputType}
placeholder={option.placeholder}
/>
);
@ -27,11 +26,11 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
case 'select':
return (
<InputControl
as={Select}
options={option.selectOptions}
control={control}
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 (
<TextArea
invalid={invalid}
name={`${modelValue}`}
ref={register({
{...register(`${modelValue}`, {
required: option.required ? 'Required' : false,
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',
defaultValues: existing ?? defaults,
});
const validateNameIsUnique: Validate = (name: string) => {
const validateNameIsUnique: Validate<string> = (name: string) => {
return !config.template_files[name] || existing?.name === name
? true
: '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}>
<Input
width={42}
autoFocus={true}
ref={register({
{...register('name', {
required: { value: true, message: 'Required.' },
validate: { nameIsUnique: validateNameIsUnique },
})}
name="name"
width={42}
autoFocus={true}
/>
</Field>
<Field
@ -133,9 +136,8 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
invalid={!!errors.content?.message}
>
<TextArea
{...register('content', { required: { value: true, message: 'Required.' } })}
className={styles.textarea}
ref={register({ required: { value: true, message: 'Required.' } })}
name="content"
rows={12}
/>
</Field>

View File

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

View File

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

View File

@ -9,16 +9,14 @@ export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({
<div className={className}>
<Field>
<Checkbox
name={`${pathPrefix}disableResolveMessage`}
ref={register()}
{...register(`${pathPrefix}disableResolveMessage`)}
label="Disable resolved message"
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
/>
</Field>
<Field>
<Checkbox
name={`${pathPrefix}sendReminder`}
ref={register()}
{...register(`${pathPrefix}sendReminder`)}
label="Send reminders"
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 { NotificationChannelOption } from 'app/types';
import { useFormContext } from 'react-hook-form';
@ -10,18 +10,26 @@ interface Props {
}
export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) => {
const { control, register } = useFormContext();
const { control, register, unregister } = useFormContext();
const modelValue = option.secure
? `${pathPrefix}secureSettings.${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) {
case 'input':
return (
<Input
invalid={invalid}
type={option.inputType}
name={`${modelValue}`}
ref={register({
{...register(`${modelValue}`, {
required: option.required ? 'Required' : false,
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
})}
@ -32,24 +40,27 @@ export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) =
case 'select':
return (
<InputControl
as={Select}
options={option.selectOptions}
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
options={option.selectOptions}
invalid={invalid}
onChange={(value) => onChange(value.value)}
/>
)}
control={control}
name={`${modelValue}`}
invalid={invalid}
onChange={(values) => values[0].value}
/>
);
case 'textarea':
return (
<TextArea
invalid={invalid}
name={`${modelValue}`}
ref={register({
{...register(`${modelValue}`, {
required: option.required ? 'Required' : false,
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 { NotifierDTO } from 'app/types';
import React, { useCallback } from 'react';
import { useForm, FormContext, NestDataObject, FieldError, Validate } from 'react-hook-form';
import { useControlledFieldArray } from '../../../hooks/useControlledFieldArray';
import { useForm, FormProvider, FieldErrors, Validate, useFieldArray } from 'react-hook-form';
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form';
import { makeAMLink } from '../../../utils/misc';
@ -50,11 +49,20 @@ export function ReceiverForm<R extends ChannelValues>({
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) =>
takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
? 'Another receiver with this name already exists.'
@ -63,7 +71,7 @@ export function ReceiverForm<R extends ChannelValues>({
);
return (
<FormContext {...formAPI}>
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit(onSubmit)}>
<h4 className={styles.heading}>{initialValues ? 'Update contact point' : 'Create contact point'}</h4>
{error && (
@ -73,25 +81,28 @@ export function ReceiverForm<R extends ChannelValues>({
)}
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
<Input
{...register('name', {
required: 'Name is required',
validate: { nameIsAvailable: validateNameIsAvailable },
})}
width={39}
name="name"
ref={register({ required: 'Name is required', validate: { nameIsAvailable: validateNameIsAvailable } })}
/>
</Field>
{items.map((item, index) => {
const initialItem = initialValues?.items.find(({ __id }) => __id === item.__id);
{fields.map((field: R & { id: string }, index) => {
const initialItem = initialValues?.items.find(({ __id }) => __id === field.__id);
return (
<ChannelSubForm<R>
key={item.__id}
defaultValues={field}
key={field.id}
onDuplicate={() => {
const currentValues = getValues({ nest: true }).items[index];
const currentValues: R = getValues().items[index];
append({ ...currentValues, __id: String(Math.random()) });
}}
onDelete={() => remove(index)}
pathPrefix={`items.${index}.`}
notifiers={notifiers}
secureFields={initialItem?.secureFields}
errors={errors?.items?.[index] as NestDataObject<R, FieldError>}
errors={errors?.items?.[index] as FieldErrors<R>}
commonSettingsComponent={commonSettingsComponent}
/>
);
@ -115,7 +126,7 @@ export function ReceiverForm<R extends ChannelValues>({
</LinkButton>
</div>
</form>
</FormContext>
</FormProvider>
);
}

View File

@ -7,7 +7,7 @@ import { AlertTypeStep } from './AlertTypeStep';
import { ConditionsStep } from './ConditionsStep';
import { DetailsStep } from './DetailsStep';
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 { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
@ -39,7 +39,11 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
defaultValues,
});
const { handleSubmit, watch, errors } = formAPI;
const {
handleSubmit,
watch,
formState: { errors },
} = formAPI;
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);
const submit = (values: RuleFormValues, exitOnSave: boolean) => {
console.log('submit', values);
dispatch(
saveRuleFormAction({
values: {
@ -68,7 +71,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
};
return (
<FormContext {...formAPI}>
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
<PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}>
<Link to="/alerting/list">
@ -121,7 +124,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
</CustomScrollbar>
</div>
</form>
</FormContext>
</FormProvider>
);
};

View File

@ -6,7 +6,7 @@ import { css } from '@emotion/css';
import { RuleEditorSection } from './RuleEditorSection';
import { useFormContext } from 'react-hook-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 { RuleFolderPicker } from './RuleFolderPicker';
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
@ -31,7 +31,13 @@ interface Props {
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
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 dataSourceName = watch('dataSourceName');
@ -61,9 +67,8 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.name?.message}
>
<Input
{...register('name', { required: { value: true, message: 'Must enter an alert name' } })}
autoFocus={true}
ref={register({ required: { value: true, message: 'Must enter an alert name' } })}
name="name"
/>
</Field>
<div className={styles.flexRow}>
@ -75,25 +80,29 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.type?.message}
>
<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"
options={alertTypeOptions}
control={control}
rules={{
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>
{ruleFormType === RuleFormType.system && (
@ -104,21 +113,25 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.dataSourceName?.message}
>
<InputControl
as={(DataSourcePicker as unknown) as React.ComponentType<Omit<DataSourcePickerProps, 'current'>>}
valueName="current"
filter={dataSourceFilter}
render={({ field: { onChange, ref, value, ...field } }) => (
<DataSourcePicker
{...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"
noDefault={true}
control={control}
alerting={true}
rules={{
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>
)}
@ -134,10 +147,10 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.folder?.message}
>
<InputControl
as={RuleFolderPicker}
render={({ field: { ref, ...field } }) => (
<RuleFolderPicker {...field} enableCreateNew={true} enableReset={true} />
)}
name="folder"
enableCreateNew={true}
enableReset={true}
rules={{
required: { value: true, message: 'Please select a folder' },
}}

View File

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

View File

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

View File

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

View File

@ -14,7 +14,12 @@ interface Props {
}
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);
@ -44,32 +49,34 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
<>
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
<InputControl
as={SelectWithAdd}
className={inputStyle}
render={({ field: { onChange, ref, ...field } }) => (
<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"
options={namespaceOptions}
control={control}
width={42}
rules={{
required: { value: true, message: 'Required.' },
}}
onChange={(values) => {
setValue('group', ''); //reset if namespace changes
return values[0];
}}
onCustomChange={(custom: boolean) => {
custom && setCustomGroup(true);
}}
/>
</Field>
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
<InputControl
as={SelectWithAdd}
render={({ field: { ref, ...field } }) => (
<SelectWithAdd {...field} options={groupOptions} width={42} custom={customGroup} className={inputStyle} />
)}
name="group"
className={inputStyle}
options={groupOptions}
width={42}
custom={customGroup}
control={control}
rules={{
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 { css, cx } from '@emotion/css';
import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form';
interface Props {
className?: string;
@ -11,7 +10,12 @@ interface Props {
const LabelsField: FC<Props> = ({ className }) => {
const styles = useStyles(getStyles);
const { register, control, watch, errors } = useFormContext<RuleFormValues>();
const {
register,
control,
watch,
formState: { errors },
} = useFormContext();
const labels = watch('labels');
return (
<div className={cx(className, styles.wrapper)}>
@ -33,8 +37,9 @@ const LabelsField: FC<Props> = ({ className }) => {
error={errors.labels?.[index]?.key?.message}
>
<Input
ref={register({ required: { value: !!labels[index]?.value, message: 'Required.' } })}
name={`labels[${index}].key`}
{...register(`labels[${index}].key`, {
required: { value: !!labels[index]?.value, message: 'Required.' },
})}
placeholder="key"
defaultValue={field.key}
/>
@ -46,8 +51,9 @@ const LabelsField: FC<Props> = ({ className }) => {
error={errors.labels?.[index]?.value?.message}
>
<Input
ref={register({ required: { value: !!labels[index]?.key, message: 'Required.' } })}
name={`labels[${index}].value`}
{...register(`labels[${index}].value`, {
required: { value: !!labels[index]?.key, message: 'Required.' },
})}
placeholder="value"
defaultValue={field.value}
/>

View File

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

View File

@ -6,7 +6,7 @@ export interface Folder {
id: number;
}
export interface Props extends Omit<FolderPickerProps, 'initiailTitle' | 'initialFolderId'> {
export interface Props extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
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 {
const channel: GrafanaManagedReceiverConfig = {
settings: {
...(existing?.settings ?? {}),
...(existing && existing.type === values.type ? existing.settings ?? {} : {}),
...(values.settings ?? {}),
},
secureSettings: values.secureSettings ?? {},

View File

@ -26,7 +26,7 @@ export const RowOptionsForm: FC<Props> = ({ repeat, title, onUpdate, onCancel })
{({ register }) => (
<>
<Field label="Title">
<Input name="title" ref={register} type="text" />
<Input {...register('title')} type="text" />
</Field>
<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}>
<Input
name="title"
ref={register({
{...register('title', {
validate: validateDashboardName(getValues),
})}
aria-label="Save dashboard title field"
@ -102,17 +101,21 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
</Field>
<Field label="Folder">
<InputControl
as={FolderPicker}
render={({ field: { ref, ...field } }) => (
<FolderPicker
{...field}
dashboardId={dashboard.id}
initialFolderId={dashboard.meta.folderId}
initialTitle={dashboard.meta.folderTitle}
enableCreateNew
/>
)}
control={control}
name="$folder"
dashboardId={dashboard.id}
initialFolderId={dashboard.meta.folderId}
initialTitle={dashboard.meta.folderTitle}
enableCreateNew
/>
</Field>
<Field label="Copy tags">
<Switch name="copyTags" ref={register} />
<Switch {...register('copyTags')} />
</Field>
<Modal.ButtonRow>
<Button variant="secondary" onClick={onCancel} fill="outline">

View File

@ -39,23 +39,21 @@ export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard,
<div>
{hasTimeChanged && (
<Checkbox
{...register('saveTimerange')}
label="Save current time range as dashboard default"
name="saveTimerange"
ref={register}
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
/>
)}
{hasVariableChanged && (
<Checkbox
{...register('saveVariables')}
label="Save current variable values as dashboard default"
name="saveVariables"
ref={register}
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
/>
)}
{(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>
<Modal.ButtonRow>

View File

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

View File

@ -98,10 +98,9 @@ class UnthemedDashboardImport extends PureComponent<Props> {
{({ register, errors }) => (
<Field invalid={!!errors.gcomDashboard} error={errors.gcomDashboard && errors.gcomDashboard.message}>
<Input
name="gcomDashboard"
placeholder="Grafana.com dashboard URL or ID"
type="text"
ref={register({
{...register('gcomDashboard', {
required: 'A Grafana dashboard URL or ID is required',
validate: validateGcomDashboard,
})}
@ -118,8 +117,7 @@ class UnthemedDashboardImport extends PureComponent<Props> {
<>
<Field invalid={!!errors.dashboardJson} error={errors.dashboardJson && errors.dashboardJson.message}>
<TextArea
name="dashboardJson"
ref={register({
{...register('dashboardJson', {
required: 'Need a dashboard JSON model',
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 { validateTitle, validateUid } from '../utils/validation';
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> {
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'setValue'> {
uidReset: boolean;
inputs: DashboardInputs;
initialFolderId: number;
@ -47,7 +47,7 @@ export const ImportDashboardForm: FC<Props> = ({
*/
useEffect(() => {
if (isSubmitted && (errors.title || errors.uid)) {
onSubmit(getValues({ nest: true }), {} as any);
onSubmit(getValues(), {} as any);
}
}, [errors, getValues, isSubmitted, onSubmit]);
@ -56,20 +56,19 @@ export const ImportDashboardForm: FC<Props> = ({
<Legend>Options</Legend>
<Field label="Name" invalid={!!errors.title} error={errors.title && errors.title.message}>
<Input
name="title"
type="text"
ref={register({
{...register('title', {
required: 'Name is required',
validate: async (v: string) => await validateTitle(v, getValues().folder.id),
})}
type="text"
/>
</Field>
<Field label="Folder">
<InputControl
as={FolderPicker}
render={({ field: { ref, ...field } }) => (
<FolderPicker {...field} enableCreateNew initialFolderId={initialFolderId} />
)}
name="folder"
enableCreateNew
initialFolderId={initialFolderId}
control={control}
/>
</Field>
@ -84,13 +83,12 @@ export const ImportDashboardForm: FC<Props> = ({
<>
{!uidReset ? (
<Input
name="uid"
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>}
/>
) : (
<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>
@ -106,13 +104,17 @@ export const ImportDashboardForm: FC<Props> = ({
error={errors.dataSources && errors.dataSources[index] && 'A data source is required'}
>
<InputControl
as={DataSourcePicker}
noDefault={true}
pluginId={input.pluginId}
name={`${dataSourceOption}`}
current={current[index]?.name}
name={dataSourceOption as any}
render={({ field: { ref, ...field } }) => (
<DataSourcePicker
{...field}
noDefault={true}
placeholder={input.info}
pluginId={input.pluginId}
current={current[index]?.name}
/>
)}
control={control}
placeholder={input.info}
rules={{ required: true }}
/>
</Field>
@ -128,7 +130,7 @@ export const ImportDashboardForm: FC<Props> = ({
invalid={errors.constants && !!errors.constants[index]}
key={constantIndex}
>
<Input ref={register({ required: true })} name={`${constantIndex}`} defaultValue={input.value} />
<Input {...register(constantIndex as any, { required: true })} defaultValue={input.value} />
</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}>
<Input
placeholder="Org name"
name="name"
ref={register({
{...register('name', {
required: 'Organization name is required',
validate: async (orgName) => await validateOrg(orgName),
})}

View File

@ -16,7 +16,7 @@ const OrgProfile: FC<Props> = ({ onSubmit, orgName }) => {
{({ register }) => (
<FieldSet label="Organization profile">
<Field label="Organization name">
<Input name="orgName" type="text" ref={register({ required: true })} />
<Input type="text" {...register('orgName', { required: true })} />
</Field>
<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}
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 invalid={!!errors.name} label="Name">
<Input name="name" placeholder="(optional)" ref={register} />
<Input {...register('name')} placeholder="(optional)" />
</Field>
<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 label="Send invite email">
<Switch name="sendEmail" ref={register} />
<Switch {...register('sendEmail')} />
</Field>
<HorizontalGroup>
<Button type="submit">Submit</Button>

View File

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

View File

@ -16,19 +16,19 @@ export class PanelCtrl {
panel: any;
error: any;
dashboard: DashboardModel;
pluginName: string;
pluginId: string;
pluginName = '';
pluginId = '';
editorTabs: any;
$scope: any;
$injector: auto.IInjectorService;
$location: any;
$timeout: any;
editModeInitiated: boolean;
editModeInitiated = false;
height: number;
width: number;
containerHeight: any;
events: EventBusExtended;
loading: boolean;
loading = false;
timing: any;
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}>
<Input
type="text"
name="name"
ref={register({ required: 'Name is required' })}
{...register('name', { required: 'Name is required' })}
placeholder="Name"
defaultValue={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}>
<Input
type="text"
name="interval"
ref={register({ required: 'Interval is required' })}
{...register('interval', { required: 'Interval is required' })}
placeholder="5m"
defaultValue={interval ?? '5m'}
aria-label={selectors.pages.PlaylistForm.interval}

View File

@ -33,14 +33,13 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
return (
<>
<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 label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
<Input
type="password"
name="newPassword"
ref={register({
{...register('newPassword', {
required: 'New password is required',
validate: {
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}>
<Input
type="password"
name="confirmNew"
ref={register({
{...register('confirmNew', {
required: 'New password confirmation is required',
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">
<Field label="Name" invalid={!!errors.name} error="Name is required" disabled={disableLoginForm}>
<Input
name="name"
ref={register({ required: true })}
{...register('name', { required: true })}
placeholder="Name"
defaultValue={user.name}
suffix={<InputSuffix />}
@ -33,21 +32,14 @@ export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfi
</Field>
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
<Input
name="email"
ref={register({ required: true })}
{...register('email', { required: true })}
placeholder="Email"
defaultValue={user.email}
suffix={<InputSuffix />}
/>
</Field>
<Field label="Username" disabled={disableLoginForm}>
<Input
name="login"
ref={register}
defaultValue={user.login}
placeholder="Username"
suffix={<InputSuffix />}
/>
<Input {...register('login')} defaultValue={user.login} placeholder="Username" suffix={<InputSuffix />} />
</Field>
<div className="gf-form-button-row">
<Button variant="primary" disabled={isSavingUser}>

View File

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

View File

@ -24,14 +24,14 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
{({ register }) => (
<>
<Field label="Name">
<Input name="name" ref={register({ required: true })} />
<Input {...register('name', { required: true })} />
</Field>
<Field
label="Email"
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>
<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">
<Input
placeholder="email@example.com"
name="email"
ref={register({
{...register('email', {
required: 'Email is required',
pattern: {
value: /^\S+@\S+$/,
@ -84,17 +83,16 @@ export const SignupInvitedPage: FC<Props> = ({ match }) => {
/>
</Field>
<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 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 invalid={!!errors.password} error={errors.password && errors.password.message} label="Password">
<Input
{...register('password', { required: 'Password is required' })}
type="password"
placeholder="Password"
name="password"
ref={register({ required: 'Password is required' })}
/>
</Field>

View File

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

View File

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

View File

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

View File

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