SQL: Migrate (MS/My/Postgres)SQL configuration pages from Angular to React (#51891)

* Migrate SQL configuration pages from angular to react

* Move enums to types.ts and remove angular partials

* remove es lint disables and update betterer instead

* Fix automatically added type declarations

* Bump wor.. betterer ;)

* Export SecretInput component from grafana-ui

* Fix A11y issues

* Export SecretTextArea as well

* Fix typo

* Use const instead of var

* Fix typo in doc

* Add autoDetectFeatures to postgres config editor

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Oscar Kilhed
2022-07-14 13:29:08 +02:00
committed by GitHub
parent 77e87f1806
commit 9498ee3d54
23 changed files with 1068 additions and 695 deletions

View File

@ -1,94 +0,0 @@
import { find } from 'lodash';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
createChangeHandler,
createResetHandler,
PasswordFieldEnum,
} from '../../../features/datasources/utils/passwordHandlers';
export class PostgresConfigCtrl {
static templateUrl = 'partials/config.html';
// Set through angular bindings
declare current: any;
datasourceSrv: any;
showTimescaleDBHelp: boolean;
onPasswordReset: ReturnType<typeof createResetHandler>;
onPasswordChange: ReturnType<typeof createChangeHandler>;
/** @ngInject */
constructor($scope: any, datasourceSrv: DatasourceSrv) {
this.current = $scope.ctrl.current;
this.datasourceSrv = datasourceSrv;
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
this.current.jsonData.tlsConfigurationMethod = this.current.jsonData.tlsConfigurationMethod || 'file-path';
this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
this.showTimescaleDBHelp = false;
this.autoDetectFeatures();
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
this.tlsModeMapping();
}
autoDetectFeatures() {
if (!this.current.id) {
return;
}
this.datasourceSrv.loadDatasource(this.current.name).then((ds: any) => {
return ds.getVersion().then((version: any) => {
version = Number(version[0].text);
// timescaledb is only available for 9.6+
if (version >= 906) {
ds.getTimescaleDBVersion().then((version: any) => {
if (version.length === 1) {
this.current.jsonData.timescaledb = true;
}
});
}
const major = Math.trunc(version / 100);
const minor = version % 100;
let name = String(major);
if (version < 1000) {
name = String(major) + '.' + String(minor);
}
if (!find(this.postgresVersions, (p: any) => p.value === version)) {
this.postgresVersions.push({ name: name, value: version });
}
this.current.jsonData.postgresVersion = version;
});
});
}
toggleTimescaleDBHelp() {
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
}
tlsModeMapping() {
if (this.current.jsonData.sslmode === 'disable') {
this.current.jsonData.tlsAuth = false;
this.current.jsonData.tlsAuthWithCACert = false;
this.current.jsonData.tlsSkipVerify = true;
} else {
this.current.jsonData.tlsAuth = true;
this.current.jsonData.tlsAuthWithCACert = true;
this.current.jsonData.tlsSkipVerify = false;
}
}
// the value portion is derived from postgres server_version_num/100
postgresVersions = [
{ name: '9.3', value: 903 },
{ name: '9.4', value: 904 },
{ name: '9.5', value: 905 },
{ name: '9.6', value: 906 },
{ name: '10', value: 1000 },
{ name: '11', value: 1100 },
{ name: '12+', value: 1200 },
];
}

View File

