mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Adds LogQueryField for InfluxDb (#17450)
* Wip: Intial commit * Wip: Adds intial InfluxLogsQueryField * Refactor: Adds measurements to InfluxLogQueryField * Style: Tweaks styles and adds chosen measurement to measurements * Refactor: Adds remove filter row * refactor: make influx datasource typed Uses the new api for exporting the plugin. * adds metricFindQuery to DataSourceApi metricFindQuery, getTagKeys and getTagValues now returns a promise * influx: minor improvements Limits logs result to 1000. Don't show adhoc filter until measurement have been selected. * Refactor: Adds fields to Cascader and uses chosen field as log column Co-authored-by: Marcus <marcus.efraimsson@gmail.com>
This commit is contained in:
parent
a95c7bfa6f
commit
591ea0bfe3
@ -30,6 +30,11 @@ export class DataSourcePlugin<
|
||||
return this;
|
||||
}
|
||||
|
||||
setConfigCtrl(ConfigCtrl: any) {
|
||||
this.angularConfigCtrl = ConfigCtrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
setQueryCtrl(QueryCtrl: any) {
|
||||
this.components.QueryCtrl = QueryCtrl;
|
||||
return this;
|
||||
@ -199,6 +204,21 @@ export abstract class DataSourceApi<
|
||||
options?: TContextQueryOptions
|
||||
) => Promise<DataQueryResponse>;
|
||||
|
||||
/**
|
||||
* Variable query action.
|
||||
*/
|
||||
metricFindQuery?(query: any, options?: any): Promise<MetricFindValue[]>;
|
||||
|
||||
/**
|
||||
* Get tag keys for adhoc filters
|
||||
*/
|
||||
getTagKeys?(options: any): Promise<MetricFindValue[]>;
|
||||
|
||||
/**
|
||||
* Get tag values for adhoc filters
|
||||
*/
|
||||
getTagValues?(options: { key: any }): Promise<MetricFindValue[]>;
|
||||
|
||||
/**
|
||||
* Set after constructor call, as the data source instance is the most common thing to pass around
|
||||
* we attach the components to this instance for easy access
|
||||
@ -396,6 +416,10 @@ export interface QueryHint {
|
||||
fix?: QueryFix;
|
||||
}
|
||||
|
||||
export interface MetricFindValue {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface DataSourceJsonData {
|
||||
authType?: string;
|
||||
defaultRegion?: string;
|
||||
@ -440,6 +464,7 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
|
||||
jsonData: T;
|
||||
username?: string;
|
||||
password?: string; // when access is direct, for some legacy datasources
|
||||
database?: string;
|
||||
|
||||
/**
|
||||
* This is the full Authorization header if basic auth is ennabled.
|
||||
|
83
public/app/features/explore/AdHocFilter.tsx
Normal file
83
public/app/features/explore/AdHocFilter.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Select, GrafanaTheme, ThemeContext, SelectOptionItem } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
keyValueContainer: css`
|
||||
label: key-value-container;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
`,
|
||||
});
|
||||
|
||||
enum ChangeType {
|
||||
Key = 'key',
|
||||
Value = 'value',
|
||||
Operator = 'operator',
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
keys: string[];
|
||||
keysPlaceHolder?: string;
|
||||
initialKey?: string;
|
||||
initialOperator?: string;
|
||||
initialValue?: string;
|
||||
values?: string[];
|
||||
valuesPlaceHolder?: string;
|
||||
onKeyChanged: (key: string) => void;
|
||||
onValueChanged: (value: string) => void;
|
||||
onOperatorChanged: (operator: string) => void;
|
||||
}
|
||||
|
||||
export const AdHocFilter: React.FunctionComponent<Props> = props => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const onChange = (changeType: ChangeType) => (item: SelectOptionItem<string>) => {
|
||||
const { onKeyChanged, onValueChanged, onOperatorChanged } = props;
|
||||
switch (changeType) {
|
||||
case ChangeType.Key:
|
||||
onKeyChanged(item.value);
|
||||
break;
|
||||
case ChangeType.Operator:
|
||||
onOperatorChanged(item.value);
|
||||
break;
|
||||
case ChangeType.Value:
|
||||
onValueChanged(item.value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const stringToOption = (value: string) => ({ label: value, value: value });
|
||||
|
||||
const { keys, initialKey, keysPlaceHolder, initialOperator, values, initialValue, valuesPlaceHolder } = props;
|
||||
const operators = ['=', '!='];
|
||||
const keysAsOptions = keys ? keys.map(stringToOption) : [];
|
||||
const selectedKey = initialKey ? keysAsOptions.filter(option => option.value === initialKey) : null;
|
||||
const valuesAsOptions = values ? values.map(stringToOption) : [];
|
||||
const selectedValue = initialValue ? valuesAsOptions.filter(option => option.value === initialValue) : null;
|
||||
const operatorsAsOptions = operators.map(stringToOption);
|
||||
const selectedOperator = initialOperator
|
||||
? operatorsAsOptions.filter(option => option.value === initialOperator)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={cx([styles.keyValueContainer])}>
|
||||
<Select
|
||||
options={keysAsOptions}
|
||||
isSearchable
|
||||
value={selectedKey}
|
||||
onChange={onChange(ChangeType.Key)}
|
||||
placeholder={keysPlaceHolder}
|
||||
/>
|
||||
<Select options={operatorsAsOptions} value={selectedOperator} onChange={onChange(ChangeType.Operator)} />
|
||||
<Select
|
||||
options={valuesAsOptions}
|
||||
isSearchable
|
||||
value={selectedValue}
|
||||
onChange={onChange(ChangeType.Value)}
|
||||
placeholder={valuesPlaceHolder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
138
public/app/features/explore/AdHocFilterField.tsx
Normal file
138
public/app/features/explore/AdHocFilterField.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import { DataSourceApi, DataQuery, DataSourceJsonData } from '@grafana/ui';
|
||||
import { AdHocFilter } from './AdHocFilter';
|
||||
|
||||
export interface KeyValuePair {
|
||||
keys: string[];
|
||||
key: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface Props<TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData> {
|
||||
datasource: DataSourceApi<TQuery, TOptions>;
|
||||
onPairsChanged: (pairs: KeyValuePair[]) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
pairs: KeyValuePair[];
|
||||
}
|
||||
|
||||
export class AdHocFilterField<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> extends React.PureComponent<Props<TQuery, TOptions>, State> {
|
||||
state: State = { pairs: [] };
|
||||
|
||||
async componentDidMount() {
|
||||
const tagKeys = this.props.datasource.getTagKeys ? await this.props.datasource.getTagKeys({}) : [];
|
||||
const keys = tagKeys.map(tagKey => tagKey.text);
|
||||
const pairs = [{ key: null, operator: null, value: null, keys, values: [] }];
|
||||
this.setState({ pairs });
|
||||
}
|
||||
|
||||
onKeyChanged = (index: number) => async (key: string) => {
|
||||
const { datasource, onPairsChanged } = this.props;
|
||||
const tagValues = datasource.getTagValues ? await datasource.getTagValues({ key }) : [];
|
||||
const values = tagValues.map(tagValue => tagValue.text);
|
||||
const newPairs = this.updatePairAt(index, { key, values });
|
||||
|
||||
this.setState({ pairs: newPairs });
|
||||
onPairsChanged(newPairs);
|
||||
};
|
||||
|
||||
onValueChanged = (index: number) => (value: string) => {
|
||||
const newPairs = this.updatePairAt(index, { value });
|
||||
|
||||
this.setState({ pairs: newPairs });
|
||||
this.props.onPairsChanged(newPairs);
|
||||
};
|
||||
|
||||
onOperatorChanged = (index: number) => (operator: string) => {
|
||||
const newPairs = this.updatePairAt(index, { operator });
|
||||
|
||||
this.setState({ pairs: newPairs });
|
||||
this.props.onPairsChanged(newPairs);
|
||||
};
|
||||
|
||||
onAddFilter = async () => {
|
||||
const { pairs } = this.state;
|
||||
const tagKeys = this.props.datasource.getTagKeys ? await this.props.datasource.getTagKeys({}) : [];
|
||||
const keys = tagKeys.map(tagKey => tagKey.text);
|
||||
const newPairs = pairs.concat({ key: null, operator: null, value: null, keys, values: [] });
|
||||
|
||||
this.setState({ pairs: newPairs });
|
||||
};
|
||||
|
||||
onRemoveFilter = async (index: number) => {
|
||||
const { pairs } = this.state;
|
||||
const newPairs = pairs.reduce((allPairs, pair, pairIndex) => {
|
||||
if (pairIndex === index) {
|
||||
return allPairs;
|
||||
}
|
||||
return allPairs.concat(pair);
|
||||
}, []);
|
||||
|
||||
this.setState({ pairs: newPairs });
|
||||
};
|
||||
|
||||
private updatePairAt = (index: number, pair: Partial<KeyValuePair>) => {
|
||||
const { pairs } = this.state;
|
||||
const newPairs: KeyValuePair[] = [];
|
||||
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
|
||||
const newPair = pairs[pairIndex];
|
||||
if (index === pairIndex) {
|
||||
newPairs.push({
|
||||
...newPair,
|
||||
key: pair.key || newPair.key,
|
||||
value: pair.value || newPair.value,
|
||||
operator: pair.operator || newPair.operator,
|
||||
keys: pair.keys || newPair.keys,
|
||||
values: pair.values || newPair.values,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
newPairs.push(newPair);
|
||||
}
|
||||
|
||||
return newPairs;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { pairs } = this.state;
|
||||
return (
|
||||
<>
|
||||
{pairs.map((pair, index) => {
|
||||
const adHocKey = `adhoc-filter-${index}-${pair.key}-${pair.value}`;
|
||||
return (
|
||||
<div className="align-items-center flex-grow-1" key={adHocKey}>
|
||||
<AdHocFilter
|
||||
keys={pair.keys}
|
||||
values={pair.values}
|
||||
initialKey={pair.key}
|
||||
initialOperator={pair.operator}
|
||||
initialValue={pair.value}
|
||||
onKeyChanged={this.onKeyChanged(index)}
|
||||
onOperatorChanged={this.onOperatorChanged(index)}
|
||||
onValueChanged={this.onValueChanged(index)}
|
||||
/>
|
||||
{index < pairs.length - 1 && <span> AND </span>}
|
||||
{index < pairs.length - 1 && (
|
||||
<button className="gf-form-label gf-form-label--btn" onClick={() => this.onRemoveFilter(index)}>
|
||||
<i className="fa fa-minus" />
|
||||
</button>
|
||||
)}
|
||||
{index === pairs.length - 1 && (
|
||||
<button className="gf-form-label gf-form-label--btn" onClick={this.onAddFilter}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { ExploreQueryFieldProps } from '@grafana/ui';
|
||||
// @ts-ignore
|
||||
import Cascader from 'rc-cascader';
|
||||
|
||||
import InfluxQueryModel from '../influx_query_model';
|
||||
import { AdHocFilterField, KeyValuePair } from 'app/features/explore/AdHocFilterField';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import InfluxDatasource from '../datasource';
|
||||
import { InfluxQueryBuilder } from '../query_builder';
|
||||
import { InfluxQuery, InfluxOptions } from '../types';
|
||||
import { CascaderOption } from '../../loki/components/LokiQueryFieldForm';
|
||||
|
||||
export interface Props extends ExploreQueryFieldProps<InfluxDatasource, InfluxQuery, InfluxOptions> {}
|
||||
|
||||
export interface State {
|
||||
measurements: CascaderOption[];
|
||||
measurement: string;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
||||
templateSrv: TemplateSrv = new TemplateSrv();
|
||||
state: State = { measurements: [], measurement: null, field: null };
|
||||
|
||||
async componentDidMount() {
|
||||
const { datasource } = this.props;
|
||||
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, datasource.database);
|
||||
const measureMentsQuery = queryBuilder.buildExploreQuery('MEASUREMENTS');
|
||||
const influxMeasurements = await datasource.metricFindQuery(measureMentsQuery);
|
||||
|
||||
const measurements = [];
|
||||
for (let index = 0; index < influxMeasurements.length; index++) {
|
||||
const measurementObj = influxMeasurements[index];
|
||||
const queryBuilder = new InfluxQueryBuilder({ measurement: measurementObj.text, tags: [] }, datasource.database);
|
||||
const fieldsQuery = queryBuilder.buildExploreQuery('FIELDS');
|
||||
const influxFields = await datasource.metricFindQuery(fieldsQuery);
|
||||
const fields = influxFields.map((field: any) => ({
|
||||
label: field.text,
|
||||
value: field.text,
|
||||
children: [],
|
||||
}));
|
||||
measurements.push({
|
||||
label: measurementObj.text,
|
||||
value: measurementObj.text,
|
||||
children: fields,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ measurements });
|
||||
}
|
||||
|
||||
onMeasurementsChange = async (values: string[]) => {
|
||||
const { query } = this.props;
|
||||
const measurement = values[0];
|
||||
const field = values[1];
|
||||
|
||||
this.setState({ measurement, field }, () => {
|
||||
this.onPairsChanged((query as any).tags);
|
||||
});
|
||||
};
|
||||
|
||||
onPairsChanged = (pairs: KeyValuePair[]) => {
|
||||
const { query } = this.props;
|
||||
const { measurement, field } = this.state;
|
||||
const queryModel = new InfluxQueryModel(
|
||||
{
|
||||
...query,
|
||||
resultFormat: 'table',
|
||||
groupBy: [],
|
||||
select: [[{ type: 'field', params: [field] }]],
|
||||
tags: pairs,
|
||||
limit: '1000',
|
||||
measurement,
|
||||
},
|
||||
this.templateSrv
|
||||
);
|
||||
|
||||
this.props.onChange(queryModel.target);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource } = this.props;
|
||||
const { measurements, measurement, field } = this.state;
|
||||
const cascadeText = measurement ? `Measurements (${measurement}/${field})` : 'Measurements';
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||
<div className="gf-form flex-shrink-0">
|
||||
<Cascader options={measurements} onChange={this.onMeasurementsChange}>
|
||||
<button className="gf-form-label gf-form-label--btn">
|
||||
{cascadeText} <i className="fa fa-caret-down" />
|
||||
</button>
|
||||
</Cascader>
|
||||
</div>
|
||||
<div className="flex-shrink-1 flex-flow-column-nowrap">
|
||||
{measurement && <AdHocFilterField onPairsChanged={this.onPairsChanged} datasource={datasource} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -2,11 +2,16 @@ import _ from 'lodash';
|
||||
|
||||
import * as dateMath from '@grafana/ui/src/utils/datemath';
|
||||
import InfluxSeries from './influx_series';
|
||||
import InfluxQuery from './influx_query';
|
||||
import InfluxQueryModel from './influx_query_model';
|
||||
import ResponseParser from './response_parser';
|
||||
import { InfluxQueryBuilder } from './query_builder';
|
||||
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/ui';
|
||||
import { InfluxQuery, InfluxOptions } from './types';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { IQService } from 'angular';
|
||||
|
||||
export default class InfluxDatasource {
|
||||
export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxOptions> {
|
||||
type: string;
|
||||
urls: any;
|
||||
username: string;
|
||||
@ -20,7 +25,13 @@ export default class InfluxDatasource {
|
||||
httpMode: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private $q, private backendSrv, private templateSrv) {
|
||||
constructor(
|
||||
instanceSettings: DataSourceInstanceSettings<InfluxOptions>,
|
||||
private $q: IQService,
|
||||
private backendSrv: BackendSrv,
|
||||
private templateSrv: TemplateSrv
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.type = 'influxdb';
|
||||
this.urls = _.map(instanceSettings.url.split(','), url => {
|
||||
return url.trim();
|
||||
@ -32,9 +43,10 @@ export default class InfluxDatasource {
|
||||
this.database = instanceSettings.database;
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.interval = (instanceSettings.jsonData || {}).timeInterval;
|
||||
const settingsData = instanceSettings.jsonData || ({} as InfluxOptions);
|
||||
this.interval = settingsData.timeInterval;
|
||||
this.httpMode = settingsData.httpMode || 'GET';
|
||||
this.responseParser = new ResponseParser();
|
||||
this.httpMode = instanceSettings.jsonData.httpMode || 'GET';
|
||||
}
|
||||
|
||||
query(options) {
|
||||
@ -55,7 +67,7 @@ export default class InfluxDatasource {
|
||||
// backward compatibility
|
||||
scopedVars.interval = scopedVars.__interval;
|
||||
|
||||
queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
|
||||
queryModel = new InfluxQueryModel(target, this.templateSrv, scopedVars);
|
||||
return queryModel.render(true);
|
||||
}).reduce((acc, current) => {
|
||||
if (current !== '') {
|
||||
|
@ -1,17 +1,19 @@
|
||||
import _ from 'lodash';
|
||||
import queryPart from './query_part';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { InfluxQuery } from './types';
|
||||
|
||||
export default class InfluxQuery {
|
||||
target: any;
|
||||
export default class InfluxQueryModel {
|
||||
target: InfluxQuery;
|
||||
selectModels: any[];
|
||||
queryBuilder: any;
|
||||
groupByParts: any;
|
||||
templateSrv: any;
|
||||
scopedVars: any;
|
||||
refId: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(target, templateSrv?, scopedVars?) {
|
||||
constructor(target: InfluxQuery, templateSrv?, scopedVars?) {
|
||||
this.target = target;
|
||||
this.templateSrv = templateSrv;
|
||||
this.scopedVars = scopedVars;
|
@ -1,10 +1,12 @@
|
||||
import InfluxDatasource from './datasource';
|
||||
import { InfluxQueryCtrl } from './query_ctrl';
|
||||
import { InfluxLogsQueryField } from './components/InfluxLogsQueryField';
|
||||
import {
|
||||
createChangeHandler,
|
||||
createResetHandler,
|
||||
PasswordFieldEnum,
|
||||
} from '../../../features/datasources/utils/passwordHandlers';
|
||||
import { DataSourcePlugin } from '@grafana/ui';
|
||||
|
||||
class InfluxConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
@ -25,9 +27,8 @@ class InfluxAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
}
|
||||
|
||||
export {
|
||||
InfluxDatasource as Datasource,
|
||||
InfluxQueryCtrl as QueryCtrl,
|
||||
InfluxConfigCtrl as ConfigCtrl,
|
||||
InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
export const plugin = new DataSourcePlugin(InfluxDatasource)
|
||||
.setConfigCtrl(InfluxConfigCtrl)
|
||||
.setQueryCtrl(InfluxQueryCtrl)
|
||||
.setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl)
|
||||
.setExploreLogsQueryField(InfluxLogsQueryField);
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
"defaultMatchFormat": "regex values",
|
||||
"metrics": true,
|
||||
"logs": true,
|
||||
"annotations": true,
|
||||
"alerting": true,
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { InfluxQueryBuilder } from './query_builder';
|
||||
import InfluxQuery from './influx_query';
|
||||
import InfluxQueryModel from './influx_query_model';
|
||||
import queryPart from './query_part';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
|
||||
export class InfluxQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
queryModel: InfluxQuery;
|
||||
queryModel: InfluxQueryModel;
|
||||
queryBuilder: any;
|
||||
groupBySegment: any;
|
||||
resultFormats: any[];
|
||||
@ -23,7 +23,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
|
||||
constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
|
||||
super($scope, $injector);
|
||||
this.target = this.target;
|
||||
this.queryModel = new InfluxQuery(this.target, templateSrv, this.panel.scopedVars);
|
||||
this.queryModel = new InfluxQueryModel(this.target, templateSrv, this.panel.scopedVars);
|
||||
this.queryBuilder = new InfluxQueryBuilder(this.target, this.datasource.database);
|
||||
this.groupBySegment = this.uiSegmentSrv.newPlusButton();
|
||||
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
|
||||
|
@ -1,12 +1,13 @@
|
||||
import InfluxQuery from '../influx_query';
|
||||
import InfluxQueryModel from '../influx_query_model';
|
||||
|
||||
describe('InfluxQuery', () => {
|
||||
const templateSrv = { replace: val => val };
|
||||
|
||||
describe('render series with mesurement only', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
},
|
||||
templateSrv,
|
||||
@ -20,8 +21,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('render series with policy only', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
policy: '5m_avg',
|
||||
},
|
||||
@ -38,8 +40,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('render series with math and alias', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [
|
||||
[
|
||||
@ -63,8 +66,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('series with single tag only', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [{ type: 'time', params: ['auto'] }],
|
||||
tags: [{ key: 'hostname', value: 'server\\1' }],
|
||||
@ -82,8 +86,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should switch regex operator with tag value is regex', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [{ type: 'time', params: ['auto'] }],
|
||||
tags: [{ key: 'app', value: '/e.*/' }],
|
||||
@ -101,8 +106,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('series with multiple tags only', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [{ type: 'time', params: ['auto'] }],
|
||||
tags: [{ key: 'hostname', value: 'server1' }, { key: 'app', value: 'email', condition: 'AND' }],
|
||||
@ -121,8 +127,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('series with tags OR condition', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [{ type: 'time', params: ['auto'] }],
|
||||
tags: [{ key: 'hostname', value: 'server1' }, { key: 'hostname', value: 'server2', condition: 'OR' }],
|
||||
@ -141,8 +148,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('field name with single quote should be escaped and', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [{ type: 'time', params: ['auto'] }],
|
||||
tags: [{ key: 'name', value: "Let's encrypt." }, { key: 'hostname', value: 'server2', condition: 'OR' }],
|
||||
@ -161,8 +169,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('query with value condition', () => {
|
||||
it('should not quote value', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [],
|
||||
tags: [{ key: 'value', value: '5', operator: '>' }],
|
||||
@ -178,8 +187,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('series with groupByTag', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
tags: [],
|
||||
groupBy: [{ type: 'time', interval: 'auto' }, { type: 'tag', params: ['host'] }],
|
||||
@ -195,8 +205,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('render series without group by', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }]],
|
||||
groupBy: [],
|
||||
@ -211,8 +222,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('render series without group by and fill', () => {
|
||||
it('should generate correct query', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }]],
|
||||
groupBy: [{ type: 'time' }, { type: 'fill', params: ['0'] }],
|
||||
@ -227,8 +239,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('when adding group by part', () => {
|
||||
it('should add tag before fill', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [{ type: 'time' }, { type: 'fill' }],
|
||||
},
|
||||
@ -244,8 +257,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should add tag last if no fill', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
groupBy: [],
|
||||
},
|
||||
@ -261,8 +275,9 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('when adding select part', () => {
|
||||
it('should add mean after after field', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }]],
|
||||
},
|
||||
@ -276,8 +291,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should replace sum by mean', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]],
|
||||
},
|
||||
@ -291,8 +307,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should add math before alias', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'alias' }]],
|
||||
},
|
||||
@ -306,8 +323,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should add math last', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]],
|
||||
},
|
||||
@ -321,8 +339,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should replace math', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'math' }]],
|
||||
},
|
||||
@ -336,8 +355,9 @@ describe('InfluxQuery', () => {
|
||||
});
|
||||
|
||||
it('should add math when one only query part', () => {
|
||||
const query = new InfluxQuery(
|
||||
const query = new InfluxQueryModel(
|
||||
{
|
||||
refId: 'A',
|
||||
measurement: 'cpu',
|
||||
select: [[{ type: 'field', params: ['value'] }]],
|
||||
},
|
||||
@ -352,7 +372,7 @@ describe('InfluxQuery', () => {
|
||||
|
||||
describe('when render adhoc filters', () => {
|
||||
it('should generate correct query segment', () => {
|
||||
const query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {});
|
||||
const query = new InfluxQueryModel({ refId: 'A', measurement: 'cpu' }, templateSrv, {});
|
||||
|
||||
const queryText = query.renderAdhocFilters([
|
||||
{ key: 'key1', operator: '=', value: 'value1' },
|
35
public/app/plugins/datasource/influxdb/types.ts
Normal file
35
public/app/plugins/datasource/influxdb/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { DataQuery, DataSourceJsonData } from '@grafana/ui/src/types';
|
||||
|
||||
export interface InfluxOptions extends DataSourceJsonData {
|
||||
timeInterval: string;
|
||||
httpMode: string;
|
||||
}
|
||||
|
||||
export interface InfluxQueryPart {
|
||||
type: string;
|
||||
params?: string[];
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export interface InfluxQueryTag {
|
||||
key: string;
|
||||
operator?: string;
|
||||
condition?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface InfluxQuery extends DataQuery {
|
||||
policy?: string;
|
||||
measurement?: string;
|
||||
resultFormat?: 'time_series' | 'table';
|
||||
orderByTime?: string;
|
||||
tags?: InfluxQueryTag[];
|
||||
groupBy?: InfluxQueryPart[];
|
||||
select?: InfluxQueryPart[][];
|
||||
limit?: string;
|
||||
slimit?: string;
|
||||
tz?: string;
|
||||
fill?: string;
|
||||
rawQuery?: boolean;
|
||||
query?: string;
|
||||
}
|
@ -5,6 +5,7 @@ import {
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
MetricFindValue,
|
||||
} from '@grafana/ui/src/types';
|
||||
import { InputQuery, InputOptions } from './types';
|
||||
|
||||
@ -27,7 +28,7 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
||||
return `Shared Data From: ${this.name} (${describeSeriesData(this.data)})`;
|
||||
}
|
||||
|
||||
metricFindQuery(query: string, options?: any) {
|
||||
metricFindQuery(query: string, options?: any): Promise<MetricFindValue[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const names = [];
|
||||
for (const series of this.data) {
|
||||
|
@ -95,6 +95,11 @@ button.close {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flex-flow-column-nowrap {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.center-vh {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -103,3 +108,9 @@ button.close {
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
display: flex;
|
||||
flex-direction: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user