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 story
This commit is contained in:
Dominik Prokop 2019-10-18 12:09:53 +02:00 committed by Torkel Ödegaard
parent cb0e80e7b9
commit c9b11bfc7a
24 changed files with 760 additions and 197 deletions

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -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&nbsp;
<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>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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;
}

View File

@ -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} />;
});

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -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>
);
}
}

View File

@ -37,6 +37,7 @@ export { RefreshPicker } from './RefreshPicker/RefreshPicker';
export { TimePicker } from './TimePicker/TimePicker';
export { TimeOfDayPicker } from './TimePicker/TimeOfDayPicker';
export { List } from './List/List';
export { TagsInput } from './TagsInput/TagsInput';
export { Modal } from './Modal/Modal';
// Renderless
@ -87,8 +88,8 @@ export { JSONFormatter } from './JSONFormatter/JSONFormatter';
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings';
export { Spinner } from './Spinner/Spinner';
export { FadeTransition } from './transitions/FadeTransition';
export { SlideOutTransition } from './transitions/SlideOutTransition';
// Segment
export { Segment, SegmentAsync, SegmentSelect } from './Segment/';

View File

@ -10,6 +10,7 @@ import {
DataFrameDTO,
AnnotationEvent,
ScopedVars,
KeyValue,
} from '@grafana/data';
import { PluginMeta, GrafanaPlugin } from './plugin';
import { PanelData } from './panel';
@ -504,6 +505,7 @@ export interface DataSourceSettings<T extends DataSourceJsonData = DataSourceJso
isDefault: boolean;
jsonData: T;
secureJsonData?: S;
secureJsonFields?: KeyValue<boolean>;
readOnly: boolean;
withCredentials: boolean;
}

View File

@ -7,6 +7,7 @@ export * from './validate';
export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
export * from './slate';
export * from './dataLinks';
export * from './tags';
export { default as ansicolor } from './ansicolor';
// Export with a namespace

View File

@ -1,6 +1,8 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
interface StateHolderProps<T> {
logState?: boolean;
initialState: T;
children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode;
}
@ -32,6 +34,9 @@ export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T
};
render() {
if (this.props.logState) {
action('UseState current state')(this.state.value);
}
return this.props.children(this.state.value, this.handleStateUpdate);
}
}

View File

@ -7,7 +7,13 @@ 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, SecretFormField, DataLinksEditor } from '@grafana/ui';
import {
ColorPicker,
SeriesColorPickerPopoverWithTheme,
SecretFormField,
DataLinksEditor,
DataSourceHttpSettings,
} from '@grafana/ui';
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
import { SearchField } from './components/search/SearchField';
import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu';
@ -111,4 +117,10 @@ export function registerAngularDirectives() {
'onChange',
['datasource', { watchDepth: 'reference' }],
]);
react2AngularDirective('datasourceHttpSettingsNext', DataSourceHttpSettings, [
'defaultUrl',
'showAccessOptions',
'dataSourceConfig',
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
}

View File

@ -1,5 +1,5 @@
import React from 'react';
import tags from 'app/core/utils/tags';
import { getTagColorsFromName } from '@grafana/ui';
export interface Props {
label: string;
@ -15,7 +15,7 @@ export class TagBadge extends React.Component<Props, any> {
render() {
const { label, removeIcon, count } = this.props;
const { color, borderColor } = tags.getTagColorsFromName(label);
const { color, borderColor } = getTagColorsFromName(label);
const tagStyle = {
backgroundColor: color,
borderColor: borderColor,

View File

@ -1,11 +1,11 @@
import angular from 'angular';
import { getTagColorsFromName } from '@grafana/ui';
import $ from 'jquery';
import coreModule from '../core_module';
import tags from 'app/core/utils/tags';
import 'vendor/tagsinput/bootstrap-tagsinput.js';
function setColor(name: string, element: JQuery) {
const { color, borderColor } = tags.getTagColorsFromName(name);
const { color, borderColor } = getTagColorsFromName(name);
element.css('background-color', color);
element.css('border-color', borderColor);
}

View File

@ -1,6 +1,6 @@
import { getTagColorsFromName } from '@grafana/ui';
import { BackendSrv } from 'app/core/services/backend_srv';
import { NavModelSrv } from 'app/core/core';
import tags from 'app/core/utils/tags';
export default class AdminListUsersCtrl {
users: any;
@ -63,7 +63,7 @@ function getAuthLabelStyle(label: string) {
return {};
}
const { color, borderColor } = tags.getTagColorsFromName(label);
const { color, borderColor } = getTagColorsFromName(label);
return {
'background-color': color,
'border-color': borderColor,

View File

@ -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&nbsp;
<i class="fa fa-caret-down" ng-show="showAccessHelp"></i>
<i class="fa fa-caret-right" ng-hide="showAccessHelp">&nbsp;</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>

View File

@ -0,0 +1 @@
<datasource-http-settings-next on-change="onChange" dataSourceConfig="current" showAccessOptions="showAccessOption" defaultUrl="suggestUrl" />

View File

@ -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>

View File

@ -1,5 +1,4 @@
import { coreModule } from 'app/core/core';
import { createChangeHandler, createResetHandler, PasswordFieldEnum } from '../utils/passwordHandlers';
coreModule.directive('datasourceHttpSettings', () => {
return {
@ -8,22 +7,14 @@ coreModule.directive('datasourceHttpSettings', () => {
suggestUrl: '@',
noDirectAccess: '@',
},
templateUrl: 'public/app/features/datasources/partials/http_settings.html',
templateUrl: 'public/app/features/datasources/partials/http_settings_next.html',
link: {
pre: ($scope: any, elem, attrs) => {
pre: ($scope: any) => {
// do not show access option if direct access is disabled
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
$scope.showAccessHelp = false;
$scope.toggleAccessHelp = () => {
$scope.showAccessHelp = !$scope.showAccessHelp;
$scope.onChange = (datasourceSetting: any) => {
$scope.current = datasourceSetting;
};
$scope.getSuggestUrls = () => {
return [$scope.suggestUrl];
};
$scope.onBasicAuthPasswordReset = createResetHandler($scope, PasswordFieldEnum.BasicAuthPassword);
$scope.onBasicAuthPasswordChange = createChangeHandler($scope, PasswordFieldEnum.BasicAuthPassword);
},
},
};