mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
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} />;
|
||||
});
|
48
packages/grafana-ui/src/components/TagsInput/TagItem.tsx
Normal file
48
packages/grafana-ui/src/components/TagsInput/TagItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
});
|
121
packages/grafana-ui/src/components/TagsInput/TagsInput.tsx
Normal file
121
packages/grafana-ui/src/components/TagsInput/TagsInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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/';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 }],
|
||||
]);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user