diff --git a/.betterer.results b/.betterer.results index bb458de4bdc..cb6e137d371 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/docs/sources/datasources/postgres.md b/docs/sources/datasources/postgres.md index 27f37a6c3a6..1f3a7881bc1 100644 --- a/docs/sources/datasources/postgres.md +++ b/docs/sources/datasources/postgres.md @@ -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: diff --git a/packages/grafana-data/src/utils/datasource.ts b/packages/grafana-data/src/utils/datasource.ts index 95f5ec9af0d..627a4bf8541 100644 --- a/packages/grafana-data/src/utils/datasource.ts +++ b/packages/grafana-data/src/utils/datasource.ts @@ -56,7 +56,7 @@ export const onUpdateDatasourceJsonDataOption = export const onUpdateDatasourceSecureJsonDataOption = (props: DataSourcePluginOptionsEditorProps, key: string) => - (event: React.SyntheticEvent) => { + (event: React.SyntheticEvent) => { updateDatasourcePluginSecureJsonDataOption(props, key, event.currentTarget.value); }; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index e9dd8b85ada..7d5605ebc2c 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx b/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx new file mode 100644 index 00000000000..5b9dad73c73 --- /dev/null +++ b/public/app/features/plugins/sql/components/configuration/ConnectionLimits.tsx @@ -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 { + onPropertyChanged: (property: keyof T, value?: number) => void; + labelWidth: number; + jsonData: SQLConnectionLimits; +} + +export const ConnectionLimits = (props: Props) => { + const { onPropertyChanged, labelWidth, jsonData } = props; + + const onJSONDataNumberChanged = (property: keyof SQLConnectionLimits) => { + return (number?: number) => { + if (onPropertyChanged) { + onPropertyChanged(property, number); + } + }; + }; + + return ( +
+ + The maximum number of open connections to the database.If Max idle connections is greater than 0 and + the Max open connections is less than Max idle connections, then + Max idle connections will be reduced to match the Max open connections limit. If set to 0, + there is no limit on the number of open connections. + + } + labelWidth={labelWidth} + label="Max open" + > + + + + The maximum number of connections in the idle connection pool.If Max open connections is greater than + 0 but less than the Max idle connections, then the Max idle connections will be reduced to + match the Max open connections limit. If set to 0, no idle connections are retained. + + } + labelWidth={labelWidth} + label="Max idle" + > + + + + + +
+ ); +}; diff --git a/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx b/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx new file mode 100644 index 00000000000..e3912bf0ff3 --- /dev/null +++ b/public/app/features/plugins/sql/components/configuration/TLSSecretsConfig.tsx @@ -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 { + editorProps: DataSourcePluginOptionsEditorProps; + showCACert?: boolean; + secureJsonFields?: KeyValue; + labelWidth?: number; +} + +export const TLSSecretsConfig = (props: Props) => { + const { labelWidth, editorProps, showCACert } = props; + const { secureJsonFields } = editorProps.options; + return ( + <> + To authenticate with an TLS/SSL client certificate, provide the client certificate here.} + labelWidth={labelWidth} + label="TLS/SSL Client Certificate" + > + { + updateDatasourcePluginResetOption(editorProps, 'tlsClientCert'); + }} + > + + {showCACert ? ( + If the selected TLS/SSL mode requires a server root certificate, provide it here.} + labelWidth={labelWidth} + label="TLS/SSL Root Certificate" + > + { + updateDatasourcePluginResetOption(editorProps, 'tlsCACert'); + }} + > + + ) : null} + + To authenticate with a client TLS/SSL certificate, provide the key here.} + labelWidth={labelWidth} + label="TLS/SSL Client Key" + > + { + updateDatasourcePluginResetOption(editorProps, 'tlsClientKey'); + }} + > + + + ); +}; diff --git a/public/app/features/plugins/sql/components/configuration/types.ts b/public/app/features/plugins/sql/components/configuration/types.ts new file mode 100644 index 00000000000..00678993762 --- /dev/null +++ b/public/app/features/plugins/sql/components/configuration/types.ts @@ -0,0 +1,5 @@ +export interface SQLConnectionLimits { + maxOpenConns: number; + maxIdleConns: number; + connMaxLifetime: number; +} diff --git a/public/app/plugins/datasource/mssql/config_ctrl.ts b/public/app/plugins/datasource/mssql/config_ctrl.ts deleted file mode 100644 index 0bc9df6e95b..00000000000 --- a/public/app/plugins/datasource/mssql/config_ctrl.ts +++ /dev/null @@ -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; - onPasswordChange: ReturnType; - 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; - } -} diff --git a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx new file mode 100644 index 00000000000..911b8f60d04 --- /dev/null +++ b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx @@ -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) => { + 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) => { + onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } }); + }; + }; + + const onSkipTLSVerifyChanged = (event: SyntheticEvent) => { + 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> = [ + { value: MSSQLAuthenticationType.sqlAuth, label: 'SQL Server Authentication' }, + { value: MSSQLAuthenticationType.windowsAuth, label: 'Windows Authentication' }, + ]; + + const encryptOptions: Array> = [ + { 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 ( + <> +
+ + + + + + + +
  • + SQL Server Authentication 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. +
  • +
  • + Windows Authentication Windows Integrated Security - single sign on for users who are already + logged onto Windows and have enabled this option for MS SQL Server. +
  • + + } + > + +
    + {jsonData.authenticationType === MSSQLAuthenticationType.windowsAuth ? null : ( + + + + + + + + + )} +
    + +
    + + Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. +
      +
    • + disable - Data sent between client and server is not encrypted. +
    • +
    • + false - Data sent between client and server is not encrypted beyond the login packet. (default) +
    • +
    • + true - Data sent between client and server is encrypted. +
    • +
    + 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" + > + +
    + + {jsonData.encrypt === MSSQLEncryptOptions.true ? ( + <> + + + + {jsonData.tlsSkipVerify ? null : ( + <> + + 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. + + } + label="TLS/SSL Root Certificate" + > + + + + + + + )} + + ) : null} +
    + + { + updateDatasourcePluginJsonDataOption(props, property, value); + }} + > + +
    + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example + 1m if your data is written every minute. + + } + label="Min time interval" + > + + +
    + + + 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 USE otherdb; and DROP TABLE user; would be executed. To protect + against this we highly recommmend you create a specific MS SQL user with restricted permissions. + + + ); +}; + +function getStyles(theme: GrafanaTheme2) { + return { + ulPadding: css({ + margin: theme.spacing(1, 0), + paddingLeft: theme.spacing(5), + }), + }; +} diff --git a/public/app/plugins/datasource/mssql/module.ts b/public/app/plugins/datasource/mssql/module.ts index 2426595a230..bc6efb99f7e 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -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) .setQueryCtrl(MssqlQueryCtrl) - .setConfigCtrl(MssqlConfigCtrl) + .setConfigEditor(ConfigurationEditor) .setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/mssql/partials/config.html b/public/app/plugins/datasource/mssql/partials/config.html deleted file mode 100644 index 680ba4dd75b..00000000000 --- a/public/app/plugins/datasource/mssql/partials/config.html +++ /dev/null @@ -1,161 +0,0 @@ -

    MS SQL connection

    - -
    -
    - Host - -
    - -
    - Database - -
    - -
    - -
    - - -
      -
    • SQL Server Authentication 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.
    • -
    • Windows Authentication Windows Integrated Security - single sign on for users who are already - logged onto Windows and have enabled this option for MS SQL Server.
    • -
    -
    -
    -
    -
    -
    - User - -
    -
    - -
    -
    - -
    - -

    TLS/SSL Auth

    - -
    - -
    - -
    - - - Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. -
      -
    • disable - Data sent between client and server is not encrypted.
    • -
    • false - Data sent between client and server is not encrypted beyond the login packet. (default) -
    • -
    • true - Data sent between client and server is encrypted.
    • -
    - 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. -
    -
    -
    - -
    - - -
    - -
    - TLS/SSL Root Certificate - - - 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. - -
    - -
    - Hostname in server certificate - - - Specifies the Common Name (CN) in the server certificate. Default is the server host. - -
    - -
    - -

    Connection limits

    - -
    -
    - Max open - - - The maximum number of open connections to the database. If Max idle connections is greater than 0 and the - Max open connections is less than Max idle connections, then Max idle connections will be - reduced to match the Max open connections limit. If set to 0, there is no limit on the number of open - connections. - -
    -
    - Max idle - - - The maximum number of connections in the idle connection pool. If Max open connections is greater than 0 - but - less than the Max idle connections, then the Max idle connections will be reduced to match the - Max open connections limit. If set to 0, no idle connections are retained. - -
    -
    - Max lifetime - - - The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever. - -
    -
    - -

    MS SQL details

    - -
    -
    -
    - Min time interval - - - A lower limit for the auto group by time interval. Recommended to be set to write frequency, - for example 1m if your data is written every minute. - -
    -
    -
    - -
    -
    -
    User Permission
    -

    - 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 USE otherdb; and DROP TABLE user; would be executed. To protect against this we - highly recommmend you create a specific MS SQL user with restricted permissions. -

    -
    -
    diff --git a/public/app/plugins/datasource/mssql/types.ts b/public/app/plugins/datasource/mssql/types.ts index 8359e566f32..58ffb949a28 100644 --- a/public/app/plugins/datasource/mssql/types.ts +++ b/public/app/plugins/datasource/mssql/types.ts @@ -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 { +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; } diff --git a/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx new file mode 100644 index 00000000000..e26efcfc1ba --- /dev/null +++ b/public/app/plugins/datasource/mysql/configuration/ConfigurationEditor.tsx @@ -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) => { + const { options, onOptionsChange } = props; + const jsonData = options.jsonData; + + const onResetPassword = () => { + updateDatasourcePluginResetOption(props, 'password'); + }; + + const onDSOptionChanged = (property: keyof MySQLOptions) => { + return (event: SyntheticEvent) => { + onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } }); + }; + }; + + const onSwitchChanged = (property: keyof MySQLOptions) => { + return (event: SyntheticEvent) => { + updateDatasourcePluginJsonDataOption(props, property, event.currentTarget.checked); + }; + }; + + const mediumWidth = 20; + const shortWidth = 15; + const longWidth = 40; + + return ( + <> +
    + + + + + + + + + + + + + + + + Specify the time zone used in the database session, e.g. Europe/Berlin or + +02:00. 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 + SET time_zone='...'. If you leave this field empty, the timezone is not updated. + You can find more information in the MySQL documentation. + + } + label="Session timezone" + labelWidth={mediumWidth} + > + + + + + + + + + + + + + +
    + + {options.jsonData.tlsAuth ? ( +
    + +
    + ) : null} + + { + updateDatasourcePluginJsonDataOption(props, property, value); + }} + > + +
    + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example + 1m if your data is written every minute. + + } + labelWidth={mediumWidth} + label="Min time interval" + > + + +
    + + + 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 USE otherdb; and DROP TABLE user; would be executed. To protect + against this we + Highly recommmend you create a specific MySQL user with restricted permissions. Checkout the{' '} + + MySQL Data Source Docs + + for more information. + + + ); +}; diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index e412a473cc5..7044b6dd35b 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -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; - onPasswordChange: ReturnType; - - constructor() { - this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); - this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); - } -} - const defaultQuery = `SELECT UNIX_TIMESTAMP() as time_sec, 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) .setQueryCtrl(MysqlQueryCtrl) - .setConfigCtrl(MysqlConfigCtrl) + .setConfigEditor(ConfigurationEditor) .setAnnotationQueryCtrl(MysqlAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html deleted file mode 100644 index c8d8afbc64f..00000000000 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ /dev/null @@ -1,132 +0,0 @@ -

    MySQL Connection

    - -
    -
    - Host - -
    - -
    - Database - -
    - -
    -
    - User - -
    -
    - -
    -
    - -
    - Session Timezone - - - Specify the time zone used in the database session, e.g. Europe/Berlin or +02:00. - 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 SET time_zone='...'. If you leave this field empty, - the timezone is not updated. You can find more information in the - MySQL documentation. - -
    - -
    -
    - - -
    -
    - -
    -
    - - - - -Connection limits - -
    -
    - Max open - - - The maximum number of open connections to the database. If Max idle connections is greater than 0 and the - Max open connections is less than Max idle connections, then Max idle connections will be - reduced to match the Max open connections limit. If set to 0, there is no limit on the number of open - connections. - -
    -
    - Max idle - - - The maximum number of connections in the idle connection pool. If Max open connections is greater than 0 but - less than the Max idle connections, then the Max idle connections will be reduced to match the - Max open connections limit. If set to 0, no idle connections are retained. - -
    -
    - Max lifetime - - - The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.

    - This should always be lower than configured wait_timeout in MySQL. -
    -
    -
    - -

    MySQL details

    - -
    -
    -
    - Min time interval - - - A lower limit for the auto group by time interval. Recommended to be set to write frequency, - for example 1m if your data is written every minute. - -
    -
    -
    - -
    -
    -
    User Permission
    -

    - 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 USE otherdb; and DROP TABLE user; would be executed. To protect against this we - Highly recommmend you create a specific MySQL user with restricted permissions. - - Checkout the MySQL Data Source Docs for more information. -

    -
    -
    diff --git a/public/app/plugins/datasource/mysql/types.ts b/public/app/plugins/datasource/mysql/types.ts index 2f24c8e7db5..88b20f3aade 100644 --- a/public/app/plugins/datasource/mysql/types.ts +++ b/public/app/plugins/datasource/mysql/types.ts @@ -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; } diff --git a/public/app/plugins/datasource/postgres/config_ctrl.ts b/public/app/plugins/datasource/postgres/config_ctrl.ts deleted file mode 100644 index f2bc26e8da6..00000000000 --- a/public/app/plugins/datasource/postgres/config_ctrl.ts +++ /dev/null @@ -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; - onPasswordChange: ReturnType; - - /** @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 }, - ]; -} diff --git a/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx new file mode 100644 index 00000000000..dba4997f55c --- /dev/null +++ b/public/app/plugins/datasource/postgres/configuration/ConfigurationEditor.tsx @@ -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> = [ + { 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) => { + const [versionOptions, setVersionOptions] = useState(postgresVersions); + + useAutoDetectFeatures({ props, setVersionOptions }); + + const { options, onOptionsChange } = props; + const jsonData = options.jsonData; + + const onResetPassword = () => { + updateDatasourcePluginResetOption(props, 'password'); + }; + + const tlsModes: Array> = [ + { 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> = [ + { 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) => { + updateDatasourcePluginJsonDataOption(props, 'timescaledb', event.currentTarget.checked); + }; + + const onDSOptionChanged = (property: keyof PostgresOptions) => { + return (event: SyntheticEvent) => { + onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } }); + }; + }; + + const labelWidthSSLDetails = 25; + const labelWidthConnection = 20; + const labelWidthShort = 20; + + return ( + <> +
    + + + + + + + + + + + + + + + + + + {options.jsonData.sslmode !== PostgresTLSModes.disable ? ( + + This option determines how TLS/SSL certifications are configured. Selecting File system path 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. +
    +
    + Selecting Certificate content 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. + + } + > + +
    + ) : null} +
    + + {options.jsonData.sslmode !== 'disable' ? ( +
    + {options.jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? ( + + ) : ( + <> + + If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here. + + } + labelWidth={labelWidthSSLDetails} + label="TLS/SSL Root Certificate" + > + + + + 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. + + } + labelWidth={labelWidthSSLDetails} + label="TLS/SSL Client Certificate" + > + + + + To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file + here. Be sure that the file is only readable by the user executing the grafana process. + + } + labelWidth={labelWidthSSLDetails} + label="TLS/SSL Client Key" + > + + + + )} +
    + ) : null} + + { + updateDatasourcePluginJsonDataOption(props, property, value); + }} + > + +
    + + + + + TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use + time_bucket in the $__timeGroup macro and display TimescaleDB specific aggregate + functions in the query builder. + + } + labelWidth={labelWidthShort} + label="TimescaleDB" + htmlFor="timescaledb" + > + + + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example + 1m if your data is written every minute. + + } + labelWidth={labelWidthShort} + label="Min time interval" + > + + +
    + + + 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 DELETE FROM user; and DROP TABLE user; would be executed. To protect + against this we + Highly recommmend you create a specific PostgreSQL user with restricted permissions. + + + ); +}; diff --git a/public/app/plugins/datasource/postgres/configuration/useAutoDetectFeatures.ts b/public/app/plugins/datasource/postgres/configuration/useAutoDetectFeatures.ts new file mode 100644 index 00000000000..be52d37ffa6 --- /dev/null +++ b/public/app/plugins/datasource/postgres/configuration/useAutoDetectFeatures.ts @@ -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; + setVersionOptions: Dispatch>>>; +}; + +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) { + 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 + ); +} diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 073b3306a55..6cb7a122c5b 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -184,12 +184,25 @@ export class PostgresDatasource extends DataSourceWithBackend { - return lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100")); + async getVersion(): Promise { + 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 { - return lastValueFrom(this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'")); + async getTimescaleDBVersion(): Promise { + 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 { diff --git a/public/app/plugins/datasource/postgres/module.ts b/public/app/plugins/datasource/postgres/module.ts index 28891a1e862..2ac3b2c7c84 100644 --- a/public/app/plugins/datasource/postgres/module.ts +++ b/public/app/plugins/datasource/postgres/module.ts @@ -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) +export const plugin = new DataSourcePlugin( + PostgresDatasource +) .setQueryCtrl(PostgresQueryCtrl) - .setConfigCtrl(PostgresConfigCtrl) + .setConfigEditor(PostgresConfigEditor) .setAnnotationQueryCtrl(PostgresAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html deleted file mode 100644 index 9b3b0f2fee2..00000000000 --- a/public/app/plugins/datasource/postgres/partials/config.html +++ /dev/null @@ -1,196 +0,0 @@ - -

    PostgreSQL Connection

    - -
    -
    - Host - -
    - -
    - Database - -
    - -
    -
    - User - -
    -
    - -
    -
    - -
    - -
    - - - This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server. - -
    -
    - -
    - -
    - - - This option determines how TLS/SSL certifications are configured. Selecting File system path 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.

    - - Selecting Certificate content 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. -
    -
    -
    -
    - -
    -
    -
    TLS/SSL Auth Details
    -
    -
    - TLS/SSL Root Certificate - - - If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here. - -
    -
    - TLS/SSL Client Certificate - - - 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. - -
    -
    - TLS/SSL Client Key - - - To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file here. - Be sure that the file is only readable by the user executing the grafana process. - -
    -
    - - - -Connection limits - -
    -
    - Max open - - - The maximum number of open connections to the database. If Max idle connections is greater than 0 and the - Max open connections is less than Max idle connections, then Max idle connections will be - reduced to match the Max open connections limit. If set to 0, there is no limit on the number of open - connections. - -
    -
    - Max idle - - - The maximum number of connections in the idle connection pool. If Max open connections is greater than 0 but - less than the Max idle connections, then the Max idle connections will be reduced to match the - Max open connections limit. If set to 0, no idle connections are retained. - -
    -
    - Max lifetime - - - The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever. - -
    -
    - -

    PostgreSQL details

    - -
    -
    - - Version - - This option controls what functions are available in the PostgreSQL query builder. - - - - - -
    -
    - - -
    - -
    -
    - Min time interval - - - A lower limit for the auto group by time interval. Recommended to be set to write frequency, - for example 1m if your data is written every minute. - -
    -
    -
    -
    -

    - TimescaleDB is a - time-series database built as a PostgreSQL extension. If enabled, Grafana will use time_bucket in - the $__timeGroup macro and display TimescaleDB specific aggregate functions in the query builder. -

    -
    -
    -
    - -
    -
    -
    User Permission
    -

    - 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 DELETE FROM user; and DROP TABLE user; would be executed. To protect against this we - Highly recommmend you create a specific PostgreSQL user with restricted permissions. -

    -
    -
    diff --git a/public/app/plugins/datasource/postgres/types.ts b/public/app/plugins/datasource/postgres/types.ts index 8a9c679840d..7f5cdc1f18d 100644 --- a/public/app/plugins/datasource/postgres/types.ts +++ b/public/app/plugins/datasource/postgres/types.ts @@ -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; +}