@ -0,0 +1,281 @@
import React, { SyntheticEvent, useState } from 'react';
import {
DataSourcePluginOptionsEditorProps,
onUpdateDatasourceJsonDataOption,
onUpdateDatasourceSecureJsonDataOption,
SelectableValue,
updateDatasourcePluginJsonDataOption,
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { Alert, InlineSwitch, FieldSet, InlineField, InlineFieldRow, Input, Select, SecretInput } from '@grafana/ui';
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits';
import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig';
import { PostgresOptions, PostgresTLSMethods, PostgresTLSModes, SecureJsonData } from '../types';
import { useAutoDetectFeatures } from './useAutoDetectFeatures';
export const postgresVersions: Array<SelectableValue<number>> = [
{ label: '9.0', value: 900 },
{ label: '9.1', value: 901 },
{ label: '9.2', value: 902 },
{ label: '9.3', value: 903 },
{ label: '9.4', value: 904 },
{ label: '9.5', value: 905 },
{ label: '9.6', value: 906 },
{ label: '10', value: 1000 },
{ label: '11', value: 1100 },
{ label: '12', value: 1200 },
{ label: '13', value: 1300 },
{ label: '14', value: 1400 },
{ label: '15', value: 1500 },
];
export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<PostgresOptions, SecureJsonData>) => {
const [versionOptions, setVersionOptions] = useState(postgresVersions);
useAutoDetectFeatures({ props, setVersionOptions });
const { options, onOptionsChange } = props;
const jsonData = options.jsonData;
const onResetPassword = () => {
updateDatasourcePluginResetOption(props, 'password');
};
const tlsModes: Array<SelectableValue<PostgresTLSModes>> = [
{ value: PostgresTLSModes.disable, label: 'disable' },
{ value: PostgresTLSModes.require, label: 'require' },
{ value: PostgresTLSModes.verifyCA, label: 'verify-ca' },
{ value: PostgresTLSModes.verifyFull, label: 'verify-full' },
];
const tlsMethods: Array<SelectableValue<PostgresTLSMethods>> = [
{ value: PostgresTLSMethods.filePath, label: 'File system path' },
{ value: PostgresTLSMethods.fileContent, label: 'Certificate content' },
];
const onJSONDataOptionSelected = (property: keyof PostgresOptions) => {
return (value: SelectableValue) => {
updateDatasourcePluginJsonDataOption(props, property, value.value);
};
};
const onTimeScaleDBChanged = (event: SyntheticEvent<HTMLInputElement>) => {
updateDatasourcePluginJsonDataOption(props, 'timescaledb', event.currentTarget.checked);
};
const onDSOptionChanged = (property: keyof PostgresOptions) => {
return (event: SyntheticEvent<HTMLInputElement>) => {
onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } });
};
};
const labelWidthSSLDetails = 25;
const labelWidthConnection = 20;
const labelWidthShort = 20;
return (
<>
<FieldSet label="PostgreSQL Connection" width={400}>
<InlineField labelWidth={labelWidthConnection} label="Host">
<Input
width={40}
name="host"
type="text"
value={options.url || ''}
placeholder="localhost:5432"
onChange={onDSOptionChanged('url')}
></Input>
</InlineField>
<InlineField labelWidth={labelWidthConnection} label="Database">
<Input
width={40}
name="database"
value={options.database || ''}
placeholder="datbase name"
onChange={onDSOptionChanged('database')}
></Input>
</InlineField>
<InlineFieldRow>
<InlineField labelWidth={labelWidthConnection} label="User">
<Input value={options.user || ''} placeholder="user" onChange={onDSOptionChanged('user')}></Input>
</InlineField>
<InlineField label="Password">
<SecretInput
placeholder="Password"
isConfigured={options.secureJsonFields?.password}
onReset={onResetPassword}
onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
></SecretInput>
</InlineField>
</InlineFieldRow>
<InlineField
labelWidth={labelWidthConnection}
label="TLS/SSL Mode"
htmlFor="tlsMode"
tooltip="This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server."
>
<Select
options={tlsModes}
inputId="tlsMode"
value={jsonData.sslmode || PostgresTLSModes.verifyFull}
onChange={onJSONDataOptionSelected('sslmode')}
></Select>
</InlineField>
{options.jsonData.sslmode !== PostgresTLSModes.disable ? (
<InlineField
labelWidth={labelWidthConnection}
label="TLS/SSL Method"
htmlFor="tlsMethod"
tooltip={
<span>
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will
allow you to configure certificates by specifying paths to existing certificates on the local file
system where Grafana is running. Be sure that the file is readable by the user executing the Grafana
process.
<br />
<br />
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content.
The content will be stored encrypted in Grafana&apos;s database. When connecting to the database the
certificates will be written as files to Grafana&apos;s configured data path on the local file system.
</span>
}
>
<Select
options={tlsMethods}
inputId="tlsMethod"
value={jsonData.tlsConfigurationMethod || PostgresTLSMethods.filePath}
onChange={onJSONDataOptionSelected('tlsConfigurationMethod')}
></Select>
</InlineField>
) : null}
</FieldSet>
{options.jsonData.sslmode !== 'disable' ? (
<FieldSet label="TLS/SSL Auth Details">
{options.jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? (
<TLSSecretsConfig editorProps={props} labelWidth={labelWidthSSLDetails}></TLSSecretsConfig>
) : (
<>
<InlineField
tooltip={
<span>
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here.
</span>
}
labelWidth={labelWidthSSLDetails}
label="TLS/SSL Root Certificate"
>
<Input
value={jsonData.sslRootCertFile || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'sslRootCertFile')}
placeholder="TLS/SSL root cert file"
></Input>
</InlineField>
<InlineField
tooltip={
<span>
To authenticate with an TLS/SSL client certificate, provide the path to the file here. Be sure that
the file is readable by the user executing the grafana process.
</span>
}
labelWidth={labelWidthSSLDetails}
label="TLS/SSL Client Certificate"
>
<Input
value={jsonData.sslCertFile || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'sslCertFile')}
placeholder="TLS/SSL client cert file"
></Input>
</InlineField>
<InlineField
tooltip={
<span>
To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file
here. Be sure that the file is <i>only</i> readable by the user executing the grafana process.
</span>
}
labelWidth={labelWidthSSLDetails}
label="TLS/SSL Client Key"
>
<Input
value={jsonData.sslKeyFile || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'sslKeyFile')}
placeholder="TLS/SSL client key file"
></Input>
</InlineField>
</>
)}
</FieldSet>
) : null}
<ConnectionLimits
labelWidth={labelWidthShort}
jsonData={jsonData}
onPropertyChanged={(property, value) => {
updateDatasourcePluginJsonDataOption(props, property, value);
}}
></ConnectionLimits>
<FieldSet label="PostgreSQL details">
<InlineField
tooltip="This option controls what functions are available in the PostgreSQL query builder"
labelWidth={labelWidthShort}
htmlFor="postgresVersion"
label="Version"
>
<Select
value={jsonData.postgresVersion || 903}
inputId="postgresVersion"
onChange={onJSONDataOptionSelected('postgresVersion')}
options={versionOptions}
></Select>
</InlineField>
<InlineField
tooltip={
<span>
TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use
<code>time_bucket</code> in the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate
functions in the query builder.
</span>
}
labelWidth={labelWidthShort}
label="TimescaleDB"
htmlFor="timescaledb"
>
<InlineSwitch
id="timescaledb"
value={jsonData.timescaledb || false}
onChange={onTimeScaleDBChanged}
></InlineSwitch>
</InlineField>
<InlineField
tooltip={
<span>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example
<code>1m</code> if your data is written every minute.
</span>
}
labelWidth={labelWidthShort}
label="Min time interval"
>
<Input
placeholder="1m"
value={jsonData.timeInterval || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
></Input>
</InlineField>
</FieldSet>
<Alert title="User Permission" severity="info">
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
statements like <code>DELETE FROM user;</code> and <code>DROP TABLE user;</code> would be executed. To protect
against this we
<strong>Highly</strong> recommmend you create a specific PostgreSQL user with restricted permissions.
</Alert>
</>
);
};

