Commit 591ea0bf by Hugo Häggmark Committed by GitHub

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>
parent a95c7bfa
...@@ -30,6 +30,11 @@ export class DataSourcePlugin< ...@@ -30,6 +30,11 @@ export class DataSourcePlugin<
return this; return this;
} }
setConfigCtrl(ConfigCtrl: any) {
this.angularConfigCtrl = ConfigCtrl;
return this;
}
setQueryCtrl(QueryCtrl: any) { setQueryCtrl(QueryCtrl: any) {
this.components.QueryCtrl = QueryCtrl; this.components.QueryCtrl = QueryCtrl;
return this; return this;
...@@ -200,6 +205,21 @@ export abstract class DataSourceApi< ...@@ -200,6 +205,21 @@ export abstract class DataSourceApi<
) => Promise<DataQueryResponse>; ) => 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 * 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 * we attach the components to this instance for easy access
*/ */
...@@ -396,6 +416,10 @@ export interface QueryHint { ...@@ -396,6 +416,10 @@ export interface QueryHint {
fix?: QueryFix; fix?: QueryFix;
} }
export interface MetricFindValue {
text: string;
}
export interface DataSourceJsonData { export interface DataSourceJsonData {
authType?: string; authType?: string;
defaultRegion?: string; defaultRegion?: string;
...@@ -440,6 +464,7 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS ...@@ -440,6 +464,7 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
jsonData: T; jsonData: T;
username?: string; username?: string;
password?: string; // when access is direct, for some legacy datasources password?: string; // when access is direct, for some legacy datasources
database?: string;
/** /**
* This is the full Authorization header if basic auth is ennabled. * This is the full Authorization header if basic auth is ennabled.
......
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>
);
};
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>&nbsp;AND&nbsp;</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>
);
})}
</>
);
}
}
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'; ...@@ -2,11 +2,16 @@ import _ from 'lodash';
import * as dateMath from '@grafana/ui/src/utils/datemath'; import * as dateMath from '@grafana/ui/src/utils/datemath';
import InfluxSeries from './influx_series'; import InfluxSeries from './influx_series';
import InfluxQuery from './influx_query'; 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 { 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; type: string;
urls: any; urls: any;
username: string; username: string;
...@@ -20,7 +25,13 @@ export default class InfluxDatasource { ...@@ -20,7 +25,13 @@ export default class InfluxDatasource {
httpMode: string; httpMode: string;
/** @ngInject */ /** @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.type = 'influxdb';
this.urls = _.map(instanceSettings.url.split(','), url => { this.urls = _.map(instanceSettings.url.split(','), url => {
return url.trim(); return url.trim();
...@@ -32,9 +43,10 @@ export default class InfluxDatasource { ...@@ -32,9 +43,10 @@ export default class InfluxDatasource {
this.database = instanceSettings.database; this.database = instanceSettings.database;
this.basicAuth = instanceSettings.basicAuth; this.basicAuth = instanceSettings.basicAuth;
this.withCredentials = instanceSettings.withCredentials; 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.responseParser = new ResponseParser();
this.httpMode = instanceSettings.jsonData.httpMode || 'GET';
} }
query(options) { query(options) {
...@@ -55,7 +67,7 @@ export default class InfluxDatasource { ...@@ -55,7 +67,7 @@ export default class InfluxDatasource {
// backward compatibility // backward compatibility
scopedVars.interval = scopedVars.__interval; scopedVars.interval = scopedVars.__interval;
queryModel = new InfluxQuery(target, this.templateSrv, scopedVars); queryModel = new InfluxQueryModel(target, this.templateSrv, scopedVars);
return queryModel.render(true); return queryModel.render(true);
}).reduce((acc, current) => { }).reduce((acc, current) => {
if (current !== '') { if (current !== '') {
......
import _ from 'lodash'; import _ from 'lodash';
import queryPart from './query_part'; import queryPart from './query_part';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { InfluxQuery } from './types';
export default class InfluxQuery { export default class InfluxQueryModel {
target: any; target: InfluxQuery;
selectModels: any[]; selectModels: any[];
queryBuilder: any; queryBuilder: any;
groupByParts: any; groupByParts: any;
templateSrv: any; templateSrv: any;
scopedVars: any; scopedVars: any;
refId: string;
/** @ngInject */ /** @ngInject */
constructor(target, templateSrv?, scopedVars?) { constructor(target: InfluxQuery, templateSrv?, scopedVars?) {
this.target = target; this.target = target;
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
this.scopedVars = scopedVars; this.scopedVars = scopedVars;
......
import InfluxDatasource from './datasource'; import InfluxDatasource from './datasource';
import { InfluxQueryCtrl } from './query_ctrl'; import { InfluxQueryCtrl } from './query_ctrl';
import { InfluxLogsQueryField } from './components/InfluxLogsQueryField';
import { import {
createChangeHandler, createChangeHandler,
createResetHandler, createResetHandler,
PasswordFieldEnum, PasswordFieldEnum,
} from '../../../features/datasources/utils/passwordHandlers'; } from '../../../features/datasources/utils/passwordHandlers';
import { DataSourcePlugin } from '@grafana/ui';
class InfluxConfigCtrl { class InfluxConfigCtrl {
static templateUrl = 'partials/config.html'; static templateUrl = 'partials/config.html';
...@@ -25,9 +27,8 @@ class InfluxAnnotationsQueryCtrl { ...@@ -25,9 +27,8 @@ class InfluxAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html'; static templateUrl = 'partials/annotations.editor.html';
} }
export { export const plugin = new DataSourcePlugin(InfluxDatasource)
InfluxDatasource as Datasource, .setConfigCtrl(InfluxConfigCtrl)
InfluxQueryCtrl as QueryCtrl, .setQueryCtrl(InfluxQueryCtrl)
InfluxConfigCtrl as ConfigCtrl, .setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl)
InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl, .setExploreLogsQueryField(InfluxLogsQueryField);
};
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"defaultMatchFormat": "regex values", "defaultMatchFormat": "regex values",
"metrics": true, "metrics": true,
"logs": true,
"annotations": true, "annotations": true,
"alerting": true, "alerting": true,
......
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import { InfluxQueryBuilder } from './query_builder'; import { InfluxQueryBuilder } from './query_builder';
import InfluxQuery from './influx_query'; 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';
export class InfluxQueryCtrl extends QueryCtrl { export class InfluxQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html'; static templateUrl = 'partials/query.editor.html';
queryModel: InfluxQuery; queryModel: InfluxQueryModel;
queryBuilder: any; queryBuilder: any;
groupBySegment: any; groupBySegment: any;
resultFormats: any[]; resultFormats: any[];
...@@ -23,7 +23,7 @@ export class InfluxQueryCtrl extends QueryCtrl { ...@@ -23,7 +23,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
super($scope, $injector); super($scope, $injector);
this.target = this.target; 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.queryBuilder = new InfluxQueryBuilder(this.target, this.datasource.database);
this.groupBySegment = this.uiSegmentSrv.newPlusButton(); this.groupBySegment = this.uiSegmentSrv.newPlusButton();
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
......
import InfluxQuery from '../influx_query'; import InfluxQueryModel from '../influx_query_model';
describe('InfluxQuery', () => { describe('InfluxQuery', () => {
const templateSrv = { replace: val => val }; const templateSrv = { replace: val => val };
describe('render series with mesurement only', () => { describe('render series with mesurement only', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
}, },
templateSrv, templateSrv,
...@@ -20,8 +21,9 @@ describe('InfluxQuery', () => { ...@@ -20,8 +21,9 @@ describe('InfluxQuery', () => {
describe('render series with policy only', () => { describe('render series with policy only', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
policy: '5m_avg', policy: '5m_avg',
}, },
...@@ -38,8 +40,9 @@ describe('InfluxQuery', () => { ...@@ -38,8 +40,9 @@ describe('InfluxQuery', () => {
describe('render series with math and alias', () => { describe('render series with math and alias', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [ select: [
[ [
...@@ -63,8 +66,9 @@ describe('InfluxQuery', () => { ...@@ -63,8 +66,9 @@ describe('InfluxQuery', () => {
describe('series with single tag only', () => { describe('series with single tag only', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [{ type: 'time', params: ['auto'] }], groupBy: [{ type: 'time', params: ['auto'] }],
tags: [{ key: 'hostname', value: 'server\\1' }], tags: [{ key: 'hostname', value: 'server\\1' }],
...@@ -82,8 +86,9 @@ describe('InfluxQuery', () => { ...@@ -82,8 +86,9 @@ describe('InfluxQuery', () => {
}); });
it('should switch regex operator with tag value is regex', () => { it('should switch regex operator with tag value is regex', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [{ type: 'time', params: ['auto'] }], groupBy: [{ type: 'time', params: ['auto'] }],
tags: [{ key: 'app', value: '/e.*/' }], tags: [{ key: 'app', value: '/e.*/' }],
...@@ -101,8 +106,9 @@ describe('InfluxQuery', () => { ...@@ -101,8 +106,9 @@ describe('InfluxQuery', () => {
describe('series with multiple tags only', () => { describe('series with multiple tags only', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [{ type: 'time', params: ['auto'] }], groupBy: [{ type: 'time', params: ['auto'] }],
tags: [{ key: 'hostname', value: 'server1' }, { key: 'app', value: 'email', condition: 'AND' }], tags: [{ key: 'hostname', value: 'server1' }, { key: 'app', value: 'email', condition: 'AND' }],
...@@ -121,8 +127,9 @@ describe('InfluxQuery', () => { ...@@ -121,8 +127,9 @@ describe('InfluxQuery', () => {
describe('series with tags OR condition', () => { describe('series with tags OR condition', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [{ type: 'time', params: ['auto'] }], groupBy: [{ type: 'time', params: ['auto'] }],
tags: [{ key: 'hostname', value: 'server1' }, { key: 'hostname', value: 'server2', condition: 'OR' }], tags: [{ key: 'hostname', value: 'server1' }, { key: 'hostname', value: 'server2', condition: 'OR' }],
...@@ -141,8 +148,9 @@ describe('InfluxQuery', () => { ...@@ -141,8 +148,9 @@ describe('InfluxQuery', () => {
describe('field name with single quote should be escaped and', () => { describe('field name with single quote should be escaped and', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [{ type: 'time', params: ['auto'] }], groupBy: [{ type: 'time', params: ['auto'] }],
tags: [{ key: 'name', value: "Let's encrypt." }, { key: 'hostname', value: 'server2', condition: 'OR' }], tags: [{ key: 'name', value: "Let's encrypt." }, { key: 'hostname', value: 'server2', condition: 'OR' }],
...@@ -161,8 +169,9 @@ describe('InfluxQuery', () => { ...@@ -161,8 +169,9 @@ describe('InfluxQuery', () => {
describe('query with value condition', () => { describe('query with value condition', () => {
it('should not quote value', () => { it('should not quote value', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [], groupBy: [],
tags: [{ key: 'value', value: '5', operator: '>' }], tags: [{ key: 'value', value: '5', operator: '>' }],
...@@ -178,8 +187,9 @@ describe('InfluxQuery', () => { ...@@ -178,8 +187,9 @@ describe('InfluxQuery', () => {
describe('series with groupByTag', () => { describe('series with groupByTag', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
tags: [], tags: [],
groupBy: [{ type: 'time', interval: 'auto' }, { type: 'tag', params: ['host'] }], groupBy: [{ type: 'time', interval: 'auto' }, { type: 'tag', params: ['host'] }],
...@@ -195,8 +205,9 @@ describe('InfluxQuery', () => { ...@@ -195,8 +205,9 @@ describe('InfluxQuery', () => {
describe('render series without group by', () => { describe('render series without group by', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }]], select: [[{ type: 'field', params: ['value'] }]],
groupBy: [], groupBy: [],
...@@ -211,8 +222,9 @@ describe('InfluxQuery', () => { ...@@ -211,8 +222,9 @@ describe('InfluxQuery', () => {
describe('render series without group by and fill', () => { describe('render series without group by and fill', () => {
it('should generate correct query', () => { it('should generate correct query', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }]], select: [[{ type: 'field', params: ['value'] }]],
groupBy: [{ type: 'time' }, { type: 'fill', params: ['0'] }], groupBy: [{ type: 'time' }, { type: 'fill', params: ['0'] }],
...@@ -227,8 +239,9 @@ describe('InfluxQuery', () => { ...@@ -227,8 +239,9 @@ describe('InfluxQuery', () => {
describe('when adding group by part', () => { describe('when adding group by part', () => {
it('should add tag before fill', () => { it('should add tag before fill', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [{ type: 'time' }, { type: 'fill' }], groupBy: [{ type: 'time' }, { type: 'fill' }],
}, },
...@@ -244,8 +257,9 @@ describe('InfluxQuery', () => { ...@@ -244,8 +257,9 @@ describe('InfluxQuery', () => {
}); });
it('should add tag last if no fill', () => { it('should add tag last if no fill', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
groupBy: [], groupBy: [],
}, },
...@@ -261,8 +275,9 @@ describe('InfluxQuery', () => { ...@@ -261,8 +275,9 @@ describe('InfluxQuery', () => {
describe('when adding select part', () => { describe('when adding select part', () => {
it('should add mean after after field', () => { it('should add mean after after field', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }]], select: [[{ type: 'field', params: ['value'] }]],
}, },
...@@ -276,8 +291,9 @@ describe('InfluxQuery', () => { ...@@ -276,8 +291,9 @@ describe('InfluxQuery', () => {
}); });
it('should replace sum by mean', () => { it('should replace sum by mean', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]], select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]],
}, },
...@@ -291,8 +307,9 @@ describe('InfluxQuery', () => { ...@@ -291,8 +307,9 @@ describe('InfluxQuery', () => {
}); });
it('should add math before alias', () => { it('should add math before alias', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'alias' }]], select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'alias' }]],
}, },
...@@ -306,8 +323,9 @@ describe('InfluxQuery', () => { ...@@ -306,8 +323,9 @@ describe('InfluxQuery', () => {
}); });
it('should add math last', () => { it('should add math last', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]], select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]],
}, },
...@@ -321,8 +339,9 @@ describe('InfluxQuery', () => { ...@@ -321,8 +339,9 @@ describe('InfluxQuery', () => {
}); });
it('should replace math', () => { it('should replace math', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'math' }]], select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'math' }]],
}, },
...@@ -336,8 +355,9 @@ describe('InfluxQuery', () => { ...@@ -336,8 +355,9 @@ describe('InfluxQuery', () => {
}); });
it('should add math when one only query part', () => { it('should add math when one only query part', () => {
const query = new InfluxQuery( const query = new InfluxQueryModel(
{ {
refId: 'A',
measurement: 'cpu', measurement: 'cpu',
select: [[{ type: 'field', params: ['value'] }]], select: [[{ type: 'field', params: ['value'] }]],
}, },
...@@ -352,7 +372,7 @@ describe('InfluxQuery', () => { ...@@ -352,7 +372,7 @@ describe('InfluxQuery', () => {
describe('when render adhoc filters', () => { describe('when render adhoc filters', () => {
it('should generate correct query segment', () => { 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([ const queryText = query.renderAdhocFilters([
{ key: 'key1', operator: '=', value: 'value1' }, { key: 'key1', operator: '=', value: 'value1' },
......
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 { ...@@ -5,6 +5,7 @@ import {
DataQueryResponse, DataQueryResponse,
DataSourceApi, DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
MetricFindValue,
} from '@grafana/ui/src/types'; } from '@grafana/ui/src/types';
import { InputQuery, InputOptions } from './types'; import { InputQuery, InputOptions } from './types';
...@@ -27,7 +28,7 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> { ...@@ -27,7 +28,7 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
return `Shared Data From: ${this.name} (${describeSeriesData(this.data)})`; 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) => { return new Promise((resolve, reject) => {
const names = []; const names = [];
for (const series of this.data) { for (const series of this.data) {
......
...@@ -95,6 +95,11 @@ button.close { ...@@ -95,6 +95,11 @@ button.close {
flex-shrink: 0; flex-shrink: 0;
} }
.flex-flow-column-nowrap {
display: flex;
flex-flow: column nowrap;
}
.center-vh { .center-vh {
height: 100%; height: 100%;
display: flex; display: flex;
...@@ -103,3 +108,9 @@ button.close { ...@@ -103,3 +108,9 @@ button.close {
justify-content: center; justify-content: center;
justify-items: center; justify-items: center;
} }
.align-items-center {
display: flex;
flex-direction: row nowrap;
align-items: center;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment