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