View File

@ -0,0 +1,87 @@
import { Dispatch, SetStateAction, useState } from 'react';
import { useDeepCompareEffect } from 'react-use';
import {
DataSourcePluginOptionsEditorProps,
DataSourceSettings,
SelectableValue,
updateDatasourcePluginJsonDataOption,
updateDatasourcePluginOption,
} from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { PostgresDatasource } from '../datasource';
import { PostgresOptions, PostgresTLSModes, SecureJsonData } from '../types';
import { postgresVersions } from './ConfigurationEditor';
type Options = {
props: DataSourcePluginOptionsEditorProps<PostgresOptions, SecureJsonData>;
setVersionOptions: Dispatch<SetStateAction<Array<SelectableValue<number>>>>;
};
export function useAutoDetectFeatures({ props, setVersionOptions }: Options) {
const [saved, setSaved] = useState(false);
const { options, onOptionsChange } = props;
useDeepCompareEffect(() => {
const getVersion = async () => {
if (!saved) {
// We need to save the datasource before we can get the version so we can query the database with the options we have.
const result = await getBackendSrv().put<{ datasource: DataSourceSettings }>(
`/api/datasources/${options.id}`,
options
);
setSaved(true);
// This is needed or else we get an error when we try to save the datasource.
updateDatasourcePluginOption({ options, onOptionsChange }, 'version', result.datasource.version);
} else {
const datasource = await getDatasourceSrv().loadDatasource(options.name);
if (datasource instanceof PostgresDatasource) {
const version = await datasource.getVersion();
const versionNumber = parseInt(version, 10);
// timescaledb is only available for 9.6+
if (versionNumber >= 906 && !options.jsonData.timescaledb) {
const timescaledbVersion = await datasource.getTimescaleDBVersion();
if (timescaledbVersion?.length) {
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'timescaledb', true);
}
}
const major = Math.trunc(versionNumber / 100);
const minor = versionNumber % 100;
let name = String(major);
if (versionNumber < 1000) {
name = String(major) + '.' + String(minor);
}
if (!postgresVersions.find((p) => p.value === versionNumber)) {
setVersionOptions((prev) => [...prev, { label: name, value: versionNumber }]);
}
if (options.jsonData.postgresVersion === undefined || options.jsonData.postgresVersion !== versionNumber) {
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'postgresVersion', versionNumber);
}
}
}
};
// This logic is only going to run when we create a new datasource
if (isValidConfig(options)) {
getVersion();
}
}, [options, saved, setVersionOptions]);
}
function isValidConfig(options: DataSourceSettings<PostgresOptions, SecureJsonData>) {
return (
options.url &&
options.database &&
options.user &&
(options.secureJsonData?.password || options.secureJsonFields?.password) &&
(options.jsonData.sslmode === PostgresTLSModes.disable ||
(options.jsonData.sslCertFile && options.jsonData.sslKeyFile && options.jsonData.sslRootCertFile)) &&
!options.jsonData.postgresVersion &&
!options.readOnly
);
}

