Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
0091b86e
Unverified
Commit
0091b86e
authored
Mar 20, 2019
by
Torkel Ödegaard
Committed by
GitHub
Mar 20, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16078 from grafana/secret-input-field-component
Secret input field component
parents
abd89484
a26dc64e
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
221 additions
and
31 deletions
+221
-31
packages/grafana-ui/src/components/FormField/FormField.test.tsx
+16
-3
packages/grafana-ui/src/components/FormField/FormField.tsx
+8
-3
packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
+19
-1
packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx
+38
-0
packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx
+71
-0
packages/grafana-ui/src/components/index.ts
+1
-0
packages/grafana-ui/src/utils/storybook/UseState.tsx
+1
-1
public/app/core/angular_wrappers.ts
+8
-1
public/app/core/services/ng_react.ts
+15
-6
public/app/plugins/datasource/mssql/config_ctrl.ts
+14
-0
public/app/plugins/datasource/mssql/partials/config.html
+8
-8
public/app/plugins/datasource/postgres/config_ctrl.ts
+13
-0
public/app/plugins/datasource/postgres/partials/config.html
+9
-8
No files found.
packages/grafana-ui/src/components/FormField/FormField.test.tsx
View file @
0091b86e
...
...
@@ -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
();
});
});
packages/grafana-ui/src/components/FormField/FormField.tsx
View file @
0091b86e
...
...
@@ -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
};
packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
View file @
0091b86e
// 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"
>
...
...
packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx
0 → 100644
View file @
0091b86e
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
>
);
});
packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx
0 → 100644
View file @
0091b86e
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'
;
packages/grafana-ui/src/components/index.ts
View file @
0091b86e
...
...
@@ -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'
;
...
...
packages/grafana-ui/src/utils/storybook/UseState.tsx
View file @
0091b86e
...
...
@@ -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
}
>
{
...
...
public/app/core/angular_wrappers.ts
View file @
0091b86e
...
...
@@ -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
}],
]);
}
public/app/core/services/ng_react.ts
View file @
0091b86e
...
...
@@ -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.
...
...
public/app/plugins/datasource/mssql/config_ctrl.ts
View file @
0091b86e
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
;
};
}
public/app/plugins/datasource/mssql/partials/config.html
View file @
0091b86e
...
...
@@ -17,14 +17,14 @@
<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>
...
...
public/app/plugins/datasource/postgres/config_ctrl.ts
View file @
0091b86e
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
},
...
...
public/app/plugins/datasource/postgres/partials/config.html
View file @
0091b86e
...
...
@@ -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"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment