From 5afcf79c59d3756e643d6c8974508165d90c0845 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 22 Jan 2020 15:26:03 +0100 Subject: [PATCH] Migrations: migrate admin user create page (#21506) * Create basic react page for AdminUserCreate * Forms.Field - render asterisk when field is marked as required * Add validation to user create form wih react-hook-form * Remove Angular code for UserCreatePage * Remove commented route for admin settings * Remove unused import * Hide react-hooks-form behind Form component * Fix webkit autofill * Webkit autofill on inpiuts - bring back focus shadow * Temporarily fix story (before 21635 is merged) * Form: docs and minor updates to new form elements (#21635) * Allow Switch, checkbox to be uncontrolled, forward refs, styles update * Add Form docs * User create page update * Remove unused import * Apply review notes --- packages/grafana-ui/package.json | 1 + .../src/components/Forms/Checkbox.story.tsx | 17 +- .../src/components/Forms/Checkbox.tsx | 80 ++++--- .../src/components/Forms/Field.story.tsx | 10 +- .../grafana-ui/src/components/Forms/Field.tsx | 5 +- .../grafana-ui/src/components/Forms/Form.mdx | 155 ++++++++++++++ .../src/components/Forms/Form.story.tsx | 199 ++++++++++-------- .../grafana-ui/src/components/Forms/Form.tsx | 27 ++- .../Forms/RadioButtonGroup/RadioButton.tsx | 2 +- .../RadioButtonGroup/RadioButtonGroup.tsx | 20 +- .../src/components/Forms/Switch.story.tsx | 21 +- .../src/components/Forms/Switch.tsx | 114 ++++++---- .../src/components/Forms/commonStyles.ts | 12 ++ .../grafana-ui/src/components/Forms/index.ts | 2 + .../app/features/admin/AdminEditUserCtrl.ts | 14 -- public/app/features/admin/UserCreatePage.tsx | 82 ++++++++ .../app/features/admin/partials/new_user.html | 32 --- public/app/routes/routes.ts | 11 +- yarn.lock | 5 + 19 files changed, 555 insertions(+), 254 deletions(-) create mode 100644 packages/grafana-ui/src/components/Forms/Form.mdx create mode 100644 public/app/features/admin/UserCreatePage.tsx delete mode 100644 public/app/features/admin/partials/new_user.html diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 141cefd75f6..8e2656d6baf 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -51,6 +51,7 @@ "react-custom-scrollbars": "4.2.1", "react-dom": "16.12.0", "react-highlight-words": "0.11.0", + "react-hook-form": "4.5.3", "react-popper": "1.3.3", "react-storybook-addon-props-combinations": "1.1.0", "react-table": "7.0.0-rc.15", diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx index 14f37f09bec..f7ae482bbf3 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import mdx from './Checkbox.mdx'; import { Checkbox } from './Checkbox'; @@ -12,12 +12,23 @@ export default { }, }; -export const simple = () => { +export const controlled = () => { const [checked, setChecked] = useState(false); + const onChange = useCallback(e => setChecked(e.currentTarget.checked), [setChecked]); return ( + ); +}; + +export const uncontrolled = () => { + return ( + diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.tsx index 13f868ed18a..141ea75c3c4 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.tsx @@ -1,15 +1,14 @@ -import React, { HTMLProps } from 'react'; +import React, { HTMLProps, useCallback } from 'react'; import { GrafanaTheme } from '@grafana/data'; import { getLabelStyles } from './Label'; import { useTheme, stylesFactory } from '../../themes'; import { css, cx } from 'emotion'; import { getFocusCss } from './commonStyles'; -export interface CheckboxProps extends Omit, 'onChange' | 'value'> { +export interface CheckboxProps extends Omit, 'value'> { label?: string; description?: string; - value: boolean; - onChange?: (checked: boolean) => void; + value?: boolean; } export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { @@ -31,6 +30,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { wrapper: css` position: relative; padding-left: ${checkboxSize}; + vertical-align: middle; `, input: css` position: absolute; @@ -87,43 +87,41 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { }; }); -export const Checkbox: React.FC = ({ - label, - description, - value, - onChange, - id, - disabled, - ...inputProps -}) => { - const theme = useTheme(); - const styles = getCheckboxStyles(theme); +export const Checkbox = React.forwardRef( + ({ label, description, value, onChange, disabled, ...inputProps }, ref) => { + const theme = useTheme(); + const handleOnChange = useCallback( + (e: React.ChangeEvent) => { + if (onChange) { + onChange(e); + } + }, + [onChange] + ); + const styles = getCheckboxStyles(theme); - return ( - - ); -}; + return ( + + ); + } +); Checkbox.displayName = 'Checkbox'; diff --git a/packages/grafana-ui/src/components/Forms/Field.story.tsx b/packages/grafana-ui/src/components/Forms/Field.story.tsx index addd24e918d..7533f34b62e 100644 --- a/packages/grafana-ui/src/components/Forms/Field.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Field.story.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { boolean, number, text } from '@storybook/addon-knobs'; import { Field } from './Field'; import { Input } from './Input/Input'; @@ -52,16 +52,12 @@ export const simple = () => { export const horizontalLayout = () => { const [checked, setChecked] = useState(false); + const onChange = useCallback(e => setChecked(e.currentTarget.checked), [setChecked]); const { containerWidth, ...otherProps } = getKnobs(); return (
- { - setChecked(checked); - }} - /> +
); diff --git a/packages/grafana-ui/src/components/Forms/Field.tsx b/packages/grafana-ui/src/components/Forms/Field.tsx index 9dc6975de53..82e06830863 100644 --- a/packages/grafana-ui/src/components/Forms/Field.tsx +++ b/packages/grafana-ui/src/components/Forms/Field.tsx @@ -18,6 +18,8 @@ export interface FieldProps { loading?: boolean; /** Indicates if field is disabled */ disabled?: boolean; + /** Indicates if field is required */ + required?: boolean; /** Error message to display */ error?: string; /** Indicates horizontal layout of the field */ @@ -53,6 +55,7 @@ export const Field: React.FC = ({ invalid, loading, disabled, + required, error, children, className, @@ -73,7 +76,7 @@ export const Field: React.FC = ({
{label && ( )}
diff --git a/packages/grafana-ui/src/components/Forms/Form.mdx b/packages/grafana-ui/src/components/Forms/Form.mdx new file mode 100644 index 00000000000..b49fe4fd0e8 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Form.mdx @@ -0,0 +1,155 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks'; +import { Form } from './Form'; + + + +# Form + +Form component provides a way to build simple forms at Grafana. It is built on top of [react-hook-form](https://react-hook-form.com/) library and incorporates the same concepts while adjusting the API slightly. + +## Usage + +```jsx +import { Forms } from '@grafana/ui'; + +interface UserDTO { + name: string; + email: string; + //... +} + +const defaultUser: Partial = { + name: 'Roger Waters', + // ... +} + + await createrUser(user)} +>{({register, errors}) => { + return ( + + + + + + ) +}} +``` + +### Form API + +`Form` component exposes API via render prop. Three properties are exposed: `register`, `errors` and `control` + +#### `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: + +```jsx + +``` + +Register accepts an object which describes validation rules for a given input: + +```jsx + { // custom validation rule } + })} +/> +``` + +#### `errors` + +`errors` in an object that contains validation errors of the form. To show error message and invalid input indication in your form, wrap input element with `` component and pass `invalid` and `error` props to it: + +```jsx + + + +``` + +#### `control` + +By default `Form` component assumes form elements are uncontrolled (https://reactjs.org/docs/glossary.html#controlled-vs-uncontrolled-components). There are some components like `RadioButton` or `Select` that are controlled-only and require some extra work. To make them work with the form, you need to render those using `Forms.InputControl` component: + +```jsx +import { Forms } from '@grafana/ui'; + +// render function +{({register, errors, control}) => ( + <> + + + + + + + + +)} + +``` + +### Default values + +Default values of the form can be passed either via `defaultValues` property on the `Form` element, or directly on form's input via `defaultValue` prop: + +```jsx +// Passing default values to the Form + +interface FormDTO { + name: string; + isAdmin: boolean; +} + +const defaultValues: FormDto { + name: 'Roger Waters', + isAdmin: false, +} + +{...} +``` + +```jsx +// Passing default value directly to form inputs + +interface FormDTO { + name: string; + isAdmin: boolean; +} + +const defaultValues: FormDto { + name: 'Roger Waters', + isAdmin: false, +} + +{ + ({register}) => ( + <> + + + )} + +``` + +### Props + + diff --git a/packages/grafana-ui/src/components/Forms/Form.story.tsx b/packages/grafana-ui/src/components/Forms/Form.story.tsx index 8396fd60aa3..338289f6da0 100644 --- a/packages/grafana-ui/src/components/Forms/Form.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.story.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Legend } from './Legend'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; @@ -8,96 +8,131 @@ import { Input } from './Input/Input'; import { Button } from './Button'; import { Form } from './Form'; import { Switch } from './Switch'; -import { Icon } from '../Icon/Icon'; import { Checkbox } from './Checkbox'; -import { TextArea } from './TextArea/TextArea'; + +import { RadioButtonGroup } from './RadioButtonGroup/RadioButtonGroup'; +import { Select } from './Select/Select'; +import Forms from './index'; +import mdx from './Form.mdx'; export default { - title: 'UI/Forms/Test forms/Server admin', + title: 'UI/Forms/Test forms', decorators: [withStoryContainer, withCenteredStory], + parameters: { + docs: { + page: mdx, + }, + }, }; -export const users = () => { - const [name, setName] = useState(); - const [email, setEmail] = useState(); - const [username, setUsername] = useState(); - const [password, setPassword] = useState(); - const [disabledUser, setDisabledUser] = useState(false); - const [checked, setChecked] = useState(false); +const selectOptions = [ + { + label: 'Option 1', + value: 'option1', + }, + { + label: 'Option 2', + value: 'option2', + }, + { + label: 'Option 3', + value: 'option3', + }, +]; +interface FormDTO { + name: string; + email: string; + username: string; + checkbox: boolean; + switch: boolean; + radio: string; + select: string; + nested: { + path: string; + }; +} + +const renderForm = (defaultValues?: Partial) => ( +
{ + console.log(data); + }} + > + {({ register, control, errors }) => + (console.log(errors) as any) || ( + <> + Edit user + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } +
+); + +export const basic = () => { + return <>{renderForm()}; +}; + +export const defaultValues = () => { return ( <> -
- Edit user - - setName(e.currentTarget.value)} - size="md" - /> - - - setEmail(e.currentTarget.value)} - size="md" - /> - - - setUsername(e.currentTarget.value)} - size="md" - /> - - - setDisabledUser(checked)} /> - - - - - -
-
- Change password - - setPassword(e.currentTarget.value)} - size="md" - prefix={} - /> - - -
- -
-
- CERT validation - -