View File

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

View File

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

View File

@ -1,196 +0,0 @@
<h3 class="page-heading">PostgreSQL Connection</h3>
<div class="gf-form-group">
<div class="gf-form max-width-30">
<span class="gf-form-label width-10">Host</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.url' placeholder="localhost:5432"
bs-typeahead="{{['localhost:5432', 'localhost:5433']}}" required></input>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-10">Database</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.database' placeholder="database name" required></input>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-15">
<span class="gf-form-label width-10">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div>
<div class="gf-form">
<secret-form-field
isConfigured="ctrl.current.secureJsonFields.password"
value="ctrl.current.secureJsonData.password"
on-reset="ctrl.onPasswordReset"
on-change="ctrl.onPasswordChange"
inputWidth="9"
aria-label="'Password'"
/>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-10" for="tls-mode-select">TLS/SSL Mode</label>
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
<select id="tls-mode-select" class="gf-form-input" ng-model="ctrl.current.jsonData.sslmode"
ng-options="mode for mode in ['disable', 'require', 'verify-ca', 'verify-full']"
ng-init="ctrl.current.jsonData.sslmode" ng-change="ctrl.tlsModeMapping()"></select>
<info-popover mode="right-absolute">
This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server.
</info-popover>
</div>
</div>
<div class="gf-form" ng-if="ctrl.current.jsonData.sslmode != 'disable'">
<label class="gf-form-label width-10">TLS/SSL Method</label>
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.tlsConfigurationMethod"
ng-options="f.id as f.label for f in [{ id: 'file-path', label: 'File system path' }, { id: 'file-content', label: 'Certificate content' }]"
ng-init="ctrl.current.jsonData.tlsConfigurationMethod"></select>
<info-popover mode="right-absolute">
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will allow
you to configure certificates by specifying paths to existing certificates on the local file system where
Grafana is running. Be sure that the file is readable by the user executing the Grafana process.<br><br>
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content.
The content will be stored encrypted in Grafana's database. When connecting to the database the certificates
will be written as files to Grafana's configured data path on the local file system.
</info-popover>
</div>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-path'">
<div class="gf-form">
<h6>TLS/SSL Auth Details</h6>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-11">TLS/SSL Root Certificate</span>
<input type="text" class="gf-form-input gf-form-input--has-help-icon"
ng-model='ctrl.current.jsonData.sslRootCertFile' placeholder="TLS/SSL root cert file"></input>
<info-popover mode="right-absolute">
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here.
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-11">TLS/SSL Client Certificate</span>
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslCertFile'
placeholder="TLS/SSL client cert file"></input>
<info-popover mode="right-absolute">
To authenticate with an TLS/SSL client certificate, provide the path to the file here.
Be sure that the file is readable by the user executing the grafana process.
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-11">TLS/SSL Client Key</span>
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslKeyFile'
placeholder="TLS/SSL client key file"></input>
<info-popover mode="right-absolute">
To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file here.
Be sure that the file is <i>only</i> readable by the user executing the grafana process.
</info-popover>
</div>
</div>
<datasource-tls-auth-settings current="ctrl.current"
ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-content'">
</datasource-tls-auth-settings>
<b>Connection limits</b>
<div class="gf-form-group">
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max open</span>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon"
ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<info-popover mode="right-absolute">
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be
reduced to match the <i>Max open connections</i> limit. If set to 0, there is no limit on the number of open
connections.
</info-popover>
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max idle</span>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon"
ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<info-popover mode="right-absolute">
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the
<i>Max open connections</i> limit. If set to 0, no idle connections are retained.
</info-popover>
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max lifetime</span>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon"
ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<info-popover mode="right-absolute">
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.
</info-popover>
</div>
</div>
<h3 class="page-heading">PostgreSQL details</h3>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-9" id="version-label">
Version
<info-popover mode="right-normal" position="top center">
This option controls what functions are available in the PostgreSQL query builder.
</info-popover>
</span>
<span class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.postgresVersion"
ng-options="f.value as f.name for f in ctrl.postgresVersions" aria-labelledby="version-label"></select>
</span>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label="TimescaleDB" label-class="width-9"
checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleTimescaleDBHelp()">
Help&nbsp;
<icon name="'angle-down'" ng-show="ctrl.showTimescaleDBHelp"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showTimescaleDBHelp">&nbsp;</icon>
</label>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min time interval</span>
<input
type="text"
class="gf-form-input width-6 gf-form-input--has-help-icon"
ng-model="ctrl.current.jsonData.timeInterval"
spellcheck='false'
placeholder="1m"
ng-pattern="/^\d+(ms|[Mwdhmsy])$/"
></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.
</info-popover>
</div>
</div>
<div class="grafana-info-box alert alert-info" ng-show="ctrl.showTimescaleDBHelp">
<div class="alert-body">
<p>
<a href="https://github.com/timescale/timescaledb" class="pointer" target="_blank">TimescaleDB</a> is a
time-series database built as a PostgreSQL extension. If enabled, Grafana will use <code>time_bucket</code> in
the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate functions in the query builder.
</p>
</div>
</div>
</div>
<div class="gf-form-group">
<div class="grafana-info-box">
<h5>User Permission</h5>
<p>
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to query.
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements
like <code>DELETE FROM user;</code> and <code>DROP TABLE user;</code> would be executed. To protect against this we
<strong>Highly</strong> recommmend you create a specific PostgreSQL user with restricted permissions.
</p>
</div>
</div>

