InfluxDB: different config UI for 1x vs 2x (#25723)

This commit is contained in:
Ryan McKinley 2020-06-22 13:03:34 -07:00 committed by GitHub
parent 0797fe88a1
commit 8d1ed33e20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1277 additions and 889 deletions

View File

@ -57,7 +57,7 @@ export const SecretFormField: FunctionComponent<Props> = ({
<> <>
<input <input
type="text" type="text"
className={cx(`gf-form-input width-${inputWidth! - 2}`, styles.noRadiusInput)} className={cx(`gf-form-input width-${inputWidth}`, styles.noRadiusInput)}
disabled={true} disabled={true}
value="configured" value="configured"
{...omit(inputProps, 'value')} {...omit(inputProps, 'value')}

View File

@ -43,36 +43,18 @@ func init() {
tsdb.RegisterTsdbQueryEndpoint("influxdb", NewInfluxDBExecutor) tsdb.RegisterTsdbQueryEndpoint("influxdb", NewInfluxDBExecutor)
} }
func AllFlux(queries *tsdb.TsdbQuery) (bool, error) {
var hasFlux bool
var allFlux bool
for i, q := range queries.Queries {
qType := q.Model.Get("queryType").MustString("")
if qType == "Flux" {
hasFlux = true
if i == 0 && hasFlux {
allFlux = true
continue
}
}
if allFlux && qType != "Flux" {
return true, fmt.Errorf("when using flux, all queries must be a flux query")
}
}
return allFlux, nil
}
func (e *InfluxDBExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { func (e *InfluxDBExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
result := &tsdb.Response{}
allFlux, err := AllFlux(tsdbQuery)
if err != nil {
return nil, err
}
if allFlux { glog.Info("query", "q", tsdbQuery.Queries)
version := dsInfo.JsonData.Get("version").MustString("")
if version == "Flux" {
return flux.Query(ctx, dsInfo, tsdbQuery) return flux.Query(ctx, dsInfo, tsdbQuery)
} }
// NOTE: the following path is currently only called from alerting queries
// In dashboards, the request runs through proxy and are managed in the frontend
query, err := e.getQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) query, err := e.getQuery(dsInfo, tsdbQuery.Queries, tsdbQuery)
if err != nil { if err != nil {
return nil, err return nil, err
@ -120,6 +102,7 @@ func (e *InfluxDBExecutor) Query(ctx context.Context, dsInfo *models.DataSource,
return nil, response.Err return nil, response.Err
} }
result := &tsdb.Response{}
result.Results = make(map[string]*tsdb.QueryResult) result.Results = make(map[string]*tsdb.QueryResult)
result.Results["A"] = e.ResponseParser.Parse(&response, query) result.Results["A"] = e.ResponseParser.Parse(&response, query)

View File

@ -10,41 +10,179 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { DataSourceHttpSettings, InlineFormLabel, LegacyForms } from '@grafana/ui'; import { DataSourceHttpSettings, InlineFormLabel, LegacyForms } from '@grafana/ui';
const { Select, Input, SecretFormField } = LegacyForms; const { Select, Input, SecretFormField } = LegacyForms;
import { InfluxOptions, InfluxSecureJsonData } from '../types'; import { InfluxOptions, InfluxSecureJsonData, InfluxVersion } from '../types';
const httpModes = [ const httpModes = [
{ label: 'GET', value: 'GET' }, { label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' }, { label: 'POST', value: 'POST' },
] as SelectableValue[]; ] as SelectableValue[];
const versions = [
{
label: 'InfluxQL',
value: InfluxVersion.InfluxQL,
description: 'The InfluxDB SQL-like query language. Supported in InfluxDB 1.x',
},
{
label: 'Flux',
value: InfluxVersion.Flux,
description: 'Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+ (beta)',
},
] as Array<SelectableValue<InfluxVersion>>;
export type Props = DataSourcePluginOptionsEditorProps<InfluxOptions>; export type Props = DataSourcePluginOptionsEditorProps<InfluxOptions>;
export class ConfigEditor extends PureComponent<Props> { export class ConfigEditor extends PureComponent<Props> {
// 1x
onResetPassword = () => { onResetPassword = () => {
updateDatasourcePluginResetOption(this.props, 'password'); updateDatasourcePluginResetOption(this.props, 'password');
}; };
// 2x
onResetToken = () => { onResetToken = () => {
updateDatasourcePluginResetOption(this.props, 'token'); updateDatasourcePluginResetOption(this.props, 'token');
}; };
onToggleFlux = (event: React.SyntheticEvent<HTMLInputElement>) => { onVersionChanged = (selected: SelectableValue<InfluxVersion>) => {
const { options, onOptionsChange } = this.props; const { options, onOptionsChange } = this.props;
onOptionsChange({
const copy = {
...options, ...options,
jsonData: { jsonData: {
...options.jsonData, ...options.jsonData,
enableFlux: !options.jsonData.enableFlux, version: selected.value,
}, },
};
if (selected.value === InfluxVersion.Flux) {
copy.access = 'proxy';
copy.basicAuth = true;
copy.jsonData.httpMode = 'POST';
// Remove old 1x configs
delete copy.user;
delete copy.database;
}
onOptionsChange(copy);
};
onUpdateInflux2xURL = (e: React.SyntheticEvent<HTMLInputElement>) => {
const { options, onOptionsChange } = this.props;
onOptionsChange({
...options,
url: e.currentTarget.value,
access: 'proxy',
basicAuth: true,
}); });
}; };
render() { renderInflux2x() {
const { options } = this.props;
const { secureJsonFields } = options;
const secureJsonData = (options.secureJsonData || {}) as InfluxSecureJsonData;
return (
<div>
<div className="gf-form-group">
<div className="width-30 grafana-info-box">
<h5>Support for flux in Grafana is currently in beta</h5>
<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>
</div>
</div>
<br />
<h3 className="page-heading">Connection</h3>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
className="width-10"
tooltip="This URL needs to be accessible from the grafana backend/server."
>
URL
</InlineFormLabel>
<div className="width-20">
<Input
className="width-20"
value={options.url || ''}
placeholder="http://localhost:9999/api/v2"
onChange={this.onUpdateInflux2xURL}
/>
</div>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-10">Organization</InlineFormLabel>
<div className="width-10">
<Input
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={(secureJsonFields && secureJsonFields.token) as boolean}
value={secureJsonData.token || ''}
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>
</div>
);
}
renderInflux1x() {
const { options, onOptionsChange } = this.props; const { options, onOptionsChange } = this.props;
const { secureJsonFields } = options; const { secureJsonFields } = options;
const secureJsonData = (options.secureJsonData || {}) as InfluxSecureJsonData; const secureJsonData = (options.secureJsonData || {}) as InfluxSecureJsonData;
return ( return (
<> <div>
<DataSourceHttpSettings <DataSourceHttpSettings
showAccessOptions={true} showAccessOptions={true}
dataSourceConfig={options} dataSourceConfig={options}
@ -54,50 +192,6 @@ export class ConfigEditor extends PureComponent<Props> {
<h3 className="page-heading">InfluxDB Details</h3> <h3 className="page-heading">InfluxDB Details</h3>
<div className="gf-form-group"> <div className="gf-form-group">
<div className="gf-form-inline">
<LegacyForms.Switch
label="Enable flux"
labelClass="width-10"
checked={options.jsonData.enableFlux || false}
onChange={this.onToggleFlux}
tooltip="Suport flux query endpoint"
/>
</div>
{options.jsonData.enableFlux && (
<>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-10">Organization</InlineFormLabel>
<div className="width-10">
<Input
className="width-10"
placeholder="enter organization"
value={options.jsonData.organization || ''}
onChange={onUpdateDatasourceJsonDataOption(this.props, 'organization')}
/>
</div>
</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-10"
placeholder="default bucket"
value={options.jsonData.defaultBucket || ''}
onChange={onUpdateDatasourceJsonDataOption(this.props, 'defaultBucket')}
/>
</div>
</div>
</div>
<br />
<br />
</>
)}
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel className="width-10">Database</InlineFormLabel> <InlineFormLabel className="width-10">Database</InlineFormLabel>
@ -135,19 +229,6 @@ export class ConfigEditor extends PureComponent<Props> {
/> />
</div> </div>
</div> </div>
<div className="gf-form-inline">
<div className="gf-form">
<SecretFormField
isConfigured={(secureJsonFields && secureJsonFields.token) as boolean}
value={secureJsonData.token || ''}
label="Token"
labelWidth={10}
inputWidth={20}
onReset={this.onResetPassword}
onChange={onUpdateDatasourceSecureJsonDataOption(this.props, 'token')}
/>
</div>
</div>
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel <InlineFormLabel
@ -168,6 +249,7 @@ export class ConfigEditor extends PureComponent<Props> {
</div> </div>
</div> </div>
</div> </div>
<div className="gf-form-group"> <div className="gf-form-group">
<div className="grafana-info-box"> <div className="grafana-info-box">
<h5>Database Access</h5> <h5>Database Access</h5>
@ -202,6 +284,31 @@ export class ConfigEditor extends PureComponent<Props> {
</div> </div>
</div> </div>
</div> </div>
</div>
);
}
render() {
const { options } = this.props;
return (
<>
<h3 className="page-heading">Query Language</h3>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<Select
className="width-30"
value={options.jsonData.version === InfluxVersion.Flux ? versions[1] : versions[0]}
options={versions}
defaultValue={versions[0]}
onChange={this.onVersionChanged}
/>
</div>
</div>
</div>
{options.jsonData.version === InfluxVersion.Flux ? this.renderInflux2x() : this.renderInflux1x()}
</> </>
); );
} }

View File

@ -0,0 +1,142 @@
import React, { Component } from 'react';
import coreModule from 'app/core/core_module';
import { InfluxQuery } from '../types';
import { SelectableValue } from '@grafana/data';
import { InlineFormLabel, LinkButton, Select, TextArea } from '@grafana/ui';
interface Props {
target: InfluxQuery;
change: (target: InfluxQuery) => void;
refresh: () => void;
}
const samples: Array<SelectableValue<string>> = [
{ label: 'Show buckets', description: 'List the avaliable buckets (table)', value: 'buckets()' },
{
label: 'Simple query',
description: 'filter by measurment and field',
value: `from(bucket: "db/rp")
|> range(start: v.timeRangeStart, stop:timeRangeStop)
|> filter(fn: (r) =>
r._measurement == "example-measurement" and
r._field == "example-field"
)`,
},
{
label: 'Grouped Query',
description: 'Group by (min/max/sum/median)',
value: `// v.windowPeriod is a variable referring to the current optimized window period (currently: $interval)
from(bucket: v.bucket)
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "measurement1" or r["_measurement"] =~ /^.*?regex.*$/)
|> filter(fn: (r) => r["_field"] == "field2" or r["_field"] =~ /^.*?regex.*$/)
|> aggregateWindow(every: v.windowPeriod, fn: mean|median|max|count|derivative|sum)
|> yield(name: "some-name")`,
},
{
label: 'Filter by value',
description: 'Results between a min/max',
value: `// v.bucket, v.timeRangeStart, and v.timeRange stop are all variables supported by the flux plugin and influxdb
from(bucket: v.bucket)
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_value"] >= 10 and r["_value"] <= 20)`,
},
{
label: 'Schema Exploration: (measurements)',
description: 'Get a list of measurement using flux',
value: `import "influxdata/influxdb/v1"
v1.measurements(bucket: v.bucket)`,
},
{
label: 'Schema Exploration: (fields)',
description: 'Return every possible key in a single table',
value: `from(bucket: v.bucket)
|> range(start: -30m)
|> keys()
|> keep(columns: ["_value"])
|> group()
|> distinct()`,
},
{
label: 'Schema Exploration: (tag keys)',
description: 'Get a list of tag keys using flux',
value: `import "influxdata/influxdb/v1"
v1.tagKeys(bucket: v.bucket)`,
},
{
label: 'Schema Exploration: (tag values)',
description: 'Get a list of tag values using flux',
value: `import "influxdata/influxdb/v1"
v1.tagValues(
bucket: v.bucket,
tag: "host",
predicate: (r) => true,
start: -1d
)`,
},
];
export class FluxQueryEditor extends Component<Props> {
onFluxQueryChange = (e: any) => {
const { target, change } = this.props;
change({ ...target, query: e.currentTarget.value });
};
onFluxBlur = (e: any) => {
this.props.refresh();
};
onSampleChange = (val: SelectableValue<string>) => {
this.props.change({
...this.props.target,
query: val.value!,
});
// Angular HACK: Since the target does not actually change!
this.forceUpdate();
this.props.refresh();
};
render() {
const { target } = this.props;
return (
<>
<div className="gf-form">
<TextArea
value={target.query || ''}
onChange={this.onFluxQueryChange}
onBlur={this.onFluxBlur}
placeholder="Flux query"
rows={10}
/>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel width={6} tooltip="This supports queries copied from chronograph">
Help
</InlineFormLabel>
<Select width={20} options={samples} placeholder="Sample Query" onChange={this.onSampleChange} />
<LinkButton
icon="external-link-alt"
variant="secondary"
target="blank"
href="https://docs.influxdata.com/flux/latest/introduction/getting-started/"
>
Flux docs
</LinkButton>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</>
);
}
}
coreModule.directive('fluxQueryEditor', [
'reactDirective',
(reactDirective: any) => {
return reactDirective(FluxQueryEditor, ['target', 'change']);
},
]);

View File

@ -2,6 +2,76 @@
exports[`Render should disable basic auth password input 1`] = ` exports[`Render should disable basic auth password input 1`] = `
<Fragment> <Fragment>
<h3
className="page-heading"
>
Query Language
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
},
Object {
"description": "Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+ (beta)",
"label": "Flux",
"value": "Flux",
},
]
}
tabSelectsValue={true}
value={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
/>
</div>
</div>
</div>
<div>
<Component <Component
dataSourceConfig={ dataSourceConfig={
Object { Object {
@ -41,17 +111,6 @@ exports[`Render should disable basic auth password input 1`] = `
<div <div
className="gf-form-group" className="gf-form-group"
> >
<div
className="gf-form-inline"
>
<Switch
checked={false}
label="Enable flux"
labelClass="width-10"
onChange={[Function]}
tooltip="Suport flux query endpoint"
/>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -112,22 +171,6 @@ exports[`Render should disable basic auth password input 1`] = `
/> />
</div> </div>
</div> </div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<SecretFormField
inputWidth={20}
label="Token"
labelWidth={10}
onChange={[Function]}
onReset={[Function]}
value=""
/>
</div>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -241,11 +284,82 @@ exports[`Render should disable basic auth password input 1`] = `
</div> </div>
</div> </div>
</div> </div>
</div>
</Fragment> </Fragment>
`; `;
exports[`Render should hide basic auth fields when switch off 1`] = ` exports[`Render should hide basic auth fields when switch off 1`] = `
<Fragment> <Fragment>
<h3
className="page-heading"
>
Query Language
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
},
Object {
"description": "Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+ (beta)",
"label": "Flux",
"value": "Flux",
},
]
}
tabSelectsValue={true}
value={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
/>
</div>
</div>
</div>
<div>
<Component <Component
dataSourceConfig={ dataSourceConfig={
Object { Object {
@ -285,17 +399,6 @@ exports[`Render should hide basic auth fields when switch off 1`] = `
<div <div
className="gf-form-group" className="gf-form-group"
> >
<div
className="gf-form-inline"
>
<Switch
checked={false}
label="Enable flux"
labelClass="width-10"
onChange={[Function]}
tooltip="Suport flux query endpoint"
/>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -356,22 +459,6 @@ exports[`Render should hide basic auth fields when switch off 1`] = `
/> />
</div> </div>
</div> </div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<SecretFormField
inputWidth={20}
label="Token"
labelWidth={10}
onChange={[Function]}
onReset={[Function]}
value=""
/>
</div>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -485,11 +572,82 @@ exports[`Render should hide basic auth fields when switch off 1`] = `
</div> </div>
</div> </div>
</div> </div>
</div>
</Fragment> </Fragment>
`; `;
exports[`Render should hide white listed cookies input when browser access chosen 1`] = ` exports[`Render should hide white listed cookies input when browser access chosen 1`] = `
<Fragment> <Fragment>
<h3
className="page-heading"
>
Query Language
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
},
Object {
"description": "Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+ (beta)",
"label": "Flux",
"value": "Flux",
},
]
}
tabSelectsValue={true}
value={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
/>
</div>
</div>
</div>
<div>
<Component <Component
dataSourceConfig={ dataSourceConfig={
Object { Object {
@ -529,17 +687,6 @@ exports[`Render should hide white listed cookies input when browser access chose
<div <div
className="gf-form-group" className="gf-form-group"
> >
<div
className="gf-form-inline"
>
<Switch
checked={false}
label="Enable flux"
labelClass="width-10"
onChange={[Function]}
tooltip="Suport flux query endpoint"
/>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -600,22 +747,6 @@ exports[`Render should hide white listed cookies input when browser access chose
/> />
</div> </div>
</div> </div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<SecretFormField
inputWidth={20}
label="Token"
labelWidth={10}
onChange={[Function]}
onReset={[Function]}
value=""
/>
</div>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -729,11 +860,82 @@ exports[`Render should hide white listed cookies input when browser access chose
</div> </div>
</div> </div>
</div> </div>
</div>
</Fragment> </Fragment>
`; `;
exports[`Render should render component 1`] = ` exports[`Render should render component 1`] = `
<Fragment> <Fragment>
<h3
className="page-heading"
>
Query Language
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
},
Object {
"description": "Advanced data scripting and query language. Supported in InfluxDB 2.x and 1.8+ (beta)",
"label": "Flux",
"value": "Flux",
},
]
}
tabSelectsValue={true}
value={
Object {
"description": "The InfluxDB SQL-like query language. Supported in InfluxDB 1.x",
"label": "InfluxQL",
"value": "InfluxQL",
}
}
/>
</div>
</div>
</div>
<div>
<Component <Component
dataSourceConfig={ dataSourceConfig={
Object { Object {
@ -773,17 +975,6 @@ exports[`Render should render component 1`] = `
<div <div
className="gf-form-group" className="gf-form-group"
> >
<div
className="gf-form-inline"
>
<Switch
checked={false}
label="Enable flux"
labelClass="width-10"
onChange={[Function]}
tooltip="Suport flux query endpoint"
/>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -844,22 +1035,6 @@ exports[`Render should render component 1`] = `
/> />
</div> </div>
</div> </div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<SecretFormField
inputWidth={20}
label="Token"
labelWidth={10}
onChange={[Function]}
onReset={[Function]}
value=""
/>
</div>
</div>
<div <div
className="gf-form-inline" className="gf-form-inline"
> >
@ -973,5 +1148,6 @@ exports[`Render should render component 1`] = `
</div> </div>
</div> </div>
</div> </div>
</div>
</Fragment> </Fragment>
`; `;

View File

@ -1,11 +1,19 @@
import _ from 'lodash'; import _ from 'lodash';
import { dateMath, DataSourceInstanceSettings, ScopedVars, DataQueryRequest, DataQueryResponse } from '@grafana/data'; import {
dateMath,
DataSourceInstanceSettings,
ScopedVars,
DataQueryRequest,
DataQueryResponse,
dateTime,
LoadingState,
} from '@grafana/data';
import InfluxSeries from './influx_series'; import InfluxSeries from './influx_series';
import InfluxQueryModel from './influx_query_model'; import InfluxQueryModel from './influx_query_model';
import ResponseParser from './response_parser'; import ResponseParser from './response_parser';
import { InfluxQueryBuilder } from './query_builder'; import { InfluxQueryBuilder } from './query_builder';
import { InfluxQuery, InfluxOptions, InfluxQueryType } from './types'; import { InfluxQuery, InfluxOptions, InfluxVersion } from './types';
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
import { Observable, from } from 'rxjs'; import { Observable, from } from 'rxjs';
@ -21,7 +29,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
interval: any; interval: any;
responseParser: any; responseParser: any;
httpMode: string; httpMode: string;
enableFlux: boolean; is2x: boolean;
constructor(instanceSettings: DataSourceInstanceSettings<InfluxOptions>) { constructor(instanceSettings: DataSourceInstanceSettings<InfluxOptions>) {
super(instanceSettings); super(instanceSettings);
@ -40,41 +48,11 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
this.interval = settingsData.timeInterval; this.interval = settingsData.timeInterval;
this.httpMode = settingsData.httpMode || 'GET'; this.httpMode = settingsData.httpMode || 'GET';
this.responseParser = new ResponseParser(); this.responseParser = new ResponseParser();
this.enableFlux = !!settingsData.enableFlux; this.is2x = settingsData.version === InfluxVersion.Flux;
} }
query(request: DataQueryRequest<InfluxQuery>): Observable<DataQueryResponse> { query(request: DataQueryRequest<InfluxQuery>): Observable<DataQueryResponse> {
let hasFlux = false; if (this.is2x) {
let allFlux = true;
// Update the queryType fields and manage migrations
for (const target of request.targets) {
if (target.queryType === InfluxQueryType.Flux) {
hasFlux = true;
} else {
allFlux = false;
if (target.queryType === InfluxQueryType.Classic) {
delete target.rawQuery;
} else if (target.rawQuery) {
target.queryType = InfluxQueryType.InfluxQL;
} else if (target.queryType === InfluxQueryType.InfluxQL) {
target.rawQuery = true; // so the old version works
} else {
target.queryType = InfluxQueryType.Classic; // Explicitly set it to classic
delete target.rawQuery;
}
}
}
// Process flux queries (data frame request)
if (hasFlux) {
if (!this.enableFlux) {
throw 'Flux not enabled for this datasource';
}
if (!allFlux) {
throw 'All queries must be flux';
}
// Calls /api/tsdb/query
return super.query(request); return super.query(request);
} }
@ -311,8 +289,40 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
).join('&'); ).join('&');
} }
// TODO: remove this so that everything gets sent to /healthcheck!
testDatasource() { testDatasource() {
if (this.is2x) {
// TODO: eventually use the real /health endpoint
const request: DataQueryRequest<InfluxQuery> = {
targets: [{ refId: 'test', query: 'buckets()' }],
requestId: `${this.id}-health-${Date.now()}`,
dashboardId: 0,
panelId: 0,
interval: '1m',
intervalMs: 60000,
maxDataPoints: 423,
range: {
from: dateTime(1000),
to: dateTime(2000),
},
} as DataQueryRequest<InfluxQuery>;
return super
.query(request)
.toPromise()
.then((res: any) => {
const data: DataQueryResponse = res.data;
if (data && data.state === LoadingState.Done) {
const buckets = data.data[0].length;
return { status: 'success', message: `Data source is working (${buckets} buckets)` };
}
console.log('InfluxDB Error', data);
return { status: 'error', message: 'Error reading buckets' };
})
.catch((err: any) => {
return { status: 'error', message: err.message };
});
}
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database); const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
const query = queryBuilder.buildExploreQuery('RETENTION POLICIES'); const query = queryBuilder.buildExploreQuery('RETENTION POLICIES');

View File

@ -5,6 +5,9 @@ import InfluxStartPage from './components/InfluxStartPage';
import { DataSourcePlugin } from '@grafana/data'; import { DataSourcePlugin } from '@grafana/data';
import ConfigEditor from './components/ConfigEditor'; import ConfigEditor from './components/ConfigEditor';
// This adds a directive that is used in the query editor
import './components/FluxQueryEditor';
class InfluxAnnotationsQueryCtrl { class InfluxAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html'; static templateUrl = 'partials/annotations.editor.html';
} }

View File

@ -1,37 +1,10 @@
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="true">
<div ng-if="ctrl.datasource.enableFlux" class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">QUERY</label>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input gf-size-auto"
ng-model="ctrl.target.queryType"
ng-options="f.value as f.text for f in ctrl.queryTypes"
ng-change="ctrl.refresh()"
></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- TODO use monaco flux editor --> <query-editor-row ng-if="ctrl.datasource.is2x" query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
<div ng-if="ctrl.target.queryType === 'Flux'"> <flux-query-editor target="ctrl.target" change="ctrl.onChange" refresh="ctrl.refresh"></flux-query-editor>
<div class="gf-form"> </query-editor-row>
<textarea
rows="3"
class="gf-form-input"
ng-model="ctrl.target.query"
spellcheck="false"
placeholder="Flux Query"
ng-model-onblur
ng-change="ctrl.refresh()"
></textarea>
</div>
</div>
<div ng-if="ctrl.target.queryType === 'InfluxQL' || !ctrl.target.queryType"> <query-editor-row ng-if="!ctrl.datasource.is2x" query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="true">
<div ng-if="ctrl.target.rawQuery">
<div class="gf-form"> <div class="gf-form">
<textarea <textarea
rows="3" rows="3"
@ -72,7 +45,7 @@
</div> </div>
</div> </div>
<div ng-if="ctrl.target.queryType === 'Classic'"> <div ng-if="!ctrl.target.rawQuery">
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">FROM</label> <label class="gf-form-label query-keyword width-7">FROM</label>

View File

@ -5,16 +5,17 @@ import InfluxQueryModel from './influx_query_model';
import queryPart from './query_part'; import queryPart from './query_part';
import { QueryCtrl } from 'app/plugins/sdk'; import { QueryCtrl } from 'app/plugins/sdk';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { InfluxQueryType } from './types'; import { InfluxQuery } from './types';
import InfluxDatasource from './datasource';
export class InfluxQueryCtrl extends QueryCtrl { export class InfluxQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html'; static templateUrl = 'partials/query.editor.html';
datasource: InfluxDatasource;
queryModel: InfluxQueryModel; queryModel: InfluxQueryModel;
queryBuilder: any; queryBuilder: any;
groupBySegment: any; groupBySegment: any;
resultFormats: any[]; resultFormats: any[];
queryTypes: any[];
orderByTime: any[]; orderByTime: any[];
policySegment: any; policySegment: any;
tagSegments: any[]; tagSegments: any[];
@ -39,15 +40,6 @@ export class InfluxQueryCtrl extends QueryCtrl {
{ text: 'Table', value: 'table' }, { text: 'Table', value: 'table' },
]; ];
// Show a dropdown for flux
if (this.datasource.enableFlux) {
this.queryTypes = [
{ text: 'Classic', value: InfluxQueryType.Classic },
{ text: 'InfluxQL', value: InfluxQueryType.InfluxQL },
{ text: 'Flux', value: InfluxQueryType.Flux },
];
}
this.policySegment = uiSegmentSrv.newSegment(this.target.policy); this.policySegment = uiSegmentSrv.newSegment(this.target.policy);
if (!this.target.measurement) { if (!this.target.measurement) {
@ -83,6 +75,10 @@ export class InfluxQueryCtrl extends QueryCtrl {
}); });
} }
onChange = (target: InfluxQuery) => {
this.target.query = target.query;
};
removeOrderByTime() { removeOrderByTime() {
this.target.orderByTime = 'ASC'; this.target.orderByTime = 'ASC';
} }
@ -194,6 +190,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]); return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]);
} }
} }
return Promise.resolve();
} }
handleGroupByPartEvent(part: any, index: any, evt: { name: any }) { handleGroupByPartEvent(part: any, index: any, evt: { name: any }) {
@ -218,6 +215,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]); return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]);
} }
} }
return Promise.resolve();
} }
fixTagSegments() { fixTagSegments() {
@ -247,21 +245,14 @@ export class InfluxQueryCtrl extends QueryCtrl {
this.panelCtrl.refresh(); this.panelCtrl.refresh();
} }
// Only valid for InfluxQL queries
toggleEditorMode() { toggleEditorMode() {
try { try {
this.target.query = this.queryModel.render(false); this.target.query = this.queryModel.render(false);
} catch (err) { } catch (err) {
console.log('query render error'); console.log('query render error');
} }
const { queryType } = this.target; this.target.rawQuery = !this.target.rawQuery;
if (queryType === InfluxQueryType.Flux || queryType === InfluxQueryType.InfluxQL) {
this.target.queryType = InfluxQueryType.Classic;
this.target.rawQuery = false;
} else if (this.datasource.enableFlux) {
this.target.queryType = InfluxQueryType.Flux;
} else {
this.target.queryType = InfluxQueryType.InfluxQL;
}
} }
getMeasurements(measurementFilter: any) { getMeasurements(measurementFilter: any) {

View File

@ -1,14 +1,15 @@
import '../query_ctrl'; import '../query_ctrl';
import { uiSegmentSrv } from 'app/core/services/segment_srv'; import { uiSegmentSrv } from 'app/core/services/segment_srv';
import { InfluxQueryCtrl } from '../query_ctrl'; import { InfluxQueryCtrl } from '../query_ctrl';
import InfluxDatasource from '../datasource';
describe('InfluxDBQueryCtrl', () => { describe('InfluxDBQueryCtrl', () => {
const ctx = {} as any; const ctx = {} as any;
beforeEach(() => { beforeEach(() => {
InfluxQueryCtrl.prototype.datasource = { InfluxQueryCtrl.prototype.datasource = ({
metricFindQuery: () => Promise.resolve([]), metricFindQuery: () => Promise.resolve([]),
}; } as unknown) as InfluxDatasource;
InfluxQueryCtrl.prototype.target = { target: {} }; InfluxQueryCtrl.prototype.target = { target: {} };
InfluxQueryCtrl.prototype.panelCtrl = { InfluxQueryCtrl.prototype.panelCtrl = {
panel: { panel: {

View File

@ -1,19 +1,28 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data'; import { DataQuery, DataSourceJsonData } from '@grafana/data';
export enum InfluxVersion {
InfluxQL = 'InfluxQL',
Flux = 'Flux',
}
export interface InfluxOptions extends DataSourceJsonData { export interface InfluxOptions extends DataSourceJsonData {
version?: InfluxVersion;
timeInterval: string; timeInterval: string;
httpMode: string; httpMode: string;
// Influx 2.0 // With Flux
enableFlux?: boolean;
organization?: string; organization?: string;
defaultBucket?: string; defaultBucket?: string;
maxSeries?: number; maxSeries?: number;
} }
export interface InfluxSecureJsonData { export interface InfluxSecureJsonData {
password?: string; // For Flux
token?: string; token?: string;
// In 1x a different password can be sent than then HTTP auth
password?: string;
} }
export interface InfluxQueryPart { export interface InfluxQueryPart {
@ -29,14 +38,7 @@ export interface InfluxQueryTag {
value: string; value: string;
} }
export enum InfluxQueryType {
Classic = 'Classic', // IFQL query builder
InfluxQL = 'InfluxQL', // raw ifql
Flux = 'Flux',
}
export interface InfluxQuery extends DataQuery { export interface InfluxQuery extends DataQuery {
queryType?: InfluxQueryType;
policy?: string; policy?: string;
measurement?: string; measurement?: string;
resultFormat?: 'time_series' | 'table'; resultFormat?: 'time_series' | 'table';
@ -48,6 +50,6 @@ export interface InfluxQuery extends DataQuery {
slimit?: string; slimit?: string;
tz?: string; tz?: string;
fill?: string; fill?: string;
rawQuery?: boolean; // deprecated (use raw InfluxQL) rawQuery?: boolean;
query?: string; query?: string;
} }

View File

@ -386,7 +386,7 @@ $panel-grid-placeholder-bg: darken(#1f60c4, 30%);
$panel-grid-placeholder-shadow: 0 0 4px #3274d9; $panel-grid-placeholder-shadow: 0 0 4px #3274d9;
// logs // logs
$logs-color-unkown: $gray-2; $logs-color-unknown: $gray-2;
// toggle-group // toggle-group
$button-toggle-group-btn-active-bg: linear-gradient(90deg, #eb7b18, #d44a3a); $button-toggle-group-btn-active-bg: linear-gradient(90deg, #eb7b18, #d44a3a);

View File

@ -379,7 +379,7 @@ $panel-grid-placeholder-bg: lighten(#5794f2, 30%);
$panel-grid-placeholder-shadow: 0 0 4px #5794f2; $panel-grid-placeholder-shadow: 0 0 4px #5794f2;
// logs // logs
$logs-color-unkown: $gray-5; $logs-color-unknown: $gray-5;
// toggle-group // toggle-group
$button-toggle-group-btn-active-bg: $brand-primary; $button-toggle-group-btn-active-bg: $brand-primary;