Bläddra i källkod

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>
Hugo Häggmark 6 år sedan
förälder
incheckning
591ea0bfe3

+ 25 - 0
packages/grafana-ui/src/types/datasource.ts

@@ -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;
@@ -199,6 +204,21 @@ export abstract class DataSourceApi<
     options?: TContextQueryOptions
     options?: TContextQueryOptions
   ) => 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 {
   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
   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.

+ 83 - 0
public/app/features/explore/AdHocFilter.tsx

@@ -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 - 0
public/app/features/explore/AdHocFilterField.tsx

@@ -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>&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>
+          );
+        })}
+      </>
+    );
+  }
+}

+ 102 - 0
public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx

@@ -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>
+    );
+  }
+}

+ 18 - 6
public/app/plugins/datasource/influxdb/datasource.ts

@@ -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 {
   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 {
     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 {
       // 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 !== '') {

+ 5 - 3
public/app/plugins/datasource/influxdb/influx_query.ts → public/app/plugins/datasource/influxdb/influx_query_model.ts

@@ -1,17 +1,19 @@
 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 {
-  target: any;
+export default class InfluxQueryModel {
+  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;

+ 7 - 6
public/app/plugins/datasource/influxdb/module.ts

@@ -1,10 +1,12 @@
 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 {
   static templateUrl = 'partials/annotations.editor.html';
   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);

+ 1 - 0
public/app/plugins/datasource/influxdb/plugin.json

@@ -6,6 +6,7 @@
 
 
   "defaultMatchFormat": "regex values",
   "defaultMatchFormat": "regex values",
   "metrics": true,
   "metrics": true,
+  "logs": true,
   "annotations": true,
   "annotations": true,
   "alerting": true,
   "alerting": true,
 
 

+ 3 - 3
public/app/plugins/datasource/influxdb/query_ctrl.ts

@@ -1,14 +1,14 @@
 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 {
   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' }];

+ 42 - 22
public/app/plugins/datasource/influxdb/specs/influx_query.test.ts → public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts

@@ -1,12 +1,13 @@
-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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
     });
     });
 
 
     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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
 
 
   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', () => {
     });
     });
 
 
     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', () => {
 
 
   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', () => {
     });
     });
 
 
     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', () => {
     });
     });
 
 
     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', () => {
     });
     });
 
 
     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', () => {
     });
     });
 
 
     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', () => {
     });
     });
 
 
     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', () => {
 
 
     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' },

+ 35 - 0
public/app/plugins/datasource/influxdb/types.ts

@@ -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;
+}

+ 2 - 1
public/app/plugins/datasource/input/InputDatasource.ts

@@ -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> {
     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) {

+ 11 - 0
public/sass/utils/_utils.scss

@@ -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 {
   justify-content: center;
   justify-content: center;
   justify-items: center;
   justify-items: center;
 }
 }
+
+.align-items-center {
+  display: flex;
+  flex-direction: row nowrap;
+  align-items: center;
+}