mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
InfluxDB: different config UI for 1x vs 2x (#25723)
This commit is contained in:
parent
0797fe88a1
commit
8d1ed33e20
@ -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')}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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']);
|
||||||
|
},
|
||||||
|
]);
|
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||||
|
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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: {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user