mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana/UI: Add SecretTextArea component (#51021)
* Add secret TextArea Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
parent
a0ffb9093c
commit
370d6a6f7b
@ -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,
|
||||
};
|
@ -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');
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { SecretTextArea } from './SecretTextArea';
|
Loading…
Reference in New Issue
Block a user