View File

@ -1,4 +1,41 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
export enum PostgresTLSModes {
disable = 'disable',
require = 'require',
verifyCA = 'verify-ca',
verifyFull = 'verify-full',
}
export enum PostgresTLSMethods {
filePath = 'file-path',
fileContent = 'file-content',
}
export interface PostgresOptions extends DataSourceJsonData, SQLConnectionLimits {
url: string;
timeInterval: string;
database: string;
user: string;
tlsConfigurationMethod: PostgresTLSMethods;
sslmode: PostgresTLSModes;
sslRootCertFile: string;
sslCertFile: string;
sslKeyFile: string;
postgresVersion: number;
timescaledb: boolean;
}
export interface SecureJsonData {
password: string;
}
export type ResultFormat = 'time_series' | 'table';
export interface PostgresQuery extends DataQuery {
alias?: string;
format?: ResultFormat;
rawSql?: any;
}
export interface PostgresQueryForInterpolation {
alias?: any;
@ -7,15 +44,3 @@ export interface PostgresQueryForInterpolation {
refId: any;
hide?: any;
}
export interface PostgresOptions extends DataSourceJsonData {
timeInterval: string;
}
export type ResultFormat = 'time_series' | 'table';
export interface PostgresQuery extends DataQuery {
alias?: string;
format?: ResultFormat;
rawSql?: any;
}