Explorar o código

Explore: Tag and Values for Influx are filtered by the selected measurement (#17539)

* Fix: Filters Tags and Values depending on options passed
Fixes: #17507

* Fix: Makes sure options is not undefined

* Fix: Fixes tests and small button refactor

* Chore: PR comments
Hugo Häggmark %!s(int64=6) %!d(string=hai) anos
pai
achega
51c6b50582

+ 1 - 1
packages/grafana-ui/src/types/datasource.ts

@@ -217,7 +217,7 @@ export abstract class DataSourceApi<
   /**
    * Get tag values for adhoc filters
    */
-  getTagValues?(options: { key: any }): Promise<MetricFindValue[]>;
+  getTagValues?(options: any): Promise<MetricFindValue[]>;
 
   /**
    * Set after constructor call, as the data source instance is the most common thing to pass around

+ 184 - 12
public/app/features/explore/AdHocFilterField.test.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
-import { shallow } from 'enzyme';
+import { mount, shallow } from 'enzyme';
 import { DataSourceApi } from '@grafana/ui';
 
-import { AdHocFilterField, DEFAULT_REMOVE_FILTER_VALUE } from './AdHocFilterField';
+import { AdHocFilterField, DEFAULT_REMOVE_FILTER_VALUE, KeyValuePair, Props } from './AdHocFilterField';
 import { AdHocFilter } from './AdHocFilter';
 import { MockDataSourceApi } from '../../../test/mocks/datasource_srv';
 
@@ -20,7 +20,7 @@ describe('<AdHocFilterField />', () => {
     expect(wrapper.find(AdHocFilter).exists()).toBeFalsy();
   });
 
-  it('should add <AdHocFilter /> when onAddFilter is invoked', () => {
+  it('should add <AdHocFilter /> when onAddFilter is invoked', async () => {
     const mockOnPairsChanged = jest.fn();
     const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />);
     expect(wrapper.state('pairs')).toEqual([]);
@@ -28,10 +28,13 @@ describe('<AdHocFilterField />', () => {
       .find('button')
       .first()
       .simulate('click');
-    expect(wrapper.find(AdHocFilter).exists()).toBeTruthy();
+    const asyncCheck = setImmediate(() => {
+      expect(wrapper.find(AdHocFilter).exists()).toBeTruthy();
+    });
+    global.clearImmediate(asyncCheck);
   });
 
-  it(`should remove the relavant filter when the '${DEFAULT_REMOVE_FILTER_VALUE}' key is selected`, () => {
+  it(`should remove the relevant filter when the '${DEFAULT_REMOVE_FILTER_VALUE}' key is selected`, () => {
     const mockOnPairsChanged = jest.fn();
     const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />);
     expect(wrapper.state('pairs')).toEqual([]);
@@ -40,10 +43,13 @@ describe('<AdHocFilterField />', () => {
       .find('button')
       .first()
       .simulate('click');
-    expect(wrapper.find(AdHocFilter).exists()).toBeTruthy();
+    const asyncCheck = setImmediate(() => {
+      expect(wrapper.find(AdHocFilter).exists()).toBeTruthy();
 
-    wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE);
-    expect(wrapper.find(AdHocFilter).exists()).toBeFalsy();
+      wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE);
+      expect(wrapper.find(AdHocFilter).exists()).toBeFalsy();
+    });
+    global.clearImmediate(asyncCheck);
   });
 
   it('it should call onPairsChanged when a filter is removed', async () => {
@@ -55,11 +61,177 @@ describe('<AdHocFilterField />', () => {
       .find('button')
       .first()
       .simulate('click');
-    expect(wrapper.find(AdHocFilter).exists()).toBeTruthy();
+    const asyncCheck = setImmediate(() => {
+      expect(wrapper.find(AdHocFilter).exists()).toBeTruthy();
 
-    wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE);
-    expect(wrapper.find(AdHocFilter).exists()).toBeFalsy();
+      wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE);
+      expect(wrapper.find(AdHocFilter).exists()).toBeFalsy();
+
+      expect(mockOnPairsChanged.mock.calls.length).toBe(1);
+    });
+    global.clearImmediate(asyncCheck);
+  });
+});
+
+const setup = (propOverrides?: Partial<Props>) => {
+  const datasource: DataSourceApi<any, any> = ({
+    getTagKeys: jest.fn().mockReturnValue([{ text: 'key 1' }, { text: 'key 2' }]),
+    getTagValues: jest.fn().mockReturnValue([{ text: 'value 1' }, { text: 'value 2' }]),
+  } as unknown) as DataSourceApi<any, any>;
+
+  const props: Props = {
+    datasource,
+    onPairsChanged: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = mount(<AdHocFilterField {...props} />);
+  const instance = wrapper.instance() as AdHocFilterField;
+
+  return {
+    instance,
+    wrapper,
+    datasource,
+  };
+};
+
+describe('AdHocFilterField', () => {
+  describe('loadTagKeys', () => {
+    describe('when called and there is no extendedOptions', () => {
+      const { instance, datasource } = setup({ extendedOptions: undefined });
+
+      it('then it should return correct keys', async () => {
+        const keys = await instance.loadTagKeys();
+
+        expect(keys).toEqual(['key 1', 'key 2']);
+      });
+
+      it('then datasource.getTagKeys should be called with an empty object', async () => {
+        await instance.loadTagKeys();
+
+        expect(datasource.getTagKeys).toBeCalledWith({});
+      });
+    });
+
+    describe('when called and there is extendedOptions', () => {
+      const extendedOptions = { measurement: 'default' };
+      const { instance, datasource } = setup({ extendedOptions });
+
+      it('then it should return correct keys', async () => {
+        const keys = await instance.loadTagKeys();
+
+        expect(keys).toEqual(['key 1', 'key 2']);
+      });
+
+      it('then datasource.getTagKeys should be called with extendedOptions', async () => {
+        await instance.loadTagKeys();
+
+        expect(datasource.getTagKeys).toBeCalledWith(extendedOptions);
+      });
+    });
+  });
+
+  describe('loadTagValues', () => {
+    describe('when called and there is no extendedOptions', () => {
+      const { instance, datasource } = setup({ extendedOptions: undefined });
+
+      it('then it should return correct values', async () => {
+        const values = await instance.loadTagValues('key 1');
+
+        expect(values).toEqual(['value 1', 'value 2']);
+      });
+
+      it('then datasource.getTagValues should be called with the correct key', async () => {
+        await instance.loadTagValues('key 1');
+
+        expect(datasource.getTagValues).toBeCalledWith({ key: 'key 1' });
+      });
+    });
+
+    describe('when called and there is extendedOptions', () => {
+      const extendedOptions = { measurement: 'default' };
+      const { instance, datasource } = setup({ extendedOptions });
+
+      it('then it should return correct values', async () => {
+        const values = await instance.loadTagValues('key 1');
+
+        expect(values).toEqual(['value 1', 'value 2']);
+      });
+
+      it('then datasource.getTagValues should be called with extendedOptions and the correct key', async () => {
+        await instance.loadTagValues('key 1');
+
+        expect(datasource.getTagValues).toBeCalledWith({ measurement: 'default', key: 'key 1' });
+      });
+    });
+  });
+
+  describe('updatePairs', () => {
+    describe('when called with an empty pairs array', () => {
+      describe('and called with keys', () => {
+        it('then it should return correct pairs', async () => {
+          const { instance } = setup();
+          const pairs: KeyValuePair[] = [];
+          const index = 0;
+          const key: string = undefined;
+          const keys: string[] = ['key 1', 'key 2'];
+          const value: string = undefined;
+          const values: string[] = undefined;
+          const operator: string = undefined;
+
+          const result = instance.updatePairs(pairs, index, { key, keys, value, values, operator });
+
+          expect(result).toEqual([{ key: '', keys, value: '', values: [], operator: '' }]);
+        });
+      });
+    });
+
+    describe('when called with an non empty pairs array', () => {
+      it('then it should update correct pairs at supplied index', async () => {
+        const { instance } = setup();
+        const pairs: KeyValuePair[] = [
+          {
+            key: 'prev key 1',
+            keys: ['prev key 1', 'prev key 2'],
+            value: 'prev value 1',
+            values: ['prev value 1', 'prev value 2'],
+            operator: '=',
+          },
+          {
+            key: 'prev key 3',
+            keys: ['prev key 3', 'prev key 4'],
+            value: 'prev value 3',
+            values: ['prev value 3', 'prev value 4'],
+            operator: '!=',
+          },
+        ];
+        const index = 1;
+        const key = 'key 3';
+        const keys = ['key 3', 'key 4'];
+        const value = 'value 3';
+        const values = ['value 3', 'value 4'];
+        const operator = '=';
+
+        const result = instance.updatePairs(pairs, index, { key, keys, value, values, operator });
 
-    expect(mockOnPairsChanged.mock.calls.length).toBe(1);
+        expect(result).toEqual([
+          {
+            key: 'prev key 1',
+            keys: ['prev key 1', 'prev key 2'],
+            value: 'prev value 1',
+            values: ['prev value 1', 'prev value 2'],
+            operator: '=',
+          },
+          {
+            key: 'key 3',
+            keys: ['key 3', 'key 4'],
+            value: 'value 3',
+            values: ['value 3', 'value 4'],
+            operator: '=',
+          },
+        ]);
+      });
+    });
   });
 });

+ 81 - 53
public/app/features/explore/AdHocFilterField.tsx

@@ -1,9 +1,15 @@
 import React from 'react';
+import _ from 'lodash';
 import { DataSourceApi, DataQuery, DataSourceJsonData } from '@grafana/ui';
 import { AdHocFilter } from './AdHocFilter';
-
 export const DEFAULT_REMOVE_FILTER_VALUE = '-- remove filter --';
 
+const addFilterButton = (onAddFilter: (event: React.MouseEvent) => void) => (
+  <button className="gf-form-label gf-form-label--btn query-part" onClick={onAddFilter}>
+    <i className="fa fa-plus" />
+  </button>
+);
+
 export interface KeyValuePair {
   keys: string[];
   key: string;
@@ -15,6 +21,7 @@ export interface KeyValuePair {
 export interface Props<TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData> {
   datasource: DataSourceApi<TQuery, TOptions>;
   onPairsChanged: (pairs: KeyValuePair[]) => void;
+  extendedOptions?: any;
 }
 
 export interface State {
@@ -27,58 +34,45 @@ export class AdHocFilterField<
 > extends React.PureComponent<Props<TQuery, TOptions>, State> {
   state: State = { pairs: [] };
 
-  onKeyChanged = (index: number) => async (key: string) => {
-    if (key !== DEFAULT_REMOVE_FILTER_VALUE) {
-      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 });
+  componentDidUpdate(prevProps: Props) {
+    if (_.isEqual(prevProps.extendedOptions, this.props.extendedOptions) === false) {
+      const pairs = [];
 
-      this.setState({ pairs: newPairs });
-      onPairsChanged(newPairs);
-    } else {
-      this.onRemoveFilter(index);
+      this.setState({ pairs }, () => this.props.onPairsChanged(pairs));
     }
-  };
-
-  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({}) : [];
+  loadTagKeys = async () => {
+    const { datasource, extendedOptions } = this.props;
+    const options = extendedOptions || {};
+    const tagKeys = datasource.getTagKeys ? await datasource.getTagKeys(options) : [];
     const keys = tagKeys.map(tagKey => tagKey.text);
-    const newPairs = pairs.concat({ key: null, operator: null, value: null, keys, values: [] });
 
-    this.setState({ pairs: newPairs });
+    return keys;
   };
 
-  onRemoveFilter = async (index: number) => {
-    const { pairs } = this.state;
-    const newPairs = pairs.reduce((allPairs, pair, pairIndex) => {
-      if (pairIndex === index) {
-        return allPairs;
-      }
-      return allPairs.concat(pair);
-    }, []);
+  loadTagValues = async (key: string) => {
+    const { datasource, extendedOptions } = this.props;
+    const options = extendedOptions || {};
+    const tagValues = datasource.getTagValues ? await datasource.getTagValues({ ...options, key }) : [];
+    const values = tagValues.map(tagValue => tagValue.text);
 
-    this.setState({ pairs: newPairs });
-    this.props.onPairsChanged(newPairs);
+    return values;
   };
 
-  private updatePairAt = (index: number, pair: Partial<KeyValuePair>) => {
-    const { pairs } = this.state;
+  updatePairs(pairs: KeyValuePair[], index: number, pair: Partial<KeyValuePair>) {
+    if (pairs.length === 0) {
+      return [
+        {
+          key: pair.key || '',
+          keys: pair.keys || [],
+          operator: pair.operator || '',
+          value: pair.value || '',
+          values: pair.values || [],
+        },
+      ];
+    }
+
     const newPairs: KeyValuePair[] = [];
     for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
       const newPair = pairs[pairIndex];
@@ -98,17 +92,55 @@ export class AdHocFilterField<
     }
 
     return newPairs;
+  }
+
+  onKeyChanged = (index: number) => async (key: string) => {
+    if (key !== DEFAULT_REMOVE_FILTER_VALUE) {
+      const { onPairsChanged } = this.props;
+      const values = await this.loadTagValues(key);
+      const pairs = this.updatePairs(this.state.pairs, index, { key, values });
+
+      this.setState({ pairs }, () => onPairsChanged(pairs));
+    } else {
+      this.onRemoveFilter(index);
+    }
+  };
+
+  onValueChanged = (index: number) => (value: string) => {
+    const pairs = this.updatePairs(this.state.pairs, index, { value });
+
+    this.setState({ pairs }, () => this.props.onPairsChanged(pairs));
+  };
+
+  onOperatorChanged = (index: number) => (operator: string) => {
+    const pairs = this.updatePairs(this.state.pairs, index, { operator });
+
+    this.setState({ pairs }, () => this.props.onPairsChanged(pairs));
+  };
+
+  onAddFilter = async () => {
+    const keys = await this.loadTagKeys();
+    const pairs = this.state.pairs.concat(this.updatePairs([], 0, { keys }));
+
+    this.setState({ pairs }, () => this.props.onPairsChanged(pairs));
+  };
+
+  onRemoveFilter = async (index: number) => {
+    const pairs = this.state.pairs.reduce((allPairs, pair, pairIndex) => {
+      if (pairIndex === index) {
+        return allPairs;
+      }
+      return allPairs.concat(pair);
+    }, []);
+
+    this.setState({ pairs });
   };
 
   render() {
     const { pairs } = this.state;
     return (
       <>
-        {pairs.length < 1 && (
-          <button className="gf-form-label gf-form-label--btn query-part" onClick={this.onAddFilter}>
-            <i className="fa fa-plus" />
-          </button>
-        )}
+        {pairs.length < 1 && addFilterButton(this.onAddFilter)}
         {pairs.map((pair, index) => {
           const adHocKey = `adhoc-filter-${index}-${pair.key}-${pair.value}`;
           return (
@@ -129,11 +161,7 @@ export class AdHocFilterField<
                   <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>
-              )}
+              {index === pairs.length - 1 && addFilterButton(this.onAddFilter)}
             </div>
           );
         })}

+ 7 - 1
public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx

@@ -118,7 +118,13 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
           </Cascader>
         </div>
         <div className="flex-shrink-1 flex-flow-column-nowrap">
-          {measurement && <AdHocFilterField onPairsChanged={this.onPairsChanged} datasource={datasource} />}
+          {measurement && (
+            <AdHocFilterField
+              onPairsChanged={this.onPairsChanged}
+              datasource={datasource}
+              extendedOptions={{ measurement }}
+            />
+          )}
         </div>
       </div>
     );

+ 4 - 4
public/app/plugins/datasource/influxdb/datasource.ts

@@ -182,14 +182,14 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO
     return this._seriesQuery(interpolated, options).then(_.curry(this.responseParser.parse)(query));
   }
 
-  getTagKeys(options) {
-    const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
+  getTagKeys(options: any = {}) {
+    const queryBuilder = new InfluxQueryBuilder({ measurement: options.measurement || '', tags: [] }, this.database);
     const query = queryBuilder.buildExploreQuery('TAG_KEYS');
     return this.metricFindQuery(query, options);
   }
 
-  getTagValues(options) {
-    const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
+  getTagValues(options: any = {}) {
+    const queryBuilder = new InfluxQueryBuilder({ measurement: options.measurement || '', tags: [] }, this.database);
     const query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
     return this.metricFindQuery(query, options);
   }