mirror of https://github.com/grafana/grafana
feat: add a new `<SecretInput>` component (#46073)
parent
590ea19c3f
commit
47d1d83673
@ -0,0 +1,60 @@ |
||||
import React, { useState, ChangeEvent } from 'react'; |
||||
import { Story, Meta } from '@storybook/react'; |
||||
import { SecretInput, Props } from './SecretInput'; |
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; |
||||
|
||||
export default { |
||||
title: 'Forms/SecretInput', |
||||
component: SecretInput, |
||||
decorators: [withCenteredStory], |
||||
parameters: { |
||||
controls: { |
||||
exclude: [ |
||||
'prefix', |
||||
'suffix', |
||||
'addonBefore', |
||||
'addonAfter', |
||||
'type', |
||||
'disabled', |
||||
'invalid', |
||||
'loading', |
||||
'before', |
||||
'after', |
||||
], |
||||
}, |
||||
}, |
||||
args: { |
||||
width: 50, |
||||
placeholder: 'Enter your secret...', |
||||
}, |
||||
argTypes: { |
||||
width: { control: { type: 'range', min: 10, max: 200, step: 10 } }, |
||||
}, |
||||
} as Meta; |
||||
|
||||
const Template: Story<Props> = (args) => { |
||||
const [secret, setSecret] = useState(''); |
||||
|
||||
return ( |
||||
<SecretInput |
||||
width={args.width} |
||||
value={secret} |
||||
isConfigured={args.isConfigured} |
||||
placeholder={args.placeholder} |
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => setSecret(event.target.value.trim())} |
||||
onReset={() => setSecret('')} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export const basic = Template.bind({}); |
||||
|
||||
basic.args = { |
||||
isConfigured: false, |
||||
}; |
||||
|
||||
export const secretIsConfigured = Template.bind({}); |
||||
|
||||
secretIsConfigured.args = { |
||||
isConfigured: true, |
||||
}; |
@ -0,0 +1,65 @@ |
||||
import React from 'react'; |
||||
import { render, screen } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import { SecretInput, RESET_BUTTON_TEXT, CONFIGURED_TEXT } from './SecretInput'; |
||||
|
||||
const PLACEHOLDER_TEXT = 'Your secret...'; |
||||
|
||||
describe('<SecretInput />', () => { |
||||
it('should render an input if the secret is not configured', () => { |
||||
render(<SecretInput isConfigured={false} onChange={() => {}} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />); |
||||
|
||||
const input = screen.getByPlaceholderText(PLACEHOLDER_TEXT); |
||||
|
||||
// Should show an enabled input
|
||||
expect(input).toBeInTheDocument(); |
||||
expect(input).not.toBeDisabled(); |
||||
|
||||
// Should not show a "Reset" button
|
||||
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should render a disabled input with a reset button if the secret is already configured', () => { |
||||
render(<SecretInput isConfigured={true} onChange={() => {}} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />); |
||||
|
||||
const input = screen.getByPlaceholderText(PLACEHOLDER_TEXT); |
||||
|
||||
// Should show a disabled input
|
||||
expect(input).toBeInTheDocument(); |
||||
expect(input).toBeDisabled(); |
||||
expect(input).toHaveValue(CONFIGURED_TEXT); |
||||
|
||||
// Should show a reset button
|
||||
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should be possible to reset a configured secret', () => { |
||||
const onReset = jest.fn(); |
||||
|
||||
render(<SecretInput isConfigured={true} onChange={() => {}} onReset={onReset} placeholder={PLACEHOLDER_TEXT} />); |
||||
|
||||
// Should show a reset button and a disabled input
|
||||
expect(screen.queryByPlaceholderText(PLACEHOLDER_TEXT)).toBeDisabled(); |
||||
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); |
||||
|
||||
// Click on "Reset"
|
||||
userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT })); |
||||
|
||||
expect(onReset).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('should be possible to change the value of the secret', () => { |
||||
const onChange = jest.fn(); |
||||
|
||||
render(<SecretInput isConfigured={false} onChange={onChange} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />); |
||||
|
||||
const input = screen.getByPlaceholderText(PLACEHOLDER_TEXT); |
||||
|
||||
expect(input).toHaveValue(''); |
||||
|
||||
userEvent.type(input, 'Foo'); |
||||
|
||||
expect(onChange).toHaveBeenCalled(); |
||||
expect(input).toHaveValue('Foo'); |
||||
}); |
||||
}); |
@ -0,0 +1,26 @@ |
||||
import * as React from 'react'; |
||||
import { Input } from '../Input/Input'; |
||||
import { HorizontalGroup } from '../Layout/Layout'; |
||||
import { Button } from '../Button'; |
||||
|
||||
export type Props = React.ComponentProps<typeof Input> & { |
||||
/** TRUE if the secret was already configured. (It is needed as often the backend doesn't send back the actual secret, only the information that it was configured) */ |
||||
isConfigured: boolean; |
||||
/** Called when the user clicks on the "Reset" button in order to clear the secret */ |
||||
onReset: () => void; |
||||
}; |
||||
|
||||
export const CONFIGURED_TEXT = 'configured'; |
||||
export const RESET_BUTTON_TEXT = 'Reset'; |
||||
|
||||
export const SecretInput = ({ isConfigured, onReset, ...props }: Props) => ( |
||||
<HorizontalGroup> |
||||
{!isConfigured && <Input {...props} type="password" />} |
||||
{isConfigured && <Input {...props} type="text" disabled={true} value={CONFIGURED_TEXT} />} |
||||
{isConfigured && ( |
||||
<Button onClick={onReset} variant="secondary"> |
||||
{RESET_BUTTON_TEXT} |
||||
</Button> |
||||
)} |
||||
</HorizontalGroup> |
||||
); |
@ -0,0 +1 @@ |
||||
export { SecretInput } from './SecretInput'; |
Loading…
Reference in new issue