mirror of https://github.com/grafana/grafana
SQL: Migrate (MS/My/Postgres)SQL configuration pages from Angular to React (#51891)
* Migrate SQL configuration pages from angular to react * Move enums to types.ts and remove angular partials * remove es lint disables and update betterer instead * Fix automatically added type declarations * Bump wor.. betterer ;) * Export SecretInput component from grafana-ui * Fix A11y issues * Export SecretTextArea as well * Fix typo * Use const instead of var * Fix typo in doc * Add autoDetectFeatures to postgres config editor Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>pull/52225/head^2
parent
77e87f1806
commit
9498ee3d54
@ -0,0 +1,75 @@ |
||||
import React from 'react'; |
||||
|
||||
import { FieldSet, InlineField } from '@grafana/ui'; |
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; |
||||
|
||||
import { SQLConnectionLimits } from './types'; |
||||
|
||||
interface Props<T> { |
||||
onPropertyChanged: (property: keyof T, value?: number) => void; |
||||
labelWidth: number; |
||||
jsonData: SQLConnectionLimits; |
||||
} |
||||
|
||||
export const ConnectionLimits = <T extends SQLConnectionLimits>(props: Props<T>) => { |
||||
const { onPropertyChanged, labelWidth, jsonData } = props; |
||||
|
||||
const onJSONDataNumberChanged = (property: keyof SQLConnectionLimits) => { |
||||
return (number?: number) => { |
||||
if (onPropertyChanged) { |
||||
onPropertyChanged(property, number); |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
return ( |
||||
<FieldSet label="Connection limits"> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
The maximum number of open connections to the database.If <i>Max idle connections</i> is greater than 0 and |
||||
the <i>Max open connections</i> is less than <i>Max idle connections</i>, then |
||||
<i>Max idle connections</i> will be reduced to match the <i>Max open connections</i> limit. If set to 0, |
||||
there is no limit on the number of open connections. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidth} |
||||
label="Max open" |
||||
> |
||||
<NumberInput |
||||
placeholder="unlimited" |
||||
value={jsonData.maxOpenConns} |
||||
onChange={onJSONDataNumberChanged('maxOpenConns')} |
||||
></NumberInput> |
||||
</InlineField> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
The maximum number of connections in the idle connection pool.If <i>Max open connections</i> is greater than |
||||
0 but less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to |
||||
match the <i>Max open connections</i> limit. If set to 0, no idle connections are retained. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidth} |
||||
label="Max idle" |
||||
> |
||||
<NumberInput |
||||
placeholder="2" |
||||
value={jsonData.maxIdleConns} |
||||
onChange={onJSONDataNumberChanged('maxIdleConns')} |
||||
></NumberInput> |
||||
</InlineField> |
||||
<InlineField |
||||
tooltip="The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever." |
||||
labelWidth={labelWidth} |
||||
label="Max lifetime" |
||||
> |
||||
<NumberInput |
||||
placeholder="14400" |
||||
value={jsonData.connMaxLifetime} |
||||
onChange={onJSONDataNumberChanged('connMaxLifetime')} |
||||
></NumberInput> |
||||
</InlineField> |
||||
</FieldSet> |
||||
); |
||||
}; |
@ -0,0 +1,77 @@ |
||||
import React from 'react'; |
||||
|
||||
import { |
||||
DataSourceJsonData, |
||||
DataSourcePluginOptionsEditorProps, |
||||
KeyValue, |
||||
onUpdateDatasourceSecureJsonDataOption, |
||||
updateDatasourcePluginResetOption, |
||||
} from '@grafana/data'; |
||||
import { InlineField, SecretTextArea } from '@grafana/ui'; |
||||
|
||||
export interface Props<T, S> { |
||||
editorProps: DataSourcePluginOptionsEditorProps<T, S>; |
||||
showCACert?: boolean; |
||||
secureJsonFields?: KeyValue<Boolean>; |
||||
labelWidth?: number; |
||||
} |
||||
|
||||
export const TLSSecretsConfig = <T extends DataSourceJsonData, S = {}>(props: Props<T, S>) => { |
||||
const { labelWidth, editorProps, showCACert } = props; |
||||
const { secureJsonFields } = editorProps.options; |
||||
return ( |
||||
<> |
||||
<InlineField |
||||
tooltip={<span>To authenticate with an TLS/SSL client certificate, provide the client certificate here.</span>} |
||||
labelWidth={labelWidth} |
||||
label="TLS/SSL Client Certificate" |
||||
> |
||||
<SecretTextArea |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
cols={45} |
||||
rows={7} |
||||
isConfigured={secureJsonFields && secureJsonFields.tlsClientCert} |
||||
onChange={onUpdateDatasourceSecureJsonDataOption(editorProps, 'tlsClientCert')} |
||||
onReset={() => { |
||||
updateDatasourcePluginResetOption(editorProps, 'tlsClientCert'); |
||||
}} |
||||
></SecretTextArea> |
||||
</InlineField> |
||||
{showCACert ? ( |
||||
<InlineField |
||||
tooltip={<span>If the selected TLS/SSL mode requires a server root certificate, provide it here.</span>} |
||||
labelWidth={labelWidth} |
||||
label="TLS/SSL Root Certificate" |
||||
> |
||||
<SecretTextArea |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
cols={45} |
||||
rows={7} |
||||
isConfigured={secureJsonFields && secureJsonFields.tlsCACert} |
||||
onChange={onUpdateDatasourceSecureJsonDataOption(editorProps, 'tlsCACert')} |
||||
onReset={() => { |
||||
updateDatasourcePluginResetOption(editorProps, 'tlsCACert'); |
||||
}} |
||||
></SecretTextArea> |
||||
</InlineField> |
||||
) : null} |
||||
|
||||
<InlineField |
||||
tooltip={<span>To authenticate with a client TLS/SSL certificate, provide the key here.</span>} |
||||
labelWidth={labelWidth} |
||||
label="TLS/SSL Client Key" |
||||
> |
||||
<SecretTextArea |
||||
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" |
||||
cols={45} |
||||
rows={7} |
||||
isConfigured={secureJsonFields && secureJsonFields.tlsClientKey} |
||||
onChange={onUpdateDatasourceSecureJsonDataOption(editorProps, 'tlsClientKey')} |
||||
onReset={() => { |
||||
updateDatasourcePluginResetOption(editorProps, 'tlsClientKey'); |
||||
}} |
||||
></SecretTextArea> |
||||
</InlineField> |
||||
</> |
||||
); |
||||
}; |
@ -0,0 +1,5 @@ |
||||
export interface SQLConnectionLimits { |
||||
maxOpenConns: number; |
||||
maxIdleConns: number; |
||||
connMaxLifetime: number; |
||||
} |
@ -1,47 +0,0 @@ |
||||
import { |
||||
createChangeHandler, |
||||
createResetHandler, |
||||
PasswordFieldEnum, |
||||
} from '../../../features/datasources/utils/passwordHandlers'; |
||||
|
||||
export class MssqlConfigCtrl { |
||||
static templateUrl = 'partials/config.html'; |
||||
|
||||
// Set through angular bindings
|
||||
declare current: any; |
||||
|
||||
onPasswordReset: ReturnType<typeof createResetHandler>; |
||||
onPasswordChange: ReturnType<typeof createChangeHandler>; |
||||
showUserCredentials = false; |
||||
showTlsConfig = false; |
||||
showCertificateConfig = false; |
||||
|
||||
/** @ngInject */ |
||||
constructor($scope: any) { |
||||
this.current = $scope.ctrl.current; |
||||
this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false'; |
||||
this.current.jsonData.sslRootCertFile = this.current.jsonData.sslRootCertFile || ''; |
||||
this.current.jsonData.tlsSkipVerify = this.current.jsonData.tlsSkipVerify || false; |
||||
this.current.jsonData.serverName = this.current.jsonData.serverName || ''; |
||||
this.current.jsonData.authenticationType = this.current.jsonData.authenticationType || 'SQL Server Authentication'; |
||||
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); |
||||
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); |
||||
this.onAuthenticationTypeChange(); |
||||
this.onEncryptChange(); |
||||
} |
||||
|
||||
onAuthenticationTypeChange() { |
||||
// This is using the fallback in https://github.com/denisenkom/go-mssqldb to use Windows Auth if login/user id is empty.
|
||||
if (this.current.jsonData.authenticationType === 'Windows Authentication') { |
||||
this.current.user = ''; |
||||
this.current.password = ''; |
||||
} |
||||
|
||||
this.showUserCredentials = this.current.jsonData.authenticationType !== 'Windows Authentication'; |
||||
} |
||||
|
||||
onEncryptChange() { |
||||
this.showTlsConfig = this.current.jsonData.encrypt === 'true'; |
||||
this.showCertificateConfig = this.showTlsConfig && this.current.jsonData.tlsSkipVerify === false; |
||||
} |
||||
} |
@ -0,0 +1,262 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { SyntheticEvent } from 'react'; |
||||
|
||||
import { |
||||
DataSourcePluginOptionsEditorProps, |
||||
GrafanaTheme2, |
||||
onUpdateDatasourceJsonDataOption, |
||||
onUpdateDatasourceSecureJsonDataOption, |
||||
SelectableValue, |
||||
updateDatasourcePluginJsonDataOption, |
||||
updateDatasourcePluginResetOption, |
||||
} from '@grafana/data'; |
||||
import { |
||||
Alert, |
||||
FieldSet, |
||||
InlineField, |
||||
InlineFieldRow, |
||||
InlineSwitch, |
||||
Input, |
||||
SecretInput, |
||||
Select, |
||||
useStyles2, |
||||
} from '@grafana/ui'; |
||||
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits'; |
||||
|
||||
import { MSSQLAuthenticationType, MSSQLEncryptOptions, MssqlOptions } from '../types'; |
||||
|
||||
export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<MssqlOptions>) => { |
||||
const { options, onOptionsChange } = props; |
||||
const styles = useStyles2(getStyles); |
||||
const jsonData = options.jsonData; |
||||
|
||||
const onResetPassword = () => { |
||||
updateDatasourcePluginResetOption(props, 'password'); |
||||
}; |
||||
|
||||
const onDSOptionChanged = (property: keyof MssqlOptions) => { |
||||
return (event: SyntheticEvent<HTMLInputElement>) => { |
||||
onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } }); |
||||
}; |
||||
}; |
||||
|
||||
const onSkipTLSVerifyChanged = (event: SyntheticEvent<HTMLInputElement>) => { |
||||
updateDatasourcePluginJsonDataOption(props, 'tlsSkipVerify', event.currentTarget.checked); |
||||
}; |
||||
|
||||
const onEncryptChanged = (value: SelectableValue) => { |
||||
updateDatasourcePluginJsonDataOption(props, 'encrypt', value.value); |
||||
}; |
||||
|
||||
const onAuthenticationMethodChanged = (value: SelectableValue) => { |
||||
onOptionsChange({ |
||||
...options, |
||||
...{ |
||||
jsonData: { ...jsonData, ...{ authenticationType: value.value } }, |
||||
secureJsonData: { ...options.secureJsonData, ...{ password: '' } }, |
||||
secureJsonFields: { ...options.secureJsonFields, ...{ password: false } }, |
||||
user: '', |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
const authenticationOptions: Array<SelectableValue<MSSQLAuthenticationType>> = [ |
||||
{ value: MSSQLAuthenticationType.sqlAuth, label: 'SQL Server Authentication' }, |
||||
{ value: MSSQLAuthenticationType.windowsAuth, label: 'Windows Authentication' }, |
||||
]; |
||||
|
||||
const encryptOptions: Array<SelectableValue<string>> = [ |
||||
{ value: MSSQLEncryptOptions.disable, label: 'disable' }, |
||||
{ value: MSSQLEncryptOptions.false, label: 'false' }, |
||||
{ value: MSSQLEncryptOptions.true, label: 'true' }, |
||||
]; |
||||
|
||||
const shortWidth = 15; |
||||
const longWidth = 46; |
||||
const labelWidthSSL = 25; |
||||
|
||||
return ( |
||||
<> |
||||
<FieldSet label="MS SQL Connection" width={400}> |
||||
<InlineField labelWidth={shortWidth} label="Host"> |
||||
<Input |
||||
width={longWidth} |
||||
name="host" |
||||
type="text" |
||||
value={options.url || ''} |
||||
placeholder="localhost:1433" |
||||
onChange={onDSOptionChanged('url')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField labelWidth={shortWidth} label="Database"> |
||||
<Input |
||||
width={longWidth} |
||||
name="database" |
||||
value={options.database || ''} |
||||
placeholder="datbase name" |
||||
onChange={onDSOptionChanged('database')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField |
||||
label="Authentication" |
||||
labelWidth={shortWidth} |
||||
htmlFor="authenticationType" |
||||
tooltip={ |
||||
<ul className={styles.ulPadding}> |
||||
<li> |
||||
<i>SQL Server Authentication</i> This is the default mechanism to connect to MS SQL Server. Enter the |
||||
SQL Server Authentication login or the Windows Authentication login in the DOMAIN\User format. |
||||
</li> |
||||
<li> |
||||
<i>Windows Authentication</i> Windows Integrated Security - single sign on for users who are already |
||||
logged onto Windows and have enabled this option for MS SQL Server. |
||||
</li> |
||||
</ul> |
||||
} |
||||
> |
||||
<Select |
||||
value={jsonData.authenticationType || MSSQLAuthenticationType.sqlAuth} |
||||
inputId="authenticationType" |
||||
options={authenticationOptions} |
||||
onChange={onAuthenticationMethodChanged} |
||||
></Select> |
||||
</InlineField> |
||||
{jsonData.authenticationType === MSSQLAuthenticationType.windowsAuth ? null : ( |
||||
<InlineFieldRow> |
||||
<InlineField labelWidth={shortWidth} label="User"> |
||||
<Input |
||||
width={shortWidth} |
||||
value={options.user || ''} |
||||
placeholder="user" |
||||
onChange={onDSOptionChanged('user')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField label="Password" labelWidth={shortWidth}> |
||||
<SecretInput |
||||
width={shortWidth} |
||||
placeholder="Password" |
||||
isConfigured={options.secureJsonFields && options.secureJsonFields.password} |
||||
onReset={onResetPassword} |
||||
onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')} |
||||
></SecretInput> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
)} |
||||
</FieldSet> |
||||
|
||||
<FieldSet label="TLS/SSL Auth"> |
||||
<InlineField |
||||
labelWidth={labelWidthSSL} |
||||
htmlFor="encrypt" |
||||
tooltip={ |
||||
<> |
||||
Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. |
||||
<ul className={styles.ulPadding}> |
||||
<li> |
||||
<i>disable</i> - Data sent between client and server is not encrypted. |
||||
</li> |
||||
<li> |
||||
<i>false</i> - Data sent between client and server is not encrypted beyond the login packet. (default) |
||||
</li> |
||||
<li> |
||||
<i>true</i> - Data sent between client and server is encrypted. |
||||
</li> |
||||
</ul> |
||||
If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable |
||||
encryption to be able to connect. |
||||
</> |
||||
} |
||||
label="Encrypt" |
||||
> |
||||
<Select |
||||
options={encryptOptions} |
||||
value={jsonData.encrypt || MSSQLEncryptOptions.disable} |
||||
inputId="encrypt" |
||||
onChange={onEncryptChanged} |
||||
></Select> |
||||
</InlineField> |
||||
|
||||
{jsonData.encrypt === MSSQLEncryptOptions.true ? ( |
||||
<> |
||||
<InlineField labelWidth={labelWidthSSL} htmlFor="skipTlsVerify" label="Skip TLS Verify"> |
||||
<InlineSwitch |
||||
id="skipTlsVerify" |
||||
onChange={onSkipTLSVerifyChanged} |
||||
value={jsonData.tlsSkipVerify || false} |
||||
></InlineSwitch> |
||||
</InlineField> |
||||
{jsonData.tlsSkipVerify ? null : ( |
||||
<> |
||||
<InlineField |
||||
labelWidth={labelWidthSSL} |
||||
tooltip={ |
||||
<span> |
||||
Path to file containing the public key certificate of the CA that signed the SQL Server |
||||
certificate. Needed when the server certificate is self signed. |
||||
</span> |
||||
} |
||||
label="TLS/SSL Root Certificate" |
||||
> |
||||
<Input |
||||
value={jsonData.sslRootCertFile || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslRootCertFile')} |
||||
placeholder="TLS/SSL root certificate file path" |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField labelWidth={labelWidthSSL} label="Hostname in server certificate"> |
||||
<Input |
||||
placeholder="Common Name (CN) in server certificate" |
||||
value={jsonData.serverName || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'serverName')} |
||||
></Input> |
||||
</InlineField> |
||||
</> |
||||
)} |
||||
</> |
||||
) : null} |
||||
</FieldSet> |
||||
|
||||
<ConnectionLimits |
||||
labelWidth={shortWidth} |
||||
jsonData={jsonData} |
||||
onPropertyChanged={(property, value) => { |
||||
updateDatasourcePluginJsonDataOption(props, property, value); |
||||
}} |
||||
></ConnectionLimits> |
||||
|
||||
<FieldSet label="MS SQL details"> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example |
||||
<code>1m</code> if your data is written every minute. |
||||
</span> |
||||
} |
||||
label="Min time interval" |
||||
> |
||||
<Input |
||||
placeholder="1m" |
||||
value={jsonData.timeInterval || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')} |
||||
></Input> |
||||
</InlineField> |
||||
</FieldSet> |
||||
|
||||
<Alert title="User Permission" severity="info"> |
||||
The database user should only be granted SELECT permissions on the specified database and tables you want to |
||||
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, |
||||
statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect |
||||
against this we <em>highly</em> recommmend you create a specific MS SQL user with restricted permissions. |
||||
</Alert> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
function getStyles(theme: GrafanaTheme2) { |
||||
return { |
||||
ulPadding: css({ |
||||
margin: theme.spacing(1, 0), |
||||
paddingLeft: theme.spacing(5), |
||||
}), |
||||
}; |
||||
} |
@ -1,161 +0,0 @@ |
||||
<h3 class="page-heading">MS SQL connection</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-7">Host</span> |
||||
<input type="text" class="gf-form-input" style="width: 352px" ng-model='ctrl.current.url' placeholder="localhost" |
||||
bs-typeahead="{{['localhost', 'localhost:1433']}}" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-7">Database</span> |
||||
<input type="text" class="gf-form-input" style="width: 352px" ng-model='ctrl.current.database' |
||||
placeholder="database name" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-7" for="auth-select">Authentication</label> |
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon"> |
||||
<select id="auth-select" class="gf-form-input" ng-model="ctrl.current.jsonData.authenticationType" |
||||
ng-options="mode for mode in ['Windows Authentication', 'SQL Server Authentication']" |
||||
ng-init="ctrl.current.jsonData.authenticationType" ng-change="ctrl.onAuthenticationTypeChange()"></select> |
||||
<info-popover mode="right-absolute"> |
||||
<ul> |
||||
<li><i>SQL Server Authentication</i> This is the default mechanism to connect to MS SQL Server. Enter the SQL |
||||
Server Authentication login or the Windows Authentication login in the DOMAIN\User format.</li> |
||||
<li><i>Windows Authentication</i> Windows Integrated Security - single sign on for users who are already |
||||
logged onto Windows and have enabled this option for MS SQL Server.</li> |
||||
</ul> |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline" ng-show="ctrl.showUserCredentials"> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">User</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<secret-form-field isConfigured="ctrl.current.secureJsonFields.password" |
||||
value="ctrl.current.secureJsonData.password" on-reset="ctrl.onPasswordReset" on-change="ctrl.onPasswordChange" |
||||
labelWidth="7" inputWidth="7" aria-label="'Password'" /> |
||||
</div> |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
<h3 class="page-heading">TLS/SSL Auth</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
|
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-15" for="encrypt-select">Encrypt</label> |
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon"> |
||||
<select id="encrypt-select" class="gf-form-input" ng-model="ctrl.current.jsonData.encrypt" |
||||
ng-options="mode for mode in ['disable', 'false', 'true']" ng-init="ctrl.current.jsonData.encrypt" |
||||
ng-change="ctrl.onEncryptChange()" aria-labelledby="encrypt-label"></select> |
||||
<info-popover mode="right-absolute"> |
||||
Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. |
||||
<ul> |
||||
<li><i>disable</i> - Data sent between client and server is not encrypted.</li> |
||||
<li><i>false</i> - Data sent between client and server is not encrypted beyond the login packet. (default) |
||||
</li> |
||||
<li><i>true</i> - Data sent between client and server is encrypted.</li> |
||||
</ul> |
||||
If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption |
||||
to be able to connect. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form" ng-show="ctrl.showTlsConfig"> |
||||
<gf-form-switch class="gf-form" label="Skip TLS/SSL Verify" label-class="width-15" |
||||
tooltip="Skip verifying Server Certificate for TLS/SSL. If this is enabled, any certificate presented by the server and any host name in that certificate will be accepted. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing." |
||||
checked="ctrl.current.jsonData.tlsSkipVerify" switch-class="max-width-8" on-change="ctrl.onEncryptChange()"> |
||||
</gf-form-switch> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-30" ng-show="ctrl.showCertificateConfig"> |
||||
<span class="gf-form-label width-15">TLS/SSL Root Certificate</span> |
||||
<input type="text" class="gf-form-input" style="width: 352px" ng-model='ctrl.current.jsonData.sslRootCertFile' |
||||
placeholder="TLS/SSL root certificate file"></input> |
||||
<info-popover mode="right-absolute"> |
||||
Path to file containing the public key certificate of the CA that signed the SQL Server certificate. Needed when |
||||
the server certificate is self signed. |
||||
</info-popover> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-30" ng-show="ctrl.showCertificateConfig"> |
||||
<span class="gf-form-label width-15">Hostname in server certificate</span> |
||||
<input type="text" class="gf-form-input" style="width: 352px" ng-model='ctrl.current.jsonData.serverName' |
||||
placeholder="Common Name (CN) in server certificate"></input> |
||||
<info-popover mode="right-absolute"> |
||||
Specifies the Common Name (CN) in the server certificate. Default is the server host. |
||||
</info-popover> |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
<h3 class="page-heading">Connection limits</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max open</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the |
||||
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be |
||||
reduced to match the <i>Max open connections</i> limit. If set to 0, there is no limit on the number of open |
||||
connections. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max idle</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 |
||||
but |
||||
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the |
||||
<i>Max open connections</i> limit. If set to 0, no idle connections are retained. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max lifetime</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
|
||||
<h3 class="page-heading">MS SQL details</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-9">Min time interval</span> |
||||
<input type="text" class="gf-form-input width-6 gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m" |
||||
ng-pattern="/^\d+(ms|[Mwdhmsy])$/"></input> |
||||
<info-popover mode="right-absolute"> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, |
||||
for example <code>1m</code> if your data is written every minute. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="grafana-info-box"> |
||||
<h5>User Permission</h5> |
||||
<p> |
||||
The database user should only be granted SELECT permissions on the specified database and tables you want to |
||||
query. |
||||
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements |
||||
like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect against this we |
||||
<emphasis>highly</emphasis> recommmend you create a specific MS SQL user with restricted permissions. |
||||
</p> |
||||
</div> |
||||
</div> |
@ -0,0 +1,181 @@ |
||||
import React, { SyntheticEvent } from 'react'; |
||||
|
||||
import { |
||||
DataSourcePluginOptionsEditorProps, |
||||
onUpdateDatasourceJsonDataOption, |
||||
onUpdateDatasourceSecureJsonDataOption, |
||||
updateDatasourcePluginJsonDataOption, |
||||
updateDatasourcePluginResetOption, |
||||
} from '@grafana/data'; |
||||
import { Alert, FieldSet, InlineField, InlineFieldRow, InlineSwitch, Input, Link, SecretInput } from '@grafana/ui'; |
||||
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits'; |
||||
import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig'; |
||||
|
||||
import { MySQLOptions } from '../types'; |
||||
|
||||
export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<MySQLOptions>) => { |
||||
const { options, onOptionsChange } = props; |
||||
const jsonData = options.jsonData; |
||||
|
||||
const onResetPassword = () => { |
||||
updateDatasourcePluginResetOption(props, 'password'); |
||||
}; |
||||
|
||||
const onDSOptionChanged = (property: keyof MySQLOptions) => { |
||||
return (event: SyntheticEvent<HTMLInputElement>) => { |
||||
onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } }); |
||||
}; |
||||
}; |
||||
|
||||
const onSwitchChanged = (property: keyof MySQLOptions) => { |
||||
return (event: SyntheticEvent<HTMLInputElement>) => { |
||||
updateDatasourcePluginJsonDataOption(props, property, event.currentTarget.checked); |
||||
}; |
||||
}; |
||||
|
||||
const mediumWidth = 20; |
||||
const shortWidth = 15; |
||||
const longWidth = 40; |
||||
|
||||
return ( |
||||
<> |
||||
<FieldSet label="MySQL Connection" width={400}> |
||||
<InlineField labelWidth={shortWidth} label="Host"> |
||||
<Input |
||||
width={longWidth} |
||||
name="host" |
||||
type="text" |
||||
value={options.url || ''} |
||||
placeholder="localhost:3306" |
||||
onChange={onDSOptionChanged('url')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField labelWidth={shortWidth} label="Database"> |
||||
<Input |
||||
width={longWidth} |
||||
name="database" |
||||
value={options.database || ''} |
||||
placeholder="datbase name" |
||||
onChange={onDSOptionChanged('database')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineFieldRow> |
||||
<InlineField labelWidth={shortWidth} label="User"> |
||||
<Input |
||||
width={shortWidth} |
||||
value={options.user || ''} |
||||
placeholder="user" |
||||
onChange={onDSOptionChanged('user')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField labelWidth={shortWidth - 5} label="Password"> |
||||
<SecretInput |
||||
width={shortWidth} |
||||
placeholder="Password" |
||||
isConfigured={options.secureJsonFields && options.secureJsonFields.password} |
||||
onReset={onResetPassword} |
||||
onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')} |
||||
></SecretInput> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
Specify the time zone used in the database session, e.g. <code>Europe/Berlin</code> or |
||||
<code>+02:00</code>. This is necessary, if the timezone of the database (or the host of the database) is |
||||
set to something other than UTC. The value is set in the session with |
||||
<code>SET time_zone='...'</code>. If you leave this field empty, the timezone is not updated. |
||||
You can find more information in the MySQL documentation. |
||||
</span> |
||||
} |
||||
label="Session timezone" |
||||
labelWidth={mediumWidth} |
||||
> |
||||
<Input |
||||
width={longWidth - 5} |
||||
value={jsonData.timezone || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timezone')} |
||||
placeholder="(default)" |
||||
></Input> |
||||
</InlineField> |
||||
<InlineFieldRow> |
||||
<InlineField labelWidth={mediumWidth} htmlFor="tlsAuth" label="TLS Client Auth"> |
||||
<InlineSwitch |
||||
id="tlsAuth" |
||||
onChange={onSwitchChanged('tlsAuth')} |
||||
value={jsonData.tlsAuth || false} |
||||
></InlineSwitch> |
||||
</InlineField> |
||||
<InlineField |
||||
labelWidth={mediumWidth} |
||||
tooltip="Needed for verifing self-signed TLS Certs" |
||||
htmlFor="tlsCaCert" |
||||
label="With CA Cert" |
||||
> |
||||
<InlineSwitch |
||||
id="tlsCaCert" |
||||
onChange={onSwitchChanged('tlsAuthWithCACert')} |
||||
value={jsonData.tlsAuthWithCACert || false} |
||||
></InlineSwitch> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineField labelWidth={mediumWidth} htmlFor="skipTLSVerify" label="Skip TLS Verify"> |
||||
<InlineSwitch |
||||
id="skipTLSVerify" |
||||
onChange={onSwitchChanged('tlsSkipVerify')} |
||||
value={jsonData.tlsSkipVerify || false} |
||||
></InlineSwitch> |
||||
</InlineField> |
||||
</FieldSet> |
||||
|
||||
{options.jsonData.tlsAuth ? ( |
||||
<FieldSet label="TLS/SSL Auth Details"> |
||||
<TLSSecretsConfig |
||||
showCACert={jsonData.tlsAuthWithCACert} |
||||
editorProps={props} |
||||
labelWidth={25} |
||||
></TLSSecretsConfig> |
||||
</FieldSet> |
||||
) : null} |
||||
|
||||
<ConnectionLimits |
||||
labelWidth={shortWidth} |
||||
jsonData={jsonData} |
||||
onPropertyChanged={(property, value) => { |
||||
updateDatasourcePluginJsonDataOption(props, property, value); |
||||
}} |
||||
></ConnectionLimits> |
||||
|
||||
<FieldSet label="MySQL details"> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example |
||||
<code>1m</code> if your data is written every minute. |
||||
</span> |
||||
} |
||||
labelWidth={mediumWidth} |
||||
label="Min time interval" |
||||
> |
||||
<Input |
||||
placeholder="1m" |
||||
value={jsonData.timeInterval || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')} |
||||
></Input> |
||||
</InlineField> |
||||
</FieldSet> |
||||
|
||||
<Alert title="User Permission" severity="info"> |
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to |
||||
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, |
||||
statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect |
||||
against this we |
||||
<strong>Highly</strong> recommmend you create a specific MySQL user with restricted permissions. Checkout the{' '} |
||||
<Link rel="noreferrer" target="_blank" href="http://docs.grafana.org/features/datasources/mysql/"> |
||||
MySQL Data Source Docs |
||||
</Link> |
||||
for more information. |
||||
</Alert> |
||||
</> |
||||
); |
||||
}; |
@ -1,132 +0,0 @@ |
||||
<h3 class="page-heading">MySQL Connection</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-7">Host</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.url' placeholder="localhost:3306" bs-typeahead="{{['localhost:3306', 'localhost:3307']}}" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-7">Database</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.database' placeholder="database name" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">User</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<secret-form-field |
||||
isConfigured="ctrl.current.secureJsonFields.password" |
||||
value="ctrl.current.secureJsonData.password" |
||||
on-reset="ctrl.onPasswordReset" |
||||
on-change="ctrl.onPasswordChange" |
||||
inputWidth="9" |
||||
aria-label="'Password'" |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-10">Session Timezone</span> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.timezone" |
||||
spellcheck='false' |
||||
placeholder="(default)" |
||||
></input> |
||||
<info-popover mode="right-absolute"> |
||||
Specify the time zone used in the database session, e.g. <code>Europe/Berlin</code> or <code>+02:00</code>. |
||||
This is necessary, if the timezone of the database (or the host of the database) is set to something other than UTC. |
||||
The value is set in the session with <code>SET time_zone='...'</code>. If you leave this field empty, |
||||
the timezone is not updated. You can find more information in the |
||||
<a href="https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html">MySQL documentation</a>. |
||||
</info-popover> |
||||
</div> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<gf-form-checkbox class="gf-form" label="TLS Client Auth" label-class="width-10" |
||||
checked="ctrl.current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-checkbox> |
||||
<gf-form-checkbox class="gf-form" label="With CA Cert" tooltip="Needed for |
||||
verifing self-signed TLS Certs" checked="ctrl.current.jsonData.tlsAuthWithCACert" label-class="width-11" |
||||
switch-class="max-width-6"></gf-form-checkbox> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<gf-form-checkbox class="gf-form" label="Skip TLS Verify" label-class="width-10" |
||||
checked="ctrl.current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-checkbox> |
||||
</div> |
||||
</div> |
||||
|
||||
<datasource-tls-auth-settings current="ctrl.current" ng-if="(ctrl.current.jsonData.tlsAuth || ctrl.current.jsonData.tlsAuthWithCACert)"> |
||||
</datasource-tls-auth-settings> |
||||
|
||||
<b>Connection limits</b> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max open</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the |
||||
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be |
||||
reduced to match the <i>Max open connections</i> limit. If set to 0, there is no limit on the number of open |
||||
connections. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max idle</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but |
||||
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the |
||||
<i>Max open connections</i> limit. If set to 0, no idle connections are retained. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max lifetime</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.<br/><br/> |
||||
This should always be lower than configured <a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_wait_timeout" target="_blank">wait_timeout</a> in MySQL. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
|
||||
<h3 class="page-heading">MySQL details</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-9">Min time interval</span> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-6 gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.timeInterval" |
||||
spellcheck='false' |
||||
placeholder="1m" |
||||
ng-pattern="/^\d+(ms|[Mwdhmsy])$/" |
||||
></input> |
||||
<info-popover mode="right-absolute"> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, |
||||
for example <code>1m</code> if your data is written every minute. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="grafana-info-box"> |
||||
<h5>User Permission</h5> |
||||
<p> |
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to query. |
||||
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements |
||||
like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect against this we |
||||
<strong>Highly</strong> recommmend you create a specific MySQL user with restricted permissions. |
||||
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/features/datasources/mysql/">MySQL Data Source Docs</a> for more information. |
||||
</p> |
||||
</div> |
||||
</div> |
@ -1,94 +0,0 @@ |
||||
import { find } from 'lodash'; |
||||
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; |
||||
|
||||
import { |
||||
createChangeHandler, |
||||
createResetHandler, |
||||
PasswordFieldEnum, |
||||
} from '../../../features/datasources/utils/passwordHandlers'; |
||||
|
||||
export class PostgresConfigCtrl { |
||||
static templateUrl = 'partials/config.html'; |
||||
|
||||
// Set through angular bindings
|
||||
declare current: any; |
||||
|
||||
datasourceSrv: any; |
||||
showTimescaleDBHelp: boolean; |
||||
onPasswordReset: ReturnType<typeof createResetHandler>; |
||||
onPasswordChange: ReturnType<typeof createChangeHandler>; |
||||
|
||||
/** @ngInject */ |
||||
constructor($scope: any, datasourceSrv: DatasourceSrv) { |
||||
this.current = $scope.ctrl.current; |
||||
this.datasourceSrv = datasourceSrv; |
||||
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full'; |
||||
this.current.jsonData.tlsConfigurationMethod = this.current.jsonData.tlsConfigurationMethod || 'file-path'; |
||||
this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903; |
||||
this.showTimescaleDBHelp = false; |
||||
this.autoDetectFeatures(); |
||||
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); |
||||
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); |
||||
this.tlsModeMapping(); |
||||
} |
||||
|
||||
autoDetectFeatures() { |
||||
if (!this.current.id) { |
||||
return; |
||||
} |
||||
|
||||
this.datasourceSrv.loadDatasource(this.current.name).then((ds: any) => { |
||||
return ds.getVersion().then((version: any) => { |
||||
version = Number(version[0].text); |
||||
|
||||
// timescaledb is only available for 9.6+
|
||||
if (version >= 906) { |
||||
ds.getTimescaleDBVersion().then((version: any) => { |
||||
if (version.length === 1) { |
||||
this.current.jsonData.timescaledb = true; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
const major = Math.trunc(version / 100); |
||||
const minor = version % 100; |
||||
let name = String(major); |
||||
if (version < 1000) { |
||||
name = String(major) + '.' + String(minor); |
||||
} |
||||
if (!find(this.postgresVersions, (p: any) => p.value === version)) { |
||||
this.postgresVersions.push({ name: name, value: version }); |
||||
} |
||||
this.current.jsonData.postgresVersion = version; |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
toggleTimescaleDBHelp() { |
||||
this.showTimescaleDBHelp = !this.showTimescaleDBHelp; |
||||
} |
||||
|
||||
tlsModeMapping() { |
||||
if (this.current.jsonData.sslmode === 'disable') { |
||||
this.current.jsonData.tlsAuth = false; |
||||
this.current.jsonData.tlsAuthWithCACert = false; |
||||
this.current.jsonData.tlsSkipVerify = true; |
||||
} else { |
||||
this.current.jsonData.tlsAuth = true; |
||||
this.current.jsonData.tlsAuthWithCACert = true; |
||||
this.current.jsonData.tlsSkipVerify = false; |
||||
} |
||||
} |
||||
|
||||
// the value portion is derived from postgres server_version_num/100
|
||||
postgresVersions = [ |
||||
{ name: '9.3', value: 903 }, |
||||
{ name: '9.4', value: 904 }, |
||||
{ name: '9.5', value: 905 }, |
||||
{ name: '9.6', value: 906 }, |
||||
{ name: '10', value: 1000 }, |
||||
{ name: '11', value: 1100 }, |
||||
{ name: '12+', value: 1200 }, |
||||
]; |
||||
} |
@ -0,0 +1,281 @@ |
||||
import React, { SyntheticEvent, useState } from 'react'; |
||||
|
||||
import { |
||||
DataSourcePluginOptionsEditorProps, |
||||
onUpdateDatasourceJsonDataOption, |
||||
onUpdateDatasourceSecureJsonDataOption, |
||||
SelectableValue, |
||||
updateDatasourcePluginJsonDataOption, |
||||
updateDatasourcePluginResetOption, |
||||
} from '@grafana/data'; |
||||
import { Alert, InlineSwitch, FieldSet, InlineField, InlineFieldRow, Input, Select, SecretInput } from '@grafana/ui'; |
||||
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits'; |
||||
import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig'; |
||||
|
||||
import { PostgresOptions, PostgresTLSMethods, PostgresTLSModes, SecureJsonData } from '../types'; |
||||
|
||||
import { useAutoDetectFeatures } from './useAutoDetectFeatures'; |
||||
|
||||
export const postgresVersions: Array<SelectableValue<number>> = [ |
||||
{ label: '9.0', value: 900 }, |
||||
{ label: '9.1', value: 901 }, |
||||
{ label: '9.2', value: 902 }, |
||||
{ label: '9.3', value: 903 }, |
||||
{ label: '9.4', value: 904 }, |
||||
{ label: '9.5', value: 905 }, |
||||
{ label: '9.6', value: 906 }, |
||||
{ label: '10', value: 1000 }, |
||||
{ label: '11', value: 1100 }, |
||||
{ label: '12', value: 1200 }, |
||||
{ label: '13', value: 1300 }, |
||||
{ label: '14', value: 1400 }, |
||||
{ label: '15', value: 1500 }, |
||||
]; |
||||
|
||||
export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<PostgresOptions, SecureJsonData>) => { |
||||
const [versionOptions, setVersionOptions] = useState(postgresVersions); |
||||
|
||||
useAutoDetectFeatures({ props, setVersionOptions }); |
||||
|
||||
const { options, onOptionsChange } = props; |
||||
const jsonData = options.jsonData; |
||||
|
||||
const onResetPassword = () => { |
||||
updateDatasourcePluginResetOption(props, 'password'); |
||||
}; |
||||
|
||||
const tlsModes: Array<SelectableValue<PostgresTLSModes>> = [ |
||||
{ value: PostgresTLSModes.disable, label: 'disable' }, |
||||
{ value: PostgresTLSModes.require, label: 'require' }, |
||||
{ value: PostgresTLSModes.verifyCA, label: 'verify-ca' }, |
||||
{ value: PostgresTLSModes.verifyFull, label: 'verify-full' }, |
||||
]; |
||||
|
||||
const tlsMethods: Array<SelectableValue<PostgresTLSMethods>> = [ |
||||
{ value: PostgresTLSMethods.filePath, label: 'File system path' }, |
||||
{ value: PostgresTLSMethods.fileContent, label: 'Certificate content' }, |
||||
]; |
||||
|
||||
const onJSONDataOptionSelected = (property: keyof PostgresOptions) => { |
||||
return (value: SelectableValue) => { |
||||
updateDatasourcePluginJsonDataOption(props, property, value.value); |
||||
}; |
||||
}; |
||||
|
||||
const onTimeScaleDBChanged = (event: SyntheticEvent<HTMLInputElement>) => { |
||||
updateDatasourcePluginJsonDataOption(props, 'timescaledb', event.currentTarget.checked); |
||||
}; |
||||
|
||||
const onDSOptionChanged = (property: keyof PostgresOptions) => { |
||||
return (event: SyntheticEvent<HTMLInputElement>) => { |
||||
onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } }); |
||||
}; |
||||
}; |
||||
|
||||
const labelWidthSSLDetails = 25; |
||||
const labelWidthConnection = 20; |
||||
const labelWidthShort = 20; |
||||
|
||||
return ( |
||||
<> |
||||
<FieldSet label="PostgreSQL Connection" width={400}> |
||||
<InlineField labelWidth={labelWidthConnection} label="Host"> |
||||
<Input |
||||
width={40} |
||||
name="host" |
||||
type="text" |
||||
value={options.url || ''} |
||||
placeholder="localhost:5432" |
||||
onChange={onDSOptionChanged('url')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField labelWidth={labelWidthConnection} label="Database"> |
||||
<Input |
||||
width={40} |
||||
name="database" |
||||
value={options.database || ''} |
||||
placeholder="datbase name" |
||||
onChange={onDSOptionChanged('database')} |
||||
></Input> |
||||
</InlineField> |
||||
<InlineFieldRow> |
||||
<InlineField labelWidth={labelWidthConnection} label="User"> |
||||
<Input value={options.user || ''} placeholder="user" onChange={onDSOptionChanged('user')}></Input> |
||||
</InlineField> |
||||
<InlineField label="Password"> |
||||
<SecretInput |
||||
placeholder="Password" |
||||
isConfigured={options.secureJsonFields?.password} |
||||
onReset={onResetPassword} |
||||
onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')} |
||||
></SecretInput> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineField |
||||
labelWidth={labelWidthConnection} |
||||
label="TLS/SSL Mode" |
||||
htmlFor="tlsMode" |
||||
tooltip="This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server." |
||||
> |
||||
<Select |
||||
options={tlsModes} |
||||
inputId="tlsMode" |
||||
value={jsonData.sslmode || PostgresTLSModes.verifyFull} |
||||
onChange={onJSONDataOptionSelected('sslmode')} |
||||
></Select> |
||||
</InlineField> |
||||
{options.jsonData.sslmode !== PostgresTLSModes.disable ? ( |
||||
<InlineField |
||||
labelWidth={labelWidthConnection} |
||||
label="TLS/SSL Method" |
||||
htmlFor="tlsMethod" |
||||
tooltip={ |
||||
<span> |
||||
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will |
||||
allow you to configure certificates by specifying paths to existing certificates on the local file |
||||
system where Grafana is running. Be sure that the file is readable by the user executing the Grafana |
||||
process. |
||||
<br /> |
||||
<br /> |
||||
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content. |
||||
The content will be stored encrypted in Grafana's database. When connecting to the database the |
||||
certificates will be written as files to Grafana's configured data path on the local file system. |
||||
</span> |
||||
} |
||||
> |
||||
<Select |
||||
options={tlsMethods} |
||||
inputId="tlsMethod" |
||||
value={jsonData.tlsConfigurationMethod || PostgresTLSMethods.filePath} |
||||
onChange={onJSONDataOptionSelected('tlsConfigurationMethod')} |
||||
></Select> |
||||
</InlineField> |
||||
) : null} |
||||
</FieldSet> |
||||
|
||||
{options.jsonData.sslmode !== 'disable' ? ( |
||||
<FieldSet label="TLS/SSL Auth Details"> |
||||
{options.jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? ( |
||||
<TLSSecretsConfig editorProps={props} labelWidth={labelWidthSSLDetails}></TLSSecretsConfig> |
||||
) : ( |
||||
<> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidthSSLDetails} |
||||
label="TLS/SSL Root Certificate" |
||||
> |
||||
<Input |
||||
value={jsonData.sslRootCertFile || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslRootCertFile')} |
||||
placeholder="TLS/SSL root cert file" |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
To authenticate with an TLS/SSL client certificate, provide the path to the file here. Be sure that |
||||
the file is readable by the user executing the grafana process. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidthSSLDetails} |
||||
label="TLS/SSL Client Certificate" |
||||
> |
||||
<Input |
||||
value={jsonData.sslCertFile || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslCertFile')} |
||||
placeholder="TLS/SSL client cert file" |
||||
></Input> |
||||
</InlineField> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file |
||||
here. Be sure that the file is <i>only</i> readable by the user executing the grafana process. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidthSSLDetails} |
||||
label="TLS/SSL Client Key" |
||||
> |
||||
<Input |
||||
value={jsonData.sslKeyFile || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslKeyFile')} |
||||
placeholder="TLS/SSL client key file" |
||||
></Input> |
||||
</InlineField> |
||||
</> |
||||
)} |
||||
</FieldSet> |
||||
) : null} |
||||
|
||||
<ConnectionLimits |
||||
labelWidth={labelWidthShort} |
||||
jsonData={jsonData} |
||||
onPropertyChanged={(property, value) => { |
||||
updateDatasourcePluginJsonDataOption(props, property, value); |
||||
}} |
||||
></ConnectionLimits> |
||||
|
||||
<FieldSet label="PostgreSQL details"> |
||||
<InlineField |
||||
tooltip="This option controls what functions are available in the PostgreSQL query builder" |
||||
labelWidth={labelWidthShort} |
||||
htmlFor="postgresVersion" |
||||
label="Version" |
||||
> |
||||
<Select |
||||
value={jsonData.postgresVersion || 903} |
||||
inputId="postgresVersion" |
||||
onChange={onJSONDataOptionSelected('postgresVersion')} |
||||
options={versionOptions} |
||||
></Select> |
||||
</InlineField> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use |
||||
<code>time_bucket</code> in the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate |
||||
functions in the query builder. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidthShort} |
||||
label="TimescaleDB" |
||||
htmlFor="timescaledb" |
||||
> |
||||
<InlineSwitch |
||||
id="timescaledb" |
||||
value={jsonData.timescaledb || false} |
||||
onChange={onTimeScaleDBChanged} |
||||
></InlineSwitch> |
||||
</InlineField> |
||||
<InlineField |
||||
tooltip={ |
||||
<span> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example |
||||
<code>1m</code> if your data is written every minute. |
||||
</span> |
||||
} |
||||
labelWidth={labelWidthShort} |
||||
label="Min time interval" |
||||
> |
||||
<Input |
||||
placeholder="1m" |
||||
value={jsonData.timeInterval || ''} |
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')} |
||||
></Input> |
||||
</InlineField> |
||||
</FieldSet> |
||||
|
||||
<Alert title="User Permission" severity="info"> |
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to |
||||
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, |
||||
statements like <code>DELETE FROM user;</code> and <code>DROP TABLE user;</code> would be executed. To protect |
||||
against this we |
||||
<strong>Highly</strong> recommmend you create a specific PostgreSQL user with restricted permissions. |
||||
</Alert> |
||||
</> |
||||
); |
||||
}; |
@ -0,0 +1,87 @@ |
||||
import { Dispatch, SetStateAction, useState } from 'react'; |
||||
import { useDeepCompareEffect } from 'react-use'; |
||||
|
||||
import { |
||||
DataSourcePluginOptionsEditorProps, |
||||
DataSourceSettings, |
||||
SelectableValue, |
||||
updateDatasourcePluginJsonDataOption, |
||||
updateDatasourcePluginOption, |
||||
} from '@grafana/data'; |
||||
import { getBackendSrv } from '@grafana/runtime'; |
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; |
||||
|
||||
import { PostgresDatasource } from '../datasource'; |
||||
import { PostgresOptions, PostgresTLSModes, SecureJsonData } from '../types'; |
||||
|
||||
import { postgresVersions } from './ConfigurationEditor'; |
||||
|
||||
type Options = { |
||||
props: DataSourcePluginOptionsEditorProps<PostgresOptions, SecureJsonData>; |
||||
setVersionOptions: Dispatch<SetStateAction<Array<SelectableValue<number>>>>; |
||||
}; |
||||
|
||||
export function useAutoDetectFeatures({ props, setVersionOptions }: Options) { |
||||
const [saved, setSaved] = useState(false); |
||||
const { options, onOptionsChange } = props; |
||||
|
||||
useDeepCompareEffect(() => { |
||||
const getVersion = async () => { |
||||
if (!saved) { |
||||
// We need to save the datasource before we can get the version so we can query the database with the options we have.
|
||||
const result = await getBackendSrv().put<{ datasource: DataSourceSettings }>( |
||||
`/api/datasources/${options.id}`, |
||||
options |
||||
); |
||||
|
||||
setSaved(true); |
||||
// This is needed or else we get an error when we try to save the datasource.
|
||||
updateDatasourcePluginOption({ options, onOptionsChange }, 'version', result.datasource.version); |
||||
} else { |
||||
const datasource = await getDatasourceSrv().loadDatasource(options.name); |
||||
|
||||
if (datasource instanceof PostgresDatasource) { |
||||
const version = await datasource.getVersion(); |
||||
const versionNumber = parseInt(version, 10); |
||||
|
||||
// timescaledb is only available for 9.6+
|
||||
if (versionNumber >= 906 && !options.jsonData.timescaledb) { |
||||
const timescaledbVersion = await datasource.getTimescaleDBVersion(); |
||||
if (timescaledbVersion?.length) { |
||||
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'timescaledb', true); |
||||
} |
||||
} |
||||
const major = Math.trunc(versionNumber / 100); |
||||
const minor = versionNumber % 100; |
||||
let name = String(major); |
||||
if (versionNumber < 1000) { |
||||
name = String(major) + '.' + String(minor); |
||||
} |
||||
if (!postgresVersions.find((p) => p.value === versionNumber)) { |
||||
setVersionOptions((prev) => [...prev, { label: name, value: versionNumber }]); |
||||
} |
||||
if (options.jsonData.postgresVersion === undefined || options.jsonData.postgresVersion !== versionNumber) { |
||||
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'postgresVersion', versionNumber); |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
// This logic is only going to run when we create a new datasource
|
||||
if (isValidConfig(options)) { |
||||
getVersion(); |
||||
} |
||||
}, [options, saved, setVersionOptions]); |
||||
} |
||||
|
||||
function isValidConfig(options: DataSourceSettings<PostgresOptions, SecureJsonData>) { |
||||
return ( |
||||
options.url && |
||||
options.database && |
||||
options.user && |
||||
(options.secureJsonData?.password || options.secureJsonFields?.password) && |
||||
(options.jsonData.sslmode === PostgresTLSModes.disable || |
||||
(options.jsonData.sslCertFile && options.jsonData.sslKeyFile && options.jsonData.sslRootCertFile)) && |
||||
!options.jsonData.postgresVersion && |
||||
!options.readOnly |
||||
); |
||||
} |
@ -1,196 +0,0 @@ |
||||
|
||||
<h3 class="page-heading">PostgreSQL Connection</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-10">Host</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.url' placeholder="localhost:5432" |
||||
bs-typeahead="{{['localhost:5432', 'localhost:5433']}}" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-10">Database</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.database' placeholder="database name" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-10">User</span> |
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<secret-form-field |
||||
isConfigured="ctrl.current.secureJsonFields.password" |
||||
value="ctrl.current.secureJsonData.password" |
||||
on-reset="ctrl.onPasswordReset" |
||||
on-change="ctrl.onPasswordChange" |
||||
inputWidth="9" |
||||
aria-label="'Password'" |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10" for="tls-mode-select">TLS/SSL Mode</label> |
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon"> |
||||
<select id="tls-mode-select" class="gf-form-input" ng-model="ctrl.current.jsonData.sslmode" |
||||
ng-options="mode for mode in ['disable', 'require', 'verify-ca', 'verify-full']" |
||||
ng-init="ctrl.current.jsonData.sslmode" ng-change="ctrl.tlsModeMapping()"></select> |
||||
<info-popover mode="right-absolute"> |
||||
This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form" ng-if="ctrl.current.jsonData.sslmode != 'disable'"> |
||||
<label class="gf-form-label width-10">TLS/SSL Method</label> |
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon"> |
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.tlsConfigurationMethod" |
||||
ng-options="f.id as f.label for f in [{ id: 'file-path', label: 'File system path' }, { id: 'file-content', label: 'Certificate content' }]" |
||||
ng-init="ctrl.current.jsonData.tlsConfigurationMethod"></select> |
||||
<info-popover mode="right-absolute"> |
||||
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will allow |
||||
you to configure certificates by specifying paths to existing certificates on the local file system where |
||||
Grafana is running. Be sure that the file is readable by the user executing the Grafana process.<br><br> |
||||
|
||||
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content. |
||||
The content will be stored encrypted in Grafana's database. When connecting to the database the certificates |
||||
will be written as files to Grafana's configured data path on the local file system. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-path'"> |
||||
<div class="gf-form"> |
||||
<h6>TLS/SSL Auth Details</h6> |
||||
</div> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-11">TLS/SSL Root Certificate</span> |
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model='ctrl.current.jsonData.sslRootCertFile' placeholder="TLS/SSL root cert file"></input> |
||||
<info-popover mode="right-absolute"> |
||||
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-11">TLS/SSL Client Certificate</span> |
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslCertFile' |
||||
placeholder="TLS/SSL client cert file"></input> |
||||
<info-popover mode="right-absolute"> |
||||
To authenticate with an TLS/SSL client certificate, provide the path to the file here. |
||||
Be sure that the file is readable by the user executing the grafana process. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-11">TLS/SSL Client Key</span> |
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslKeyFile' |
||||
placeholder="TLS/SSL client key file"></input> |
||||
<info-popover mode="right-absolute"> |
||||
To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file here. |
||||
Be sure that the file is <i>only</i> readable by the user executing the grafana process. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
<datasource-tls-auth-settings current="ctrl.current" |
||||
ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-content'"> |
||||
</datasource-tls-auth-settings> |
||||
|
||||
<b>Connection limits</b> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max open</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the |
||||
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be |
||||
reduced to match the <i>Max open connections</i> limit. If set to 0, there is no limit on the number of open |
||||
connections. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max idle</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but |
||||
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the |
||||
<i>Max open connections</i> limit. If set to 0, no idle connections are retained. |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form max-width-15"> |
||||
<span class="gf-form-label width-7">Max lifetime</span> |
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input> |
||||
<info-popover mode="right-absolute"> |
||||
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
|
||||
<h3 class="page-heading">PostgreSQL details</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-9" id="version-label"> |
||||
Version |
||||
<info-popover mode="right-normal" position="top center"> |
||||
This option controls what functions are available in the PostgreSQL query builder. |
||||
</info-popover> |
||||
</span> |
||||
<span class="gf-form-select-wrapper"> |
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.postgresVersion" |
||||
ng-options="f.value as f.name for f in ctrl.postgresVersions" aria-labelledby="version-label"></select> |
||||
</span> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<gf-form-switch class="gf-form" label="TimescaleDB" label-class="width-9" |
||||
checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch> |
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleTimescaleDBHelp()"> |
||||
Help |
||||
<icon name="'angle-down'" ng-show="ctrl.showTimescaleDBHelp"></icon> |
||||
<icon name="'angle-right'" ng-hide="ctrl.showTimescaleDBHelp"> </icon> |
||||
</label> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-9">Min time interval</span> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-6 gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.timeInterval" |
||||
spellcheck='false' |
||||
placeholder="1m" |
||||
ng-pattern="/^\d+(ms|[Mwdhmsy])$/" |
||||
></input> |
||||
<info-popover mode="right-absolute"> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, |
||||
for example <code>1m</code> if your data is written every minute. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
<div class="grafana-info-box alert alert-info" ng-show="ctrl.showTimescaleDBHelp"> |
||||
<div class="alert-body"> |
||||
<p> |
||||
<a href="https://github.com/timescale/timescaledb" class="pointer" target="_blank">TimescaleDB</a> is a |
||||
time-series database built as a PostgreSQL extension. If enabled, Grafana will use <code>time_bucket</code> in |
||||
the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate functions in the query builder. |
||||
</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="grafana-info-box"> |
||||
<h5>User Permission</h5> |
||||
<p> |
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to query. |
||||
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements |
||||
like <code>DELETE FROM user;</code> and <code>DROP TABLE user;</code> would be executed. To protect against this we |
||||
<strong>Highly</strong> recommmend you create a specific PostgreSQL user with restricted permissions. |
||||
</p> |
||||
</div> |
||||
</div> |
@ -1,21 +1,46 @@ |
||||
import { DataQuery, DataSourceJsonData } from '@grafana/data'; |
||||
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types'; |
||||
|
||||
export interface PostgresQueryForInterpolation { |
||||
alias?: any; |
||||
format?: any; |
||||
rawSql?: any; |
||||
refId: any; |
||||
hide?: any; |
||||
export enum PostgresTLSModes { |
||||
disable = 'disable', |
||||
require = 'require', |
||||
verifyCA = 'verify-ca', |
||||
verifyFull = 'verify-full', |
||||
} |
||||
|
||||
export interface PostgresOptions extends DataSourceJsonData { |
||||
export enum PostgresTLSMethods { |
||||
filePath = 'file-path', |
||||
fileContent = 'file-content', |
||||
} |
||||
export interface PostgresOptions extends DataSourceJsonData, SQLConnectionLimits { |
||||
url: string; |
||||
timeInterval: string; |
||||
database: string; |
||||
user: string; |
||||
tlsConfigurationMethod: PostgresTLSMethods; |
||||
sslmode: PostgresTLSModes; |
||||
sslRootCertFile: string; |
||||
sslCertFile: string; |
||||
sslKeyFile: string; |
||||
postgresVersion: number; |
||||
timescaledb: boolean; |
||||
} |
||||
|
||||
export type ResultFormat = 'time_series' | 'table'; |
||||
export interface SecureJsonData { |
||||
password: string; |
||||
} |
||||
|
||||
export type ResultFormat = 'time_series' | 'table'; |
||||
export interface PostgresQuery extends DataQuery { |
||||
alias?: string; |
||||
format?: ResultFormat; |
||||
rawSql?: any; |
||||
} |
||||
|
||||
export interface PostgresQueryForInterpolation { |
||||
alias?: any; |
||||
format?: any; |
||||
rawSql?: any; |
||||
refId: any; |
||||
hide?: any; |
||||
} |
||||
|
Loading…
Reference in new issue