From 51c6b5058204dffa5bb7dec542f8d05129efb00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 14 Jun 2019 10:13:06 +0200 Subject: [PATCH] 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 --- packages/grafana-ui/src/types/datasource.ts | 2 +- .../explore/AdHocFilterField.test.tsx | 196 ++++++++++++++++-- .../app/features/explore/AdHocFilterField.tsx | 134 +++++++----- .../components/InfluxLogsQueryField.tsx | 8 +- .../plugins/datasource/influxdb/datasource.ts | 8 +- 5 files changed, 277 insertions(+), 71 deletions(-) diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index b9f4a574972..d6d98639cba 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/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; + getTagValues?(options: any): Promise; /** * Set after constructor call, as the data source instance is the most common thing to pass around diff --git a/public/app/features/explore/AdHocFilterField.test.tsx b/public/app/features/explore/AdHocFilterField.test.tsx index febf1b73404..1f56b80cadd 100644 --- a/public/app/features/explore/AdHocFilterField.test.tsx +++ b/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('', () => { expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); }); - it('should add when onAddFilter is invoked', () => { + it('should add when onAddFilter is invoked', async () => { const mockOnPairsChanged = jest.fn(); const wrapper = shallow(); expect(wrapper.state('pairs')).toEqual([]); @@ -28,10 +28,13 @@ describe('', () => { .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(); expect(wrapper.state('pairs')).toEqual([]); @@ -40,10 +43,13 @@ describe('', () => { .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('', () => { .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); + expect(mockOnPairsChanged.mock.calls.length).toBe(1); + }); + global.clearImmediate(asyncCheck); + }); +}); + +const setup = (propOverrides?: Partial) => { + const datasource: DataSourceApi = ({ + getTagKeys: jest.fn().mockReturnValue([{ text: 'key 1' }, { text: 'key 2' }]), + getTagValues: jest.fn().mockReturnValue([{ text: 'value 1' }, { text: 'value 2' }]), + } as unknown) as DataSourceApi; + + const props: Props = { + datasource, + onPairsChanged: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = mount(); + 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(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: '=', + }, + ]); + }); + }); }); }); diff --git a/public/app/features/explore/AdHocFilterField.tsx b/public/app/features/explore/AdHocFilterField.tsx index 152e9f831c0..ed410c0ea4e 100644 --- a/public/app/features/explore/AdHocFilterField.tsx +++ b/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) => ( + +); + export interface KeyValuePair { keys: string[]; key: string; @@ -15,6 +21,7 @@ export interface KeyValuePair { export interface Props { datasource: DataSourceApi; onPairsChanged: (pairs: KeyValuePair[]) => void; + extendedOptions?: any; } export interface State { @@ -27,58 +34,45 @@ export class AdHocFilterField< > extends React.PureComponent, 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) => { - const { pairs } = this.state; + updatePairs(pairs: KeyValuePair[], index: number, pair: Partial) { + 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 && ( - - )} + {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< )} - {index === pairs.length - 1 && ( - - )} + {index === pairs.length - 1 && addFilterButton(this.onAddFilter)} ); })} diff --git a/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx b/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx index 2d4f01dcbe6..c81aecdc9d6 100644 --- a/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx +++ b/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx @@ -118,7 +118,13 @@ export class InfluxLogsQueryField extends React.PureComponent {
- {measurement && } + {measurement && ( + + )}
); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 7eb00d10036..e2726df7ecc 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -182,14 +182,14 @@ export default class InfluxDatasource extends DataSourceApi