From 8f0d92ef8523078aa2554d4497592bb5a11029d6 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 19 Mar 2019 11:23:57 +0100 Subject: [PATCH 1/6] Allow angular react bridge to use kebab case attribute names --- public/app/core/services/ng_react.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 6a712b29dab..5292b924925 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -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. From 0d84a3fbe218edf5a336c4412cdc41c813ac0abf Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 19 Mar 2019 13:22:35 +0100 Subject: [PATCH 2/6] Add possibility to pass custom input component to FormField --- .../components/FormField/FormField.test.tsx | 19 +++++++++++++++--- .../src/components/FormField/FormField.tsx | 18 ++++++++++++++--- .../__snapshots__/FormField.test.tsx.snap | 20 ++++++++++++++++++- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/components/FormField/FormField.test.tsx b/packages/grafana-ui/src/components/FormField/FormField.test.tsx index 3c89a347e86..95ccb57b658 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.test.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.test.tsx @@ -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) => { const props: Props = { label: 'Test', labelWidth: 11, @@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => { return shallow(); }; -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: ( + <> + Input + + + ), + }); + + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx index 89e879ccc64..a8adeee35d1 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -1,10 +1,12 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react'; +// import React, { InputHTMLAttributes } from 'react'; import { FormLabel } from '../FormLabel/FormLabel'; export interface Props extends InputHTMLAttributes { label: string; labelWidth?: number; inputWidth?: number; + inputEl?: React.ReactNode; } const defaultProps = { @@ -12,14 +14,24 @@ const defaultProps = { inputWidth: 12, }; -const FormField: FunctionComponent = ({ label, labelWidth, inputWidth, ...inputProps }) => { +/** + * Default form field including label used in Grafana UI. Default input element is simple . You can also pass + * custom inputEl if required in which case inputWidth and inputProps are ignored. + * @param label + * @param labelWidth + * @param inputWidth + * @param inputEl + * @param inputProps + * @constructor + */ +export const FormField: FunctionComponent = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => { return (
{label} - + {inputEl || }
); }; +FormField.displayName = 'FormField'; FormField.defaultProps = defaultProps; -export { FormField }; diff --git a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap index 99eb0803149..049d927b50e 100644 --- a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap +++ b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap @@ -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`] = ` +
+ + Test + + + Input + + +
+`; + +exports[`FormField should render component with default inputEl 1`] = `
From d8167ffb884e7cc7349d992bb17098f6d35d706c Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 19 Mar 2019 13:24:45 +0100 Subject: [PATCH 3/6] Add SecretFormField component --- .../SecretFormFied/SecretFormField.story.tsx | 38 +++++++++++++ .../SecretFormFied/SecretFormField.tsx | 56 +++++++++++++++++++ packages/grafana-ui/src/components/index.ts | 1 + .../src/utils/storybook/UseState.tsx | 2 +- 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx create mode 100644 packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx diff --git a/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx b/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx new file mode 100644 index 00000000000..be0b19b8f68 --- /dev/null +++ b/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx @@ -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 ( + + {(value, setValue) => ( + setValue(e.currentTarget.value)} + onReset={() => { + action('Value was reset')(''); + setValue(''); + }} + /> + )} + + ); +}); diff --git a/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx b/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx new file mode 100644 index 00000000000..04bb38578dd --- /dev/null +++ b/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx @@ -0,0 +1,56 @@ +import { omit } from 'lodash'; +import React, { InputHTMLAttributes, FunctionComponent } from 'react'; +import { FormField } from '..'; + +interface Props extends InputHTMLAttributes { + onReset: () => void; + isConfigured: boolean; + + label?: string; + labelWidth?: number; + inputWidth?: number; +} + +export const SecretFormField: FunctionComponent = ({ + label, + labelWidth, + inputWidth, + onReset, + isConfigured, + ...inputProps +}: Props) => { + return ( + + + + + ) : ( + + ) + } + /> + ); +}; + +SecretFormField.defaultProps = { + inputWidth: 12, +}; +SecretFormField.displayName = 'SecretFormField'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index e20a52f6485..cc6fb0b376b 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/utils/storybook/UseState.tsx b/packages/grafana-ui/src/utils/storybook/UseState.tsx index cc263b9a456..fb9bf06bfb2 100644 --- a/packages/grafana-ui/src/utils/storybook/UseState.tsx +++ b/packages/grafana-ui/src/utils/storybook/UseState.tsx @@ -2,7 +2,7 @@ import React from 'react'; interface StateHolderProps { initialState: T; - children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element; + children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode; } export class UseState extends React.Component, { value: T; initialState: T }> { From 150b97692dadb65cccc290b2e6332c3520f62248 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 19 Mar 2019 13:38:54 +0100 Subject: [PATCH 4/6] Use SecretFormField in MSSql and Postgres datasources --- public/app/core/angular_wrappers.ts | 9 ++++++++- .../plugins/datasource/mssql/config_ctrl.ts | 14 ++++++++++++++ .../datasource/mssql/partials/config.html | 18 +++++++++--------- .../plugins/datasource/postgres/config_ctrl.ts | 13 +++++++++++++ .../datasource/postgres/partials/config.html | 17 +++++++++-------- 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 9105de82d53..60e292cc24b 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -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 }], + ]); } diff --git a/public/app/plugins/datasource/mssql/config_ctrl.ts b/public/app/plugins/datasource/mssql/config_ctrl.ts index c80d657a914..4555e6f67d8 100644 --- a/public/app/plugins/datasource/mssql/config_ctrl.ts +++ b/public/app/plugins/datasource/mssql/config_ctrl.ts @@ -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) => { + event.preventDefault(); + this.current.secureJsonFields.password = false; + this.current.secureJsonData = this.current.secureJsonData || {}; + this.current.secureJsonData.password = ''; + }; + + onPasswordChange = (event: SyntheticEvent) => { + this.current.secureJsonData = this.current.secureJsonData || {}; + this.current.secureJsonData.password = event.currentTarget.value; + }; } diff --git a/public/app/plugins/datasource/mssql/partials/config.html b/public/app/plugins/datasource/mssql/partials/config.html index f3ac7e87064..6428b45dfe4 100644 --- a/public/app/plugins/datasource/mssql/partials/config.html +++ b/public/app/plugins/datasource/mssql/partials/config.html @@ -17,15 +17,15 @@ User
-
- Password - -
-
- Password - - reset -
+
+ +
diff --git a/public/app/plugins/datasource/postgres/config_ctrl.ts b/public/app/plugins/datasource/postgres/config_ctrl.ts index a396b9f9aa4..e547f5697a2 100644 --- a/public/app/plugins/datasource/postgres/config_ctrl.ts +++ b/public/app/plugins/datasource/postgres/config_ctrl.ts @@ -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) => { + event.preventDefault(); + this.current.secureJsonFields.password = false; + this.current.secureJsonData = this.current.secureJsonData || {}; + this.current.secureJsonData.password = ''; + }; + + onPasswordChange = (event: SyntheticEvent) => { + 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 }, diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html index c0e5a30496d..e7680707672 100644 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ b/public/app/plugins/datasource/postgres/partials/config.html @@ -17,16 +17,17 @@ User
-
- Password - -
-
- Password - - reset +
+
+
From aa14d7528c782e0cbd5c19c168deb2698321e30a Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 19 Mar 2019 14:16:01 +0100 Subject: [PATCH 5/6] Updated comments --- .../src/components/FormField/FormField.tsx | 6 ----- .../SecretFormFied/SecretFormField.tsx | 25 +++++++++++++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx index a8adeee35d1..f35f60ef2ba 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -17,12 +17,6 @@ const defaultProps = { /** * Default form field including label used in Grafana UI. Default input element is simple . You can also pass * custom inputEl if required in which case inputWidth and inputProps are ignored. - * @param label - * @param labelWidth - * @param inputWidth - * @param inputEl - * @param inputProps - * @constructor */ export const FormField: FunctionComponent = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => { return ( diff --git a/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx b/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx index 04bb38578dd..234b36c2a43 100644 --- a/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx +++ b/packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx @@ -3,25 +3,42 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react'; import { FormField } from '..'; interface Props extends InputHTMLAttributes { + // 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 = ({ label, labelWidth, inputWidth, onReset, isConfigured, + placeholder, ...inputProps }: Props) => { return ( = ({ ) @@ -50,7 +67,5 @@ export const SecretFormField: FunctionComponent = ({ ); }; -SecretFormField.defaultProps = { - inputWidth: 12, -}; +SecretFormField.defaultProps = defaultProps; SecretFormField.displayName = 'SecretFormField'; From a26dc64ebe0771e560d6244c839740059d7443b4 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Wed, 20 Mar 2019 09:20:30 +0100 Subject: [PATCH 6/6] Remove commented code --- packages/grafana-ui/src/components/FormField/FormField.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx index f35f60ef2ba..310af17c5dd 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -1,5 +1,4 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react'; -// import React, { InputHTMLAttributes } from 'react'; import { FormLabel } from '../FormLabel/FormLabel'; export interface Props extends InputHTMLAttributes {