mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
36aab3a738
commit
5afcf79c59
@ -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",
|
||||
|
@ -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 (
|
||||
<Checkbox
|
||||
value={checked}
|
||||
onChange={setChecked}
|
||||
onChange={onChange}
|
||||
label="Skip SLL cert validation"
|
||||
description="Set to true if you want to skip sll cert validation"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const uncontrolled = () => {
|
||||
return (
|
||||
<Checkbox
|
||||
defaultChecked={true}
|
||||
label="Skip SLL cert validation"
|
||||
description="Set to true if you want to skip sll cert validation"
|
||||
/>
|
||||
|
@ -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<HTMLProps<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
export interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, '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<CheckboxProps> = ({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
id,
|
||||
disabled,
|
||||
...inputProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getCheckboxStyles(theme);
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
({ label, description, value, onChange, disabled, ...inputProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const handleOnChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const styles = getCheckboxStyles(theme);
|
||||
|
||||
return (
|
||||
<label className={styles.wrapper}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.input}
|
||||
id={id}
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={event => {
|
||||
if (onChange) {
|
||||
onChange(event.target.checked);
|
||||
}
|
||||
}}
|
||||
{...inputProps}
|
||||
/>
|
||||
<span className={styles.checkmark} />
|
||||
{label && <span className={styles.label}>{label}</span>}
|
||||
{description && (
|
||||
<>
|
||||
<br />
|
||||
<span className={styles.description}>{description}</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<label className={styles.wrapper}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.input}
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={handleOnChange}
|
||||
{...inputProps}
|
||||
ref={ref}
|
||||
/>
|
||||
<span className={styles.checkmark} />
|
||||
{label && <span className={styles.label}>{label}</span>}
|
||||
{description && (
|
||||
<>
|
||||
<br />
|
||||
<span className={styles.description}>{description}</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Checkbox.displayName = 'Checkbox';
|
||||
|
@ -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 (
|
||||
<div style={{ width: containerWidth }}>
|
||||
<Field horizontal label="Show labels" description="Display thresholds's labels" {...otherProps}>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={(e, checked) => {
|
||||
setChecked(checked);
|
||||
}}
|
||||
/>
|
||||
<Switch checked={checked} onChange={onChange} />
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
@ -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<FieldProps> = ({
|
||||
invalid,
|
||||
loading,
|
||||
disabled,
|
||||
required,
|
||||
error,
|
||||
children,
|
||||
className,
|
||||
@ -73,7 +76,7 @@ export const Field: React.FC<FieldProps> = ({
|
||||
<div className={cx(styles.field, horizontal && styles.fieldHorizontal, className)}>
|
||||
{label && (
|
||||
<Label htmlFor={inputId} description={description}>
|
||||
{label}
|
||||
{`${label}${required ? ' *' : ''}`}
|
||||
</Label>
|
||||
)}
|
||||
<div>
|
||||
|
155
packages/grafana-ui/src/components/Forms/Form.mdx
Normal file
155
packages/grafana-ui/src/components/Forms/Form.mdx
Normal file
@ -0,0 +1,155 @@
|
||||
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||
import { Form } from './Form';
|
||||
|
||||
<Meta title="MDX|Form" component={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<UserDTO> = {
|
||||
name: 'Roger Waters',
|
||||
// ...
|
||||
}
|
||||
|
||||
<Forms.Form
|
||||
defaultValues={defaultUser}
|
||||
onSubmit={async (user: UserDTO) => await createrUser(user)}
|
||||
>{({register, errors}) => {
|
||||
return (
|
||||
<Forms.Field>
|
||||
<Forms.Input name="name" ref={register}/>
|
||||
<Forms.Input type="email" name="email" ref={register({required: true})}/>
|
||||
<Button type="submit">Create User</Button>
|
||||
</Forms.Field>
|
||||
)
|
||||
}}</Forms.Form>
|
||||
```
|
||||
|
||||
### 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
|
||||
<Forms.Input name="inputName" ref={register} />
|
||||
```
|
||||
|
||||
Register accepts an object which describes validation rules for a given input:
|
||||
|
||||
```jsx
|
||||
<Forms.Input
|
||||
name="inputName"
|
||||
ref={register({
|
||||
required: true,
|
||||
minLength: 10,
|
||||
validate: v => { // 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 `<Forms.Field ...>` component and pass `invalid` and `error` props to it:
|
||||
|
||||
```jsx
|
||||
<Forms.Field label="Name" invalid={!!errors.name} error={!!errors.name && 'Name is required'}>
|
||||
<Forms.Input name="name" ref={register({ required: true })} />
|
||||
</Forms.Field>
|
||||
```
|
||||
|
||||
#### `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
|
||||
<Forms.Form ...>{({register, errors, control}) => (
|
||||
<>
|
||||
<Field label="RadioButtonExample">
|
||||
<Forms.InputControl
|
||||
{/* Render InputControl as controlled input (RadioButtonGroup) */}
|
||||
as={RadioButtonGroup}
|
||||
{/* Pass control exposed from Form render prop */}
|
||||
control={control}
|
||||
name="radio"
|
||||
options={...}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="SelectExample">
|
||||
<Forms.InputControl
|
||||
{/* Render InputControl as controlled input (Select) */}
|
||||
as={Select}
|
||||
{/* Pass control exposed from Form render prop */}
|
||||
control={control}
|
||||
name="select"
|
||||
options={...}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</Forms.Form>
|
||||
```
|
||||
|
||||
### 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,
|
||||
}
|
||||
|
||||
<Forms.Form defaultValues={defaultValues} ...>{...}</Forms.Form>
|
||||
```
|
||||
|
||||
```jsx
|
||||
// Passing default value directly to form inputs
|
||||
|
||||
interface FormDTO {
|
||||
name: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const defaultValues: FormDto {
|
||||
name: 'Roger Waters',
|
||||
isAdmin: false,
|
||||
}
|
||||
|
||||
<Forms.Form ...>{
|
||||
({register}) => (
|
||||
<>
|
||||
<Forms.Input defaultValue={default.name} name="name" ref={register} />
|
||||
</>
|
||||
)}
|
||||
</Forms.Form>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
<Props of={Form} />
|
@ -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<FormDTO>) => (
|
||||
<Form
|
||||
defaultValues={defaultValues}
|
||||
onSubmit={(data: FormDTO) => {
|
||||
console.log(data);
|
||||
}}
|
||||
>
|
||||
{({ register, control, errors }) =>
|
||||
(console.log(errors) as any) || (
|
||||
<>
|
||||
<Legend>Edit user</Legend>
|
||||
|
||||
<Field label="Name" invalid={!!errors.name} error="Name is required">
|
||||
<Input name="name" placeholder="Roger Waters" size="md" ref={register({ required: true })} />
|
||||
</Field>
|
||||
|
||||
<Field label="Email" invalid={!!errors.email} error="E-mail is required">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="roger.waters@grafana.com"
|
||||
size="md"
|
||||
ref={register({ required: true })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Username">
|
||||
<Input name="username" placeholder="mr.waters" size="md" ref={register} />
|
||||
</Field>
|
||||
<Field label="Nested object">
|
||||
<Input name="nested.path" placeholder="Nested path" size="md" ref={register} />
|
||||
</Field>
|
||||
|
||||
<Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent">
|
||||
<Checkbox name="checkbox" label="Do you consent?" ref={register({ required: true })} />
|
||||
</Field>
|
||||
|
||||
<Field label="Switch">
|
||||
<Switch name="switch" ref={register} />
|
||||
</Field>
|
||||
|
||||
<Field label="RadioButton">
|
||||
<Forms.InputControl name="radio" control={control} options={selectOptions} as={RadioButtonGroup} />
|
||||
</Field>
|
||||
|
||||
<Field label="RadioButton" invalid={!!errors.select}>
|
||||
<Forms.InputControl
|
||||
name="select"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
options={selectOptions}
|
||||
as={Select}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Button type="submit">Update</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Form>
|
||||
);
|
||||
|
||||
export const basic = () => {
|
||||
return <>{renderForm()}</>;
|
||||
};
|
||||
|
||||
export const defaultValues = () => {
|
||||
return (
|
||||
<>
|
||||
<Form>
|
||||
<Legend>Edit user</Legend>
|
||||
<Field label="Name">
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Roger Waters"
|
||||
value={name}
|
||||
onChange={e => setName(e.currentTarget.value)}
|
||||
size="md"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Email">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="roger.waters@grafana.com"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.currentTarget.value)}
|
||||
size="md"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Username">
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="mr.waters"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.currentTarget.value)}
|
||||
size="md"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Disable" description="Added for testing purposes">
|
||||
<Switch checked={disabledUser} onChange={(_e, checked) => setDisabledUser(checked)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<Checkbox
|
||||
label="Skip SLL cert validation"
|
||||
description="Set to true if you want to skip sll cert validation"
|
||||
value={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
</Field>
|
||||
<Button>Update</Button>
|
||||
</Form>
|
||||
<Form>
|
||||
<Legend>Change password</Legend>
|
||||
<Field label="Password">
|
||||
<Input
|
||||
id="password>"
|
||||
type="password"
|
||||
placeholder="Be safe..."
|
||||
value={password}
|
||||
onChange={e => setPassword(e.currentTarget.value)}
|
||||
size="md"
|
||||
prefix={<Icon name="lock" />}
|
||||
/>
|
||||
</Field>
|
||||
<Button>Update</Button>
|
||||
</Form>
|
||||
|
||||
<Form>
|
||||
<fieldset>
|
||||
<Legend>CERT validation</Legend>
|
||||
<Field
|
||||
label="Path to client cert"
|
||||
description="Authentication against LDAP servers requiring client certificates if not required leave empty "
|
||||
>
|
||||
<TextArea id="clientCert" value={''} size="lg" />
|
||||
</Field>
|
||||
</fieldset>
|
||||
<Button>Update</Button>
|
||||
</Form>
|
||||
{renderForm({
|
||||
name: 'Roger Waters',
|
||||
nested: {
|
||||
path: 'Nested path default value',
|
||||
},
|
||||
radio: 'option2',
|
||||
select: 'option1',
|
||||
switch: true,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -3,9 +3,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { useForm, Mode, OnSubmit, DeepPartial, FormContextValues } from 'react-hook-form';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
|
||||
const getFormStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
@ -15,8 +16,26 @@ const getFormStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export const Form: React.FC = ({ children }) => {
|
||||
type FormAPI<T> = Pick<FormContextValues<T>, 'register' | 'errors' | 'control'>;
|
||||
|
||||
interface FormProps<T> {
|
||||
validateOn?: Mode;
|
||||
defaultValues?: DeepPartial<T>;
|
||||
onSubmit: OnSubmit<T>;
|
||||
children: (api: FormAPI<T>) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function Form<T>({ validateOn, defaultValues, onSubmit, children }: FormProps<T>) {
|
||||
const theme = useTheme();
|
||||
const { handleSubmit, register, errors, control } = useForm<T>({
|
||||
mode: validateOn || 'onSubmit',
|
||||
defaultValues,
|
||||
});
|
||||
const styles = getFormStyles(theme);
|
||||
return <div className={styles.form}>{children}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
|
||||
{children({ register, errors, control })}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
|
||||
buttonActive: css`
|
||||
background: ${bgActive};
|
||||
border: ${borderActive};
|
||||
border-left: none;
|
||||
border-left: 0;
|
||||
color: ${textColorActive};
|
||||
text-shadow: ${fakeBold};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { RadioButtonSize, RadioButton } from './RadioButton';
|
||||
@ -14,11 +14,11 @@ const getRadioButtonGroupStyles = () => {
|
||||
};
|
||||
};
|
||||
interface RadioButtonGroupProps<T> {
|
||||
value: T;
|
||||
value?: T;
|
||||
disabled?: boolean;
|
||||
disabledOptions?: T[];
|
||||
options: Array<SelectableValue<T>>;
|
||||
onChange: (value?: T) => void;
|
||||
onChange?: (value?: T) => void;
|
||||
size?: RadioButtonSize;
|
||||
}
|
||||
|
||||
@ -30,6 +30,16 @@ export function RadioButtonGroup<T>({
|
||||
disabledOptions,
|
||||
size = 'md',
|
||||
}: RadioButtonGroupProps<T>) {
|
||||
const handleOnClick = useCallback(
|
||||
(option: SelectableValue<T>) => {
|
||||
return () => {
|
||||
if (onChange) {
|
||||
onChange(option.value);
|
||||
}
|
||||
};
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const styles = getRadioButtonGroupStyles();
|
||||
|
||||
return (
|
||||
@ -42,9 +52,7 @@ export function RadioButtonGroup<T>({
|
||||
disabled={isItemDisabled || disabled}
|
||||
active={value === o.value}
|
||||
key={o.label}
|
||||
onClick={() => {
|
||||
onChange(o.value);
|
||||
}}
|
||||
onClick={handleOnClick(o)}
|
||||
>
|
||||
{o.label}
|
||||
</RadioButton>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { Switch } from './Switch';
|
||||
@ -15,17 +15,16 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
export const simple = () => {
|
||||
export const controlled = () => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const onChange = useCallback(e => setChecked(e.currentTarget.checked), [setChecked]);
|
||||
const BEHAVIOUR_GROUP = 'Behaviour props';
|
||||
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
|
||||
return (
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(e, checked) => {
|
||||
setChecked(checked);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <Switch checked={checked} disabled={disabled} onChange={onChange} />;
|
||||
};
|
||||
|
||||
export const uncontrolled = () => {
|
||||
const BEHAVIOUR_GROUP = 'Behaviour props';
|
||||
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
|
||||
return <Switch disabled={disabled} />;
|
||||
};
|
||||
|
@ -1,25 +1,47 @@
|
||||
import React from 'react';
|
||||
import React, { HTMLProps } from 'react';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css, cx } from 'emotion';
|
||||
import { getFocusStyle } from './commonStyles';
|
||||
import { getFocusCss } from './commonStyles';
|
||||
|
||||
export interface SwitchProps {
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (e: React.SyntheticEvent<HTMLButtonElement>, checked: boolean) => void;
|
||||
export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> {
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
slider: cx(
|
||||
css`
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
background: ${theme.colors.formSwitchBg};
|
||||
transition: all 0.30s ease;
|
||||
border-radius: 50px;
|
||||
switch: css`
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus ~ div {
|
||||
${getFocusCss(theme)};
|
||||
}
|
||||
&[disabled] {
|
||||
background: ${theme.colors.formSwitchBgDisabled};
|
||||
}
|
||||
}
|
||||
|
||||
input ~ div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: red;
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: ${theme.colors.formSwitchBg};
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 50px;
|
||||
border: none;
|
||||
display: block;
|
||||
padding: 0;
|
||||
@ -30,6 +52,7 @@ export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
content: '';
|
||||
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: 50%;
|
||||
display: block;
|
||||
width: 12px;
|
||||
@ -38,42 +61,41 @@ export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
border-radius: 6px;
|
||||
transform: translate3d(2px, -50%, 0);
|
||||
}
|
||||
&:focus {
|
||||
/* border: 1px solid ${theme.colors.formSwitchDot}; */
|
||||
}
|
||||
&[disabled] {
|
||||
background: ${theme.colors.formSwitchBgDisabled};
|
||||
}
|
||||
`,
|
||||
getFocusStyle(theme)
|
||||
),
|
||||
sliderActive: css`
|
||||
background: ${theme.colors.formSwitchBgActive};
|
||||
&:hover {
|
||||
background: ${theme.colors.formSwitchBgActiveHover};
|
||||
}
|
||||
&:after {
|
||||
transform: translate3d(16px, -50%, 0);
|
||||
input:checked ~ div {
|
||||
background: ${theme.colors.formSwitchBgActive};
|
||||
&:hover {
|
||||
background: ${theme.colors.formSwitchBgActiveHover};
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: translate3d(16px, -50%, 0);
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
export const Switch: React.FC<SwitchProps> = ({ checked = false, disabled = false, onChange }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSwitchStyles(theme);
|
||||
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||
({ value, checked, disabled = false, onChange, ...inputProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSwitchStyles(theme);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={!!checked}
|
||||
disabled={disabled}
|
||||
className={cx(styles.slider, checked && styles.sliderActive)}
|
||||
onClick={e => {
|
||||
if (onChange) {
|
||||
onChange(e, !!!checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className={cx(styles.switch)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={event => {
|
||||
if (onChange) {
|
||||
onChange(event);
|
||||
}
|
||||
}}
|
||||
{...inputProps}
|
||||
ref={ref}
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -27,6 +27,18 @@ export const sharedInputStyle = (theme: GrafanaTheme, invalid = false) => {
|
||||
border: 1px solid ${borderColor};
|
||||
padding: 0 ${theme.spacing.sm} 0 ${theme.spacing.sm};
|
||||
|
||||
&:-webkit-autofill,
|
||||
&:-webkit-autofill:hover {
|
||||
/* Welcome to 2005. This is a HACK to get rid od Chromes default autofill styling */
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0), inset 0 0 0 100px ${colors.formInputBg}!important;
|
||||
}
|
||||
|
||||
&:-webkit-autofill:focus {
|
||||
/* Welcome to 2005. This is a HACK to get rid od Chromes default autofill styling */
|
||||
box-shadow: 0 0 0 2px ${theme.colors.pageBg}, 0 0 0px 4px ${theme.colors.formFocusOutline},
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0), inset 0 0 0 100px ${colors.formInputBg}!important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: ${borderColor};
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { Select } from './Select/Select';
|
||||
import { Form } from './Form';
|
||||
import { Field } from './Field';
|
||||
import { Button, LinkButton } from './Button';
|
||||
import { Controller as InputControl } from 'react-hook-form';
|
||||
|
||||
const Forms = {
|
||||
getFormStyles,
|
||||
@ -15,6 +16,7 @@ const Forms = {
|
||||
Button,
|
||||
LinkButton,
|
||||
Select,
|
||||
InputControl,
|
||||
};
|
||||
|
||||
export default Forms;
|
||||
|
@ -108,20 +108,6 @@ export default class AdminEditUserCtrl {
|
||||
getBackendSrv().put('/api/admin/users/' + $scope.user_id + '/permissions', payload);
|
||||
};
|
||||
|
||||
$scope.create = () => {
|
||||
if (!$scope.userForm.$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
promiseToDigest($scope)(
|
||||
getBackendSrv()
|
||||
.post('/api/admin/users', $scope.user)
|
||||
.then(() => {
|
||||
$location.path('/admin/users');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
$scope.getUserOrgs = (id: number) => {
|
||||
return getBackendSrv()
|
||||
.get('/api/users/' + id + '/orgs')
|
||||
|
82
public/app/features/admin/UserCreatePage.tsx
Normal file
82
public/app/features/admin/UserCreatePage.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { Forms } from '@grafana/ui';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { StoreState } from '../../types';
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
interface UserCreatePageProps {
|
||||
navModel: NavModel;
|
||||
updateLocation: typeof updateLocation;
|
||||
}
|
||||
interface UserDTO {
|
||||
name: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
login?: string;
|
||||
}
|
||||
|
||||
const createUser = async (user: UserDTO) => getBackendSrv().post('/api/admin/users', user);
|
||||
|
||||
const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel, updateLocation }) => {
|
||||
const onSubmit = useCallback(async (data: UserDTO) => {
|
||||
await createUser(data);
|
||||
updateLocation({ path: '/admin/users' });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h1>Add new user</h1>
|
||||
<Forms.Form onSubmit={onSubmit} validateOn="onBlur">
|
||||
{({ register, errors }) => {
|
||||
return (
|
||||
<>
|
||||
<Forms.Field label="Name" required invalid={!!errors.name} error={!!errors.name && 'Name is required'}>
|
||||
<Forms.Input name="name" size="md" ref={register({ required: true })} />
|
||||
</Forms.Field>
|
||||
|
||||
<Forms.Field label="E-mail">
|
||||
<Forms.Input name="email" size="md" ref={register} />
|
||||
</Forms.Field>
|
||||
|
||||
<Forms.Field label="Username">
|
||||
<Forms.Input name="login" size="md" ref={register} />
|
||||
</Forms.Field>
|
||||
<Forms.Field
|
||||
label="Password"
|
||||
required
|
||||
invalid={!!errors.password}
|
||||
error={!!errors.password && 'Password is required and must contain at least 4 characters'}
|
||||
>
|
||||
<Forms.Input
|
||||
size="md"
|
||||
type="password"
|
||||
name="password"
|
||||
ref={register({
|
||||
validate: value => value.trim() !== '' && value.length >= 4,
|
||||
})}
|
||||
/>
|
||||
</Forms.Field>
|
||||
<Forms.Button type="submit">Create user</Forms.Button>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Forms.Form>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
navModel: getNavModel(state.navIndex, 'global-users'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation,
|
||||
};
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UserCreatePage));
|
@ -1,32 +0,0 @@
|
||||
<page-header model="navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<div class="page-sub-heading">
|
||||
<h3 class="page-sub-heading">Add new user</h3>
|
||||
</div>
|
||||
|
||||
<form name="userForm" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Name</span>
|
||||
<input type="text" required ng-model="user.name" class="gf-form-input max-width-20" >
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Email</span>
|
||||
<input type="email" ng-model="user.email" class="gf-form-input max-width-20" >
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Username</span>
|
||||
<input type="text" ng-model="user.login" class="gf-form-input max-width-20" >
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Password</span>
|
||||
<input type="password" required ng-model="user.password" class="gf-form-input max-width-20" >
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-primary" ng-click="create()">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer />
|
@ -302,13 +302,12 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/admin/users/create', {
|
||||
templateUrl: 'public/app/features/admin/partials/new_user.html',
|
||||
controller: 'AdminEditUserCtrl',
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(import(/* webpackChunkName: "UserCreatePage" */ 'app/features/admin/UserCreatePage')),
|
||||
},
|
||||
})
|
||||
// .when('/admin/users/edit/:id', {
|
||||
// templateUrl: 'public/app/features/admin/partials/edit_user.html',
|
||||
// controller: 'AdminEditUserCtrl',
|
||||
// })
|
||||
.when('/admin/users/edit/:id', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
|
@ -17970,6 +17970,11 @@ react-highlight-words@0.11.0:
|
||||
highlight-words-core "^1.2.0"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-hook-form@4.5.3:
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-4.5.3.tgz#3f9abac7bd78eedf0624d02aa9e1f8487d729e18"
|
||||
integrity sha512-oQB6s3zzXbFwM8xaWEkZJZR+5KD2LwUUYTexQbpdUuFzrfs41Qg0UE3kzfzxG8shvVlzADdkYKLMXqOLWQSS/Q==
|
||||
|
||||
react-hot-loader@4.8.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.8.0.tgz#0b7c7dd9407415e23eb8246fdd28b0b839f54cb6"
|
||||
|
Loading…
Reference in New Issue
Block a user