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-custom-scrollbars": "4.2.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-highlight-words": "0.16.0",
|
"react-highlight-words": "0.16.0",
|
||||||
"react-hook-form": "5.1.3",
|
"react-hook-form": "7.2.3",
|
||||||
"react-popper": "2.2.4",
|
"react-popper": "2.2.4",
|
||||||
"react-storybook-addon-props-combinations": "1.1.0",
|
"react-storybook-addon-props-combinations": "1.1.0",
|
||||||
"react-table": "7.0.0",
|
"react-table": "7.0.0",
|
||||||
|
@ -17,7 +17,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const simple = () => {
|
export const simple = () => {
|
||||||
const defaultValues = {
|
const defaultValues: any = {
|
||||||
people: [{ firstName: 'Janis', lastName: 'Joplin' }],
|
people: [{ firstName: 'Janis', lastName: 'Joplin' }],
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -30,8 +30,16 @@ export const simple = () => {
|
|||||||
<div style={{ marginBottom: '1rem' }}>
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<HorizontalGroup key={field.id}>
|
<HorizontalGroup key={field.id}>
|
||||||
<Input ref={register()} name={`people[${index}].firstName`} value={field.firstName} />
|
<Input
|
||||||
<Input ref={register()} name={`people[${index}].lastName`} value={field.lastName} />
|
key={field.id}
|
||||||
|
{...register(`people.${index}.firstName` as const)}
|
||||||
|
defaultValue={field.firstName}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
key={field.id}
|
||||||
|
{...register(`people.${index}.lastName` as const)}
|
||||||
|
defaultValue={field.lastName}
|
||||||
|
/>
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
|
import { Meta, Props } from "@storybook/addon-docs/blocks";
|
||||||
import { Form } from "./Form";
|
import { Form } from "./Form";
|
||||||
|
|
||||||
<Meta title="MDX|Form" component={Form} />
|
<Meta title="MDX|Form" component={Form} />
|
||||||
@ -29,8 +29,8 @@ const defaultUser: Partial<UserDTO> = {
|
|||||||
>{({register, errors}) => {
|
>{({register, errors}) => {
|
||||||
return (
|
return (
|
||||||
<Field>
|
<Field>
|
||||||
<Input name="name" ref={register}/>
|
<Input {...register("name")}/>
|
||||||
<Input type="email" name="email" ref={register({required: true})}/>
|
<Input {...register("email", {required: true})} type="email" />
|
||||||
<Button type="submit">Create User</Button>
|
<Button type="submit">Create User</Button>
|
||||||
</Field>
|
</Field>
|
||||||
)
|
)
|
||||||
@ -43,18 +43,17 @@ const defaultUser: Partial<UserDTO> = {
|
|||||||
|
|
||||||
#### `register`
|
#### `register`
|
||||||
|
|
||||||
`register` allows to register form elements(inputs, selects, radios, etc) in the form. In order to do that you need to pass `register` as a `ref` property to the form input. For example:
|
`register` allows registering form elements (inputs, selects, radios, etc) in the form. In order to do that you need to invoke the function itself and spread the props into the input. For example:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Input name="inputName" ref={register} />
|
<Input {...register("inputName")} />
|
||||||
```
|
```
|
||||||
|
|
||||||
Register accepts an object which describes validation rules for a given input:
|
The first argument for `register` is the field name. It also accepts an object, which describes validation rules for a given input:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Input
|
<Input
|
||||||
name="inputName"
|
{...register("inputName", {
|
||||||
ref={register({
|
|
||||||
required: true,
|
required: true,
|
||||||
minLength: 10,
|
minLength: 10,
|
||||||
validate: v => { // custom validation rule }
|
validate: v => { // custom validation rule }
|
||||||
@ -70,7 +69,7 @@ See [Validation](#validation) for examples on validation and validation rules.
|
|||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
||||||
<Input name="name" ref={register({ required: true })} />
|
<Input {...register('name', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -89,22 +88,20 @@ import { Form, Field, InputControl } from '@grafana/ui';
|
|||||||
<Field label="RadioButtonExample">
|
<Field label="RadioButtonExample">
|
||||||
<InputControl
|
<InputControl
|
||||||
{/* Render InputControl as controlled input (RadioButtonGroup) */}
|
{/* Render InputControl as controlled input (RadioButtonGroup) */}
|
||||||
as={RadioButtonGroup}
|
render={({field}) => <RadioButtonGroup {...field} options={...} />}
|
||||||
{/* Pass control exposed from Form render prop */}
|
{/* Pass control exposed from Form render prop */}
|
||||||
control={control}
|
control={control}
|
||||||
name="radio"
|
name="radio"
|
||||||
options={...}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="SelectExample">
|
<Field label="SelectExample">
|
||||||
<InputControl
|
<InputControl
|
||||||
{/* Render InputControl as controlled input (Select) */}
|
{/* Render InputControl as controlled input (Select) */}
|
||||||
as={Select}
|
render={({field}) => <Select {...field} options={...} />}
|
||||||
{/* Pass control exposed from Form render prop */}
|
{/* Pass control exposed from Form render prop */}
|
||||||
control={control}
|
control={control}
|
||||||
name="select"
|
name="select"
|
||||||
options={...}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</>
|
</>
|
||||||
@ -112,32 +109,30 @@ import { Form, Field, InputControl } from '@grafana/ui';
|
|||||||
</Form>
|
</Form>
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that when using `InputControl`, it expects the name of the prop that handles input change to be called `onChange`.
|
In case we want to modify the selected value before passing it to the form, we can use the `onChange` callback from the render's `field` argument:
|
||||||
If the property is named differently for any specific component, additional `onChangeName` prop has to be provided, specifying the name.
|
|
||||||
Additionally, the `onChange` arguments passed as an array. Check [react-hook-form docs](https://react-hook-form.com/api/#Controller)
|
|
||||||
for more prop options.
|
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
{/* DashboardPicker has onSelected prop instead of onChange */}
|
|
||||||
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
|
|
||||||
|
|
||||||
{/* In case of Select the value has to be returned as an object with a `value` key for the value to be saved to form data */}
|
|
||||||
const onSelectChange = ([value]) => {
|
|
||||||
// ...
|
|
||||||
return { value };
|
|
||||||
}
|
|
||||||
|
|
||||||
<Field label="SelectExample">
|
<Field label="SelectExample">
|
||||||
<InputControl
|
<InputControl
|
||||||
as={DashboardPicker}
|
{/* Here `value` has a nested `value` property, which we want to save onto the form. */}
|
||||||
|
render={(field: {onChange, ...field}) => <Select {...field} onChange={(value) => onChange(value.value)}/>}
|
||||||
control={control}
|
control={control}
|
||||||
name="select"
|
name="select"
|
||||||
onSelected={onSelectChange}
|
|
||||||
{/* Pass the name of the onChange handler */}
|
|
||||||
onChangeName='onSelected'
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `field` also contains `ref` prop, which is passed down to the rendered component by default. In case if that component doesn't support this prop, it will need to be removed before spreading the `field`.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Field label="SelectExample">
|
||||||
|
<InputControl
|
||||||
|
{/*Remove `ref` prop, so it doesn't get passed down to the component that doesn't support it. */}
|
||||||
|
render={(field: {onChange, ref, ...field}) => <Select {...field} onChange={(value) => onChange(value.value)}/>}
|
||||||
|
control={control}
|
||||||
|
name="select"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Default values
|
### Default values
|
||||||
@ -179,7 +174,7 @@ const defaultValues: FormDto {
|
|||||||
<Form ...>{
|
<Form ...>{
|
||||||
({register}) => (
|
({register}) => (
|
||||||
<>
|
<>
|
||||||
<Input defaultValue={default.name} name="name" ref={register} />
|
<Input {...register("name")} defaultValue={default.name} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
@ -197,9 +192,8 @@ Validation can be performed either synchronously or asynchronously. What's impor
|
|||||||
<>
|
<>
|
||||||
<Field invalid={!!errors.name} error={errors.name && 'Name is required'}
|
<Field invalid={!!errors.name} error={errors.name && 'Name is required'}
|
||||||
<Input
|
<Input
|
||||||
|
{...register("name", { required: true })}
|
||||||
defaultValue={default.name}
|
defaultValue={default.name}
|
||||||
name="name"
|
|
||||||
ref={register({ required: true })}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -217,8 +211,7 @@ One important thing to note is that if you want to provide different error messa
|
|||||||
<Field invalid={!!errors.name} error={errors.name?.message }
|
<Field invalid={!!errors.name} error={errors.name?.message }
|
||||||
<Input
|
<Input
|
||||||
defaultValue={default.name}
|
defaultValue={default.name}
|
||||||
name="name"
|
{...register("name", {
|
||||||
ref={register({
|
|
||||||
required: 'Name is required',
|
required: 'Name is required',
|
||||||
validation: v => {
|
validation: v => {
|
||||||
return v !== 'John' && 'Name must be John'
|
return v !== 'John' && 'Name must be John'
|
||||||
@ -258,8 +251,7 @@ validateAsync = (newValue: string) => {
|
|||||||
<Field invalid={!!errors.name} error={errors.name?.message}
|
<Field invalid={!!errors.name} error={errors.name?.message}
|
||||||
<Input
|
<Input
|
||||||
defaultValue={default.name}
|
defaultValue={default.name}
|
||||||
name="name"
|
{...register("name", {
|
||||||
ref={register({
|
|
||||||
required: 'Name is required',
|
required: 'Name is required',
|
||||||
validation: async v => {
|
validation: async v => {
|
||||||
return await validateAsync(v);
|
return await validateAsync(v);
|
||||||
@ -271,6 +263,26 @@ validateAsync = (newValue: string) => {
|
|||||||
</Form>
|
</Form>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Upgrading to v8
|
||||||
|
Version 8 of Grafana-UI is using version 7 of `react-hook-form` (previously version 5 was used), which introduced a few breaking changes to the `Form` API. The detailed list of changes can be found in the library's migration guides:
|
||||||
|
- [Migration guide v5 to v6](https://react-hook-form.com/migrate-v5-to-v6/)
|
||||||
|
- [Migration guide v6 to v7](https://react-hook-form.com/migrate-v6-to-v7/)
|
||||||
|
|
||||||
|
In a nutshell, the two most important changes are:
|
||||||
|
- register method is no longer passed as a `ref`, but instead its result is spread onto the input component:
|
||||||
|
```jsx
|
||||||
|
- <input ref={register({ required: true })} name="test" />
|
||||||
|
+ <input {...register('test', { required: true })} />
|
||||||
|
```
|
||||||
|
- `InputControl`'s `as` prop has been replaced with `render`, which has `field` and `fieldState` objects as arguments. `onChange`, `onBlur`, `value`, `name`, and `ref` are parts of `field`.
|
||||||
|
```jsx
|
||||||
|
- <Controller as={<input />} />
|
||||||
|
+ <Controller render={({ field }) => <input {...field} />}
|
||||||
|
// or
|
||||||
|
+ <Controller render={({ field, fieldState }) => <input {...field} />} />
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
<Props of={Form} />
|
<Props of={Form} />
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
|
||||||
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
|
|
||||||
import mdx from './Form.mdx';
|
|
||||||
import { ValidateResult } from 'react-hook-form';
|
import { ValidateResult } from 'react-hook-form';
|
||||||
import { Story } from '@storybook/react';
|
import { Story } from '@storybook/react';
|
||||||
import {
|
import {
|
||||||
@ -18,9 +14,12 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
RadioButtonGroup,
|
RadioButtonGroup,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
|
||||||
|
import mdx from './Form.mdx';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Forms/Example forms',
|
title: 'Forms/Form',
|
||||||
decorators: [withStoryContainer, withCenteredStory],
|
decorators: [withStoryContainer, withCenteredStory],
|
||||||
parameters: {
|
parameters: {
|
||||||
docs: {
|
docs: {
|
||||||
@ -48,20 +47,20 @@ const selectOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface FormDTO {
|
interface FormDTO {
|
||||||
name: string;
|
name?: string;
|
||||||
email: string;
|
email?: string;
|
||||||
username: string;
|
username?: string;
|
||||||
checkbox: boolean;
|
checkbox?: boolean;
|
||||||
switch: boolean;
|
switch: boolean;
|
||||||
radio: string;
|
radio: string;
|
||||||
select: string;
|
select: string;
|
||||||
text: string;
|
text?: string;
|
||||||
nested: {
|
nested: {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderForm = (defaultValues?: Partial<FormDTO>) => (
|
const renderForm = (defaultValues?: FormDTO) => (
|
||||||
<Form
|
<Form
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
onSubmit={(data: FormDTO) => {
|
onSubmit={(data: FormDTO) => {
|
||||||
@ -74,34 +73,38 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => (
|
|||||||
<Legend>Edit user</Legend>
|
<Legend>Edit user</Legend>
|
||||||
|
|
||||||
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
||||||
<Input name="name" placeholder="Roger Waters" ref={register({ required: true })} />
|
<Input {...register('name', { required: true })} placeholder="Roger Waters" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Email" invalid={!!errors.email} error="E-mail is required">
|
<Field label="Email" invalid={!!errors.email} error="E-mail is required">
|
||||||
<Input id="email" name="email" placeholder="roger.waters@grafana.com" ref={register({ required: true })} />
|
<Input {...register('email', { required: true })} id="email" placeholder="roger.waters@grafana.com" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Username">
|
<Field label="Username">
|
||||||
<Input name="username" placeholder="mr.waters" ref={register} />
|
<Input {...register('username')} placeholder="mr.waters" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Nested object">
|
<Field label="Nested object">
|
||||||
<Input name="nested.path" placeholder="Nested path" ref={register} />
|
<Input {...register('nested.path')} placeholder="Nested path" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Textarea" invalid={!!errors.text} error="Text is required">
|
<Field label="Textarea" invalid={!!errors.text} error="Text is required">
|
||||||
<TextArea name="text" placeholder="Long text" ref={register({ required: true })} />
|
<TextArea {...register('text', { required: true })} placeholder="Long text" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent">
|
<Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent">
|
||||||
<Checkbox name="checkbox" label="Do you consent?" ref={register({ required: true })} />
|
<Checkbox {...register('checkbox', { required: true })} label="Do you consent?" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Switch">
|
<Field label="Switch">
|
||||||
<Switch name="switch" ref={register} />
|
<Switch name="switch" {...register} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="RadioButton">
|
<Field label="RadioButton">
|
||||||
<InputControl name="radio" control={control} options={selectOptions} as={RadioButtonGroup} />
|
<InputControl
|
||||||
|
name="radio"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => <RadioButtonGroup {...field} options={selectOptions} />}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Select" invalid={!!errors.select} error="Select is required">
|
<Field label="Select" invalid={!!errors.select} error="Select is required">
|
||||||
@ -111,8 +114,7 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => (
|
|||||||
rules={{
|
rules={{
|
||||||
required: true,
|
required: true,
|
||||||
}}
|
}}
|
||||||
options={selectOptions}
|
render={({ field }) => <Select {...field} options={selectOptions} />}
|
||||||
as={Select}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@ -158,9 +160,8 @@ export const AsyncValidation: Story = ({ passAsyncValidation }) => {
|
|||||||
|
|
||||||
<Field label="Name" invalid={!!errors.name} error="Username is already taken">
|
<Field label="Name" invalid={!!errors.name} error="Username is already taken">
|
||||||
<Input
|
<Input
|
||||||
name="name"
|
|
||||||
placeholder="Roger Waters"
|
placeholder="Roger Waters"
|
||||||
ref={register({ validate: validateAsync(passAsyncValidation) })}
|
{...register('name', { validate: validateAsync(passAsyncValidation) })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import React, { HTMLProps, useEffect } from 'react';
|
import React, { HTMLProps, useEffect } from 'react';
|
||||||
import { useForm, Mode, OnSubmit, DeepPartial } from 'react-hook-form';
|
import { useForm, Mode, DeepPartial, UnpackNestedValue, SubmitHandler } from 'react-hook-form';
|
||||||
import { FormAPI } from '../../types';
|
import { FormAPI } from '../../types';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
interface FormProps<T> extends Omit<HTMLProps<HTMLFormElement>, 'onSubmit'> {
|
interface FormProps<T> extends Omit<HTMLProps<HTMLFormElement>, 'onSubmit'> {
|
||||||
validateOn?: Mode;
|
validateOn?: Mode;
|
||||||
validateOnMount?: boolean;
|
validateOnMount?: boolean;
|
||||||
validateFieldsOnMount?: string[];
|
validateFieldsOnMount?: string | string[];
|
||||||
defaultValues?: DeepPartial<T>;
|
defaultValues?: UnpackNestedValue<DeepPartial<T>>;
|
||||||
onSubmit: OnSubmit<T>;
|
onSubmit: SubmitHandler<T>;
|
||||||
children: (api: FormAPI<T>) => React.ReactNode;
|
children: (api: FormAPI<T>) => React.ReactNode;
|
||||||
/** Sets max-width for container. Use it instead of setting individual widths on inputs.*/
|
/** Sets max-width for container. Use it instead of setting individual widths on inputs.*/
|
||||||
maxWidth?: number | 'none';
|
maxWidth?: number | 'none';
|
||||||
@ -24,16 +24,17 @@ export function Form<T>({
|
|||||||
maxWidth = 600,
|
maxWidth = 600,
|
||||||
...htmlProps
|
...htmlProps
|
||||||
}: FormProps<T>) {
|
}: FormProps<T>) {
|
||||||
const { handleSubmit, register, errors, control, triggerValidation, getValues, formState, watch } = useForm<T>({
|
const { handleSubmit, register, control, trigger, getValues, formState, watch, setValue } = useForm<T>({
|
||||||
mode: validateOn,
|
mode: validateOn,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (validateOnMount) {
|
if (validateOnMount) {
|
||||||
triggerValidation(validateFieldsOnMount);
|
//@ts-expect-error
|
||||||
|
trigger(validateFieldsOnMount);
|
||||||
}
|
}
|
||||||
}, [triggerValidation, validateFieldsOnMount, validateOnMount]);
|
}, [trigger, validateFieldsOnMount, validateOnMount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@ -44,7 +45,7 @@ export function Form<T>({
|
|||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
{...htmlProps}
|
{...htmlProps}
|
||||||
>
|
>
|
||||||
{children({ register, errors, control, getValues, formState, watch })}
|
{children({ register, errors: formState.errors, control, getValues, formState, watch, setValue })}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { FormContextValues, FieldValues, ArrayField } from 'react-hook-form';
|
import { UseFormReturn, FieldValues, FieldErrors } from 'react-hook-form';
|
||||||
export { OnSubmit as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form';
|
export { SubmitHandler as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form';
|
||||||
|
|
||||||
export type FormAPI<T> = Pick<
|
export type FormAPI<T> = Pick<
|
||||||
FormContextValues<T>,
|
UseFormReturn<T>,
|
||||||
'register' | 'errors' | 'control' | 'formState' | 'getValues' | 'watch'
|
'register' | 'control' | 'formState' | 'getValues' | 'watch' | 'setValue'
|
||||||
>;
|
> & {
|
||||||
|
errors: FieldErrors<T>;
|
||||||
|
};
|
||||||
|
|
||||||
type FieldArrayValue = Partial<FieldValues> | Array<Partial<FieldValues>>;
|
type FieldArrayValue = Partial<FieldValues> | Array<Partial<FieldValues>>;
|
||||||
|
|
||||||
export interface FieldArrayApi {
|
export interface FieldArrayApi {
|
||||||
fields: Array<Partial<ArrayField<FieldValues, 'id'>>>;
|
fields: Array<Record<string, any>>;
|
||||||
append: (value: FieldArrayValue) => void;
|
append: (value: FieldArrayValue) => void;
|
||||||
prepend: (value: FieldArrayValue) => void;
|
prepend: (value: FieldArrayValue) => void;
|
||||||
remove: (index?: number | number[]) => void;
|
remove: (index?: number | number[]) => void;
|
||||||
|
@ -24,8 +24,7 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
|
|||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
type="password"
|
type="password"
|
||||||
name="newPassword"
|
{...register('newPassword', {
|
||||||
ref={register({
|
|
||||||
required: 'New password required',
|
required: 'New password required',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@ -33,10 +32,9 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
|
|||||||
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="confirmNew"
|
{...register('confirmNew', {
|
||||||
ref={register({
|
|
||||||
required: 'Confirmed password is required',
|
required: 'Confirmed password is required',
|
||||||
validate: (v) => v === getValues().newPassword || 'Passwords must match!',
|
validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -51,7 +51,7 @@ export const ForgottenPassword: FC = () => {
|
|||||||
invalid={!!errors.userOrEmail}
|
invalid={!!errors.userOrEmail}
|
||||||
error={errors?.userOrEmail?.message}
|
error={errors?.userOrEmail?.message}
|
||||||
>
|
>
|
||||||
<Input placeholder="Email or username" name="userOrEmail" ref={register({ required: true })} />
|
<Input placeholder="Email or username" {...register('userOrEmail', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<Button>Send reset email</Button>
|
<Button>Send reset email</Button>
|
||||||
|
@ -31,20 +31,18 @@ export const LoginForm: FC<Props> = ({ children, onSubmit, isLoggingIn, password
|
|||||||
<>
|
<>
|
||||||
<Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}>
|
<Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}>
|
||||||
<Input
|
<Input
|
||||||
|
{...register('user', { required: 'Email or username is required' })}
|
||||||
autoFocus
|
autoFocus
|
||||||
name="user"
|
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
ref={register({ required: 'Email or username is required' })}
|
|
||||||
placeholder={loginHint}
|
placeholder={loginHint}
|
||||||
aria-label={selectors.pages.Login.username}
|
aria-label={selectors.pages.Login.username}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
|
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
|
||||||
<Input
|
<Input
|
||||||
name="password"
|
{...register('password', { required: 'Password is required' })}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={passwordHint}
|
placeholder={passwordHint}
|
||||||
ref={register({ required: 'Password is required' })}
|
|
||||||
aria-label={selectors.pages.Login.password}
|
aria-label={selectors.pages.Login.password}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -63,50 +63,47 @@ export const SignupPage: FC<Props> = (props) => {
|
|||||||
{({ errors, register, getValues }) => (
|
{({ errors, register, getValues }) => (
|
||||||
<>
|
<>
|
||||||
<Field label="Your name">
|
<Field label="Your name">
|
||||||
<Input name="name" placeholder="(optional)" ref={register} />
|
<Input {...register('name')} placeholder="(optional)" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Email" invalid={!!errors.email} error={errors.email?.message}>
|
<Field label="Email" invalid={!!errors.email} error={errors.email?.message}>
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
{...register('email', {
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
ref={register({
|
|
||||||
required: 'Email is required',
|
required: 'Email is required',
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^\S+@\S+$/,
|
value: /^\S+@\S+$/,
|
||||||
message: 'Email is invalid',
|
message: 'Email is invalid',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{!getConfig().autoAssignOrg && (
|
{!getConfig().autoAssignOrg && (
|
||||||
<Field label="Org. name">
|
<Field label="Org. name">
|
||||||
<Input name="orgName" placeholder="Org. name" ref={register} />
|
<Input {...register('orgName')} placeholder="Org. name" />
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
{getConfig().verifyEmailEnabled && (
|
{getConfig().verifyEmailEnabled && (
|
||||||
<Field label="Email verification code (sent to your email)">
|
<Field label="Email verification code (sent to your email)">
|
||||||
<Input name="code" ref={register} placeholder="Code" />
|
<Input {...register('code')} placeholder="Code" />
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
|
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
{...register('password', {
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
ref={register({
|
|
||||||
required: 'Password is required',
|
required: 'Password is required',
|
||||||
})}
|
})}
|
||||||
|
autoFocus
|
||||||
|
type="password"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
|
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
{...register('confirm', {
|
||||||
name="confirm"
|
|
||||||
ref={register({
|
|
||||||
required: 'Confirmed password is required',
|
required: 'Confirmed password is required',
|
||||||
validate: (v) => v === getValues().password || 'Passwords must match!',
|
validate: (v) => v === getValues().password || 'Passwords must match!',
|
||||||
})}
|
})}
|
||||||
|
type="password"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export const VerifyEmail: FC = () => {
|
|||||||
invalid={!!(errors as any).email}
|
invalid={!!(errors as any).email}
|
||||||
error={(errors as any).email?.message}
|
error={(errors as any).email?.message}
|
||||||
>
|
>
|
||||||
<Input placeholder="Email" name="email" ref={register({ required: true })} />
|
<Input {...register('email', { required: true })} placeholder="Email" />
|
||||||
</Field>
|
</Field>
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<Button>Send verification email</Button>
|
<Button>Send verification email</Button>
|
||||||
|
@ -66,7 +66,7 @@ export const AdminEditOrgPage: FC<Props> = ({ match }) => {
|
|||||||
{({ register, errors }) => (
|
{({ register, errors }) => (
|
||||||
<>
|
<>
|
||||||
<Field label="Name" invalid={!!errors.orgName} error="Name is required">
|
<Field label="Name" invalid={!!errors.orgName} error="Name is required">
|
||||||
<Input name="orgName" ref={register({ required: true })} />
|
<Input {...register('orgName', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Button>Update</Button>
|
<Button>Update</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -46,15 +46,15 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
|
|||||||
invalid={!!errors.name}
|
invalid={!!errors.name}
|
||||||
error={errors.name ? 'Name is required' : undefined}
|
error={errors.name ? 'Name is required' : undefined}
|
||||||
>
|
>
|
||||||
<Input name="name" ref={register({ required: true })} />
|
<Input {...register('name', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Email">
|
<Field label="Email">
|
||||||
<Input name="email" ref={register} />
|
<Input {...register('email')} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Username">
|
<Field label="Username">
|
||||||
<Input name="login" ref={register} />
|
<Input {...register('login')} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label="Password"
|
label="Password"
|
||||||
@ -63,11 +63,10 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
|
|||||||
error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined}
|
error={errors.password ? 'Password is required and must contain at least 4 characters' : undefined}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
{...register('password', {
|
||||||
name="password"
|
|
||||||
ref={register({
|
|
||||||
validate: (value) => value.trim() !== '' && value.length >= 4,
|
validate: (value) => value.trim() !== '' && value.length >= 4,
|
||||||
})}
|
})}
|
||||||
|
type="password"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Button type="submit">Create user</Button>
|
<Button type="submit">Create user</Button>
|
||||||
|
@ -25,10 +25,15 @@ export const BasicSettings: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
||||||
<Input name="name" ref={register({ required: 'Name is required' })} />
|
<Input {...register('name', { required: 'Name is required' })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Type">
|
<Field label="Type">
|
||||||
<InputControl name="type" as={Select} options={channels} control={control} rules={{ required: true }} />
|
<InputControl
|
||||||
|
name="type"
|
||||||
|
render={({ field: { ref, ...field } }) => <Select {...field} options={channels} />}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<NotificationChannelOptions
|
<NotificationChannelOptions
|
||||||
selectedChannelOptions={selectedChannel.options.filter((o) => o.required)}
|
selectedChannelOptions={selectedChannel.options.filter((o) => o.required)}
|
||||||
|
@ -9,7 +9,7 @@ import { ChannelSettings } from './ChannelSettings';
|
|||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState'> {
|
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'setValue'> {
|
||||||
selectableChannels: Array<SelectableValue<string>>;
|
selectableChannels: Array<SelectableValue<string>>;
|
||||||
selectedChannel?: NotificationChannelType;
|
selectedChannel?: NotificationChannelType;
|
||||||
imageRendererAvailable: boolean;
|
imageRendererAvailable: boolean;
|
||||||
@ -19,7 +19,7 @@ interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationSettingsProps
|
export interface NotificationSettingsProps
|
||||||
extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues'> {
|
extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues' | 'setValue'> {
|
||||||
currentFormValues: NotificationChannelDTO;
|
currentFormValues: NotificationChannelDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ export const NotificationChannelForm: FC<Props> = ({
|
|||||||
<div className={styles.formButtons}>
|
<div className={styles.formButtons}>
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">Save</Button>
|
||||||
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
|
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues())}>
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
<a href={`${config.appSubUrl}/alerting/notifications`}>
|
<a href={`${config.appSubUrl}/alerting/notifications`}>
|
||||||
|
@ -4,7 +4,7 @@ import { Button, Checkbox, Field, FormAPI, Input } from '@grafana/ui';
|
|||||||
import { OptionElement } from './OptionElement';
|
import { OptionElement } from './OptionElement';
|
||||||
import { NotificationChannelDTO, NotificationChannelOption, NotificationChannelSecureFields } from '../../../types';
|
import { NotificationChannelDTO, NotificationChannelOption, NotificationChannelSecureFields } from '../../../types';
|
||||||
|
|
||||||
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch'> {
|
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch' | 'setValue'> {
|
||||||
selectedChannelOptions: NotificationChannelOption[];
|
selectedChannelOptions: NotificationChannelOption[];
|
||||||
currentFormValues: NotificationChannelDTO;
|
currentFormValues: NotificationChannelDTO;
|
||||||
secureFields: NotificationChannelSecureFields;
|
secureFields: NotificationChannelSecureFields;
|
||||||
@ -39,8 +39,9 @@ export const NotificationChannelOptions: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Field key={key}>
|
<Field key={key}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`}
|
{...register(
|
||||||
ref={register}
|
option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`
|
||||||
|
)}
|
||||||
label={option.label}
|
label={option.label}
|
||||||
description={option.description}
|
description={option.description}
|
||||||
/>
|
/>
|
||||||
|
@ -10,12 +10,11 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
|
|||||||
return (
|
return (
|
||||||
<CollapsableSection label="Notification settings" isOpen={false}>
|
<CollapsableSection label="Notification settings" isOpen={false}>
|
||||||
<Field>
|
<Field>
|
||||||
<Checkbox name="isDefault" ref={register} label="Default" description="Use this notification for all alerts" />
|
<Checkbox {...register('isDefault')} label="Default" description="Use this notification for all alerts" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="settings.uploadImage"
|
{...register('settings.uploadImage')}
|
||||||
ref={register}
|
|
||||||
label="Include image"
|
label="Include image"
|
||||||
description="Captures an image and include it in the notification"
|
description="Captures an image and include it in the notification"
|
||||||
/>
|
/>
|
||||||
@ -28,16 +27,14 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
|
|||||||
)}
|
)}
|
||||||
<Field>
|
<Field>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="disableResolveMessage"
|
{...register('disableResolveMessage')}
|
||||||
ref={register}
|
|
||||||
label="Disable Resolve Message"
|
label="Disable Resolve Message"
|
||||||
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="sendReminder"
|
{...register('sendReminder')}
|
||||||
ref={register}
|
|
||||||
label="Send reminders"
|
label="Send reminders"
|
||||||
description="Send additional notifications for triggered alerts"
|
description="Send additional notifications for triggered alerts"
|
||||||
/>
|
/>
|
||||||
@ -50,7 +47,7 @@ export const NotificationSettings: FC<Props> = ({ currentFormValues, imageRender
|
|||||||
Alert reminders are sent after rules are evaluated. A reminder can never be sent more frequently
|
Alert reminders are sent after rules are evaluated. A reminder can never be sent more frequently
|
||||||
than a configured alert rule evaluation interval."
|
than a configured alert rule evaluation interval."
|
||||||
>
|
>
|
||||||
<Input name="frequency" ref={register} width={8} />
|
<Input {...register('frequency')} width={8} />
|
||||||
</Field>
|
</Field>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -13,13 +13,12 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
|
|||||||
case 'input':
|
case 'input':
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
invalid={invalid}
|
{...register(`${modelValue}`, {
|
||||||
type={option.inputType}
|
|
||||||
name={`${modelValue}`}
|
|
||||||
ref={register({
|
|
||||||
required: option.required ? 'Required' : false,
|
required: option.required ? 'Required' : false,
|
||||||
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||||
})}
|
})}
|
||||||
|
invalid={invalid}
|
||||||
|
type={option.inputType}
|
||||||
placeholder={option.placeholder}
|
placeholder={option.placeholder}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -27,11 +26,11 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
|
|||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
<InputControl
|
<InputControl
|
||||||
as={Select}
|
|
||||||
options={option.selectOptions}
|
|
||||||
control={control}
|
control={control}
|
||||||
name={`${modelValue}`}
|
name={`${modelValue}`}
|
||||||
invalid={invalid}
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<Select {...field} options={option.selectOptions} invalid={invalid} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,8 +38,7 @@ export const OptionElement: FC<Props> = ({ control, option, register, invalid })
|
|||||||
return (
|
return (
|
||||||
<TextArea
|
<TextArea
|
||||||
invalid={invalid}
|
invalid={invalid}
|
||||||
name={`${modelValue}`}
|
{...register(`${modelValue}`, {
|
||||||
ref={register({
|
|
||||||
required: option.required ? 'Required' : false,
|
required: option.required ? 'Required' : false,
|
||||||
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||||
})}
|
})}
|
||||||
|
@ -75,12 +75,16 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handleSubmit, register, errors } = useForm<Values>({
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<Values>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
defaultValues: existing ?? defaults,
|
defaultValues: existing ?? defaults,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateNameIsUnique: Validate = (name: string) => {
|
const validateNameIsUnique: Validate<string> = (name: string) => {
|
||||||
return !config.template_files[name] || existing?.name === name
|
return !config.template_files[name] || existing?.name === name
|
||||||
? true
|
? true
|
||||||
: 'Another template with this name already exists.';
|
: 'Another template with this name already exists.';
|
||||||
@ -96,13 +100,12 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
|||||||
)}
|
)}
|
||||||
<Field label="Template name" error={errors?.name?.message} invalid={!!errors.name?.message}>
|
<Field label="Template name" error={errors?.name?.message} invalid={!!errors.name?.message}>
|
||||||
<Input
|
<Input
|
||||||
width={42}
|
{...register('name', {
|
||||||
autoFocus={true}
|
|
||||||
ref={register({
|
|
||||||
required: { value: true, message: 'Required.' },
|
required: { value: true, message: 'Required.' },
|
||||||
validate: { nameIsUnique: validateNameIsUnique },
|
validate: { nameIsUnique: validateNameIsUnique },
|
||||||
})}
|
})}
|
||||||
name="name"
|
width={42}
|
||||||
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
@ -133,9 +136,8 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
|||||||
invalid={!!errors.content?.message}
|
invalid={!!errors.content?.message}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
{...register('content', { required: { value: true, message: 'Required.' } })}
|
||||||
className={styles.textarea}
|
className={styles.textarea}
|
||||||
ref={register({ required: { value: true, message: 'Required.' } })}
|
|
||||||
name="content"
|
|
||||||
rows={12}
|
rows={12}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Checkbox, Field, Input } from '@grafana/ui';
|
import { Button, Checkbox, Field, Input } from '@grafana/ui';
|
||||||
import { OptionElement } from './OptionElement';
|
import { OptionElement } from './OptionElement';
|
||||||
import { ChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
import { ChannelValues } from '../../../types/receiver-form';
|
||||||
import { useFormContext, FieldError, NestDataObject } from 'react-hook-form';
|
import { useFormContext, FieldError, FieldErrors } from 'react-hook-form';
|
||||||
import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
|
import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
|
||||||
|
|
||||||
export interface Props<R extends ChannelValues> {
|
export interface Props<R extends ChannelValues> {
|
||||||
@ -10,7 +10,7 @@ export interface Props<R extends ChannelValues> {
|
|||||||
secureFields: NotificationChannelSecureFields;
|
secureFields: NotificationChannelSecureFields;
|
||||||
|
|
||||||
onResetSecureField: (key: string) => void;
|
onResetSecureField: (key: string) => void;
|
||||||
errors?: NestDataObject<R, FieldError>;
|
errors?: FieldErrors<R>;
|
||||||
pathPrefix?: string;
|
pathPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export function ChannelOptions<R extends ChannelValues>({
|
|||||||
errors,
|
errors,
|
||||||
pathPrefix = '',
|
pathPrefix = '',
|
||||||
}: Props<R>): JSX.Element {
|
}: Props<R>): JSX.Element {
|
||||||
const { register, watch } = useFormContext<ReceiverFormValues<R>>();
|
const { register, watch } = useFormContext();
|
||||||
const currentFormValues = watch() as Record<string, any>; // react hook form types ARE LYING!
|
const currentFormValues = watch() as Record<string, any>; // react hook form types ARE LYING!
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -41,12 +41,11 @@ export function ChannelOptions<R extends ChannelValues>({
|
|||||||
return (
|
return (
|
||||||
<Field key={key}>
|
<Field key={key}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={
|
{...register(
|
||||||
option.secure
|
option.secure
|
||||||
? `${pathPrefix}secureSettings.${option.propertyName}`
|
? `${pathPrefix}secureSettings.${option.propertyName}`
|
||||||
: `${pathPrefix}settings.${option.propertyName}`
|
: `${pathPrefix}settings.${option.propertyName}`
|
||||||
}
|
)}
|
||||||
ref={register()}
|
|
||||||
label={option.label}
|
label={option.label}
|
||||||
description={option.description}
|
description={option.description}
|
||||||
/>
|
/>
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
import { GrafanaThemeV2, SelectableValue } from '@grafana/data';
|
import { GrafanaThemeV2, SelectableValue } from '@grafana/data';
|
||||||
import { NotifierDTO } from 'app/types';
|
import { NotifierDTO } from 'app/types';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Alert, Button, Field, InputControl, Select, useStyles2 } from '@grafana/ui';
|
import { Alert, Button, Field, InputControl, Select, useStyles2 } from '@grafana/ui';
|
||||||
import { useFormContext, FieldError, NestDataObject } from 'react-hook-form';
|
import { useFormContext, FieldErrors } from 'react-hook-form';
|
||||||
import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form';
|
import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form';
|
||||||
import { ChannelOptions } from './ChannelOptions';
|
import { ChannelOptions } from './ChannelOptions';
|
||||||
import { CollapsibleSection } from './CollapsibleSection';
|
import { CollapsibleSection } from './CollapsibleSection';
|
||||||
|
|
||||||
interface Props<R> {
|
interface Props<R> {
|
||||||
|
defaultValues: R;
|
||||||
pathPrefix: string;
|
pathPrefix: string;
|
||||||
notifiers: NotifierDTO[];
|
notifiers: NotifierDTO[];
|
||||||
onDuplicate: () => void;
|
onDuplicate: () => void;
|
||||||
commonSettingsComponent: CommonSettingsComponentType;
|
commonSettingsComponent: CommonSettingsComponentType;
|
||||||
|
|
||||||
secureFields?: Record<string, boolean>;
|
secureFields?: Record<string, boolean>;
|
||||||
errors?: NestDataObject<R, FieldError>;
|
errors?: FieldErrors<R>;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChannelSubForm<R extends ChannelValues>({
|
export function ChannelSubForm<R extends ChannelValues>({
|
||||||
|
defaultValues,
|
||||||
pathPrefix,
|
pathPrefix,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onDelete,
|
onDelete,
|
||||||
@ -30,16 +32,8 @@ export function ChannelSubForm<R extends ChannelValues>({
|
|||||||
}: Props<R>): JSX.Element {
|
}: Props<R>): JSX.Element {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const name = (fieldName: string) => `${pathPrefix}${fieldName}`;
|
const name = (fieldName: string) => `${pathPrefix}${fieldName}`;
|
||||||
const { control, watch, register, unregister } = useFormContext();
|
const { control, watch } = useFormContext();
|
||||||
const selectedType = watch(name('type'));
|
const selectedType = watch(name('type')) ?? defaultValues.type; // nope, setting "default" does not work at all.
|
||||||
|
|
||||||
// keep the __id field registered so it's always passed to submit
|
|
||||||
useEffect(() => {
|
|
||||||
register({ name: `${pathPrefix}__id` });
|
|
||||||
return () => {
|
|
||||||
unregister(`${pathPrefix}__id`);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const [_secureFields, setSecureFields] = useState(secureFields ?? {});
|
const [_secureFields, setSecureFields] = useState(secureFields ?? {});
|
||||||
|
|
||||||
@ -70,15 +64,21 @@ export function ChannelSubForm<R extends ChannelValues>({
|
|||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.topRow}>
|
<div className={styles.topRow}>
|
||||||
<div>
|
<div>
|
||||||
|
<InputControl
|
||||||
|
name={name('__id')}
|
||||||
|
render={({ field }) => <input type="hidden" {...field} />}
|
||||||
|
defaultValue={defaultValues.__id}
|
||||||
|
control={control}
|
||||||
|
/>
|
||||||
<Field label="Contact point type">
|
<Field label="Contact point type">
|
||||||
<InputControl
|
<InputControl
|
||||||
name={name('type')}
|
name={name('type')}
|
||||||
as={Select}
|
defaultValue={defaultValues.type}
|
||||||
width={37}
|
render={({ field: { ref, onChange, ...field } }) => (
|
||||||
options={typeOptions}
|
<Select {...field} width={37} options={typeOptions} onChange={(value) => onChange(value?.value)} />
|
||||||
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
onChange={(values) => values[0]?.value}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,16 +9,14 @@ export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Field>
|
<Field>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={`${pathPrefix}disableResolveMessage`}
|
{...register(`${pathPrefix}disableResolveMessage`)}
|
||||||
ref={register()}
|
|
||||||
label="Disable resolved message"
|
label="Disable resolved message"
|
||||||
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={`${pathPrefix}sendReminder`}
|
{...register(`${pathPrefix}sendReminder`)}
|
||||||
ref={register()}
|
|
||||||
label="Send reminders"
|
label="Send reminders"
|
||||||
description="Send additional notifications for triggered alerts"
|
description="Send additional notifications for triggered alerts"
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { Input, InputControl, Select, TextArea } from '@grafana/ui';
|
import { Input, InputControl, Select, TextArea } from '@grafana/ui';
|
||||||
import { NotificationChannelOption } from 'app/types';
|
import { NotificationChannelOption } from 'app/types';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
@ -10,18 +10,26 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) => {
|
export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) => {
|
||||||
const { control, register } = useFormContext();
|
const { control, register, unregister } = useFormContext();
|
||||||
const modelValue = option.secure
|
const modelValue = option.secure
|
||||||
? `${pathPrefix}secureSettings.${option.propertyName}`
|
? `${pathPrefix}secureSettings.${option.propertyName}`
|
||||||
: `${pathPrefix}settings.${option.propertyName}`;
|
: `${pathPrefix}settings.${option.propertyName}`;
|
||||||
|
|
||||||
|
// workaround for https://github.com/react-hook-form/react-hook-form/issues/4993#issuecomment-829012506
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
unregister(modelValue);
|
||||||
|
},
|
||||||
|
[unregister, modelValue]
|
||||||
|
);
|
||||||
|
|
||||||
switch (option.element) {
|
switch (option.element) {
|
||||||
case 'input':
|
case 'input':
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
invalid={invalid}
|
invalid={invalid}
|
||||||
type={option.inputType}
|
type={option.inputType}
|
||||||
name={`${modelValue}`}
|
{...register(`${modelValue}`, {
|
||||||
ref={register({
|
|
||||||
required: option.required ? 'Required' : false,
|
required: option.required ? 'Required' : false,
|
||||||
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||||
})}
|
})}
|
||||||
@ -32,24 +40,27 @@ export const OptionElement: FC<Props> = ({ option, invalid, pathPrefix = '' }) =
|
|||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
<InputControl
|
<InputControl
|
||||||
as={Select}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
options={option.selectOptions}
|
<Select
|
||||||
|
{...field}
|
||||||
|
options={option.selectOptions}
|
||||||
|
invalid={invalid}
|
||||||
|
onChange={(value) => onChange(value.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name={`${modelValue}`}
|
name={`${modelValue}`}
|
||||||
invalid={invalid}
|
|
||||||
onChange={(values) => values[0].value}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<TextArea
|
<TextArea
|
||||||
invalid={invalid}
|
{...register(`${modelValue}`, {
|
||||||
name={`${modelValue}`}
|
|
||||||
ref={register({
|
|
||||||
required: option.required ? 'Required' : false,
|
required: option.required ? 'Required' : false,
|
||||||
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
validate: (v) => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||||
})}
|
})}
|
||||||
|
invalid={invalid}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@ import { Alert, Button, Field, Input, LinkButton, useStyles2 } from '@grafana/ui
|
|||||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { NotifierDTO } from 'app/types';
|
import { NotifierDTO } from 'app/types';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useForm, FormContext, NestDataObject, FieldError, Validate } from 'react-hook-form';
|
import { useForm, FormProvider, FieldErrors, Validate, useFieldArray } from 'react-hook-form';
|
||||||
import { useControlledFieldArray } from '../../../hooks/useControlledFieldArray';
|
|
||||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||||
import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form';
|
import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form';
|
||||||
import { makeAMLink } from '../../../utils/misc';
|
import { makeAMLink } from '../../../utils/misc';
|
||||||
@ -50,11 +49,20 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
|
|
||||||
const { loading, error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
|
const { loading, error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
|
||||||
|
|
||||||
const { handleSubmit, register, errors, getValues } = formAPI;
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
getValues,
|
||||||
|
control,
|
||||||
|
} = formAPI;
|
||||||
|
|
||||||
const { items, append, remove } = useControlledFieldArray<R>('items', formAPI);
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'items' as any, // bug in types
|
||||||
|
});
|
||||||
|
|
||||||
const validateNameIsAvailable: Validate = useCallback(
|
const validateNameIsAvailable: Validate<string> = useCallback(
|
||||||
(name: string) =>
|
(name: string) =>
|
||||||
takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
||||||
? 'Another receiver with this name already exists.'
|
? 'Another receiver with this name already exists.'
|
||||||
@ -63,7 +71,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContext {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<h4 className={styles.heading}>{initialValues ? 'Update contact point' : 'Create contact point'}</h4>
|
<h4 className={styles.heading}>{initialValues ? 'Update contact point' : 'Create contact point'}</h4>
|
||||||
{error && (
|
{error && (
|
||||||
@ -73,25 +81,28 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
)}
|
)}
|
||||||
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
||||||
<Input
|
<Input
|
||||||
|
{...register('name', {
|
||||||
|
required: 'Name is required',
|
||||||
|
validate: { nameIsAvailable: validateNameIsAvailable },
|
||||||
|
})}
|
||||||
width={39}
|
width={39}
|
||||||
name="name"
|
|
||||||
ref={register({ required: 'Name is required', validate: { nameIsAvailable: validateNameIsAvailable } })}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{items.map((item, index) => {
|
{fields.map((field: R & { id: string }, index) => {
|
||||||
const initialItem = initialValues?.items.find(({ __id }) => __id === item.__id);
|
const initialItem = initialValues?.items.find(({ __id }) => __id === field.__id);
|
||||||
return (
|
return (
|
||||||
<ChannelSubForm<R>
|
<ChannelSubForm<R>
|
||||||
key={item.__id}
|
defaultValues={field}
|
||||||
|
key={field.id}
|
||||||
onDuplicate={() => {
|
onDuplicate={() => {
|
||||||
const currentValues = getValues({ nest: true }).items[index];
|
const currentValues: R = getValues().items[index];
|
||||||
append({ ...currentValues, __id: String(Math.random()) });
|
append({ ...currentValues, __id: String(Math.random()) });
|
||||||
}}
|
}}
|
||||||
onDelete={() => remove(index)}
|
onDelete={() => remove(index)}
|
||||||
pathPrefix={`items.${index}.`}
|
pathPrefix={`items.${index}.`}
|
||||||
notifiers={notifiers}
|
notifiers={notifiers}
|
||||||
secureFields={initialItem?.secureFields}
|
secureFields={initialItem?.secureFields}
|
||||||
errors={errors?.items?.[index] as NestDataObject<R, FieldError>}
|
errors={errors?.items?.[index] as FieldErrors<R>}
|
||||||
commonSettingsComponent={commonSettingsComponent}
|
commonSettingsComponent={commonSettingsComponent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -115,7 +126,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
</LinkButton>
|
</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormContext>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { AlertTypeStep } from './AlertTypeStep';
|
|||||||
import { ConditionsStep } from './ConditionsStep';
|
import { ConditionsStep } from './ConditionsStep';
|
||||||
import { DetailsStep } from './DetailsStep';
|
import { DetailsStep } from './DetailsStep';
|
||||||
import { QueryStep } from './QueryStep';
|
import { QueryStep } from './QueryStep';
|
||||||
import { useForm, FormContext } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
|
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
@ -39,7 +39,11 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, watch, errors } = formAPI;
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = formAPI;
|
||||||
|
|
||||||
const hasErrors = !!Object.values(errors).filter((x) => !!x).length;
|
const hasErrors = !!Object.values(errors).filter((x) => !!x).length;
|
||||||
|
|
||||||
@ -52,7 +56,6 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
useCleanup((state) => state.unifiedAlerting.ruleForm.saveRule);
|
useCleanup((state) => state.unifiedAlerting.ruleForm.saveRule);
|
||||||
|
|
||||||
const submit = (values: RuleFormValues, exitOnSave: boolean) => {
|
const submit = (values: RuleFormValues, exitOnSave: boolean) => {
|
||||||
console.log('submit', values);
|
|
||||||
dispatch(
|
dispatch(
|
||||||
saveRuleFormAction({
|
saveRuleFormAction({
|
||||||
values: {
|
values: {
|
||||||
@ -68,7 +71,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContext {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
|
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
|
||||||
<PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}>
|
<PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}>
|
||||||
<Link to="/alerting/list">
|
<Link to="/alerting/list">
|
||||||
@ -121,7 +124,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormContext>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { css } from '@emotion/css';
|
|||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
import { DataSourcePicker, DataSourcePickerProps } from '@grafana/runtime';
|
import { DataSourcePicker } from '@grafana/runtime';
|
||||||
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
|
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
|
||||||
import { RuleFolderPicker } from './RuleFolderPicker';
|
import { RuleFolderPicker } from './RuleFolderPicker';
|
||||||
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
||||||
@ -31,7 +31,13 @@ interface Props {
|
|||||||
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
const { register, control, watch, errors, setValue } = useFormContext<RuleFormValues>();
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
|
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||||
|
|
||||||
const ruleFormType = watch('type');
|
const ruleFormType = watch('type');
|
||||||
const dataSourceName = watch('dataSourceName');
|
const dataSourceName = watch('dataSourceName');
|
||||||
@ -61,9 +67,8 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
invalid={!!errors.name?.message}
|
invalid={!!errors.name?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
{...register('name', { required: { value: true, message: 'Must enter an alert name' } })}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
ref={register({ required: { value: true, message: 'Must enter an alert name' } })}
|
|
||||||
name="name"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<div className={styles.flexRow}>
|
<div className={styles.flexRow}>
|
||||||
@ -75,25 +80,29 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
invalid={!!errors.type?.message}
|
invalid={!!errors.type?.message}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={Select}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
options={alertTypeOptions}
|
||||||
|
onChange={(v: SelectableValue) => {
|
||||||
|
const value = v?.value;
|
||||||
|
// when switching to system alerts, null out data source selection if it's not a rules source with ruler
|
||||||
|
if (
|
||||||
|
value === RuleFormType.system &&
|
||||||
|
dataSourceName &&
|
||||||
|
!rulesSourcesWithRuler.find(({ name }) => name === dataSourceName)
|
||||||
|
) {
|
||||||
|
setValue('dataSourceName', null);
|
||||||
|
}
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
name="type"
|
name="type"
|
||||||
options={alertTypeOptions}
|
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: 'Please select alert type' },
|
required: { value: true, message: 'Please select alert type' },
|
||||||
}}
|
}}
|
||||||
onChange={(values: SelectableValue[]) => {
|
|
||||||
const value = values[0]?.value;
|
|
||||||
// when switching to system alerts, null out data source selection if it's not a rules source with ruler
|
|
||||||
if (
|
|
||||||
value === RuleFormType.system &&
|
|
||||||
dataSourceName &&
|
|
||||||
!rulesSourcesWithRuler.find(({ name }) => name === dataSourceName)
|
|
||||||
) {
|
|
||||||
setValue('dataSourceName', null);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{ruleFormType === RuleFormType.system && (
|
{ruleFormType === RuleFormType.system && (
|
||||||
@ -104,21 +113,25 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
invalid={!!errors.dataSourceName?.message}
|
invalid={!!errors.dataSourceName?.message}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={(DataSourcePicker as unknown) as React.ComponentType<Omit<DataSourcePickerProps, 'current'>>}
|
render={({ field: { onChange, ref, value, ...field } }) => (
|
||||||
valueName="current"
|
<DataSourcePicker
|
||||||
filter={dataSourceFilter}
|
{...field}
|
||||||
|
current={value}
|
||||||
|
filter={dataSourceFilter}
|
||||||
|
noDefault
|
||||||
|
alerting
|
||||||
|
onChange={(ds: DataSourceInstanceSettings) => {
|
||||||
|
// reset location if switching data sources, as different rules source will have different groups and namespaces
|
||||||
|
setValue('location', undefined);
|
||||||
|
onChange(ds?.name ?? null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
name="dataSourceName"
|
name="dataSourceName"
|
||||||
noDefault={true}
|
|
||||||
control={control}
|
control={control}
|
||||||
alerting={true}
|
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: 'Please select a data source' },
|
required: { value: true, message: 'Please select a data source' },
|
||||||
}}
|
}}
|
||||||
onChange={(ds: DataSourceInstanceSettings[]) => {
|
|
||||||
// reset location if switching data sources, as differnet rules source will have different groups and namespaces
|
|
||||||
setValue('location', undefined);
|
|
||||||
return ds[0]?.name ?? null;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
@ -134,10 +147,10 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
|||||||
invalid={!!errors.folder?.message}
|
invalid={!!errors.folder?.message}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={RuleFolderPicker}
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<RuleFolderPicker {...field} enableCreateNew={true} enableReset={true} />
|
||||||
|
)}
|
||||||
name="folder"
|
name="folder"
|
||||||
enableCreateNew={true}
|
|
||||||
enableReset={true}
|
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: 'Please select a folder' },
|
required: { value: true, message: 'Please select a folder' },
|
||||||
}}
|
}}
|
||||||
|
@ -8,11 +8,16 @@ import { AnnotationKeyInput } from './AnnotationKeyInput';
|
|||||||
|
|
||||||
const AnnotationsField: FC = () => {
|
const AnnotationsField: FC = () => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
const { control, register, watch, errors } = useFormContext<RuleFormValues>();
|
const {
|
||||||
const annotations = watch('annotations');
|
control,
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext();
|
||||||
|
const annotations = watch('annotations') as RuleFormValues['annotations'];
|
||||||
|
|
||||||
const existingKeys = useCallback(
|
const existingKeys = useCallback(
|
||||||
(index: number): string[] => annotations.filter((_, idx) => idx !== index).map(({ key }) => key),
|
(index: number): string[] => annotations.filter((_, idx: number) => idx !== index).map(({ key }) => key),
|
||||||
[annotations]
|
[annotations]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -31,10 +36,10 @@ const AnnotationsField: FC = () => {
|
|||||||
error={errors.annotations?.[index]?.key?.message}
|
error={errors.annotations?.[index]?.key?.message}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={AnnotationKeyInput}
|
|
||||||
width={15}
|
|
||||||
name={`annotations[${index}].key`}
|
name={`annotations[${index}].key`}
|
||||||
existingKeys={existingKeys(index)}
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<AnnotationKeyInput {...field} existingKeys={existingKeys(index)} width={15} />
|
||||||
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||||
/>
|
/>
|
||||||
@ -45,9 +50,10 @@ const AnnotationsField: FC = () => {
|
|||||||
error={errors.annotations?.[index]?.value?.message}
|
error={errors.annotations?.[index]?.value?.message}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
name={`annotations[${index}].value`}
|
|
||||||
className={styles.annotationTextArea}
|
className={styles.annotationTextArea}
|
||||||
ref={register({ required: { value: !!annotations[index]?.key, message: 'Required.' } })}
|
{...register(`annotations[${index}].value`, {
|
||||||
|
required: { value: !!annotations[index]?.key, message: 'Required.' },
|
||||||
|
})}
|
||||||
placeholder={`value`}
|
placeholder={`value`}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
/>
|
/>
|
||||||
|
@ -5,7 +5,11 @@ import { useFormContext } from 'react-hook-form';
|
|||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { RuleFormValues } from '../../types/rule-form';
|
||||||
|
|
||||||
export const ConditionField: FC = () => {
|
export const ConditionField: FC = () => {
|
||||||
const { watch, setValue, errors } = useFormContext<RuleFormValues>();
|
const {
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const queries = watch('queries');
|
const queries = watch('queries');
|
||||||
const condition = watch('condition');
|
const condition = watch('condition');
|
||||||
@ -36,18 +40,22 @@ export const ConditionField: FC = () => {
|
|||||||
invalid={!!errors.condition?.message}
|
invalid={!!errors.condition?.message}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
width={42}
|
|
||||||
name="condition"
|
name="condition"
|
||||||
as={Select}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
onChange={(values: SelectableValue[]) => values[0]?.value ?? null}
|
<Select
|
||||||
options={options}
|
{...field}
|
||||||
|
width={42}
|
||||||
|
options={options}
|
||||||
|
onChange={(v: SelectableValue) => onChange(v?.value ?? null)}
|
||||||
|
noOptionsMessage="No queries defined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
rules={{
|
rules={{
|
||||||
required: {
|
required: {
|
||||||
value: true,
|
value: true,
|
||||||
message: 'Please select the condition to alert on',
|
message: 'Please select the condition to alert on',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
noOptionsMessage="No queries defined"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
@ -3,12 +3,12 @@ import { Field, Input, Select, useStyles, InputControl, InlineLabel, Switch } fr
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
import { useFormContext, ValidationOptions } from 'react-hook-form';
|
import { useFormContext, RegisterOptions } from 'react-hook-form';
|
||||||
import { RuleFormType, RuleFormValues, TimeOptions } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues, TimeOptions } from '../../types/rule-form';
|
||||||
import { ConditionField } from './ConditionField';
|
import { ConditionField } from './ConditionField';
|
||||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||||
|
|
||||||
const timeRangeValidationOptions: ValidationOptions = {
|
const timeRangeValidationOptions: RegisterOptions = {
|
||||||
required: {
|
required: {
|
||||||
value: true,
|
value: true,
|
||||||
message: 'Required.',
|
message: 'Required.',
|
||||||
@ -29,7 +29,12 @@ const timeOptions = Object.entries(TimeOptions).map(([key, value]) => ({
|
|||||||
export const ConditionsStep: FC = () => {
|
export const ConditionsStep: FC = () => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
||||||
const { register, control, watch, errors } = useFormContext<RuleFormValues>();
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const type = watch('type');
|
const type = watch('type');
|
||||||
|
|
||||||
@ -48,7 +53,7 @@ export const ConditionsStep: FC = () => {
|
|||||||
error={errors.evaluateEvery?.message}
|
error={errors.evaluateEvery?.message}
|
||||||
invalid={!!errors.evaluateEvery?.message}
|
invalid={!!errors.evaluateEvery?.message}
|
||||||
>
|
>
|
||||||
<Input width={8} ref={register(timeRangeValidationOptions)} name="evaluateEvery" />
|
<Input width={8} {...register('evaluateEvery', timeRangeValidationOptions)} />
|
||||||
</Field>
|
</Field>
|
||||||
<InlineLabel
|
<InlineLabel
|
||||||
width={7}
|
width={7}
|
||||||
@ -61,7 +66,7 @@ export const ConditionsStep: FC = () => {
|
|||||||
error={errors.evaluateFor?.message}
|
error={errors.evaluateFor?.message}
|
||||||
invalid={!!errors.evaluateFor?.message}
|
invalid={!!errors.evaluateFor?.message}
|
||||||
>
|
>
|
||||||
<Input width={8} ref={register(timeRangeValidationOptions)} name="evaluateFor" />
|
<Input width={8} {...register('evaluateFor', timeRangeValidationOptions)} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
@ -72,18 +77,18 @@ export const ConditionsStep: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Field label="Alert state if no data or all values are null">
|
<Field label="Alert state if no data or all values are null">
|
||||||
<InputControl
|
<InputControl
|
||||||
as={GrafanaAlertStatePicker}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
|
<GrafanaAlertStatePicker {...field} width={42} onChange={(value) => onChange(value?.value)} />
|
||||||
|
)}
|
||||||
name="noDataState"
|
name="noDataState"
|
||||||
width={42}
|
|
||||||
onChange={(values) => values[0]?.value}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Alert state if execution error or timeout">
|
<Field label="Alert state if execution error or timeout">
|
||||||
<InputControl
|
<InputControl
|
||||||
as={GrafanaAlertStatePicker}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
|
<GrafanaAlertStatePicker {...field} width={42} onChange={(value) => onChange(value?.value)} />
|
||||||
|
)}
|
||||||
name="execErrState"
|
name="execErrState"
|
||||||
width={42}
|
|
||||||
onChange={(values) => values[0]?.value}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</>
|
</>
|
||||||
@ -96,19 +101,22 @@ export const ConditionsStep: FC = () => {
|
|||||||
<div className={styles.flexRow}>
|
<div className={styles.flexRow}>
|
||||||
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
|
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
|
||||||
<Input
|
<Input
|
||||||
ref={register({ pattern: { value: /^\d+$/, message: 'Must be a postive integer.' } })}
|
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a postive integer.' } })}
|
||||||
name="forTime"
|
|
||||||
width={8}
|
width={8}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<InputControl
|
<InputControl
|
||||||
name="forTimeUnit"
|
name="forTimeUnit"
|
||||||
as={Select}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
options={timeOptions}
|
<Select
|
||||||
|
{...field}
|
||||||
|
options={timeOptions}
|
||||||
|
onChange={(value) => onChange(value?.value)}
|
||||||
|
width={15}
|
||||||
|
className={styles.timeUnit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
width={15}
|
|
||||||
className={styles.timeUnit}
|
|
||||||
onChange={(values) => values[0]?.value}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
@ -125,7 +133,6 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
flexRow: css`
|
flexRow: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
`,
|
`,
|
||||||
|
@ -14,7 +14,12 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||||
const { control, watch, errors, setValue } = useFormContext<RuleFormValues>();
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const [customGroup, setCustomGroup] = useState(false);
|
const [customGroup, setCustomGroup] = useState(false);
|
||||||
|
|
||||||
@ -44,32 +49,34 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
|||||||
<>
|
<>
|
||||||
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
|
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={SelectWithAdd}
|
render={({ field: { onChange, ref, ...field } }) => (
|
||||||
className={inputStyle}
|
<SelectWithAdd
|
||||||
|
{...field}
|
||||||
|
className={inputStyle}
|
||||||
|
onChange={(value) => {
|
||||||
|
setValue('group', ''); //reset if namespace changes
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
|
onCustomChange={(custom: boolean) => {
|
||||||
|
custom && setCustomGroup(true);
|
||||||
|
}}
|
||||||
|
options={namespaceOptions}
|
||||||
|
width={42}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
name="namespace"
|
name="namespace"
|
||||||
options={namespaceOptions}
|
|
||||||
control={control}
|
control={control}
|
||||||
width={42}
|
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: 'Required.' },
|
required: { value: true, message: 'Required.' },
|
||||||
}}
|
}}
|
||||||
onChange={(values) => {
|
|
||||||
setValue('group', ''); //reset if namespace changes
|
|
||||||
return values[0];
|
|
||||||
}}
|
|
||||||
onCustomChange={(custom: boolean) => {
|
|
||||||
custom && setCustomGroup(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
|
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={SelectWithAdd}
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<SelectWithAdd {...field} options={groupOptions} width={42} custom={customGroup} className={inputStyle} />
|
||||||
|
)}
|
||||||
name="group"
|
name="group"
|
||||||
className={inputStyle}
|
|
||||||
options={groupOptions}
|
|
||||||
width={42}
|
|
||||||
custom={customGroup}
|
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: 'Required.' },
|
required: { value: true, message: 'Required.' },
|
||||||
|
@ -3,7 +3,6 @@ import { Button, Field, FieldArray, Input, InlineLabel, Label, useStyles } from
|
|||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { RuleFormValues } from '../../types/rule-form';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -11,7 +10,12 @@ interface Props {
|
|||||||
|
|
||||||
const LabelsField: FC<Props> = ({ className }) => {
|
const LabelsField: FC<Props> = ({ className }) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
const { register, control, watch, errors } = useFormContext<RuleFormValues>();
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext();
|
||||||
const labels = watch('labels');
|
const labels = watch('labels');
|
||||||
return (
|
return (
|
||||||
<div className={cx(className, styles.wrapper)}>
|
<div className={cx(className, styles.wrapper)}>
|
||||||
@ -33,8 +37,9 @@ const LabelsField: FC<Props> = ({ className }) => {
|
|||||||
error={errors.labels?.[index]?.key?.message}
|
error={errors.labels?.[index]?.key?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
ref={register({ required: { value: !!labels[index]?.value, message: 'Required.' } })}
|
{...register(`labels[${index}].key`, {
|
||||||
name={`labels[${index}].key`}
|
required: { value: !!labels[index]?.value, message: 'Required.' },
|
||||||
|
})}
|
||||||
placeholder="key"
|
placeholder="key"
|
||||||
defaultValue={field.key}
|
defaultValue={field.key}
|
||||||
/>
|
/>
|
||||||
@ -46,8 +51,9 @@ const LabelsField: FC<Props> = ({ className }) => {
|
|||||||
error={errors.labels?.[index]?.value?.message}
|
error={errors.labels?.[index]?.value?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
ref={register({ required: { value: !!labels[index]?.key, message: 'Required.' } })}
|
{...register(`labels[${index}].value`, {
|
||||||
name={`labels[${index}].value`}
|
required: { value: !!labels[index]?.key, message: 'Required.' },
|
||||||
|
})}
|
||||||
placeholder="value"
|
placeholder="value"
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
/>
|
/>
|
||||||
|
@ -7,7 +7,11 @@ import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
|||||||
import { AlertingQueryEditor } from '../../../components/AlertingQueryEditor';
|
import { AlertingQueryEditor } from '../../../components/AlertingQueryEditor';
|
||||||
|
|
||||||
export const QueryStep: FC = () => {
|
export const QueryStep: FC = () => {
|
||||||
const { control, watch, errors } = useFormContext<RuleFormValues>();
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<RuleFormValues>();
|
||||||
const type = watch('type');
|
const type = watch('type');
|
||||||
const dataSourceName = watch('dataSourceName');
|
const dataSourceName = watch('dataSourceName');
|
||||||
return (
|
return (
|
||||||
@ -16,8 +20,7 @@ export const QueryStep: FC = () => {
|
|||||||
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
|
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
|
||||||
<InputControl
|
<InputControl
|
||||||
name="expression"
|
name="expression"
|
||||||
dataSourceName={dataSourceName}
|
render={({ field: { ref, ...field } }) => <ExpressionEditor {...field} dataSourceName={dataSourceName} />}
|
||||||
as={ExpressionEditor}
|
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: 'A valid expression is required' },
|
required: { value: true, message: 'A valid expression is required' },
|
||||||
@ -32,7 +35,7 @@ export const QueryStep: FC = () => {
|
|||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
name="queries"
|
name="queries"
|
||||||
as={AlertingQueryEditor}
|
render={({ field: { ref, ...field } }) => <AlertingQueryEditor {...field} />}
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
validate: (queries) => Array.isArray(queries) && !!queries.length,
|
validate: (queries) => Array.isArray(queries) && !!queries.length,
|
||||||
|
@ -6,7 +6,7 @@ export interface Folder {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props extends Omit<FolderPickerProps, 'initiailTitle' | 'initialFolderId'> {
|
export interface Props extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
|
||||||
value?: Folder;
|
value?: Folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
): GrafanaManagedReceiverConfig {
|
||||||
const channel: GrafanaManagedReceiverConfig = {
|
const channel: GrafanaManagedReceiverConfig = {
|
||||||
settings: {
|
settings: {
|
||||||
...(existing?.settings ?? {}),
|
...(existing && existing.type === values.type ? existing.settings ?? {} : {}),
|
||||||
...(values.settings ?? {}),
|
...(values.settings ?? {}),
|
||||||
},
|
},
|
||||||
secureSettings: values.secureSettings ?? {},
|
secureSettings: values.secureSettings ?? {},
|
||||||
|
@ -26,7 +26,7 @@ export const RowOptionsForm: FC<Props> = ({ repeat, title, onUpdate, onCancel })
|
|||||||
{({ register }) => (
|
{({ register }) => (
|
||||||
<>
|
<>
|
||||||
<Field label="Title">
|
<Field label="Title">
|
||||||
<Input name="title" ref={register} type="text" />
|
<Input {...register('title')} type="text" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Repeat for">
|
<Field label="Repeat for">
|
||||||
|
@ -92,8 +92,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
|
|||||||
<>
|
<>
|
||||||
<Field label="Dashboard name" invalid={!!errors.title} error={errors.title?.message}>
|
<Field label="Dashboard name" invalid={!!errors.title} error={errors.title?.message}>
|
||||||
<Input
|
<Input
|
||||||
name="title"
|
{...register('title', {
|
||||||
ref={register({
|
|
||||||
validate: validateDashboardName(getValues),
|
validate: validateDashboardName(getValues),
|
||||||
})}
|
})}
|
||||||
aria-label="Save dashboard title field"
|
aria-label="Save dashboard title field"
|
||||||
@ -102,17 +101,21 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label="Folder">
|
<Field label="Folder">
|
||||||
<InputControl
|
<InputControl
|
||||||
as={FolderPicker}
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<FolderPicker
|
||||||
|
{...field}
|
||||||
|
dashboardId={dashboard.id}
|
||||||
|
initialFolderId={dashboard.meta.folderId}
|
||||||
|
initialTitle={dashboard.meta.folderTitle}
|
||||||
|
enableCreateNew
|
||||||
|
/>
|
||||||
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="$folder"
|
name="$folder"
|
||||||
dashboardId={dashboard.id}
|
|
||||||
initialFolderId={dashboard.meta.folderId}
|
|
||||||
initialTitle={dashboard.meta.folderTitle}
|
|
||||||
enableCreateNew
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Copy tags">
|
<Field label="Copy tags">
|
||||||
<Switch name="copyTags" ref={register} />
|
<Switch {...register('copyTags')} />
|
||||||
</Field>
|
</Field>
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button variant="secondary" onClick={onCancel} fill="outline">
|
<Button variant="secondary" onClick={onCancel} fill="outline">
|
||||||
|
@ -39,23 +39,21 @@ export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard,
|
|||||||
<div>
|
<div>
|
||||||
{hasTimeChanged && (
|
{hasTimeChanged && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
{...register('saveTimerange')}
|
||||||
label="Save current time range as dashboard default"
|
label="Save current time range as dashboard default"
|
||||||
name="saveTimerange"
|
|
||||||
ref={register}
|
|
||||||
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
|
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasVariableChanged && (
|
{hasVariableChanged && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
{...register('saveVariables')}
|
||||||
label="Save current variable values as dashboard default"
|
label="Save current variable values as dashboard default"
|
||||||
name="saveVariables"
|
|
||||||
ref={register}
|
|
||||||
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
|
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(hasVariableChanged || hasTimeChanged) && <div className="gf-form-group" />}
|
{(hasVariableChanged || hasTimeChanged) && <div className="gf-form-group" />}
|
||||||
|
|
||||||
<TextArea name="message" ref={register} placeholder="Add a note to describe your changes." autoFocus />
|
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
|
@ -56,8 +56,7 @@ export class NewDashboardsFolder extends PureComponent<Props> {
|
|||||||
error={errors.folderName && errors.folderName.message}
|
error={errors.folderName && errors.folderName.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
name="folderName"
|
{...register('folderName', {
|
||||||
ref={register({
|
|
||||||
required: 'Folder name is required.',
|
required: 'Folder name is required.',
|
||||||
validate: async (v) => await this.validateFolderName(v),
|
validate: async (v) => await this.validateFolderName(v),
|
||||||
})}
|
})}
|
||||||
|
@ -98,10 +98,9 @@ class UnthemedDashboardImport extends PureComponent<Props> {
|
|||||||
{({ register, errors }) => (
|
{({ register, errors }) => (
|
||||||
<Field invalid={!!errors.gcomDashboard} error={errors.gcomDashboard && errors.gcomDashboard.message}>
|
<Field invalid={!!errors.gcomDashboard} error={errors.gcomDashboard && errors.gcomDashboard.message}>
|
||||||
<Input
|
<Input
|
||||||
name="gcomDashboard"
|
|
||||||
placeholder="Grafana.com dashboard URL or ID"
|
placeholder="Grafana.com dashboard URL or ID"
|
||||||
type="text"
|
type="text"
|
||||||
ref={register({
|
{...register('gcomDashboard', {
|
||||||
required: 'A Grafana dashboard URL or ID is required',
|
required: 'A Grafana dashboard URL or ID is required',
|
||||||
validate: validateGcomDashboard,
|
validate: validateGcomDashboard,
|
||||||
})}
|
})}
|
||||||
@ -118,8 +117,7 @@ class UnthemedDashboardImport extends PureComponent<Props> {
|
|||||||
<>
|
<>
|
||||||
<Field invalid={!!errors.dashboardJson} error={errors.dashboardJson && errors.dashboardJson.message}>
|
<Field invalid={!!errors.dashboardJson} error={errors.dashboardJson && errors.dashboardJson.message}>
|
||||||
<TextArea
|
<TextArea
|
||||||
name="dashboardJson"
|
{...register('dashboardJson', {
|
||||||
ref={register({
|
|
||||||
required: 'Need a dashboard JSON model',
|
required: 'Need a dashboard JSON model',
|
||||||
validate: validateDashboardJson,
|
validate: validateDashboardJson,
|
||||||
})}
|
})}
|
||||||
|
@ -15,7 +15,7 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
|||||||
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
|
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
|
||||||
import { validateTitle, validateUid } from '../utils/validation';
|
import { validateTitle, validateUid } from '../utils/validation';
|
||||||
|
|
||||||
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> {
|
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'setValue'> {
|
||||||
uidReset: boolean;
|
uidReset: boolean;
|
||||||
inputs: DashboardInputs;
|
inputs: DashboardInputs;
|
||||||
initialFolderId: number;
|
initialFolderId: number;
|
||||||
@ -47,7 +47,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSubmitted && (errors.title || errors.uid)) {
|
if (isSubmitted && (errors.title || errors.uid)) {
|
||||||
onSubmit(getValues({ nest: true }), {} as any);
|
onSubmit(getValues(), {} as any);
|
||||||
}
|
}
|
||||||
}, [errors, getValues, isSubmitted, onSubmit]);
|
}, [errors, getValues, isSubmitted, onSubmit]);
|
||||||
|
|
||||||
@ -56,20 +56,19 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
<Legend>Options</Legend>
|
<Legend>Options</Legend>
|
||||||
<Field label="Name" invalid={!!errors.title} error={errors.title && errors.title.message}>
|
<Field label="Name" invalid={!!errors.title} error={errors.title && errors.title.message}>
|
||||||
<Input
|
<Input
|
||||||
name="title"
|
{...register('title', {
|
||||||
type="text"
|
|
||||||
ref={register({
|
|
||||||
required: 'Name is required',
|
required: 'Name is required',
|
||||||
validate: async (v: string) => await validateTitle(v, getValues().folder.id),
|
validate: async (v: string) => await validateTitle(v, getValues().folder.id),
|
||||||
})}
|
})}
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Folder">
|
<Field label="Folder">
|
||||||
<InputControl
|
<InputControl
|
||||||
as={FolderPicker}
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<FolderPicker {...field} enableCreateNew initialFolderId={initialFolderId} />
|
||||||
|
)}
|
||||||
name="folder"
|
name="folder"
|
||||||
enableCreateNew
|
|
||||||
initialFolderId={initialFolderId}
|
|
||||||
control={control}
|
control={control}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@ -84,13 +83,12 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
{!uidReset ? (
|
{!uidReset ? (
|
||||||
<Input
|
<Input
|
||||||
name="uid"
|
|
||||||
disabled
|
disabled
|
||||||
ref={register({ validate: async (v: string) => await validateUid(v) })}
|
{...register('uid', { validate: async (v: string) => await validateUid(v) })}
|
||||||
addonAfter={!uidReset && <Button onClick={onUidReset}>Change uid</Button>}
|
addonAfter={!uidReset && <Button onClick={onUidReset}>Change uid</Button>}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input name="uid" ref={register({ required: true, validate: async (v: string) => await validateUid(v) })} />
|
<Input {...register('uid', { required: true, validate: async (v: string) => await validateUid(v) })} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Field>
|
</Field>
|
||||||
@ -106,13 +104,17 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
error={errors.dataSources && errors.dataSources[index] && 'A data source is required'}
|
error={errors.dataSources && errors.dataSources[index] && 'A data source is required'}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={DataSourcePicker}
|
name={dataSourceOption as any}
|
||||||
noDefault={true}
|
render={({ field: { ref, ...field } }) => (
|
||||||
pluginId={input.pluginId}
|
<DataSourcePicker
|
||||||
name={`${dataSourceOption}`}
|
{...field}
|
||||||
current={current[index]?.name}
|
noDefault={true}
|
||||||
|
placeholder={input.info}
|
||||||
|
pluginId={input.pluginId}
|
||||||
|
current={current[index]?.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
placeholder={input.info}
|
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@ -128,7 +130,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
invalid={errors.constants && !!errors.constants[index]}
|
invalid={errors.constants && !!errors.constants[index]}
|
||||||
key={constantIndex}
|
key={constantIndex}
|
||||||
>
|
>
|
||||||
<Input ref={register({ required: true })} name={`${constantIndex}`} defaultValue={input.value} />
|
<Input {...register(constantIndex as any, { required: true })} defaultValue={input.value} />
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -56,8 +56,7 @@ export const NewOrgPage: FC<PropsWithState> = ({ navModel }) => {
|
|||||||
<Field label="Organization name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
<Field label="Organization name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Org name"
|
placeholder="Org name"
|
||||||
name="name"
|
{...register('name', {
|
||||||
ref={register({
|
|
||||||
required: 'Organization name is required',
|
required: 'Organization name is required',
|
||||||
validate: async (orgName) => await validateOrg(orgName),
|
validate: async (orgName) => await validateOrg(orgName),
|
||||||
})}
|
})}
|
||||||
|
@ -16,7 +16,7 @@ const OrgProfile: FC<Props> = ({ onSubmit, orgName }) => {
|
|||||||
{({ register }) => (
|
{({ register }) => (
|
||||||
<FieldSet label="Organization profile">
|
<FieldSet label="Organization profile">
|
||||||
<Field label="Organization name">
|
<Field label="Organization name">
|
||||||
<Input name="orgName" type="text" ref={register({ required: true })} />
|
<Input type="text" {...register('orgName', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Button type="submit">Update organization name</Button>
|
<Button type="submit">Update organization name</Button>
|
||||||
|
@ -59,16 +59,20 @@ export const UserInviteForm: FC<Props> = ({}) => {
|
|||||||
error={!!errors.loginOrEmail ? 'Email or username is required' : undefined}
|
error={!!errors.loginOrEmail ? 'Email or username is required' : undefined}
|
||||||
label="Email or username"
|
label="Email or username"
|
||||||
>
|
>
|
||||||
<Input name="loginOrEmail" placeholder="email@example.com" ref={register({ required: true })} />
|
<Input {...register('loginOrEmail', { required: true })} placeholder="email@example.com" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field invalid={!!errors.name} label="Name">
|
<Field invalid={!!errors.name} label="Name">
|
||||||
<Input name="name" placeholder="(optional)" ref={register} />
|
<Input {...register('name')} placeholder="(optional)" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field invalid={!!errors.role} label="Role">
|
<Field invalid={!!errors.role} label="Role">
|
||||||
<InputControl as={RadioButtonGroup} control={control} options={roles} name="role" />
|
<InputControl
|
||||||
|
render={({ field: { ref, ...field } }) => <RadioButtonGroup {...field} options={roles} />}
|
||||||
|
control={control}
|
||||||
|
name="role"
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Send invite email">
|
<Field label="Send invite email">
|
||||||
<Switch name="sendEmail" ref={register} />
|
<Switch {...register('sendEmail')} />
|
||||||
</Field>
|
</Field>
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">Submit</Button>
|
||||||
|
@ -29,8 +29,8 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
intervalMs: any;
|
intervalMs: any;
|
||||||
resolution: any;
|
resolution: any;
|
||||||
timeInfo?: string;
|
timeInfo?: string;
|
||||||
skipDataOnInit: boolean;
|
skipDataOnInit = false;
|
||||||
dataList: LegacyResponseData[];
|
dataList: LegacyResponseData[] = [];
|
||||||
querySubscription?: Unsubscribable | null;
|
querySubscription?: Unsubscribable | null;
|
||||||
useDataFrames = false;
|
useDataFrames = false;
|
||||||
panelData?: PanelData;
|
panelData?: PanelData;
|
||||||
|
@ -16,19 +16,19 @@ export class PanelCtrl {
|
|||||||
panel: any;
|
panel: any;
|
||||||
error: any;
|
error: any;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
pluginName: string;
|
pluginName = '';
|
||||||
pluginId: string;
|
pluginId = '';
|
||||||
editorTabs: any;
|
editorTabs: any;
|
||||||
$scope: any;
|
$scope: any;
|
||||||
$injector: auto.IInjectorService;
|
$injector: auto.IInjectorService;
|
||||||
$location: any;
|
$location: any;
|
||||||
$timeout: any;
|
$timeout: any;
|
||||||
editModeInitiated: boolean;
|
editModeInitiated = false;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
containerHeight: any;
|
containerHeight: any;
|
||||||
events: EventBusExtended;
|
events: EventBusExtended;
|
||||||
loading: boolean;
|
loading = false;
|
||||||
timing: any;
|
timing: any;
|
||||||
|
|
||||||
constructor($scope: any, $injector: auto.IInjectorService) {
|
constructor($scope: any, $injector: auto.IInjectorService) {
|
||||||
|
@ -30,8 +30,7 @@ export const PlaylistForm: FC<PlaylistFormProps> = ({ onSubmit, playlist }) => {
|
|||||||
<Field label="Name" invalid={!!errors.name} error={errors?.name?.message}>
|
<Field label="Name" invalid={!!errors.name} error={errors?.name?.message}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
{...register('name', { required: 'Name is required' })}
|
||||||
ref={register({ required: 'Name is required' })}
|
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
defaultValue={name}
|
defaultValue={name}
|
||||||
aria-label={selectors.pages.PlaylistForm.name}
|
aria-label={selectors.pages.PlaylistForm.name}
|
||||||
@ -40,8 +39,7 @@ export const PlaylistForm: FC<PlaylistFormProps> = ({ onSubmit, playlist }) => {
|
|||||||
<Field label="Interval" invalid={!!errors.interval} error={errors?.interval?.message}>
|
<Field label="Interval" invalid={!!errors.interval} error={errors?.interval?.message}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="interval"
|
{...register('interval', { required: 'Interval is required' })}
|
||||||
ref={register({ required: 'Interval is required' })}
|
|
||||||
placeholder="5m"
|
placeholder="5m"
|
||||||
defaultValue={interval ?? '5m'}
|
defaultValue={interval ?? '5m'}
|
||||||
aria-label={selectors.pages.PlaylistForm.interval}
|
aria-label={selectors.pages.PlaylistForm.interval}
|
||||||
|
@ -33,14 +33,13 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}>
|
<Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}>
|
||||||
<Input type="password" name="oldPassword" ref={register({ required: 'Old password is required' })} />
|
<Input type="password" {...register('oldPassword', { required: 'Old password is required' })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="newPassword"
|
{...register('newPassword', {
|
||||||
ref={register({
|
|
||||||
required: 'New password is required',
|
required: 'New password is required',
|
||||||
validate: {
|
validate: {
|
||||||
confirm: (v) => v === getValues().confirmNew || 'Passwords must match',
|
confirm: (v) => v === getValues().confirmNew || 'Passwords must match',
|
||||||
@ -53,8 +52,7 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
|
|||||||
<Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
<Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name="confirmNew"
|
{...register('confirmNew', {
|
||||||
ref={register({
|
|
||||||
required: 'New password confirmation is required',
|
required: 'New password confirmation is required',
|
||||||
validate: (v) => v === getValues().newPassword || 'Passwords must match',
|
validate: (v) => v === getValues().newPassword || 'Passwords must match',
|
||||||
})}
|
})}
|
||||||
|
@ -24,8 +24,7 @@ export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfi
|
|||||||
<FieldSet label="Edit profile">
|
<FieldSet label="Edit profile">
|
||||||
<Field label="Name" invalid={!!errors.name} error="Name is required" disabled={disableLoginForm}>
|
<Field label="Name" invalid={!!errors.name} error="Name is required" disabled={disableLoginForm}>
|
||||||
<Input
|
<Input
|
||||||
name="name"
|
{...register('name', { required: true })}
|
||||||
ref={register({ required: true })}
|
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
defaultValue={user.name}
|
defaultValue={user.name}
|
||||||
suffix={<InputSuffix />}
|
suffix={<InputSuffix />}
|
||||||
@ -33,21 +32,14 @@ export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfi
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
|
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
{...register('email', { required: true })}
|
||||||
ref={register({ required: true })}
|
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
defaultValue={user.email}
|
defaultValue={user.email}
|
||||||
suffix={<InputSuffix />}
|
suffix={<InputSuffix />}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Username" disabled={disableLoginForm}>
|
<Field label="Username" disabled={disableLoginForm}>
|
||||||
<Input
|
<Input {...register('login')} defaultValue={user.login} placeholder="Username" suffix={<InputSuffix />} />
|
||||||
name="login"
|
|
||||||
ref={register}
|
|
||||||
defaultValue={user.login}
|
|
||||||
placeholder="Username"
|
|
||||||
suffix={<InputSuffix />}
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
<div className="gf-form-button-row">
|
<div className="gf-form-button-row">
|
||||||
<Button variant="primary" disabled={isSavingUser}>
|
<Button variant="primary" disabled={isSavingUser}>
|
||||||
|
@ -34,7 +34,7 @@ export class CreateTeam extends PureComponent<Props> {
|
|||||||
{({ register }) => (
|
{({ register }) => (
|
||||||
<FieldSet label="New Team">
|
<FieldSet label="New Team">
|
||||||
<Field label="Name">
|
<Field label="Name">
|
||||||
<Input name="name" ref={register({ required: true })} width={60} />
|
<Input {...register('name', { required: true })} width={60} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label={
|
label={
|
||||||
@ -46,7 +46,7 @@ export class CreateTeam extends PureComponent<Props> {
|
|||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Input type="email" name="email" ref={register()} placeholder="email@test.com" width={60} />
|
<Input {...register('email')} type="email" placeholder="email@test.com" width={60} />
|
||||||
</Field>
|
</Field>
|
||||||
<div className="gf-form-button-row">
|
<div className="gf-form-button-row">
|
||||||
<Button type="submit" variant="primary">
|
<Button type="submit" variant="primary">
|
||||||
|
@ -24,14 +24,14 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
|
|||||||
{({ register }) => (
|
{({ register }) => (
|
||||||
<>
|
<>
|
||||||
<Field label="Name">
|
<Field label="Name">
|
||||||
<Input name="name" ref={register({ required: true })} />
|
<Input {...register('name', { required: true })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label="Email"
|
label="Email"
|
||||||
description="This is optional and is primarily used to set the team profile avatar (via gravatar service)."
|
description="This is optional and is primarily used to set the team profile avatar (via gravatar service)."
|
||||||
>
|
>
|
||||||
<Input placeholder="team@email.com" type="email" name="email" ref={register} />
|
<Input {...register('email')} placeholder="team@email.com" type="email" />
|
||||||
</Field>
|
</Field>
|
||||||
<Button type="submit">Update</Button>
|
<Button type="submit">Update</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -73,8 +73,7 @@ export const SignupInvitedPage: FC<Props> = ({ match }) => {
|
|||||||
<Field invalid={!!errors.email} error={errors.email && errors.email.message} label="Email">
|
<Field invalid={!!errors.email} error={errors.email && errors.email.message} label="Email">
|
||||||
<Input
|
<Input
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
name="email"
|
{...register('email', {
|
||||||
ref={register({
|
|
||||||
required: 'Email is required',
|
required: 'Email is required',
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^\S+@\S+$/,
|
value: /^\S+@\S+$/,
|
||||||
@ -84,17 +83,16 @@ export const SignupInvitedPage: FC<Props> = ({ match }) => {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field invalid={!!errors.name} error={errors.name && errors.name.message} label="Name">
|
<Field invalid={!!errors.name} error={errors.name && errors.name.message} label="Name">
|
||||||
<Input placeholder="Name (optional)" name="name" ref={register} />
|
<Input placeholder="Name (optional)" {...register('name')} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field invalid={!!errors.username} error={errors.username && errors.username.message} label="Username">
|
<Field invalid={!!errors.username} error={errors.username && errors.username.message} label="Username">
|
||||||
<Input placeholder="Username" name="username" ref={register({ required: 'Username is required' })} />
|
<Input {...register('username', { required: 'Username is required' })} placeholder="Username" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field invalid={!!errors.password} error={errors.password && errors.password.message} label="Password">
|
<Field invalid={!!errors.password} error={errors.password && errors.password.message} label="Password">
|
||||||
<Input
|
<Input
|
||||||
|
{...register('password', { required: 'Password is required' })}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
name="password"
|
|
||||||
ref={register({ required: 'Password is required' })}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ function getLabelFromTrace(trace: TraceResponse): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class JaegerQueryField extends React.PureComponent<Props, State> {
|
export class JaegerQueryField extends React.PureComponent<Props, State> {
|
||||||
private _isMounted: boolean;
|
private _isMounted = false;
|
||||||
|
|
||||||
constructor(props: Props, context: React.Context<any>) {
|
constructor(props: Props, context: React.Context<any>) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -69,8 +69,8 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
|||||||
this.setState({ formatOption: option }, this.onRunQuery);
|
this.setState({ formatOption: option }, this.onRunQuery);
|
||||||
};
|
};
|
||||||
|
|
||||||
onInstantChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
onInstantChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
const instant = e.target.checked;
|
const instant = (e.target as HTMLInputElement).checked;
|
||||||
this.query.instant = instant;
|
this.query.instant = instant;
|
||||||
this.setState({ instant }, this.onRunQuery);
|
this.setState({ instant }, this.onRunQuery);
|
||||||
};
|
};
|
||||||
|
@ -35,27 +35,25 @@ export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={addPoint} maxWidth="none">
|
<Form onSubmit={addPoint} maxWidth="none">
|
||||||
{({ register, control, watch }) => {
|
{({ register, control, watch, setValue }) => {
|
||||||
const selectedPoint = watch('selectedPoint') as SelectableValue;
|
const selectedPoint = watch('selectedPoint' as any) as SelectableValue;
|
||||||
return (
|
return (
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="New value" labelWidth={14}>
|
<InlineField label="New value" labelWidth={14}>
|
||||||
<Input
|
<Input
|
||||||
|
{...register('newPointValue')}
|
||||||
width={32}
|
width={32}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="value"
|
placeholder="value"
|
||||||
id={`newPointValue-${query.refId}`}
|
id={`newPointValue-${query.refId}`}
|
||||||
name="newPointValue"
|
|
||||||
ref={register}
|
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
<InlineField label="Time" labelWidth={14}>
|
<InlineField label="Time" labelWidth={14}>
|
||||||
<Input
|
<Input
|
||||||
|
{...register('newPointTime')}
|
||||||
width={32}
|
width={32}
|
||||||
id={`newPointTime-${query.refId}`}
|
id={`newPointTime-${query.refId}`}
|
||||||
placeholder="time"
|
placeholder="time"
|
||||||
name="newPointTime"
|
|
||||||
ref={register}
|
|
||||||
defaultValue={dateTime().format()}
|
defaultValue={dateTime().format()}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
@ -64,13 +62,11 @@ export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
<InlineField label="All values">
|
<InlineField label="All values">
|
||||||
<InputControl
|
<InputControl
|
||||||
|
name={'selectedPoint' as any}
|
||||||
control={control}
|
control={control}
|
||||||
as={Select}
|
render={({ field: { ref, ...field } }) => (
|
||||||
options={pointOptions}
|
<Select {...field} options={pointOptions} width={32} placeholder="Select point" />
|
||||||
width={32}
|
)}
|
||||||
name="selectedPoint"
|
|
||||||
onChange={(value) => value[0]}
|
|
||||||
placeholder="Select point"
|
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
|
|
||||||
@ -80,7 +76,7 @@ export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
control.setValue('selectedPoint', [{ value: undefined, label: 'Select value' }]);
|
setValue('selectedPoint' as any, [{ value: undefined, label: 'Select value' }]);
|
||||||
deletePoint(selectedPoint);
|
deletePoint(selectedPoint);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -20755,10 +20755,10 @@ react-highlight-words@0.16.0:
|
|||||||
memoize-one "^4.0.0"
|
memoize-one "^4.0.0"
|
||||||
prop-types "^15.5.8"
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
react-hook-form@5.1.3:
|
react-hook-form@7.2.3:
|
||||||
version "5.1.3"
|
version "7.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.1.3.tgz#24610e11878c6bd143569ce203320f7367893e75"
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.2.3.tgz#a4be9214cab3a6e6358f95d342da2e7ded37e3f0"
|
||||||
integrity sha512-6+6wSge72A2Y7WGqMke4afOz0uDJ3gOPSysmYKkjJszSbmw8X8at7tJPzifnZ+cwLDR88b4on/D+jfH5azWbIw==
|
integrity sha512-ki83pkQH/NK6HbSWb4zHLD78s8nh6OW2j4GC5kAjhB2C3yiiVGvNAvybgAfnsXBbx+xb9mPgSpRRVOQUbss+JQ==
|
||||||
|
|
||||||
react-hot-loader@4.8.0:
|
react-hot-loader@4.8.0:
|
||||||
version "4.8.0"
|
version "4.8.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user