mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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.", "1"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/mysql/mysql_query_model.ts:5381": [
|
"public/app/plugins/datasource/mysql/mysql_query_model.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[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": [
|
"public/app/plugins/datasource/opentsdb/types.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[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, "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.", "12"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
[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.", "14"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/postgres/module.ts:5381": [
|
"public/app/plugins/datasource/postgres/module.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
@ -462,7 +462,7 @@ datasources:
|
|||||||
timescaledb: false
|
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:
|
If you encounter metric request errors or other issues:
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export const onUpdateDatasourceJsonDataOption =
|
|||||||
|
|
||||||
export const onUpdateDatasourceSecureJsonDataOption =
|
export const onUpdateDatasourceSecureJsonDataOption =
|
||||||
<J, S extends {} = KeyValue>(props: DataSourcePluginOptionsEditorProps<J, S>, key: string) =>
|
<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);
|
updateDatasourcePluginSecureJsonDataOption(props, key, event.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -212,6 +212,8 @@ export { Input, getInputStyles } from './Input/Input';
|
|||||||
export { AutoSizeInput } from './Input/AutoSizeInput';
|
export { AutoSizeInput } from './Input/AutoSizeInput';
|
||||||
export { FilterInput } from './FilterInput/FilterInput';
|
export { FilterInput } from './FilterInput/FilterInput';
|
||||||
export { FormInputSize } from './Forms/types';
|
export { FormInputSize } from './Forms/types';
|
||||||
|
export * from './SecretInput';
|
||||||
|
export * from './SecretTextArea';
|
||||||
|
|
||||||
export { Switch, InlineSwitch } from './Switch/Switch';
|
export { Switch, InlineSwitch } from './Switch/Switch';
|
||||||
export { Checkbox } from './Forms/Checkbox';
|
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 { DataSourcePlugin } from '@grafana/data';
|
||||||
|
|
||||||
import { MssqlConfigCtrl } from './config_ctrl';
|
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
|
||||||
import { MssqlDatasource } from './datasource';
|
import { MssqlDatasource } from './datasource';
|
||||||
import { MssqlQueryCtrl } from './query_ctrl';
|
import { MssqlQueryCtrl } from './query_ctrl';
|
||||||
import { MssqlQuery } from './types';
|
import { MssqlQuery } from './types';
|
||||||
@ -30,5 +30,5 @@ class MssqlAnnotationsQueryCtrl {
|
|||||||
|
|
||||||
export const plugin = new DataSourcePlugin<MssqlDatasource, MssqlQuery>(MssqlDatasource)
|
export const plugin = new DataSourcePlugin<MssqlDatasource, MssqlQuery>(MssqlDatasource)
|
||||||
.setQueryCtrl(MssqlQueryCtrl)
|
.setQueryCtrl(MssqlQueryCtrl)
|
||||||
.setConfigCtrl(MssqlConfigCtrl)
|
.setConfigEditor(ConfigurationEditor)
|
||||||
.setAnnotationQueryCtrl(MssqlAnnotationsQueryCtrl);
|
.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 { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||||
|
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
|
||||||
|
|
||||||
export interface MssqlQueryForInterpolation {
|
export interface MssqlQueryForInterpolation {
|
||||||
alias?: any;
|
alias?: any;
|
||||||
@ -16,6 +17,24 @@ export interface MssqlQuery extends DataQuery {
|
|||||||
rawSql?: any;
|
rawSql?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MssqlOptions extends DataSourceJsonData {
|
export enum MSSQLAuthenticationType {
|
||||||
timeInterval: string;
|
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 { DataSourcePlugin } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
|
||||||
createChangeHandler,
|
|
||||||
createResetHandler,
|
|
||||||
PasswordFieldEnum,
|
|
||||||
} from '../../../features/datasources/utils/passwordHandlers';
|
|
||||||
|
|
||||||
import { MysqlDatasource } from './datasource';
|
import { MysqlDatasource } from './datasource';
|
||||||
import { MysqlQueryCtrl } from './query_ctrl';
|
import { MysqlQueryCtrl } from './query_ctrl';
|
||||||
import { MySQLQuery } from './types';
|
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
|
const defaultQuery = `SELECT
|
||||||
UNIX_TIMESTAMP(<time_column>) as time_sec,
|
UNIX_TIMESTAMP(<time_column>) as time_sec,
|
||||||
<text_column> as text,
|
<text_column> as text,
|
||||||
@ -48,11 +31,10 @@ export {
|
|||||||
MysqlDatasource,
|
MysqlDatasource,
|
||||||
MysqlDatasource as Datasource,
|
MysqlDatasource as Datasource,
|
||||||
MysqlQueryCtrl as QueryCtrl,
|
MysqlQueryCtrl as QueryCtrl,
|
||||||
MysqlConfigCtrl as ConfigCtrl,
|
|
||||||
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
MysqlAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const plugin = new DataSourcePlugin<MysqlDatasource, MySQLQuery>(MysqlDatasource)
|
export const plugin = new DataSourcePlugin<MysqlDatasource, MySQLQuery>(MysqlDatasource)
|
||||||
.setQueryCtrl(MysqlQueryCtrl)
|
.setQueryCtrl(MysqlQueryCtrl)
|
||||||
.setConfigCtrl(MysqlConfigCtrl)
|
.setConfigEditor(ConfigurationEditor)
|
||||||
.setAnnotationQueryCtrl(MysqlAnnotationsQueryCtrl);
|
.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 { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||||
|
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
|
||||||
export interface MysqlQueryForInterpolation {
|
export interface MysqlQueryForInterpolation {
|
||||||
alias?: any;
|
alias?: any;
|
||||||
format?: any;
|
format?: any;
|
||||||
@ -7,7 +8,14 @@ export interface MysqlQueryForInterpolation {
|
|||||||
hide?: any;
|
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;
|
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> {
|
async getVersion(): Promise<string> {
|
||||||
return lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100"));
|
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> {
|
async getTimescaleDBVersion(): Promise<string[] | undefined> {
|
||||||
return lastValueFrom(this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"));
|
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> {
|
testDatasource(): Promise<any> {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { DataSourcePlugin } from '@grafana/data';
|
import { DataSourcePlugin } from '@grafana/data';
|
||||||
|
|
||||||
import { PostgresConfigCtrl } from './config_ctrl';
|
import { PostgresConfigEditor } from './configuration/ConfigurationEditor';
|
||||||
import { PostgresDatasource } from './datasource';
|
import { PostgresDatasource } from './datasource';
|
||||||
import { PostgresQueryCtrl } from './query_ctrl';
|
import { PostgresQueryCtrl } from './query_ctrl';
|
||||||
import { PostgresQuery } from './types';
|
import { PostgresOptions, PostgresQuery, SecureJsonData } from './types';
|
||||||
|
|
||||||
const defaultQuery = `SELECT
|
const defaultQuery = `SELECT
|
||||||
extract(epoch from time_column) AS time,
|
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)
|
.setQueryCtrl(PostgresQueryCtrl)
|
||||||
.setConfigCtrl(PostgresConfigCtrl)
|
.setConfigEditor(PostgresConfigEditor)
|
||||||
.setAnnotationQueryCtrl(PostgresAnnotationsQueryCtrl);
|
.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 { 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 {
|
export interface PostgresQueryForInterpolation {
|
||||||
alias?: any;
|
alias?: any;
|
||||||
@ -7,15 +44,3 @@ export interface PostgresQueryForInterpolation {
|
|||||||
refId: any;
|
refId: any;
|
||||||
hide?: 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