Grafana/UI: Add SecretTextArea component (#51021)

* Add secret TextArea

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Oscar Kilhed 2022-06-21 11:26:23 +02:00 committed by GitHub
parent a0ffb9093c
commit 370d6a6f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 0 deletions

View File

@ -0,0 +1,65 @@
import { Story, Meta } from '@storybook/react';
import React, { useState, ChangeEvent } from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { SecretTextArea, Props } from './SecretTextArea';
export default {
title: 'Forms/SecretTextArea',
component: SecretTextArea,
decorators: [withCenteredStory],
parameters: {
controls: {
exclude: [
'prefix',
'suffix',
'addonBefore',
'addonAfter',
'type',
'disabled',
'invalid',
'loading',
'before',
'after',
],
},
},
args: {
rows: 3,
cols: 30,
placeholder: 'Enter your secret...',
},
argTypes: {
rows: { control: { type: 'range', min: 1, max: 50, step: 1 } },
cols: { control: { type: 'range', min: 1, max: 200, step: 10 } },
},
} as Meta;
const Template: Story<Props> = (args) => {
const [secret, setSecret] = useState('');
return (
<SecretTextArea
rows={args.rows}
cols={args.cols}
value={secret}
isConfigured={args.isConfigured}
placeholder={args.placeholder}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => 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,
};

View File

@ -0,0 +1,72 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { SecretTextArea, RESET_BUTTON_TEXT, CONFIGURED_TEXT } from './SecretTextArea';
const PLACEHOLDER_TEXT = 'Your secret...';
describe('<SecretTextArea />', () => {
it('should render an input if the secret is not configured', () => {
render(
<SecretTextArea 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 textarea with a reset button if the secret is already configured', () => {
render(
<SecretTextArea isConfigured={true} onChange={() => {}} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />
);
const textArea = screen.getByPlaceholderText(PLACEHOLDER_TEXT);
// Should show a disabled input
expect(textArea).toBeInTheDocument();
expect(textArea).toBeDisabled();
expect(textArea).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', async () => {
const onReset = jest.fn();
render(<SecretTextArea 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"
await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT }));
expect(onReset).toHaveBeenCalledTimes(1);
});
it('should be possible to change the value of the secret', async () => {
const onChange = jest.fn();
render(
<SecretTextArea isConfigured={false} onChange={onChange} onReset={() => {}} placeholder={PLACEHOLDER_TEXT} />
);
const textArea = screen.getByPlaceholderText(PLACEHOLDER_TEXT);
expect(textArea).toHaveValue('');
await userEvent.type(textArea, 'Foo');
expect(onChange).toHaveBeenCalled();
expect(textArea).toHaveValue('Foo');
});
});

View File

@ -0,0 +1,50 @@
import { css, cx } from '@emotion/css';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext';
import { Button } from '../Button';
import { HorizontalGroup } from '../Layout/Layout';
import { TextArea } from '../TextArea/TextArea';
export type Props = React.ComponentProps<typeof TextArea> & {
/** 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';
const getStyles = (theme: GrafanaTheme2) => {
return {
configuredStyle: css`
min-height: ${theme.spacing(theme.components.height.md)};
padding-top: ${theme.spacing(0.5) /** Needed to mimic vertically centered text in an input box */};
resize: none;
`,
};
};
/**
* Text area that does not disclose an already configured value but lets the user reset the current value and enter a new one.
* Typically useful for asymmetric cryptography keys.
*/
export const SecretTextArea = ({ isConfigured, onReset, ...props }: Props) => {
const styles = useStyles2(getStyles);
return (
<HorizontalGroup>
{!isConfigured && <TextArea {...props} />}
{isConfigured && (
<TextArea {...props} rows={1} disabled={true} value={CONFIGURED_TEXT} className={cx(styles.configuredStyle)} />
)}
{isConfigured && (
<Button onClick={onReset} variant="secondary">
{RESET_BUTTON_TEXT}
</Button>
)}
</HorizontalGroup>
);
};

View File

@ -0,0 +1 @@
export { SecretTextArea } from './SecretTextArea';