mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
InfluxDB: SQL Support (#72167)
* Add influxdbSqlSupport feature toggle * Add SQL option to the config page * Add SQL backend * Add metadata support in config page * Implement unified querying * Fix healthcheck query * fsql tests * secure grpc by default * code cleanup * Query handing for sql mode * Implement a placeholder sql editor * Fix query language dropdown * go mod updates * make lint-go * more make lint-go * remove unused runQuery * switch statements with default case * linting again
This commit is contained in:
@@ -4,34 +4,21 @@ import React, { PureComponent } from 'react';
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
DataSourceSettings,
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceJsonDataOptionSelect,
|
||||
onUpdateDatasourceOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
SelectableValue,
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data/src';
|
||||
import {
|
||||
Alert,
|
||||
DataSourceHttpSettings,
|
||||
InfoBox,
|
||||
InlineField,
|
||||
InlineFormLabel,
|
||||
LegacyForms,
|
||||
Select,
|
||||
} from '@grafana/ui/src';
|
||||
import { Alert, DataSourceHttpSettings, InlineField, LegacyForms, Select } from '@grafana/ui/src';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { BROWSER_MODE_DISABLED_MESSAGE } from '../../../constants';
|
||||
import { InfluxOptions, InfluxOptionsV1, InfluxSecureJsonData, InfluxVersion } from '../../../types';
|
||||
import { InfluxOptions, InfluxOptionsV1, InfluxVersion } from '../../../types';
|
||||
|
||||
const { Input, SecretFormField } = LegacyForms;
|
||||
import { InfluxFluxConfig } from './InfluxFluxConfig';
|
||||
import { InfluxInfluxQLConfig } from './InfluxInfluxQLConfig';
|
||||
import { InfluxSqlConfig } from './InfluxSQLConfig';
|
||||
|
||||
const httpModes: SelectableValue[] = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
];
|
||||
const { Input } = LegacyForms;
|
||||
|
||||
const versions: Array<SelectableValue<InfluxVersion>> = [
|
||||
{
|
||||
@@ -42,7 +29,16 @@ const versions: Array<SelectableValue<InfluxVersion>> = [
|
||||
{
|
||||
label: 'Flux',
|
||||
value: InfluxVersion.Flux,
|
||||
description: 'Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+',
|
||||
description: 'Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+',
|
||||
},
|
||||
];
|
||||
|
||||
const versionsWithSQL: Array<SelectableValue<InfluxVersion>> = [
|
||||
...versions,
|
||||
{
|
||||
label: 'SQL',
|
||||
value: InfluxVersion.SQL,
|
||||
description: 'Native SQL language. Supported in InfluxDB 3.0',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -64,6 +60,11 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
this.htmlPrefix = uniqueId('influxdb-config');
|
||||
}
|
||||
|
||||
versionNotice = {
|
||||
Flux: 'Support for Flux in Grafana is currently in beta',
|
||||
SQL: 'Support for SQL in Grafana is currently in alpha',
|
||||
};
|
||||
|
||||
// 1x
|
||||
onResetPassword = () => {
|
||||
updateDatasourcePluginResetOption(this.props, 'password');
|
||||
@@ -98,196 +99,30 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
renderInflux2x() {
|
||||
const { options } = this.props;
|
||||
const { secureJsonFields } = options;
|
||||
const secureJsonData = (options.secureJsonData || {}) as InfluxSecureJsonData;
|
||||
const { htmlPrefix } = this;
|
||||
getQueryLanguageDropdownValue = (v?: InfluxVersion) => {
|
||||
switch (v) {
|
||||
case InfluxVersion.InfluxQL:
|
||||
return versionsWithSQL[0];
|
||||
case InfluxVersion.Flux:
|
||||
return versionsWithSQL[1];
|
||||
case InfluxVersion.SQL:
|
||||
return versionsWithSQL[2];
|
||||
default:
|
||||
return versionsWithSQL[0];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel htmlFor={`${htmlPrefix}-org`} className="width-10">
|
||||
Organization
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
id={`${htmlPrefix}-org`}
|
||||
className="width-20"
|
||||
value={options.jsonData.organization || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(this.props, 'organization')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<SecretFormField
|
||||
isConfigured={Boolean(secureJsonFields && secureJsonFields.token)}
|
||||
value={secureJsonData.token || ''}
|
||||
label="Token"
|
||||
aria-label="Token"
|
||||
labelWidth={10}
|
||||
inputWidth={20}
|
||||
onReset={this.onResetToken}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(this.props, 'token')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-10">Default Bucket</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
className="width-20"
|
||||
placeholder="default bucket"
|
||||
value={options.jsonData.defaultBucket || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(this.props, 'defaultBucket')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
className="width-10"
|
||||
tooltip="A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example 1m if your data is written every minute."
|
||||
>
|
||||
Min time interval
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
className="width-10"
|
||||
placeholder="10s"
|
||||
value={options.jsonData.timeInterval || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(this.props, 'timeInterval')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderInflux1x() {
|
||||
const { options } = this.props;
|
||||
const { secureJsonFields } = options;
|
||||
const secureJsonData = (options.secureJsonData || {}) as InfluxSecureJsonData;
|
||||
const { htmlPrefix } = this;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoBox>
|
||||
<h5>Database Access</h5>
|
||||
<p>
|
||||
Setting the database for this datasource does not deny access to other databases. The InfluxDB query syntax
|
||||
allows switching the database in the query. For example:
|
||||
<code>SHOW MEASUREMENTS ON _internal</code> or
|
||||
<code>SELECT * FROM "_internal".."database" LIMIT 10</code>
|
||||
<br />
|
||||
<br />
|
||||
To support data isolation and security, make sure appropriate permissions are configured in InfluxDB.
|
||||
</p>
|
||||
</InfoBox>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel htmlFor={`${htmlPrefix}-db`} className="width-10">
|
||||
Database
|
||||
</InlineFormLabel>
|
||||
<div className="width-20">
|
||||
<Input
|
||||
id={`${htmlPrefix}-db`}
|
||||
className="width-20"
|
||||
value={options.jsonData.dbName ?? options.database}
|
||||
onChange={(event) => {
|
||||
this.props.onOptionsChange({
|
||||
...options,
|
||||
database: '',
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
dbName: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel htmlFor={`${htmlPrefix}-user`} className="width-10">
|
||||
User
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
id={`${htmlPrefix}-user`}
|
||||
className="width-20"
|
||||
value={options.user || ''}
|
||||
onChange={onUpdateDatasourceOption(this.props, 'user')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<SecretFormField
|
||||
isConfigured={Boolean(secureJsonFields && secureJsonFields.password)}
|
||||
value={secureJsonData.password || ''}
|
||||
label="Password"
|
||||
aria-label="Password"
|
||||
labelWidth={10}
|
||||
inputWidth={20}
|
||||
onReset={this.onResetPassword}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(this.props, 'password')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
htmlFor={`${htmlPrefix}-http-method`}
|
||||
className="width-10"
|
||||
tooltip="You can use either GET or POST HTTP method to query your InfluxDB database. The POST
|
||||
method allows you to perform heavy requests (with a lots of WHERE clause) while the GET method
|
||||
will restrict you and return an error if the query is too large."
|
||||
>
|
||||
HTTP Method
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
inputId={`${htmlPrefix}-http-method`}
|
||||
className="width-10"
|
||||
value={httpModes.find((httpMode) => httpMode.value === options.jsonData.httpMode)}
|
||||
options={httpModes}
|
||||
defaultValue={options.jsonData.httpMode}
|
||||
onChange={onUpdateDatasourceJsonDataOptionSelect(this.props, 'httpMode')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
className="width-10"
|
||||
tooltip="A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example 1m if your data is written every minute."
|
||||
>
|
||||
Min time interval
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
className="width-10"
|
||||
placeholder="10s"
|
||||
value={options.jsonData.timeInterval || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(this.props, 'timeInterval')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
renderJsonDataOptions() {
|
||||
switch (this.props.options.jsonData.version) {
|
||||
case InfluxVersion.InfluxQL:
|
||||
return <InfluxInfluxQLConfig {...this.props} />;
|
||||
case InfluxVersion.Flux:
|
||||
return <InfluxFluxConfig {...this.props} />;
|
||||
case InfluxVersion.SQL:
|
||||
return <InfluxSqlConfig {...this.props} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -303,8 +138,8 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
<Select
|
||||
aria-label="Query language"
|
||||
className="width-30"
|
||||
value={options.jsonData.version === InfluxVersion.Flux ? versions[1] : versions[0]}
|
||||
options={versions}
|
||||
value={this.getQueryLanguageDropdownValue(options.jsonData.version)}
|
||||
options={config.featureToggles.influxdbSqlSupport ? versionsWithSQL : versions}
|
||||
defaultValue={versions[0]}
|
||||
onChange={this.onVersionChanged}
|
||||
/>
|
||||
@@ -312,16 +147,15 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{options.jsonData.version === InfluxVersion.Flux && (
|
||||
<InfoBox>
|
||||
<h5>Support for Flux in Grafana is currently in beta</h5>
|
||||
{options.jsonData.version !== InfluxVersion.InfluxQL && (
|
||||
<Alert severity="info" title={this.versionNotice[options.jsonData.version!]}>
|
||||
<p>
|
||||
Please report any issues to: <br />
|
||||
<a href="https://github.com/grafana/grafana/issues/new/choose">
|
||||
https://github.com/grafana/grafana/issues
|
||||
</a>
|
||||
</p>
|
||||
</InfoBox>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isDirectAccess && (
|
||||
@@ -342,7 +176,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
<div>
|
||||
<h3 className="page-heading">InfluxDB Details</h3>
|
||||
</div>
|
||||
{options.jsonData.version === InfluxVersion.Flux ? this.renderInflux2x() : this.renderInflux1x()}
|
||||
{this.renderJsonDataOptions()}
|
||||
<div className="gf-form-inline">
|
||||
<InlineField
|
||||
labelWidth={20}
|
||||
@@ -352,7 +186,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
<Input
|
||||
placeholder="1000"
|
||||
type="number"
|
||||
className="width-10"
|
||||
className="width-20"
|
||||
value={this.state.maxSeries}
|
||||
onChange={(event) => {
|
||||
// We duplicate this state so that we allow to write freely inside the input. We don't have
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { uniqueId } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
|
||||
|
||||
import { InfluxOptions, InfluxSecureJsonData } from '../../../types';
|
||||
|
||||
const { Input, SecretFormField } = LegacyForms;
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<InfluxOptions, InfluxSecureJsonData>;
|
||||
|
||||
export const InfluxFluxConfig = (props: Props) => {
|
||||
const {
|
||||
options: { jsonData, secureJsonData, secureJsonFields },
|
||||
} = props;
|
||||
const htmlPrefix = uniqueId('influxdb-flux-config');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel htmlFor={`${htmlPrefix}-org`} className="width-10">
|
||||
Organization
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
id={`${htmlPrefix}-org`}
|
||||
className="width-20"
|
||||
value={jsonData.organization || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'organization')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<SecretFormField
|
||||
isConfigured={Boolean(secureJsonFields && secureJsonFields.token)}
|
||||
value={secureJsonData?.token || ''}
|
||||
label="Token"
|
||||
aria-label="Token"
|
||||
labelWidth={10}
|
||||
inputWidth={20}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'token')}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'token')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-10">Default Bucket</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
className="width-20"
|
||||
placeholder="default bucket"
|
||||
value={jsonData.defaultBucket || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'defaultBucket')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
className="width-10"
|
||||
tooltip="A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example 1m if your data is written every minute."
|
||||
>
|
||||
Min time interval
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
className="width-20"
|
||||
placeholder="10s"
|
||||
value={jsonData.timeInterval || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { uniqueId } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
onUpdateDatasourceJsonDataOption,
|
||||
onUpdateDatasourceJsonDataOptionSelect,
|
||||
onUpdateDatasourceOption,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
SelectableValue,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { Alert, InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
|
||||
|
||||
import { InfluxOptions, InfluxSecureJsonData } from '../../../types';
|
||||
|
||||
const { Input, SecretFormField } = LegacyForms;
|
||||
|
||||
const httpModes: SelectableValue[] = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
];
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<InfluxOptions, InfluxSecureJsonData>;
|
||||
|
||||
export const InfluxInfluxQLConfig = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
const { database, jsonData, secureJsonData, secureJsonFields } = options;
|
||||
const htmlPrefix = uniqueId('influxdb-influxql-config');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert severity="info" title="Database Access">
|
||||
<p>
|
||||
Setting the database for this datasource does not deny access to other databases. The InfluxDB query syntax
|
||||
allows switching the database in the query. For example:
|
||||
<code>SHOW MEASUREMENTS ON _internal</code> or
|
||||
<code>SELECT * FROM "_internal".."database" LIMIT 10</code>
|
||||
<br />
|
||||
<br />
|
||||
To support data isolation and security, make sure appropriate permissions are configured in InfluxDB.
|
||||
</p>
|
||||
</Alert>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel htmlFor={`${htmlPrefix}-db`} className="width-10">
|
||||
Database
|
||||
</InlineFormLabel>
|
||||
<div className="width-20">
|
||||
<Input
|
||||
id={`${htmlPrefix}-db`}
|
||||
className="width-20"
|
||||
value={jsonData.dbName ?? database}
|
||||
onChange={(event) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
database: '',
|
||||
jsonData: {
|
||||
...jsonData,
|
||||
dbName: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel htmlFor={`${htmlPrefix}-user`} className="width-10">
|
||||
User
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
id={`${htmlPrefix}-user`}
|
||||
className="width-20"
|
||||
value={options.user || ''}
|
||||
onChange={onUpdateDatasourceOption(props, 'user')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<SecretFormField
|
||||
isConfigured={Boolean(secureJsonFields && secureJsonFields.password)}
|
||||
value={secureJsonData?.password || ''}
|
||||
label="Password"
|
||||
aria-label="Password"
|
||||
labelWidth={10}
|
||||
inputWidth={20}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'password')}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
htmlFor={`${htmlPrefix}-http-method`}
|
||||
className="width-10"
|
||||
tooltip="You can use either GET or POST HTTP method to query your InfluxDB database. The POST
|
||||
method allows you to perform heavy requests (with a lots of WHERE clause) while the GET method
|
||||
will restrict you and return an error if the query is too large."
|
||||
>
|
||||
HTTP Method
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
inputId={`${htmlPrefix}-http-method`}
|
||||
className="width-20"
|
||||
value={httpModes.find((httpMode) => httpMode.value === options.jsonData.httpMode)}
|
||||
options={httpModes}
|
||||
defaultValue={options.jsonData.httpMode}
|
||||
onChange={onUpdateDatasourceJsonDataOptionSelect(props, 'httpMode')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
className="width-10"
|
||||
tooltip="A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example 1m if your data is written every minute."
|
||||
>
|
||||
Min time interval
|
||||
</InlineFormLabel>
|
||||
<div className="width-10">
|
||||
<Input
|
||||
className="width-20"
|
||||
placeholder="10s"
|
||||
value={options.jsonData.timeInterval || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
onUpdateDatasourceSecureJsonDataOption,
|
||||
updateDatasourcePluginResetOption,
|
||||
} from '@grafana/data';
|
||||
import { InlineField, SecretInput, Input, InlineFieldRow, InlineLabel } from '@grafana/ui';
|
||||
|
||||
import { InfluxOptions, InfluxSecureJsonData } from '../../../types';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<InfluxOptions, InfluxSecureJsonData>;
|
||||
|
||||
type MetadataState = Array<{ key: string; value: string }>;
|
||||
|
||||
export const addMetaData = (setMetaData: (val: MetadataState) => void, metaDataArr: MetadataState) => {
|
||||
setMetaData([...metaDataArr, { key: '', value: '' }]);
|
||||
};
|
||||
|
||||
export const removeMetaData = (i: number, setMetaData: (val: MetadataState) => void, metaDataArr: MetadataState) => {
|
||||
const newMetaValues = [...metaDataArr];
|
||||
newMetaValues.splice(i, 1);
|
||||
setMetaData(newMetaValues);
|
||||
};
|
||||
|
||||
export const onKeyChange = (
|
||||
key: string,
|
||||
metaDataArr: MetadataState,
|
||||
index: number,
|
||||
setMetaData: (val: MetadataState) => void
|
||||
) => {
|
||||
const newMetaValues = [...metaDataArr];
|
||||
newMetaValues[index]['key'] = key;
|
||||
setMetaData(newMetaValues);
|
||||
};
|
||||
|
||||
export const onValueChange = (
|
||||
value: string,
|
||||
metaDataArr: MetadataState,
|
||||
index: number,
|
||||
setMetaData: (val: MetadataState) => void
|
||||
) => {
|
||||
const newMetaValues = [...metaDataArr];
|
||||
newMetaValues[index]['value'] = value;
|
||||
setMetaData(newMetaValues);
|
||||
};
|
||||
|
||||
export const InfluxSqlConfig = (props: Props) => {
|
||||
const {
|
||||
options: { jsonData, secureJsonData, secureJsonFields },
|
||||
} = props;
|
||||
|
||||
const existingMetadata: MetadataState = jsonData?.metadata?.length
|
||||
? jsonData?.metadata?.map((md) => ({ key: Object.keys(md)[0], value: Object.values(md)[0] }))
|
||||
: [{ key: 'bucket-name', value: '' }];
|
||||
const [metaDataArr, setMetaData] = useState<MetadataState>(existingMetadata);
|
||||
|
||||
useEffect(() => {
|
||||
const { onOptionsChange, options } = props;
|
||||
const mapData = metaDataArr?.map((m) => ({ [m.key]: m.value }));
|
||||
const jsonData = {
|
||||
...options.jsonData,
|
||||
metadata: mapData,
|
||||
};
|
||||
onOptionsChange({
|
||||
...options,
|
||||
jsonData,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metaDataArr]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="gf-form">
|
||||
<h6>Token</h6>
|
||||
</div>
|
||||
<div>
|
||||
<InlineField labelWidth={20} label="Token">
|
||||
<SecretInput
|
||||
width={40}
|
||||
name="token"
|
||||
type="text"
|
||||
value={secureJsonData?.token || ''}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'token')}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'token')}
|
||||
isConfigured={secureJsonFields?.token}
|
||||
/>
|
||||
</InlineField>
|
||||
</div>
|
||||
<div>
|
||||
<div className="gf-form">
|
||||
<h6>MetaData</h6>
|
||||
</div>
|
||||
{metaDataArr?.map((_, i) => (
|
||||
<InlineFieldRow key={i} style={{ flexFlow: 'row' }}>
|
||||
<InlineField labelWidth={20} label="Key">
|
||||
<Input
|
||||
key={i}
|
||||
width={40}
|
||||
name="key"
|
||||
type="text"
|
||||
value={metaDataArr[i]?.key || ''}
|
||||
placeholder="key"
|
||||
onChange={(e) => onKeyChange(e.currentTarget.value, metaDataArr, i, setMetaData)}
|
||||
></Input>
|
||||
</InlineField>
|
||||
<InlineField labelWidth={20} label="Value">
|
||||
<Input
|
||||
key={i}
|
||||
width={40}
|
||||
name="value"
|
||||
type="text"
|
||||
value={metaDataArr[i]?.value?.toString() ?? ''}
|
||||
placeholder="value"
|
||||
onChange={(e) => onValueChange(e.currentTarget.value, metaDataArr, i, setMetaData)}
|
||||
></Input>
|
||||
</InlineField>
|
||||
{i + 1 >= metaDataArr.length && (
|
||||
<InlineLabel as="button" className="" onClick={() => addMetaData(setMetaData, metaDataArr)} width="auto">
|
||||
+
|
||||
</InlineLabel>
|
||||
)}
|
||||
{i > 0 && (
|
||||
<InlineLabel
|
||||
as="button"
|
||||
className=""
|
||||
width="auto"
|
||||
onClick={() => removeMetaData(i, setMetaData, metaDataArr)}
|
||||
>
|
||||
-
|
||||
</InlineLabel>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,9 +5,10 @@ import { QueryEditorProps } from '@grafana/data/src';
|
||||
|
||||
import InfluxDatasource from '../../../datasource';
|
||||
import { buildRawQuery } from '../../../queryUtils';
|
||||
import { InfluxOptions, InfluxQuery } from '../../../types';
|
||||
import { InfluxOptions, InfluxQuery, InfluxVersion } from '../../../types';
|
||||
|
||||
import { FluxQueryEditor } from './flux/FluxQueryEditor';
|
||||
import { FSQLEditor } from './fsql/FSQLEditor';
|
||||
import { QueryEditorModeSwitcher } from './influxql/QueryEditorModeSwitcher';
|
||||
import { RawInfluxQLEditor } from './influxql/code/RawInfluxQLEditor';
|
||||
import { VisualInfluxQLEditor as VisualInfluxQLEditor } from './influxql/visual/VisualInfluxQLEditor';
|
||||
@@ -15,30 +16,34 @@ import { VisualInfluxQLEditor as VisualInfluxQLEditor } from './influxql/visual/
|
||||
type Props = QueryEditorProps<InfluxDatasource, InfluxQuery, InfluxOptions>;
|
||||
|
||||
export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props) => {
|
||||
if (datasource.isFlux) {
|
||||
return (
|
||||
<div className="gf-form-query-content">
|
||||
<FluxQueryEditor query={query} onChange={onChange} onRunQuery={onRunQuery} datasource={datasource} />
|
||||
</div>
|
||||
);
|
||||
switch (datasource.version) {
|
||||
case InfluxVersion.Flux:
|
||||
return (
|
||||
<div className="gf-form-query-content">
|
||||
<FluxQueryEditor query={query} onChange={onChange} onRunQuery={onRunQuery} datasource={datasource} />
|
||||
</div>
|
||||
);
|
||||
case InfluxVersion.SQL:
|
||||
return <FSQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />;
|
||||
case InfluxVersion.InfluxQL:
|
||||
default:
|
||||
return (
|
||||
<div className={css({ display: 'flex' })}>
|
||||
<div className={css({ flexGrow: 1 })}>
|
||||
{query.rawQuery ? (
|
||||
<RawInfluxQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
) : (
|
||||
<VisualInfluxQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} datasource={datasource} />
|
||||
)}
|
||||
</div>
|
||||
<QueryEditorModeSwitcher
|
||||
isRaw={query.rawQuery ?? false}
|
||||
onChange={(value) => {
|
||||
onChange({ ...query, query: buildRawQuery(query), rawQuery: value });
|
||||
onRunQuery();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css({ display: 'flex' })}>
|
||||
<div className={css({ flexGrow: 1 })}>
|
||||
{query.rawQuery ? (
|
||||
<RawInfluxQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
) : (
|
||||
<VisualInfluxQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} datasource={datasource} />
|
||||
)}
|
||||
</div>
|
||||
<QueryEditorModeSwitcher
|
||||
isRaw={query.rawQuery ?? false}
|
||||
onChange={(value) => {
|
||||
onChange({ ...query, query: buildRawQuery(query), rawQuery: value });
|
||||
onRunQuery();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
import { InfluxQuery } from '../../../../types';
|
||||
|
||||
type Props = {
|
||||
onChange: (query: InfluxQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
query: InfluxQuery;
|
||||
};
|
||||
|
||||
// Flight SQL Editor
|
||||
export const FSQLEditor = (props: Props) => {
|
||||
const onSQLQueryChange = (query?: string) => {
|
||||
if (query) {
|
||||
props.onChange({ ...props.query, query, resultFormat: 'table' });
|
||||
}
|
||||
props.onRunQuery();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={props.query.query}
|
||||
onBlur={(e) => onSQLQueryChange(e.currentTarget.value)}
|
||||
onChange={(e) => onSQLQueryChange(e.currentTarget.value)}
|
||||
/>
|
||||
<br />
|
||||
<button onClick={() => onSQLQueryChange()}>run query</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import React, { PureComponent } from 'react';
|
||||
import { InlineFormLabel, TextArea } from '@grafana/ui/src';
|
||||
|
||||
import InfluxDatasource from '../../../datasource';
|
||||
import { InfluxVersion } from '../../../types';
|
||||
import { FluxQueryEditor } from '../query/flux/FluxQueryEditor';
|
||||
|
||||
interface Props {
|
||||
@@ -18,7 +19,7 @@ export default class VariableQueryEditor extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
let { query, datasource, onChange } = this.props;
|
||||
if (datasource.isFlux) {
|
||||
if (datasource.version === InfluxVersion.Flux) {
|
||||
return (
|
||||
<FluxQueryEditor
|
||||
datasource={datasource}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
access: 'direct' | 'proxy';
|
||||
responseParser: ResponseParser;
|
||||
httpMode: string;
|
||||
isFlux: boolean;
|
||||
version?: InfluxVersion;
|
||||
isProxyAccess: boolean;
|
||||
retentionPolicies: string[];
|
||||
|
||||
@@ -81,11 +81,11 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
this.interval = settingsData.timeInterval;
|
||||
this.httpMode = settingsData.httpMode || 'GET';
|
||||
this.responseParser = new ResponseParser();
|
||||
this.isFlux = settingsData.version === InfluxVersion.Flux;
|
||||
this.version = settingsData.version ?? InfluxVersion.InfluxQL;
|
||||
this.isProxyAccess = instanceSettings.access === 'proxy';
|
||||
this.retentionPolicies = [];
|
||||
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux) {
|
||||
// When flux, use an annotation processor rather than the `annotationQuery` lifecycle
|
||||
this.annotations = {
|
||||
QueryEditor: FluxQueryEditor,
|
||||
@@ -100,9 +100,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
|
||||
async getRetentionPolicies(): Promise<string[]> {
|
||||
// Only For InfluxQL Mode
|
||||
if (this.isFlux || this.retentionPolicies.length) {
|
||||
return Promise.resolve(this.retentionPolicies);
|
||||
} else {
|
||||
if (this.version === InfluxVersion.InfluxQL && !this.retentionPolicies.length) {
|
||||
return getAllPolicies(this).catch((err) => {
|
||||
console.error(
|
||||
'Unable to fetch retention policies. Queries will be run without specifying retention policy.',
|
||||
@@ -111,6 +109,8 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
return Promise.resolve(this.retentionPolicies);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(this.retentionPolicies);
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<InfluxQuery>): Observable<DataQueryResponse> {
|
||||
@@ -171,7 +171,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
return merge(...streams);
|
||||
}
|
||||
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux || this.version === InfluxVersion.SQL) {
|
||||
return super.query(filteredRequest);
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
}
|
||||
|
||||
getQueryDisplayText(query: InfluxQuery) {
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux) {
|
||||
return query.query;
|
||||
}
|
||||
return new InfluxQueryModel(query).render(false);
|
||||
@@ -231,7 +231,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
* Returns false if the query should be skipped
|
||||
*/
|
||||
filterQuery(query: InfluxQuery): boolean {
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux) {
|
||||
return !!query.query;
|
||||
}
|
||||
return true;
|
||||
@@ -241,7 +241,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
// We want to interpolate these variables on backend
|
||||
const { __interval, __interval_ms, ...rest } = scopedVars || {};
|
||||
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux) {
|
||||
return {
|
||||
...query,
|
||||
query: this.templateSrv.replace(query.query ?? '', rest), // The raw query text
|
||||
@@ -258,7 +258,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
targetContainsTemplate(target: InfluxQuery) {
|
||||
// for flux-mode we just take target.query,
|
||||
// for influxql-mode we use InfluxQueryModel to create the text-representation
|
||||
const queryText = this.isFlux ? target.query : buildRawQuery(target);
|
||||
const queryText = this.version === InfluxVersion.Flux ? target.query : buildRawQuery(target);
|
||||
|
||||
return this.templateSrv.containsTemplate(queryText);
|
||||
}
|
||||
@@ -269,7 +269,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
}
|
||||
|
||||
return queries.map((query) => {
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux) {
|
||||
return {
|
||||
...query,
|
||||
datasource: this.getRef(),
|
||||
@@ -347,7 +347,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
}
|
||||
|
||||
async metricFindQuery(query: string, options?: any): Promise<MetricFindValue[]> {
|
||||
if (this.isFlux || this.isMigrationToggleOnAndIsAccessProxy()) {
|
||||
if (this.version === InfluxVersion.Flux || this.isMigrationToggleOnAndIsAccessProxy()) {
|
||||
const target: InfluxQuery = {
|
||||
refId: 'metricFindQuery',
|
||||
query,
|
||||
@@ -696,7 +696,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
}
|
||||
|
||||
async annotationEvents(options: DataQueryRequest, annotation: InfluxQuery): Promise<AnnotationEvent[]> {
|
||||
if (this.isFlux) {
|
||||
if (this.version === InfluxVersion.Flux) {
|
||||
return Promise.reject({
|
||||
message: 'Flux requires the standard annotation query',
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { backendSrv } from 'app/core/services/backend_srv'; // will use the vers
|
||||
|
||||
import { BROWSER_MODE_DISABLED_MESSAGE } from '../constants';
|
||||
import InfluxDatasource from '../datasource';
|
||||
import { InfluxVersion } from '../types';
|
||||
|
||||
//@ts-ignore
|
||||
const templateSrv = new TemplateSrvStub();
|
||||
@@ -295,7 +296,7 @@ describe('InfluxDataSource', () => {
|
||||
|
||||
describe('when interpolating query variables for dashboard->explore', () => {
|
||||
it('should interpolate all variables with Flux mode', () => {
|
||||
ds.isFlux = true;
|
||||
ds.version = InfluxVersion.Flux;
|
||||
const fluxQuery = {
|
||||
refId: 'x',
|
||||
query: '$interpolationVar,$interpolationVar2',
|
||||
@@ -309,7 +310,7 @@ describe('InfluxDataSource', () => {
|
||||
});
|
||||
|
||||
it('should interpolate all variables with InfluxQL mode', () => {
|
||||
ds.isFlux = false;
|
||||
ds.version = InfluxVersion.InfluxQL;
|
||||
const queries = ds.interpolateVariablesInQueries([influxQuery], {
|
||||
interpolationVar: { text: text, value: text },
|
||||
interpolationVar2: { text: text2, value: text2 },
|
||||
@@ -320,7 +321,7 @@ describe('InfluxDataSource', () => {
|
||||
|
||||
describe('when interpolating template variables', () => {
|
||||
it('should apply all template variables with Flux mode', () => {
|
||||
ds.isFlux = true;
|
||||
ds.version = InfluxVersion.Flux;
|
||||
const fluxQuery = {
|
||||
refId: 'x',
|
||||
query: '$interpolationVar',
|
||||
@@ -336,7 +337,7 @@ describe('InfluxDataSource', () => {
|
||||
});
|
||||
|
||||
it('should apply all template variables with InfluxQL mode', () => {
|
||||
ds.isFlux = false;
|
||||
ds.version = ds.version = InfluxVersion.InfluxQL;
|
||||
ds.access = 'proxy';
|
||||
config.featureToggles.influxdbBackendMigration = true;
|
||||
const query = ds.applyTemplateVariables(influxQuery, {
|
||||
@@ -347,7 +348,7 @@ describe('InfluxDataSource', () => {
|
||||
});
|
||||
|
||||
it('should apply all scopedVars to tags', () => {
|
||||
ds.isFlux = false;
|
||||
ds.version = InfluxVersion.InfluxQL;
|
||||
ds.access = 'proxy';
|
||||
config.featureToggles.influxdbBackendMigration = true;
|
||||
const query = ds.applyTemplateVariables(influxQuery, {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AdHocVariableFilter, DataQuery, DataSourceJsonData } from '@grafana/dat
|
||||
export enum InfluxVersion {
|
||||
InfluxQL = 'InfluxQL',
|
||||
Flux = 'Flux',
|
||||
SQL = 'SQL',
|
||||
}
|
||||
|
||||
export interface InfluxOptions extends DataSourceJsonData {
|
||||
@@ -17,6 +18,9 @@ export interface InfluxOptions extends DataSourceJsonData {
|
||||
organization?: string;
|
||||
defaultBucket?: string;
|
||||
maxSeries?: number;
|
||||
|
||||
// With SQL
|
||||
metadata?: Array<Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user