Merge pull request #16078 from grafana/secret-input-field-component
Secret input field component
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormField, Props } from './FormField';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
label: 'Test',
|
||||
labelWidth: 11,
|
||||
@@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => {
|
||||
return shallow(<FormField {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
describe('FormField', () => {
|
||||
it('should render component with default inputEl', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render component with custom inputEl', () => {
|
||||
const wrapper = setup({
|
||||
inputEl: (
|
||||
<>
|
||||
<span>Input</span>
|
||||
<button>Ok</button>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
labelWidth?: number;
|
||||
inputWidth?: number;
|
||||
inputEl?: React.ReactNode;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@@ -12,14 +13,18 @@ const defaultProps = {
|
||||
inputWidth: 12,
|
||||
};
|
||||
|
||||
const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
|
||||
/**
|
||||
* Default form field including label used in Grafana UI. Default input element is simple <input />. You can also pass
|
||||
* custom inputEl if required in which case inputWidth and inputProps are ignored.
|
||||
*/
|
||||
export const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => {
|
||||
return (
|
||||
<div className="form-field">
|
||||
<FormLabel width={labelWidth}>{label}</FormLabel>
|
||||
<input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
|
||||
{inputEl || <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FormField.displayName = 'FormField';
|
||||
FormField.defaultProps = defaultProps;
|
||||
export { FormField };
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
exports[`FormField should render component with custom inputEl 1`] = `
|
||||
<div
|
||||
className="form-field"
|
||||
>
|
||||
<Component
|
||||
width={11}
|
||||
>
|
||||
Test
|
||||
</Component>
|
||||
<span>
|
||||
Input
|
||||
</span>
|
||||
<button>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FormField should render component with default inputEl 1`] = `
|
||||
<div
|
||||
className="form-field"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
import { SecretFormField } from './SecretFormField';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
|
||||
const SecretFormFieldStories = storiesOf('UI/SecretFormField/SecretFormField', module);
|
||||
|
||||
SecretFormFieldStories.addDecorator(withCenteredStory);
|
||||
const getSecretFormFieldKnobs = () => {
|
||||
return {
|
||||
isConfigured: boolean('Set configured state', false),
|
||||
};
|
||||
};
|
||||
|
||||
SecretFormFieldStories.add('default', () => {
|
||||
const knobs = getSecretFormFieldKnobs();
|
||||
return (
|
||||
<UseState initialState="Input value">
|
||||
{(value, setValue) => (
|
||||
<SecretFormField
|
||||
label={'Secret field'}
|
||||
labelWidth={10}
|
||||
value={value}
|
||||
isConfigured={knobs.isConfigured}
|
||||
onChange={e => setValue(e.currentTarget.value)}
|
||||
onReset={() => {
|
||||
action('Value was reset')('');
|
||||
setValue('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { omit } from 'lodash';
|
||||
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
|
||||
import { FormField } from '..';
|
||||
|
||||
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
// Function to use when reset is clicked. Means you have to reset the input value yourself as this is uncontrolled
|
||||
// component (or do something else if required).
|
||||
onReset: () => void;
|
||||
isConfigured: boolean;
|
||||
|
||||
label?: string;
|
||||
labelWidth?: number;
|
||||
inputWidth?: number;
|
||||
// Placeholder of the input field when in non configured state.
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
inputWidth: 12,
|
||||
placeholder: 'Password',
|
||||
label: 'Password',
|
||||
};
|
||||
|
||||
/**
|
||||
* Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
|
||||
* a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
|
||||
* form field. This is used for passwords or anything that is encrypted on the server and is later returned encrypted
|
||||
* to the user (like datasource passwords).
|
||||
*/
|
||||
export const SecretFormField: FunctionComponent<Props> = ({
|
||||
label,
|
||||
labelWidth,
|
||||
inputWidth,
|
||||
onReset,
|
||||
isConfigured,
|
||||
placeholder,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
return (
|
||||
<FormField
|
||||
label={label!}
|
||||
labelWidth={labelWidth}
|
||||
inputEl={
|
||||
isConfigured ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className={`gf-form-input width-${inputWidth! - 2}`}
|
||||
disabled={true}
|
||||
value="configured"
|
||||
{...omit(inputProps, 'value')}
|
||||
/>
|
||||
<button className="btn btn-secondary gf-form-btn" onClick={onReset}>
|
||||
reset
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type="password"
|
||||
className={`gf-form-input width-${inputWidth}`}
|
||||
placeholder={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SecretFormField.defaultProps = defaultProps;
|
||||
SecretFormField.displayName = 'SecretFormField';
|
||||
@@ -14,6 +14,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||
// Forms
|
||||
export { FormLabel } from './FormLabel/FormLabel';
|
||||
export { FormField } from './FormField/FormField';
|
||||
export { SecretFormField } from './SecretFormFied/SecretFormField';
|
||||
|
||||
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
interface StateHolderProps<T> {
|
||||
initialState: T;
|
||||
children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element;
|
||||
children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode;
|
||||
}
|
||||
|
||||
export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||
import { MetricSelect } from './components/Select/MetricSelect';
|
||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
|
||||
import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField } from '@grafana/ui';
|
||||
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
@@ -59,4 +59,11 @@ export function registerAngularDirectives() {
|
||||
['datasource', { watchDepth: 'reference' }],
|
||||
['templateSrv', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('secretFormField', SecretFormField, [
|
||||
'value',
|
||||
'isConfigured',
|
||||
'inputWidth',
|
||||
['onReset', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// - reactComponent (generic directive for delegating off to React Components)
|
||||
// - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
|
||||
|
||||
import { kebabCase } from 'lodash';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import angular from 'angular';
|
||||
@@ -155,11 +156,17 @@ function getPropExpression(prop) {
|
||||
return Array.isArray(prop) ? prop[0] : prop;
|
||||
}
|
||||
|
||||
// find the normalized attribute knowing that React props accept any type of capitalization
|
||||
function findAttribute(attrs, propName) {
|
||||
const index = Object.keys(attrs).filter(attr => {
|
||||
return attr.toLowerCase() === propName.toLowerCase();
|
||||
})[0];
|
||||
/**
|
||||
* Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
|
||||
* kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
|
||||
* evaluated by the browser as such.
|
||||
* @param attrs All attributes of the component.
|
||||
* @param propName Name of the prop that react component expects.
|
||||
*/
|
||||
function findAttribute(attrs: string, propName: string): string {
|
||||
const index = Object.keys(attrs).find(attr => {
|
||||
return attr.toLowerCase() === propName.toLowerCase() || attr.toLowerCase() === kebabCase(propName);
|
||||
});
|
||||
return attrs[index];
|
||||
}
|
||||
|
||||
@@ -274,7 +281,9 @@ const reactDirective = $injector => {
|
||||
// watch each property name and trigger an update whenever something changes,
|
||||
// to update scope.props with new values
|
||||
const propExpressions = props.map(prop => {
|
||||
return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop];
|
||||
return Array.isArray(prop)
|
||||
? [findAttribute(attrs, prop[0]), getPropConfig(prop)]
|
||||
: findAttribute(attrs, prop);
|
||||
});
|
||||
|
||||
// If we don't have any props, then our watch statement won't fire.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SyntheticEvent } from 'react';
|
||||
|
||||
export class MssqlConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
|
||||
@@ -7,4 +9,16 @@ export class MssqlConfigCtrl {
|
||||
constructor($scope) {
|
||||
this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
|
||||
}
|
||||
|
||||
onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
this.current.secureJsonFields.password = false;
|
||||
this.current.secureJsonData = this.current.secureJsonData || {};
|
||||
this.current.secureJsonData.password = '';
|
||||
};
|
||||
|
||||
onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
this.current.secureJsonData = this.current.secureJsonData || {};
|
||||
this.current.secureJsonData.password = event.currentTarget.value;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
<span class="gf-form-label width-7">User</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
|
||||
<span class="gf-form-label width-7">Password</span>
|
||||
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
|
||||
<span class="gf-form-label width-7">Password</span>
|
||||
<input type="text" class="gf-form-input" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<secret-form-field
|
||||
isConfigured="ctrl.current.secureJsonFields.password"
|
||||
value="ctrl.current.secureJsonData.password"
|
||||
on-reset="ctrl.onPasswordReset"
|
||||
on-change="ctrl.onPasswordChange"
|
||||
inputWidth="9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { SyntheticEvent } from 'react';
|
||||
|
||||
export class PostgresConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
@@ -52,6 +53,18 @@ export class PostgresConfigCtrl {
|
||||
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
|
||||
}
|
||||
|
||||
onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
this.current.secureJsonFields.password = false;
|
||||
this.current.secureJsonData = this.current.secureJsonData || {};
|
||||
this.current.secureJsonData.password = '';
|
||||
};
|
||||
|
||||
onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
this.current.secureJsonData = this.current.secureJsonData || {};
|
||||
this.current.secureJsonData.password = event.currentTarget.value;
|
||||
};
|
||||
|
||||
// the value portion is derived from postgres server_version_num/100
|
||||
postgresVersions = [
|
||||
{ name: '9.3', value: 903 },
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
<span class="gf-form-label width-7">User</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
|
||||
<span class="gf-form-label width-7">Password</span>
|
||||
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
|
||||
<span class="gf-form-label width-7">Password</span>
|
||||
<input type="text" class="gf-form-input" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
|
||||
<div class="gf-form">
|
||||
<secret-form-field
|
||||
isConfigured="ctrl.current.secureJsonFields.password"
|
||||
value="ctrl.current.secureJsonData.password"
|
||||
on-reset="ctrl.onPasswordReset"
|
||||
on-change="ctrl.onPasswordChange"
|
||||
inputWidth="9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">SSL Mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
|
||||
|
||||
Reference in New Issue
Block a user