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>
This commit is contained in:
Oscar Kilhed 2022-07-14 13:29:08 +02:00 committed by GitHub
parent 77e87f1806
commit 9498ee3d54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1068 additions and 695 deletions

View File

@ -7775,10 +7775,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mssql/config_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mssql/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -7845,8 +7841,7 @@ exports[`better eslint`] = {
],
"public/app/plugins/datasource/mysql/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mysql/mysql_query_model.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -8049,15 +8044,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/opentsdb/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/postgres/config_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/plugins/datasource/postgres/datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -8078,9 +8064,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
[0, 0, 0, "Unexpected any. Specify a different type.", "14"]
],
"public/app/plugins/datasource/postgres/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -462,7 +462,7 @@ datasources:
timescaledb: false
```
> **Note:** In the above code, the `postgresVersion` value of `10` refers to version PotgreSQL 10 and above.
> **Note:** In the above code, the `postgresVersion` value of `10` refers to version PostgreSQL 10 and above.
If you encounter metric request errors or other issues:

View File

@ -56,7 +56,7 @@ export const onUpdateDatasourceJsonDataOption =
export const onUpdateDatasourceSecureJsonDataOption =
<J, S extends {} = KeyValue>(props: DataSourcePluginOptionsEditorProps<J, S>, key: string) =>
(event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => {
(event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
updateDatasourcePluginSecureJsonDataOption(props, key, event.currentTarget.value);
};

View File

@ -212,6 +212,8 @@ export { Input, getInputStyles } from './Input/Input';
export { AutoSizeInput } from './Input/AutoSizeInput';
export { FilterInput } from './FilterInput/FilterInput';
export { FormInputSize } from './Forms/types';
export * from './SecretInput';
export * from './SecretTextArea';
export { Switch, InlineSwitch } from './Switch/Switch';
export { Checkbox } from './Forms/Checkbox';

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export interface SQLConnectionLimits {
maxOpenConns: number;
maxIdleConns: number;
connMaxLifetime: number;
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { DataSourcePlugin } from '@grafana/data';
import { MssqlConfigCtrl } from './config_ctrl';
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
import { MssqlDatasource } from './datasource';
import { MssqlQueryCtrl } from './query_ctrl';
import { MssqlQuery } from './types';
@ -30,5 +30,5 @@ class MssqlAnnotationsQueryCtrl {
export const plugin = new DataSourcePlugin<MssqlDatasource, MssqlQuery>(MssqlDatasource)
.setQueryCtrl(MssqlQueryCtrl)
.setConfigCtrl(MssqlConfigCtrl)
.setConfigEditor(ConfigurationEditor)
.setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl);

View File

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

View File

@ -1,4 +1,5 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
export interface MssqlQueryForInterpolation {
alias?: any;
@ -16,6 +17,24 @@ export interface MssqlQuery extends DataQuery {
rawSql?: any;
}
export interface MssqlOptions extends DataSourceJsonData {
timeInterval: string;
export enum MSSQLAuthenticationType {
sqlAuth = 'SQL Server Authentication',
windowsAuth = 'Windows Authentication',
}
export enum MSSQLEncryptOptions {
disable = 'disable',
false = 'false',
true = 'true',
}
export interface MssqlOptions extends DataSourceJsonData, SQLConnectionLimits {
authenticationType: MSSQLAuthenticationType;
encrypt: MSSQLEncryptOptions;
serverName: string;
sslRootCertFile: string;
tlsSkipVerify: boolean;
url: string;
database: string;
timeInterval: string;
user: string;
}

View File

@ -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=&apos;...&apos;</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 &amp; 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>
</>
);
};

View File

@ -1,27 +1,10 @@
import { DataSourcePlugin } from '@grafana/data';
import {
createChangeHandler,
createResetHandler,
PasswordFieldEnum,
} from '../../../features/datasources/utils/passwordHandlers';
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
import { MysqlDatasource } from './datasource';
import { MysqlQueryCtrl } from './query_ctrl';
import { MySQLQuery } from './types';
class MysqlConfigCtrl {
static templateUrl = 'partials/config.html';
current: any;
onPasswordReset: ReturnType<typeof createResetHandler>;
onPasswordChange: ReturnType<typeof createChangeHandler>;
constructor() {
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
}
}
const defaultQuery = `SELECT
UNIX_TIMESTAMP(<time_column>) as time_sec,
<text_column> as text,
@ -48,11 +31,10 @@ export {
MysqlDatasource,
MysqlDatasource as Datasource,
MysqlQueryCtrl as QueryCtrl,
MysqlConfigCtrl as ConfigCtrl,
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};
export const plugin = new DataSourcePlugin<MysqlDatasource, MySQLQuery>(MysqlDatasource)
.setQueryCtrl(MysqlQueryCtrl)
.setConfigCtrl(MysqlConfigCtrl)
.setConfigEditor(ConfigurationEditor)
.setAnnotationQueryCtrl(MysqlAnnotationsQueryCtrl);

