mirror of https://github.com/grafana/grafana
ReactMigration: Migrate DataSource HTTP Settings to React (#19452)
* Basic components for HTTP settings migration WIP * Add secureJsonFields to DataSourceSettings * Introduce datasource-http-settings-next directive for backward compatibility * fix lint * renames * rename fix * TagsInput component * move tags from app to grafana/ui * implement tagsinput on datasourcesettings * capitalize * new file for react directive for testing * some layout touch ups * FormField story * Minor touch ups * add url validation * using prevent default to prevent updating datasource when adding tag * using Stylefactory and fix tslint issue on MouseEvent * only show tlsauthsettings if tls or ca cert * fix url input length * fix for showAccessOptions * Implemented CertTextArea, removed commented code * removed commented / not used code * Rename and add more elements to Certification component * fixing newSecureJsonData * spelling * Fix issue with checkboxes being undefined * Removed old partials and minor fix * removed unused props from storypull/19756/head^2
parent
cb0e80e7b9
commit
c9b11bfc7a
@ -0,0 +1,61 @@ |
||||
import React from 'react'; |
||||
import { HttpSettingsProps } from './types'; |
||||
import { FormField } from '../FormField/FormField'; |
||||
import { SecretFormField } from '../SecretFormFied/SecretFormField'; |
||||
|
||||
export const BasicAuthSettings: React.FC<HttpSettingsProps> = ({ dataSourceConfig, onChange }) => { |
||||
const password = dataSourceConfig.secureJsonData ? dataSourceConfig.secureJsonData.basicAuthPassword : ''; |
||||
|
||||
const onPasswordReset = () => { |
||||
onChange({ |
||||
...dataSourceConfig, |
||||
basicAuthPassword: '', |
||||
secureJsonData: { |
||||
...dataSourceConfig.secureJsonData, |
||||
basicAuthPassword: '', |
||||
}, |
||||
secureJsonFields: { |
||||
...dataSourceConfig.secureJsonFields, |
||||
basicAuthPassword: false, |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
const onPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) => { |
||||
onChange({ |
||||
...dataSourceConfig, |
||||
secureJsonData: { |
||||
...dataSourceConfig.secureJsonData, |
||||
basicAuthPassword: event.currentTarget.value, |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<div className="gf-form"> |
||||
<FormField |
||||
label="User" |
||||
labelWidth={10} |
||||
inputWidth={18} |
||||
placeholder="user" |
||||
value={dataSourceConfig.basicAuthUser} |
||||
onChange={event => onChange({ ...dataSourceConfig, basicAuthUser: event.currentTarget.value })} |
||||
/> |
||||
</div> |
||||
<div className="gf-form"> |
||||
<SecretFormField |
||||
isConfigured={ |
||||
!!dataSourceConfig.basicAuthPassword || |
||||
!!(dataSourceConfig.secureJsonFields && dataSourceConfig.secureJsonFields.basicAuthPassword) |
||||
} |
||||
value={password || ''} |
||||
inputWidth={18} |
||||
labelWidth={10} |
||||
onReset={onPasswordReset} |
||||
onChange={onPasswordChange} |
||||
/> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
||||
@ -0,0 +1,40 @@ |
||||
import React, { ChangeEvent, MouseEvent, FC } from 'react'; |
||||
|
||||
interface Props { |
||||
label: string; |
||||
hasCert: boolean; |
||||
placeholder: string; |
||||
|
||||
onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void; |
||||
onClick: (event: MouseEvent<HTMLAnchorElement>) => void; |
||||
} |
||||
|
||||
export const CertificationKey: FC<Props> = ({ hasCert, label, onChange, onClick, placeholder }) => { |
||||
return ( |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form gf-form--v-stretch"> |
||||
<label className="gf-form-label width-7">{label}</label> |
||||
</div> |
||||
{!hasCert && ( |
||||
<div className="gf-form gf-form--grow"> |
||||
<textarea |
||||
rows={7} |
||||
className="gf-form-input gf-form-textarea" |
||||
onChange={onChange} |
||||
placeholder={placeholder} |
||||
required |
||||
/> |
||||
</div> |
||||
)} |
||||
|
||||
{hasCert && ( |
||||
<div className="gf-form"> |
||||
<input type="text" className="gf-form-input max-width-12" disabled value="configured" /> |
||||
<a className="btn btn-secondary gf-form-btn" onClick={onClick}> |
||||
reset |
||||
</a> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,51 @@ |
||||
import React from 'react'; |
||||
import { storiesOf } from '@storybook/react'; |
||||
import { DataSourceHttpSettings } from './DataSourceHttpSettings'; |
||||
import { DataSourceSettings } from '../../types'; |
||||
import { UseState } from '../../utils/storybook/UseState'; |
||||
|
||||
const settingsMock: DataSourceSettings<any, any> = { |
||||
id: 4, |
||||
orgId: 1, |
||||
name: 'gdev-influxdb', |
||||
type: 'influxdb', |
||||
typeLogoUrl: '', |
||||
access: 'direct', |
||||
url: 'http://localhost:8086', |
||||
password: '', |
||||
user: 'grafana', |
||||
database: 'site', |
||||
basicAuth: false, |
||||
basicAuthUser: '', |
||||
basicAuthPassword: '', |
||||
withCredentials: false, |
||||
isDefault: false, |
||||
jsonData: { |
||||
timeInterval: '15s', |
||||
httpMode: 'GET', |
||||
keepCookies: ['cookie1', 'cookie2'], |
||||
}, |
||||
secureJsonData: { |
||||
password: true, |
||||
}, |
||||
readOnly: true, |
||||
}; |
||||
|
||||
const DataSourceHttpSettingsStories = storiesOf('UI/DataSource/DataSourceHttpSettings', module); |
||||
|
||||
DataSourceHttpSettingsStories.add('default', () => { |
||||
return ( |
||||
<UseState initialState={settingsMock} logState> |
||||
{(dataSourceSettings, updateDataSourceSettings) => { |
||||
return ( |
||||
<DataSourceHttpSettings |
||||
defaultUrl="http://localhost:9999" |
||||
dataSourceConfig={dataSourceSettings} |
||||
onChange={updateDataSourceSettings} |
||||
showAccessOptions={true} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
); |
||||
}); |
||||
@ -0,0 +1,208 @@ |
||||
import React, { useState, useCallback } from 'react'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { css, cx } from 'emotion'; |
||||
import { FormField, FormLabel, Input, Select, Switch, TagsInput } from '..'; |
||||
import { useTheme } from '../../themes'; |
||||
import { BasicAuthSettings } from './BasicAuthSettings'; |
||||
import { HttpProxySettings } from './HttpProxySettings'; |
||||
import { TLSAuthSettings } from './TLSAuthSettings'; |
||||
import { DataSourceSettings } from '../../types'; |
||||
import { HttpSettingsProps } from './types'; |
||||
|
||||
const ACCESS_OPTIONS: Array<SelectableValue<string>> = [ |
||||
{ |
||||
label: 'Server (default)', |
||||
value: 'proxy', |
||||
}, |
||||
{ |
||||
label: 'Browser', |
||||
value: 'direct', |
||||
}, |
||||
]; |
||||
|
||||
const DEFAULT_ACCESS_OPTION = { |
||||
label: 'Server (default)', |
||||
value: 'proxy', |
||||
}; |
||||
|
||||
const HttpAccessHelp = () => ( |
||||
<div className="grafana-info-box m-t-2"> |
||||
<p> |
||||
Access mode controls how requests to the data source will be handled. |
||||
<strong> |
||||
<i>Server</i> |
||||
</strong>{' '} |
||||
should be the preferred way if nothing else stated. |
||||
</p> |
||||
<div className="alert-title">Server access mode (Default):</div> |
||||
<p> |
||||
All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to |
||||
the data source and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. The URL needs |
||||
to be accessible from the grafana backend/server if you select this access mode. |
||||
</p> |
||||
<div className="alert-title">Browser access mode:</div> |
||||
<p> |
||||
All requests will be made from the browser directly to the data source and may be subject to Cross-Origin Resource |
||||
Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access mode. |
||||
</p> |
||||
</div> |
||||
); |
||||
|
||||
export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = props => { |
||||
const { defaultUrl, dataSourceConfig, onChange, showAccessOptions } = props; |
||||
let urlTooltip; |
||||
const [isAccessHelpVisible, setIsAccessHelpVisible] = useState(false); |
||||
const theme = useTheme(); |
||||
|
||||
const onSettingsChange = useCallback( |
||||
(change: Partial<DataSourceSettings<any, any>>) => { |
||||
onChange({ |
||||
...dataSourceConfig, |
||||
...change, |
||||
}); |
||||
}, |
||||
[dataSourceConfig] |
||||
); |
||||
|
||||
switch (dataSourceConfig.access) { |
||||
case 'direct': |
||||
urlTooltip = ( |
||||
<> |
||||
Your access method is <em>Browser</em>, this means the URL needs to be accessible from the browser. |
||||
</> |
||||
); |
||||
break; |
||||
case 'proxy': |
||||
urlTooltip = ( |
||||
<> |
||||
Your access method is <em>Server</em>, this means the URL needs to be accessible from the grafana |
||||
backend/server. |
||||
</> |
||||
); |
||||
break; |
||||
default: |
||||
urlTooltip = 'Specify a complete HTTP URL (for example http://your_server:8080)'; |
||||
} |
||||
|
||||
const accessSelect = ( |
||||
<Select |
||||
width={20} |
||||
options={ACCESS_OPTIONS} |
||||
value={ACCESS_OPTIONS.filter(o => o.value === dataSourceConfig.access)[0] || DEFAULT_ACCESS_OPTION} |
||||
onChange={selectedValue => onSettingsChange({ access: selectedValue.value })} |
||||
/> |
||||
); |
||||
|
||||
const isValidUrl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/.test( |
||||
dataSourceConfig.url |
||||
); |
||||
|
||||
const notValidStyle = css` |
||||
box-shadow: inset 0 0px 5px ${theme.colors.red}; |
||||
`;
|
||||
|
||||
const inputStyle = cx({ [`width-20`]: true, [notValidStyle]: !isValidUrl }); |
||||
|
||||
const urlInput = ( |
||||
<Input |
||||
className={inputStyle} |
||||
placeholder={defaultUrl} |
||||
value={dataSourceConfig.url} |
||||
onChange={event => onSettingsChange({ url: event.currentTarget.value })} |
||||
/> |
||||
); |
||||
|
||||
return ( |
||||
<div className="gf-form-group"> |
||||
<> |
||||
<h3 className="page-heading">HTTP</h3> |
||||
<div className="gf-form-group"> |
||||
<div className="gf-form"> |
||||
<FormField label="URL" labelWidth={11} tooltip={urlTooltip} inputEl={urlInput} /> |
||||
</div> |
||||
|
||||
{showAccessOptions && ( |
||||
<> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<FormField label="Access" labelWidth={11} inputWidth={20} inputEl={accessSelect} /> |
||||
</div> |
||||
<div className="gf-form"> |
||||
<label |
||||
className="gf-form-label query-keyword pointer" |
||||
onClick={() => setIsAccessHelpVisible(isVisible => !isVisible)} |
||||
> |
||||
Help |
||||
<i className={`fa fa-caret-${isAccessHelpVisible ? 'down' : 'right'}`} /> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
{isAccessHelpVisible && <HttpAccessHelp />} |
||||
</> |
||||
)} |
||||
{dataSourceConfig.access === 'proxy' && ( |
||||
<div className="gf-form"> |
||||
<FormLabel |
||||
width={11} |
||||
tooltip="Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source." |
||||
> |
||||
Whitelisted Cookies |
||||
</FormLabel> |
||||
<TagsInput |
||||
tags={dataSourceConfig.jsonData.keepCookies} |
||||
onChange={cookies => |
||||
onSettingsChange({ jsonData: { ...dataSourceConfig.jsonData, keepCookies: cookies } }) |
||||
} |
||||
width={20} |
||||
/> |
||||
</div> |
||||
)} |
||||
</div> |
||||
</> |
||||
|
||||
<> |
||||
<h3 className="page-heading">Auth</h3> |
||||
<div className="gf-form-group"> |
||||
<div className="gf-form-inline"> |
||||
<Switch |
||||
label="Basic auth" |
||||
labelClass="width-13" |
||||
checked={dataSourceConfig.basicAuth} |
||||
onChange={event => { |
||||
onSettingsChange({ basicAuth: event!.currentTarget.checked }); |
||||
}} |
||||
/> |
||||
<Switch |
||||
label="With Credentials" |
||||
labelClass="width-13" |
||||
checked={dataSourceConfig.withCredentials} |
||||
onChange={event => { |
||||
onSettingsChange({ withCredentials: event!.currentTarget.checked }); |
||||
}} |
||||
tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." |
||||
/> |
||||
</div> |
||||
|
||||
{dataSourceConfig.access === 'proxy' && ( |
||||
<HttpProxySettings |
||||
dataSourceConfig={dataSourceConfig} |
||||
onChange={jsonData => onSettingsChange({ jsonData })} |
||||
/> |
||||
)} |
||||
</div> |
||||
{dataSourceConfig.basicAuth && ( |
||||
<> |
||||
<h6>Basic Auth Details</h6> |
||||
<div className="gf-form-group"> |
||||
<BasicAuthSettings {...props} /> |
||||
</div> |
||||
</> |
||||
)} |
||||
|
||||
{(dataSourceConfig.jsonData.tlsAuth || dataSourceConfig.jsonData.tlsAuthWithCACert) && ( |
||||
<TLSAuthSettings dataSourceConfig={dataSourceConfig} onChange={onChange} /> |
||||
)} |
||||
</> |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,45 @@ |
||||
import React from 'react'; |
||||
import { HttpSettingsBaseProps } from './types'; |
||||
import { Switch } from '../Switch/Switch'; |
||||
|
||||
export const HttpProxySettings: React.FC<HttpSettingsBaseProps> = ({ dataSourceConfig, onChange }) => { |
||||
return ( |
||||
<> |
||||
<div className="gf-form-inline"> |
||||
<Switch |
||||
label="TLS Client Auth" |
||||
labelClass="width-13" |
||||
checked={dataSourceConfig.jsonData.tlsAuth || false} |
||||
onChange={event => onChange({ ...dataSourceConfig.jsonData, tlsAuth: event!.currentTarget.checked })} |
||||
/> |
||||
|
||||
<Switch |
||||
label="With CA Cert" |
||||
labelClass="width-13" |
||||
checked={dataSourceConfig.jsonData.tlsAuthWithCACert || false} |
||||
onChange={event => |
||||
onChange({ ...dataSourceConfig.jsonData, tlsAuthWithCACert: event!.currentTarget.checked }) |
||||
} |
||||
tooltip="Needed for verifying self-signed TLS Certs" |
||||
/> |
||||
</div> |
||||
<div className="gf-form-inline"> |
||||
<Switch |
||||
label="Skip TLS Verify" |
||||
labelClass="width-13" |
||||
checked={dataSourceConfig.jsonData.tlsSkipVerify || false} |
||||
onChange={event => onChange({ ...dataSourceConfig.jsonData, tlsSkipVerify: event!.currentTarget.checked })} |
||||
/> |
||||
</div> |
||||
<div className="gf-form-inline"> |
||||
<Switch |
||||
label="Forward OAuth Identity" |
||||
labelClass="width-13" |
||||
checked={dataSourceConfig.jsonData.oauthPassThru || false} |
||||
onChange={event => onChange({ ...dataSourceConfig.jsonData, oauthPassThru: event!.currentTarget.checked })} |
||||
tooltip="Forward the user's upstream OAuth identity to the data source (Their access token gets passed along)." |
||||
/> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
||||
@ -0,0 +1,87 @@ |
||||
import React from 'react'; |
||||
import { KeyValue } from '@grafana/data'; |
||||
import { css, cx } from 'emotion'; |
||||
import { Tooltip } from '..'; |
||||
import { CertificationKey } from './CertificationKey'; |
||||
import { HttpSettingsBaseProps } from './types'; |
||||
|
||||
export const TLSAuthSettings: React.FC<HttpSettingsBaseProps> = ({ dataSourceConfig, onChange }) => { |
||||
const hasTLSCACert = dataSourceConfig.secureJsonFields && dataSourceConfig.secureJsonFields.tlsCACert; |
||||
const hasTLSClientCert = dataSourceConfig.secureJsonFields && dataSourceConfig.secureJsonFields.tlsClientCert; |
||||
const hasTLSClientKey = dataSourceConfig.secureJsonFields && dataSourceConfig.secureJsonFields.tlsClientKey; |
||||
|
||||
const onResetClickFactory = (field: string) => (event: React.MouseEvent<HTMLAnchorElement>) => { |
||||
event.preventDefault(); |
||||
const newSecureJsonFields: KeyValue<boolean> = { ...dataSourceConfig.secureJsonFields }; |
||||
newSecureJsonFields[field] = false; |
||||
onChange({ |
||||
...dataSourceConfig, |
||||
secureJsonFields: newSecureJsonFields, |
||||
}); |
||||
}; |
||||
|
||||
const onCertificateChangeFactory = (field: string) => (event: React.SyntheticEvent<HTMLTextAreaElement>) => { |
||||
const newSecureJsonData = { ...dataSourceConfig.secureJsonData }; |
||||
newSecureJsonData[field] = event.currentTarget.value; |
||||
|
||||
onChange({ |
||||
...dataSourceConfig, |
||||
secureJsonData: newSecureJsonData, |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<div className="gf-form-group"> |
||||
<div |
||||
className={cx( |
||||
'gf-form', |
||||
css` |
||||
align-items: baseline; |
||||
` |
||||
)} |
||||
> |
||||
<h6>TLS Auth Details</h6> |
||||
<Tooltip |
||||
placement="right-end" |
||||
content="TLS Certs are encrypted and stored in the Grafana database." |
||||
theme="info" |
||||
> |
||||
<div className="gf-form-help-icon gf-form-help-icon--right-normal"> |
||||
<i className="fa fa-info-circle" /> |
||||
</div> |
||||
</Tooltip> |
||||
</div> |
||||
<div> |
||||
{dataSourceConfig.jsonData.tlsAuthWithCACert && ( |
||||
<CertificationKey |
||||
hasCert={!!hasTLSCACert} |
||||
onChange={onCertificateChangeFactory('tlsCACert')} |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
label="CA Cert" |
||||
onClick={onResetClickFactory('tlsCACert')} |
||||
/> |
||||
)} |
||||
|
||||
{dataSourceConfig.jsonData.tlsAuth && ( |
||||
<> |
||||
<CertificationKey |
||||
hasCert={!!hasTLSClientCert} |
||||
label="Client Cert" |
||||
onChange={onCertificateChangeFactory('tlsClientCert')} |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
onClick={onResetClickFactory('tlsClientCert')} |
||||
/> |
||||
|
||||
<CertificationKey |
||||
hasCert={!!hasTLSClientKey} |
||||
label="Client Key" |
||||
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" |
||||
onChange={onCertificateChangeFactory('tlsClientKey')} |
||||
onClick={onResetClickFactory('tlsClientKey')} |
||||
/> |
||||
</> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,11 @@ |
||||
import { DataSourceSettings } from '../../types'; |
||||
|
||||
export interface HttpSettingsBaseProps { |
||||
dataSourceConfig: DataSourceSettings<any, any>; |
||||
onChange: (config: DataSourceSettings) => void; |
||||
} |
||||
|
||||
export interface HttpSettingsProps extends HttpSettingsBaseProps { |
||||
defaultUrl: string; |
||||
showAccessOptions?: boolean; |
||||
} |
||||
@ -0,0 +1,29 @@ |
||||
import React from 'react'; |
||||
import { storiesOf } from '@storybook/react'; |
||||
import { number, text } from '@storybook/addon-knobs'; |
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; |
||||
import { FormField } from './FormField'; |
||||
|
||||
const getKnobs = () => { |
||||
return { |
||||
label: text('label', 'Test'), |
||||
tooltip: text('tooltip', 'This is a tooltip with information about this FormField'), |
||||
labelWidth: number('labelWidth', 10), |
||||
inputWidth: number('inputWidth', 20), |
||||
}; |
||||
}; |
||||
|
||||
const FormFieldStories = storiesOf('UI/FormField', module); |
||||
|
||||
FormFieldStories.addDecorator(withCenteredStory); |
||||
|
||||
FormFieldStories.add('default', () => { |
||||
const { inputWidth, label, labelWidth } = getKnobs(); |
||||
return <FormField label={label} labelWidth={labelWidth} inputWidth={inputWidth} />; |
||||
}); |
||||
|
||||
FormFieldStories.add('with tooltip', () => { |
||||
const { inputWidth, label, labelWidth, tooltip } = getKnobs(); |
||||
return <FormField label={label} labelWidth={labelWidth} inputWidth={inputWidth} tooltip={tooltip} />; |
||||
}); |
||||
@ -0,0 +1,48 @@ |
||||
import React, { FC } from 'react'; |
||||
import { css, cx } from 'emotion'; |
||||
import { getTagColorsFromName } from '../../utils'; |
||||
import { stylesFactory } from '../../themes'; |
||||
|
||||
interface Props { |
||||
name: string; |
||||
|
||||
onRemove: (tag: string) => void; |
||||
} |
||||
|
||||
export const TagItem: FC<Props> = ({ name, onRemove }) => { |
||||
const { color, borderColor } = getTagColorsFromName(name); |
||||
|
||||
const getStyles = stylesFactory(() => ({ |
||||
itemStyle: css` |
||||
background-color: ${color}; |
||||
border: 1px solid ${borderColor}; |
||||
border-radius: 3px; |
||||
padding: 3px 6px; |
||||
margin: 3px; |
||||
white-space: nowrap; |
||||
text-shadow: none; |
||||
font-weight: 500; |
||||
line-height: 14px; |
||||
display: flex; |
||||
align-items: center; |
||||
`,
|
||||
|
||||
nameStyle: css` |
||||
margin-right: 3px; |
||||
`,
|
||||
|
||||
removeStyle: cx([ |
||||
'fa fa-times', |
||||
css` |
||||
cursor: pointer; |
||||
`,
|
||||
]), |
||||
})); |
||||
|
||||
return ( |
||||
<div className={getStyles().itemStyle}> |
||||
<span className={getStyles().nameStyle}>{name}</span> |
||||
<i className={getStyles().removeStyle} onClick={() => onRemove(name)} /> |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,25 @@ |
||||
import React from 'react'; |
||||
import { storiesOf } from '@storybook/react'; |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; |
||||
import { UseState } from '../../utils/storybook/UseState'; |
||||
import { TagsInput } from './TagsInput'; |
||||
|
||||
const TagsInputStories = storiesOf('UI/TagsInput', module); |
||||
const mockTags = ['Some', 'Tags', 'With', 'This', 'New', 'Component']; |
||||
|
||||
TagsInputStories.addDecorator(withCenteredStory); |
||||
|
||||
TagsInputStories.add('default', () => { |
||||
return <TagsInput tags={[]} onChange={tags => action('tags updated')(tags)} />; |
||||
}); |
||||
|
||||
TagsInputStories.add('with mock tags', () => { |
||||
return ( |
||||
<UseState initialState={mockTags}> |
||||
{tags => { |
||||
return <TagsInput tags={tags} onChange={tags => action('tags updated')(tags)} />; |
||||
}} |
||||
</UseState> |
||||
); |
||||
}); |
||||
@ -0,0 +1,121 @@ |
||||
import React, { ChangeEvent, KeyboardEvent, PureComponent } from 'react'; |
||||
import { css, cx } from 'emotion'; |
||||
import { stylesFactory } from '../../themes/stylesFactory'; |
||||
import { Button, Input } from '..'; |
||||
import { TagItem } from './TagItem'; |
||||
|
||||
interface Props { |
||||
tags?: string[]; |
||||
width?: number; |
||||
|
||||
onChange: (tags: string[]) => void; |
||||
} |
||||
|
||||
interface State { |
||||
newTag: string; |
||||
tags: string[]; |
||||
} |
||||
|
||||
export class TagsInput extends PureComponent<Props, State> { |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
newTag: '', |
||||
tags: this.props.tags || [], |
||||
}; |
||||
} |
||||
|
||||
onNameChange = (event: ChangeEvent<HTMLInputElement>) => { |
||||
this.setState({ |
||||
newTag: event.target.value, |
||||
}); |
||||
}; |
||||
|
||||
onRemove = (tagToRemove: string) => { |
||||
this.setState( |
||||
(prevState: State) => ({ |
||||
...prevState, |
||||
tags: prevState.tags.filter(tag => tagToRemove !== tag), |
||||
}), |
||||
() => this.onChange() |
||||
); |
||||
}; |
||||
|
||||
// Using React.MouseEvent to avoid tslint error
|
||||
onAdd = (event: React.MouseEvent) => { |
||||
event.preventDefault(); |
||||
if (this.state.newTag !== '') { |
||||
this.setNewTags(); |
||||
} |
||||
}; |
||||
|
||||
onKeyboardAdd = (event: KeyboardEvent) => { |
||||
event.preventDefault(); |
||||
if (event.key === 'Enter' && this.state.newTag !== '') { |
||||
this.setNewTags(); |
||||
} |
||||
}; |
||||
|
||||
setNewTags = () => { |
||||
// We don't want to duplicate tags, clearing the input if
|
||||
// the user is trying to add the same tag.
|
||||
if (!this.state.tags.includes(this.state.newTag)) { |
||||
this.setState( |
||||
(prevState: State) => ({ |
||||
...prevState, |
||||
tags: [...prevState.tags, prevState.newTag], |
||||
newTag: '', |
||||
}), |
||||
() => this.onChange() |
||||
); |
||||
} else { |
||||
this.setState({ newTag: '' }); |
||||
} |
||||
}; |
||||
|
||||
onChange = () => { |
||||
this.props.onChange(this.state.tags); |
||||
}; |
||||
|
||||
render() { |
||||
const { tags, newTag } = this.state; |
||||
|
||||
const getStyles = stylesFactory(() => ({ |
||||
tagsCloudStyle: css` |
||||
display: flex; |
||||
justify-content: flex-start; |
||||
flex-wrap: wrap; |
||||
`,
|
||||
|
||||
addButtonStyle: css` |
||||
margin-left: 8px; |
||||
margin-top: 2px; |
||||
`,
|
||||
})); |
||||
|
||||
return ( |
||||
<div className="width-20"> |
||||
<div |
||||
className={cx( |
||||
['gf-form-inline'], |
||||
css` |
||||
margin-bottom: 4px; |
||||
` |
||||
)} |
||||
> |
||||
<Input placeholder="Add Name" onChange={this.onNameChange} value={newTag} onKeyUp={this.onKeyboardAdd} /> |
||||
<Button className={getStyles().addButtonStyle} onClick={this.onAdd} variant="secondary" size="md"> |
||||
Add |
||||
</Button> |
||||
</div> |
||||
<div className={getStyles().tagsCloudStyle}> |
||||
{tags && |
||||
tags.map((tag: string, index: number) => { |
||||
return <TagItem key={`${tag}-${index}`} name={tag} onRemove={this.onRemove} />; |
||||
})} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
@ -1,114 +0,0 @@ |
||||
<div class="gf-form-group"> |
||||
<h3 class="page-heading">HTTP</h3> |
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-10">URL</span> |
||||
<input class="gf-form-input gf-form-input--has-help-icon" type="text" |
||||
ng-model='current.url' placeholder="{{suggestUrl}}" |
||||
bs-typeahead="getSuggestUrls" min-length="0" |
||||
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input> |
||||
<info-popover mode="right-absolute"> |
||||
<p>Specify a complete HTTP URL (for example http://your_server:8080)</p> |
||||
<span ng-show="current.access === 'direct'"> |
||||
Your access method is <em>Browser</em>, this means the URL |
||||
needs to be accessible from the browser. |
||||
</span> |
||||
<span ng-show="current.access === 'proxy'"> |
||||
Your access method is <em>Server</em>, this means the URL |
||||
needs to be accessible from the grafana backend/server. |
||||
</span> |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline" ng-if="showAccessOption"> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-10">Access</span> |
||||
<div class="gf-form-select-wrapper max-width-24"> |
||||
<select class="gf-form-input" ng-model="current.access" ng-options="f.key as f.value for f in [{key: 'proxy', value: 'Server (Default)'}, { key: 'direct', value: 'Browser'}]"></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword pointer" ng-click="toggleAccessHelp()"> |
||||
Help |
||||
<i class="fa fa-caret-down" ng-show="showAccessHelp"></i> |
||||
<i class="fa fa-caret-right" ng-hide="showAccessHelp"> </i> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="grafana-info-box m-t-2" ng-show="showAccessHelp"> |
||||
<p> |
||||
Access mode controls how requests to the data source will be handled. |
||||
<strong><i>Server</i></strong> should be the preferred way if nothing else stated. |
||||
</p> |
||||
<div class="alert-title">Server access mode (Default):</div> |
||||
<p> |
||||
All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source |
||||
and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. |
||||
The URL needs to be accessible from the grafana backend/server if you select this access mode. |
||||
</p> |
||||
<div class="alert-title">Browser access mode:</div> |
||||
<p> |
||||
All requests will be made from the browser directly to the data source and may be subject to |
||||
Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this |
||||
access mode. |
||||
</div> |
||||
|
||||
<div class="gf-form-inline" ng-if="current.access=='proxy'"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-10">Whitelisted Cookies</span> |
||||
<bootstrap-tagsinput ng-model="current.jsonData.keepCookies" width-class="width-20 gf-form-input--has-help-icon" tagclass="label label-tag" placeholder="Add Name"> |
||||
</bootstrap-tagsinput> |
||||
<info-popover mode="right-absolute"> |
||||
Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<h3 class="page-heading">Auth</h3> |
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<gf-form-checkbox class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-13" switch-class="max-width-6"></gf-form-checkbox> |
||||
<gf-form-checkbox class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth |
||||
headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-13" |
||||
switch-class="max-width-6"></gf-form-checkbox> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-13" |
||||
checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-checkbox> |
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for |
||||
verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-13" |
||||
switch-class="max-width-6"></gf-form-checkbox> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verify" label-class="width-13" |
||||
checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-checkbox> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<gf-form-checkbox class="gf-form" ng-if="current.access=='proxy'" label="Forward OAuth Identity" label-class="width-13" tooltip="Forward the user's upstream OAuth identity to the datasource (Their access token gets passed along)." checked="current.jsonData.oauthPassThru" switch-class="max-width-6"></gf-form-checkbox> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-group" ng-if="current.basicAuth"> |
||||
<h6>Basic Auth Details</h6> |
||||
<div class="gf-form" ng-if="current.basicAuth"> |
||||
<span class="gf-form-label width-10">User</span> |
||||
<input class="gf-form-input max-width-21" type="text" ng-model='current.basicAuthUser' placeholder="user" required></input> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<secret-form-field |
||||
isConfigured="current.basicAuthPassword || current.secureJsonFields.basicAuthPassword" |
||||
value="current.secureJsonData.basicAuthPassword || ''" |
||||
on-reset="onBasicAuthPasswordReset" |
||||
on-change="onBasicAuthPasswordChange" |
||||
inputWidth="18" |
||||
labelWidth="10" |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<datasource-tls-auth-settings current="current" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'"> |
||||
</datasource-tls-auth-settings> |
||||
@ -0,0 +1 @@ |
||||
<datasource-http-settings-next on-change="onChange" dataSourceConfig="current" showAccessOptions="showAccessOption" defaultUrl="suggestUrl" /> |
||||
@ -1,62 +0,0 @@ |
||||
<div class="gf-form-group"> |
||||
<div class="gf-form"> |
||||
<h6>TLS Auth Details</h6> |
||||
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover> |
||||
</div> |
||||
<div ng-if="current.jsonData.tlsAuthWithCACert"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">CA Cert</label></div> |
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert"> |
||||
<textarea |
||||
rows="7" |
||||
class="gf-form-input gf-form-textarea" |
||||
ng-model="current.secureJsonData.tlsCACert" |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
></textarea> |
||||
</div> |
||||
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert"> |
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" /> |
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsCACert = false">reset</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div ng-if="current.jsonData.tlsAuth"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">Client Cert</label></div> |
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert"> |
||||
<textarea |
||||
rows="7" |
||||
class="gf-form-input gf-form-textarea" |
||||
ng-model="current.secureJsonData.tlsClientCert" |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
required |
||||
></textarea> |
||||
</div> |
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert"> |
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" /> |
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false" |
||||
>reset</a |
||||
> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">Client Key</label></div> |
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey"> |
||||
<textarea |
||||
rows="7" |
||||
class="gf-form-input gf-form-textarea" |
||||
ng-model="current.secureJsonData.tlsClientKey" |
||||
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" |
||||
required |
||||
></textarea> |
||||
</div> |
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey"> |
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" /> |
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
Loading…
Reference in new issue