mirror of
https://github.com/grafana/grafana.git
synced 2025-09-22 09:13:40 +08: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:
@ -1,94 +0,0 @@
|
||||
import { find } from 'lodash';
|
||||
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import {
|
||||
createChangeHandler,
|
||||
createResetHandler,
|
||||
PasswordFieldEnum,
|
||||
} from '../../../features/datasources/utils/passwordHandlers';
|
||||
|
||||
export class PostgresConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
|
||||
// Set through angular bindings
|
||||
declare current: any;
|
||||
|
||||
datasourceSrv: any;
|
||||
showTimescaleDBHelp: boolean;
|
||||
onPasswordReset: ReturnType<typeof createResetHandler>;
|
||||
onPasswordChange: ReturnType<typeof createChangeHandler>;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any, datasourceSrv: DatasourceSrv) {
|
||||
this.current = $scope.ctrl.current;
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
|
||||
this.current.jsonData.tlsConfigurationMethod = this.current.jsonData.tlsConfigurationMethod || 'file-path';
|
||||
this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
|
||||
this.showTimescaleDBHelp = false;
|
||||
this.autoDetectFeatures();
|
||||
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
|
||||
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
|
||||
this.tlsModeMapping();
|
||||
}
|
||||
|
||||
autoDetectFeatures() {
|
||||
if (!this.current.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.datasourceSrv.loadDatasource(this.current.name).then((ds: any) => {
|
||||
return ds.getVersion().then((version: any) => {
|
||||
version = Number(version[0].text);
|
||||
|
||||
// timescaledb is only available for 9.6+
|
||||
if (version >= 906) {
|
||||
ds.getTimescaleDBVersion().then((version: any) => {
|
||||
if (version.length === 1) {
|
||||
this.current.jsonData.timescaledb = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const major = Math.trunc(version / 100);
|
||||
const minor = version % 100;
|
||||
let name = String(major);
|
||||
if (version < 1000) {
|
||||
name = String(major) + '.' + String(minor);
|
||||
}
|
||||
if (!find(this.postgresVersions, (p: any) => p.value === version)) {
|
||||
this.postgresVersions.push({ name: name, value: version });
|
||||
}
|
||||
this.current.jsonData.postgresVersion = version;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleTimescaleDBHelp() {
|
||||
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
|
||||
}
|
||||
|
||||
tlsModeMapping() {
|
||||
if (this.current.jsonData.sslmode === 'disable') {
|
||||
this.current.jsonData.tlsAuth = false;
|
||||
this.current.jsonData.tlsAuthWithCACert = false;
|
||||
this.current.jsonData.tlsSkipVerify = true;
|
||||
} else {
|
||||
this.current.jsonData.tlsAuth = true;
|
||||
this.current.jsonData.tlsAuthWithCACert = true;
|
||||
this.current.jsonData.tlsSkipVerify = false;
|
||||
}
|
||||
}
|
||||
|
||||
// the value portion is derived from postgres server_version_num/100
|
||||
postgresVersions = [
|
||||
{ name: '9.3', value: 903 },
|
||||
{ name: '9.4', value: 904 },
|
||||
{ name: '9.5', value: 905 },
|
||||
{ name: '9.6', value: 906 },
|
||||
{ name: '10', value: 1000 },
|
||||
{ name: '11', value: 1100 },
|
||||
{ name: '12+', value: 1200 },
|
||||
];
|
||||
}
|
@ -0,0 +1,281 @@
|
||||
import React, { SyntheticEvent, useState } from 'react';
|
||||
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
SelectableValue,
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { Alert, InlineSwitch, FieldSet, InlineField, InlineFieldRow, Input, Select, SecretInput } from '@grafana/ui';
|
||||
import { ConnectionLimits } from 'app/features/plugins/sql/components/configuration/ConnectionLimits';
|
||||
import { TLSSecretsConfig } from 'app/features/plugins/sql/components/configuration/TLSSecretsConfig';
|
||||
|
||||
import { PostgresOptions, PostgresTLSMethods, PostgresTLSModes, SecureJsonData } from '../types';
|
||||
|
||||
import { useAutoDetectFeatures } from './useAutoDetectFeatures';
|
||||
|
||||
export const postgresVersions: Array<SelectableValue<number>> = [
|
||||
{ label: '9.0', value: 900 },
|
||||
{ label: '9.1', value: 901 },
|
||||
{ label: '9.2', value: 902 },
|
||||
{ label: '9.3', value: 903 },
|
||||
{ label: '9.4', value: 904 },
|
||||
{ label: '9.5', value: 905 },
|
||||
{ label: '9.6', value: 906 },
|
||||
{ label: '10', value: 1000 },
|
||||
{ label: '11', value: 1100 },
|
||||
{ label: '12', value: 1200 },
|
||||
{ label: '13', value: 1300 },
|
||||
{ label: '14', value: 1400 },
|
||||
{ label: '15', value: 1500 },
|
||||
];
|
||||
|
||||
export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<PostgresOptions, SecureJsonData>) => {
|
||||
const [versionOptions, setVersionOptions] = useState(postgresVersions);
|
||||
|
||||
useAutoDetectFeatures({ props, setVersionOptions });
|
||||
|
||||
const { options, onOptionsChange } = props;
|
||||
const jsonData = options.jsonData;
|
||||
|
||||
const onResetPassword = () => {
|
||||
updateDatasourcePluginResetOption(props, 'password');
|
||||
};
|
||||
|
||||
const tlsModes: Array<SelectableValue<PostgresTLSModes>> = [
|
||||
{ value: PostgresTLSModes.disable, label: 'disable' },
|
||||
{ value: PostgresTLSModes.require, label: 'require' },
|
||||
{ value: PostgresTLSModes.verifyCA, label: 'verify-ca' },
|
||||
{ value: PostgresTLSModes.verifyFull, label: 'verify-full' },
|
||||
];
|
||||
|
||||
const tlsMethods: Array<SelectableValue<PostgresTLSMethods>> = [
|
||||
{ value: PostgresTLSMethods.filePath, label: 'File system path' },
|
||||
{ value: PostgresTLSMethods.fileContent, label: 'Certificate content' },
|
||||
];
|
||||
|
||||
const onJSONDataOptionSelected = (property: keyof PostgresOptions) => {
|
||||
return (value: SelectableValue) => {
|
||||
updateDatasourcePluginJsonDataOption(props, property, value.value);
|
||||
};
|
||||
};
|
||||
|
||||
const onTimeScaleDBChanged = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
updateDatasourcePluginJsonDataOption(props, 'timescaledb', event.currentTarget.checked);
|
||||
};
|
||||
|
||||
const onDSOptionChanged = (property: keyof PostgresOptions) => {
|
||||
return (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
onOptionsChange({ ...options, ...{ [property]: event.currentTarget.value } });
|
||||
};
|
||||
};
|
||||
|
||||
const labelWidthSSLDetails = 25;
|
||||
const labelWidthConnection = 20;
|
||||
const labelWidthShort = 20;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldSet label="PostgreSQL Connection" width={400}>
|
||||
<InlineField labelWidth={labelWidthConnection} label="Host">
|
||||
<Input
|
||||
width={40}
|
||||
name="host"
|
||||
type="text"
|
||||
value={options.url || ''}
|
||||
placeholder="localhost:5432"
|
||||
onChange={onDSOptionChanged('url')}
|
||||
></Input>
|
||||
</InlineField>
|
||||
<InlineField labelWidth={labelWidthConnection} label="Database">
|
||||
<Input
|
||||
width={40}
|
||||
name="database"
|
||||
value={options.database || ''}
|
||||
placeholder="datbase name"
|
||||
onChange={onDSOptionChanged('database')}
|
||||
></Input>
|
||||
</InlineField>
|
||||
<InlineFieldRow>
|
||||
<InlineField labelWidth={labelWidthConnection} label="User">
|
||||
<Input value={options.user || ''} placeholder="user" onChange={onDSOptionChanged('user')}></Input>
|
||||
</InlineField>
|
||||
<InlineField label="Password">
|
||||
<SecretInput
|
||||
placeholder="Password"
|
||||
isConfigured={options.secureJsonFields?.password}
|
||||
onReset={onResetPassword}
|
||||
onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
|
||||
></SecretInput>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineField
|
||||
labelWidth={labelWidthConnection}
|
||||
label="TLS/SSL Mode"
|
||||
htmlFor="tlsMode"
|
||||
tooltip="This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server."
|
||||
>
|
||||
<Select
|
||||
options={tlsModes}
|
||||
inputId="tlsMode"
|
||||
value={jsonData.sslmode || PostgresTLSModes.verifyFull}
|
||||
onChange={onJSONDataOptionSelected('sslmode')}
|
||||
></Select>
|
||||
</InlineField>
|
||||
{options.jsonData.sslmode !== PostgresTLSModes.disable ? (
|
||||
<InlineField
|
||||
labelWidth={labelWidthConnection}
|
||||
label="TLS/SSL Method"
|
||||
htmlFor="tlsMethod"
|
||||
tooltip={
|
||||
<span>
|
||||
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will
|
||||
allow you to configure certificates by specifying paths to existing certificates on the local file
|
||||
system where Grafana is running. Be sure that the file is readable by the user executing the Grafana
|
||||
process.
|
||||
<br />
|
||||
<br />
|
||||
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content.
|
||||
The content will be stored encrypted in Grafana's database. When connecting to the database the
|
||||
certificates will be written as files to Grafana's configured data path on the local file system.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
options={tlsMethods}
|
||||
inputId="tlsMethod"
|
||||
value={jsonData.tlsConfigurationMethod || PostgresTLSMethods.filePath}
|
||||
onChange={onJSONDataOptionSelected('tlsConfigurationMethod')}
|
||||
></Select>
|
||||
</InlineField>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
|
||||
{options.jsonData.sslmode !== 'disable' ? (
|
||||
<FieldSet label="TLS/SSL Auth Details">
|
||||
{options.jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? (
|
||||
<TLSSecretsConfig editorProps={props} labelWidth={labelWidthSSLDetails}></TLSSecretsConfig>
|
||||
) : (
|
||||
<>
|
||||
<InlineField
|
||||
tooltip={
|
||||
<span>
|
||||
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here.
|
||||
</span>
|
||||
}
|
||||
labelWidth={labelWidthSSLDetails}
|
||||
label="TLS/SSL Root Certificate"
|
||||
>
|
||||
<Input
|
||||
value={jsonData.sslRootCertFile || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslRootCertFile')}
|
||||
placeholder="TLS/SSL root cert file"
|
||||
></Input>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
tooltip={
|
||||
<span>
|
||||
To authenticate with an TLS/SSL client certificate, provide the path to the file here. Be sure that
|
||||
the file is readable by the user executing the grafana process.
|
||||
</span>
|
||||
}
|
||||
labelWidth={labelWidthSSLDetails}
|
||||
label="TLS/SSL Client Certificate"
|
||||
>
|
||||
<Input
|
||||
value={jsonData.sslCertFile || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslCertFile')}
|
||||
placeholder="TLS/SSL client cert file"
|
||||
></Input>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
tooltip={
|
||||
<span>
|
||||
To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file
|
||||
here. Be sure that the file is <i>only</i> readable by the user executing the grafana process.
|
||||
</span>
|
||||
}
|
||||
labelWidth={labelWidthSSLDetails}
|
||||
label="TLS/SSL Client Key"
|
||||
>
|
||||
<Input
|
||||
value={jsonData.sslKeyFile || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'sslKeyFile')}
|
||||
placeholder="TLS/SSL client key file"
|
||||
></Input>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
<ConnectionLimits
|
||||
labelWidth={labelWidthShort}
|
||||
jsonData={jsonData}
|
||||
onPropertyChanged={(property, value) => {
|
||||
updateDatasourcePluginJsonDataOption(props, property, value);
|
||||
}}
|
||||
></ConnectionLimits>
|
||||
|
||||
<FieldSet label="PostgreSQL details">
|
||||
<InlineField
|
||||
tooltip="This option controls what functions are available in the PostgreSQL query builder"
|
||||
labelWidth={labelWidthShort}
|
||||
htmlFor="postgresVersion"
|
||||
label="Version"
|
||||
>
|
||||
<Select
|
||||
value={jsonData.postgresVersion || 903}
|
||||
inputId="postgresVersion"
|
||||
onChange={onJSONDataOptionSelected('postgresVersion')}
|
||||
options={versionOptions}
|
||||
></Select>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
tooltip={
|
||||
<span>
|
||||
TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use
|
||||
<code>time_bucket</code> in the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate
|
||||
functions in the query builder.
|
||||
</span>
|
||||
}
|
||||
labelWidth={labelWidthShort}
|
||||
label="TimescaleDB"
|
||||
htmlFor="timescaledb"
|
||||
>
|
||||
<InlineSwitch
|
||||
id="timescaledb"
|
||||
value={jsonData.timescaledb || false}
|
||||
onChange={onTimeScaleDBChanged}
|
||||
></InlineSwitch>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
tooltip={
|
||||
<span>
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example
|
||||
<code>1m</code> if your data is written every minute.
|
||||
</span>
|
||||
}
|
||||
labelWidth={labelWidthShort}
|
||||
label="Min time interval"
|
||||
>
|
||||
<Input
|
||||
placeholder="1m"
|
||||
value={jsonData.timeInterval || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
|
||||
></Input>
|
||||
</InlineField>
|
||||
</FieldSet>
|
||||
|
||||
<Alert title="User Permission" severity="info">
|
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to
|
||||
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
|
||||
statements like <code>DELETE FROM user;</code> and <code>DROP TABLE user;</code> would be executed. To protect
|
||||
against this we
|
||||
<strong>Highly</strong> recommmend you create a specific PostgreSQL user with restricted permissions.
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,87 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useDeepCompareEffect } from 'react-use';
|
||||
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
DataSourceSettings,
|
||||
SelectableValue,
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
updateDatasourcePluginOption,
|
||||
} from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import { PostgresDatasource } from '../datasource';
|
||||
import { PostgresOptions, PostgresTLSModes, SecureJsonData } from '../types';
|
||||
|
||||
import { postgresVersions } from './ConfigurationEditor';
|
||||
|
||||
type Options = {
|
||||
props: DataSourcePluginOptionsEditorProps<PostgresOptions, SecureJsonData>;
|
||||
setVersionOptions: Dispatch<SetStateAction<Array<SelectableValue<number>>>>;
|
||||
};
|
||||
|
||||
export function useAutoDetectFeatures({ props, setVersionOptions }: Options) {
|
||||
const [saved, setSaved] = useState(false);
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
const getVersion = async () => {
|
||||
if (!saved) {
|
||||
// We need to save the datasource before we can get the version so we can query the database with the options we have.
|
||||
const result = await getBackendSrv().put<{ datasource: DataSourceSettings }>(
|
||||
`/api/datasources/${options.id}`,
|
||||
options
|
||||
);
|
||||
|
||||
setSaved(true);
|
||||
// This is needed or else we get an error when we try to save the datasource.
|
||||
updateDatasourcePluginOption({ options, onOptionsChange }, 'version', result.datasource.version);
|
||||
} else {
|
||||
const datasource = await getDatasourceSrv().loadDatasource(options.name);
|
||||
|
||||
if (datasource instanceof PostgresDatasource) {
|
||||
const version = await datasource.getVersion();
|
||||
const versionNumber = parseInt(version, 10);
|
||||
|
||||
// timescaledb is only available for 9.6+
|
||||
if (versionNumber >= 906 && !options.jsonData.timescaledb) {
|
||||
const timescaledbVersion = await datasource.getTimescaleDBVersion();
|
||||
if (timescaledbVersion?.length) {
|
||||
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'timescaledb', true);
|
||||
}
|
||||
}
|
||||
const major = Math.trunc(versionNumber / 100);
|
||||
const minor = versionNumber % 100;
|
||||
let name = String(major);
|
||||
if (versionNumber < 1000) {
|
||||
name = String(major) + '.' + String(minor);
|
||||
}
|
||||
if (!postgresVersions.find((p) => p.value === versionNumber)) {
|
||||
setVersionOptions((prev) => [...prev, { label: name, value: versionNumber }]);
|
||||
}
|
||||
if (options.jsonData.postgresVersion === undefined || options.jsonData.postgresVersion !== versionNumber) {
|
||||
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'postgresVersion', versionNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// This logic is only going to run when we create a new datasource
|
||||
if (isValidConfig(options)) {
|
||||
getVersion();
|
||||
}
|
||||
}, [options, saved, setVersionOptions]);
|
||||
}
|
||||
|
||||
function isValidConfig(options: DataSourceSettings<PostgresOptions, SecureJsonData>) {
|
||||
return (
|
||||
options.url &&
|
||||
options.database &&
|
||||
options.user &&
|
||||
(options.secureJsonData?.password || options.secureJsonFields?.password) &&
|
||||
(options.jsonData.sslmode === PostgresTLSModes.disable ||
|
||||
(options.jsonData.sslCertFile && options.jsonData.sslKeyFile && options.jsonData.sslRootCertFile)) &&
|
||||
!options.jsonData.postgresVersion &&
|
||||
!options.readOnly
|
||||
);
|
||||
}
|
@ -184,12 +184,25 @@ export class PostgresDatasource extends DataSourceWithBackend<PostgresQuery, Pos
|
||||
});
|
||||
}
|
||||
|
||||
getVersion(): Promise<any> {
|
||||
return lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100"));
|
||||
async getVersion(): Promise<string> {
|
||||
const value = await lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100"));
|
||||
const results = value.data.results['meta'];
|
||||
if (results.frames) {
|
||||
// This returns number
|
||||
return results.frames[0].data?.values[0][0].toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getTimescaleDBVersion(): Promise<any> {
|
||||
return lastValueFrom(this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"));
|
||||
async getTimescaleDBVersion(): Promise<string[] | undefined> {
|
||||
const value = await lastValueFrom(
|
||||
this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'")
|
||||
);
|
||||
const results = value.data.results['meta'];
|
||||
if (results.frames) {
|
||||
return results.frames[0].data?.values[0][0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
testDatasource(): Promise<any> {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
|
||||
import { PostgresConfigCtrl } from './config_ctrl';
|
||||
import { PostgresConfigEditor } from './configuration/ConfigurationEditor';
|
||||
import { PostgresDatasource } from './datasource';
|
||||
import { PostgresQueryCtrl } from './query_ctrl';
|
||||
import { PostgresQuery } from './types';
|
||||
import { PostgresOptions, PostgresQuery, SecureJsonData } from './types';
|
||||
|
||||
const defaultQuery = `SELECT
|
||||
extract(epoch from time_column) AS time,
|
||||
@ -27,7 +27,9 @@ class PostgresAnnotationsQueryCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin = new DataSourcePlugin<PostgresDatasource, PostgresQuery>(PostgresDatasource)
|
||||
export const plugin = new DataSourcePlugin<PostgresDatasource, PostgresQuery, PostgresOptions, SecureJsonData>(
|
||||
PostgresDatasource
|
||||
)
|
||||
.setQueryCtrl(PostgresQueryCtrl)
|
||||
.setConfigCtrl(PostgresConfigCtrl)
|
||||
.setConfigEditor(PostgresConfigEditor)
|
||||
.setAnnotationQueryCtrl(PostgresAnnotationsQueryCtrl);
|
||||
|
@ -1,196 +0,0 @@
|
||||
|
||||
<h3 class="page-heading">PostgreSQL Connection</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Host</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.url' placeholder="localhost:5432"
|
||||
bs-typeahead="{{['localhost:5432', 'localhost:5433']}}" required></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Database</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.database' placeholder="database name" required></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-15">
|
||||
<span class="gf-form-label width-10">User</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<secret-form-field
|
||||
isConfigured="ctrl.current.secureJsonFields.password"
|
||||
value="ctrl.current.secureJsonData.password"
|
||||
on-reset="ctrl.onPasswordReset"
|
||||
on-change="ctrl.onPasswordChange"
|
||||
inputWidth="9"
|
||||
aria-label="'Password'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10" for="tls-mode-select">TLS/SSL Mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
|
||||
<select id="tls-mode-select" class="gf-form-input" ng-model="ctrl.current.jsonData.sslmode"
|
||||
ng-options="mode for mode in ['disable', 'require', 'verify-ca', 'verify-full']"
|
||||
ng-init="ctrl.current.jsonData.sslmode" ng-change="ctrl.tlsModeMapping()"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="ctrl.current.jsonData.sslmode != 'disable'">
|
||||
<label class="gf-form-label width-10">TLS/SSL Method</label>
|
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.tlsConfigurationMethod"
|
||||
ng-options="f.id as f.label for f in [{ id: 'file-path', label: 'File system path' }, { id: 'file-content', label: 'Certificate content' }]"
|
||||
ng-init="ctrl.current.jsonData.tlsConfigurationMethod"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will allow
|
||||
you to configure certificates by specifying paths to existing certificates on the local file system where
|
||||
Grafana is running. Be sure that the file is readable by the user executing the Grafana process.<br><br>
|
||||
|
||||
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content.
|
||||
The content will be stored encrypted in Grafana's database. When connecting to the database the certificates
|
||||
will be written as files to Grafana's configured data path on the local file system.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-path'">
|
||||
<div class="gf-form">
|
||||
<h6>TLS/SSL Auth Details</h6>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-11">TLS/SSL Root Certificate</span>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon"
|
||||
ng-model='ctrl.current.jsonData.sslRootCertFile' placeholder="TLS/SSL root cert file"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-11">TLS/SSL Client Certificate</span>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslCertFile'
|
||||
placeholder="TLS/SSL client cert file"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
To authenticate with an TLS/SSL client certificate, provide the path to the file here.
|
||||
Be sure that the file is readable by the user executing the grafana process.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-11">TLS/SSL Client Key</span>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslKeyFile'
|
||||
placeholder="TLS/SSL client key file"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
To authenticate with a client TLS/SSL certificate, provide the path to the corresponding key file here.
|
||||
Be sure that the file is <i>only</i> readable by the user executing the grafana process.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<datasource-tls-auth-settings current="ctrl.current"
|
||||
ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-content'">
|
||||
</datasource-tls-auth-settings>
|
||||
|
||||
<b>Connection limits</b>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form max-width-15">
|
||||
<span class="gf-form-label width-7">Max open</span>
|
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon"
|
||||
ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the
|
||||
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be
|
||||
reduced to match the <i>Max open connections</i> limit. If set to 0, there is no limit on the number of open
|
||||
connections.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-15">
|
||||
<span class="gf-form-label width-7">Max idle</span>
|
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon"
|
||||
ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but
|
||||
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the
|
||||
<i>Max open connections</i> limit. If set to 0, no idle connections are retained.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-15">
|
||||
<span class="gf-form-label width-7">Max lifetime</span>
|
||||
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon"
|
||||
ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">PostgreSQL details</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9" id="version-label">
|
||||
Version
|
||||
<info-popover mode="right-normal" position="top center">
|
||||
This option controls what functions are available in the PostgreSQL query builder.
|
||||
</info-popover>
|
||||
</span>
|
||||
<span class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.postgresVersion"
|
||||
ng-options="f.value as f.name for f in ctrl.postgresVersions" aria-labelledby="version-label"></select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch class="gf-form" label="TimescaleDB" label-class="width-9"
|
||||
checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleTimescaleDBHelp()">
|
||||
Help
|
||||
<icon name="'angle-down'" ng-show="ctrl.showTimescaleDBHelp"></icon>
|
||||
<icon name="'angle-right'" ng-hide="ctrl.showTimescaleDBHelp"> </icon>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Min time interval</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input width-6 gf-form-input--has-help-icon"
|
||||
ng-model="ctrl.current.jsonData.timeInterval"
|
||||
spellcheck='false'
|
||||
placeholder="1m"
|
||||
ng-pattern="/^\d+(ms|[Mwdhmsy])$/"
|
||||
></input>
|
||||
<info-popover mode="right-absolute">
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example <code>1m</code> if your data is written every minute.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grafana-info-box alert alert-info" ng-show="ctrl.showTimescaleDBHelp">
|
||||
<div class="alert-body">
|
||||
<p>
|
||||
<a href="https://github.com/timescale/timescaledb" class="pointer" target="_blank">TimescaleDB</a> is a
|
||||
time-series database built as a PostgreSQL extension. If enabled, Grafana will use <code>time_bucket</code> in
|
||||
the <code>$__timeGroup</code> macro and display TimescaleDB specific aggregate functions in the query builder.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="grafana-info-box">
|
||||
<h5>User Permission</h5>
|
||||
<p>
|
||||
The database user should only be granted SELECT permissions on the specified database & tables you want to query.
|
||||
Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements
|
||||
like <code>DELETE FROM user;</code> and <code>DROP TABLE user;</code> would be executed. To protect against this we
|
||||
<strong>Highly</strong> recommmend you create a specific PostgreSQL user with restricted permissions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,41 @@
|
||||
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||
import { SQLConnectionLimits } from 'app/features/plugins/sql/components/configuration/types';
|
||||
|
||||
export enum PostgresTLSModes {
|
||||
disable = 'disable',
|
||||
require = 'require',
|
||||
verifyCA = 'verify-ca',
|
||||
verifyFull = 'verify-full',
|
||||
}
|
||||
|
||||
export enum PostgresTLSMethods {
|
||||
filePath = 'file-path',
|
||||
fileContent = 'file-content',
|
||||
}
|
||||
export interface PostgresOptions extends DataSourceJsonData, SQLConnectionLimits {
|
||||
url: string;
|
||||
timeInterval: string;
|
||||
database: string;
|
||||
user: string;
|
||||
tlsConfigurationMethod: PostgresTLSMethods;
|
||||
sslmode: PostgresTLSModes;
|
||||
sslRootCertFile: string;
|
||||
sslCertFile: string;
|
||||
sslKeyFile: string;
|
||||
postgresVersion: number;
|
||||
timescaledb: boolean;
|
||||
}
|
||||
|
||||
export interface SecureJsonData {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type ResultFormat = 'time_series' | 'table';
|
||||
export interface PostgresQuery extends DataQuery {
|
||||
alias?: string;
|
||||
format?: ResultFormat;
|
||||
rawSql?: any;
|
||||
}
|
||||
|
||||
export interface PostgresQueryForInterpolation {
|
||||
alias?: any;
|
||||
@ -7,15 +44,3 @@ export interface PostgresQueryForInterpolation {
|
||||
refId: any;
|
||||
hide?: any;
|
||||
}
|
||||
|
||||
export interface PostgresOptions extends DataSourceJsonData {
|
||||
timeInterval: string;
|
||||
}
|
||||
|
||||
export type ResultFormat = 'time_series' | 'table';
|
||||
|
||||
export interface PostgresQuery extends DataQuery {
|
||||
alias?: string;
|
||||
format?: ResultFormat;
|
||||
rawSql?: any;
|
||||
}
|
||||
|
Reference in New Issue
Block a user