View File

@ -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 &amp; 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>

View File

@ -1,4 +1,5 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
export interface MysqlQueryForInterpolation {
alias?: any;
format?: any;
@ -7,7 +8,14 @@ export interface MysqlQueryForInterpolation {
hide?: any;
}
export interface MySQLOptions extends DataSourceJsonData {
export interface MySQLOptions extends DataSourceJsonData, SQLConnectionLimits {
tlsAuth: boolean;
tlsAuthWithCACert: boolean;
timezone: string;
tlsSkipVerify: boolean;
user: string;
database: string;
url: string;
timeInterval: string;
}

View File

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

View File

@ -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&apos;s database. When connecting to the database the
certificates will be written as files to Grafana&apos;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 &amp; 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>
</>
);
};

View File

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

View File

@ -184,12 +184,25 @@ export class PostgresDatasource extends DataSourceWithBackend<PostgresQuery, Pos
});
}
getVersion(): Promise<any> {
return lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100"));
async getVersion(): Promise<string> {
const value = await lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100"));
const results = value.data.results['meta'];
if (results.frames) {
// This returns number
return results.frames[0].data?.values[0][0].toString();
}
return '';
}
getTimescaleDBVersion(): Promise<any> {
return lastValueFrom(this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"));
async getTimescaleDBVersion(): Promise<string[] | undefined> {
const value = await lastValueFrom(
this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'")
);
const results = value.data.results['meta'];
if (results.frames) {
return results.frames[0].data?.values[0][0];
}
return undefined;
}
testDatasource(): Promise<any> {

View File

@ -1,9 +1,9 @@
import { DataSourcePlugin } from '@grafana/data';
import { PostgresConfigCtrl } from './config_ctrl';
import { PostgresConfigEditor } from './configuration/ConfigurationEditor';
import { PostgresDatasource } from './datasource';
import { PostgresQueryCtrl } from './query_ctrl';
import { PostgresQuery } from './types';
import { PostgresOptions, PostgresQuery, SecureJsonData } from './types';
const defaultQuery = `SELECT
extract(epoch from time_column) AS time,
@ -27,7 +27,9 @@ class PostgresAnnotationsQueryCtrl {
}
}
export const plugin = new DataSourcePlugin<PostgresDatasource, PostgresQuery>(PostgresDatasource)
export const plugin = new DataSourcePlugin<PostgresDatasource, PostgresQuery, PostgresOptions, SecureJsonData>(
PostgresDatasource
)
.setQueryCtrl(PostgresQueryCtrl)
.setConfigCtrl(PostgresConfigCtrl)
.setConfigEditor(PostgresConfigEditor)
.setAnnotationQueryCtrl(PostgresAnnotationsQueryCtrl);

View File

@ -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&nbsp;
<icon name="'angle-down'" ng-show="ctrl.showTimescaleDBHelp"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showTimescaleDBHelp">&nbsp;</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 &amp; 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>

View File

@ -1,4 +1,41 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
export enum PostgresTLSModes {
disable = 'disable',
require = 'require',
verifyCA = 'verify-ca',
verifyFull = 'verify-full',
}
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 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;
@ -7,15 +44,3 @@ export interface PostgresQueryForInterpolation {
refId: any;
hide?: any;
}
export interface PostgresOptions extends DataSourceJsonData {
timeInterval: string;
}
export type ResultFormat = 'time_series' | 'table';
export interface PostgresQuery extends DataQuery {
alias?: string;
format?: ResultFormat;
rawSql?: any;
}