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