mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9de2f1bb8f
commit
3b515e650c
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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)}
|
||||
|
@ -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`}>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -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),
|
||||
})}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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' },
|
||||
}}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
`,
|
||||
|
@ -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.' },
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
),
|
||||
};
|
||||
}
|
@ -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 ?? {},
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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),
|
||||
})}
|
||||
|
@ -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,
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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),
|
||||
})}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
})}
|
||||
|
@ -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}>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user