Commit 51c6b505 by Hugo Häggmark Committed by GitHub

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
parent 37e9988e
...@@ -217,7 +217,7 @@ export abstract class DataSourceApi< ...@@ -217,7 +217,7 @@ export abstract class DataSourceApi<
/** /**
* Get tag values for adhoc filters * 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 * Set after constructor call, as the data source instance is the most common thing to pass around
......
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { DataSourceApi } from '@grafana/ui'; 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 { AdHocFilter } from './AdHocFilter';
import { MockDataSourceApi } from '../../../test/mocks/datasource_srv'; import { MockDataSourceApi } from '../../../test/mocks/datasource_srv';
...@@ -20,7 +20,7 @@ describe('<AdHocFilterField />', () => { ...@@ -20,7 +20,7 @@ describe('<AdHocFilterField />', () => {
expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); 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 mockOnPairsChanged = jest.fn();
const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />); const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />);
expect(wrapper.state('pairs')).toEqual([]); expect(wrapper.state('pairs')).toEqual([]);
...@@ -28,10 +28,13 @@ describe('<AdHocFilterField />', () => { ...@@ -28,10 +28,13 @@ describe('<AdHocFilterField />', () => {
.find('button') .find('button')
.first() .first()
.simulate('click'); .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 mockOnPairsChanged = jest.fn();
const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />); const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />);
expect(wrapper.state('pairs')).toEqual([]); expect(wrapper.state('pairs')).toEqual([]);
...@@ -40,10 +43,13 @@ describe('<AdHocFilterField />', () => { ...@@ -40,10 +43,13 @@ describe('<AdHocFilterField />', () => {
.find('button') .find('button')
.first() .first()
.simulate('click'); .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); wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE);
expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); expect(wrapper.find(AdHocFilter).exists()).toBeFalsy();
});
global.clearImmediate(asyncCheck);
}); });
it('it should call onPairsChanged when a filter is removed', async () => { it('it should call onPairsChanged when a filter is removed', async () => {
...@@ -55,11 +61,177 @@ describe('<AdHocFilterField />', () => { ...@@ -55,11 +61,177 @@ describe('<AdHocFilterField />', () => {
.find('button') .find('button')
.first() .first()
.simulate('click'); .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); wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE);
expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); 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: '=',
},
]);
});
});
}); });
}); });
import React from 'react'; import React from 'react';
import _ from 'lodash';
import { DataSourceApi, DataQuery, DataSourceJsonData } from '@grafana/ui'; import { DataSourceApi, DataQuery, DataSourceJsonData } from '@grafana/ui';
import { AdHocFilter } from './AdHocFilter'; import { AdHocFilter } from './AdHocFilter';
export const DEFAULT_REMOVE_FILTER_VALUE = '-- remove filter --'; 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 { export interface KeyValuePair {
keys: string[]; keys: string[];
key: string; key: string;
...@@ -15,6 +21,7 @@ export interface KeyValuePair { ...@@ -15,6 +21,7 @@ export interface KeyValuePair {
export interface Props<TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData> { export interface Props<TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData> {
datasource: DataSourceApi<TQuery, TOptions>; datasource: DataSourceApi<TQuery, TOptions>;
onPairsChanged: (pairs: KeyValuePair[]) => void; onPairsChanged: (pairs: KeyValuePair[]) => void;
extendedOptions?: any;
} }
export interface State { export interface State {
...@@ -27,58 +34,45 @@ export class AdHocFilterField< ...@@ -27,58 +34,45 @@ export class AdHocFilterField<
> extends React.PureComponent<Props<TQuery, TOptions>, State> { > extends React.PureComponent<Props<TQuery, TOptions>, State> {
state: State = { pairs: [] }; state: State = { pairs: [] };
onKeyChanged = (index: number) => async (key: string) => { componentDidUpdate(prevProps: Props) {
if (key !== DEFAULT_REMOVE_FILTER_VALUE) { if (_.isEqual(prevProps.extendedOptions, this.props.extendedOptions) === false) {
const { datasource, onPairsChanged } = this.props; const pairs = [];
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 }); this.setState({ pairs }, () => this.props.onPairsChanged(pairs));
onPairsChanged(newPairs);
} else {
this.onRemoveFilter(index);
} }
}; }
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 () => { loadTagKeys = async () => {
const { pairs } = this.state; const { datasource, extendedOptions } = this.props;
const tagKeys = this.props.datasource.getTagKeys ? await this.props.datasource.getTagKeys({}) : []; const options = extendedOptions || {};
const tagKeys = datasource.getTagKeys ? await datasource.getTagKeys(options) : [];
const keys = tagKeys.map(tagKey => tagKey.text); 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) => { loadTagValues = async (key: string) => {
const { pairs } = this.state; const { datasource, extendedOptions } = this.props;
const newPairs = pairs.reduce((allPairs, pair, pairIndex) => { const options = extendedOptions || {};
if (pairIndex === index) { const tagValues = datasource.getTagValues ? await datasource.getTagValues({ ...options, key }) : [];
return allPairs; const values = tagValues.map(tagValue => tagValue.text);
}
return allPairs.concat(pair);
}, []);
this.setState({ pairs: newPairs }); return values;
this.props.onPairsChanged(newPairs);
}; };
private updatePairAt = (index: number, pair: Partial<KeyValuePair>) => { updatePairs(pairs: KeyValuePair[], index: number, pair: Partial<KeyValuePair>) {
const { pairs } = this.state; if (pairs.length === 0) {
return [
{
key: pair.key || '',
keys: pair.keys || [],
operator: pair.operator || '',
value: pair.value || '',
values: pair.values || [],
},
];
}
const newPairs: KeyValuePair[] = []; const newPairs: KeyValuePair[] = [];
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) { for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
const newPair = pairs[pairIndex]; const newPair = pairs[pairIndex];
...@@ -98,17 +92,55 @@ export class AdHocFilterField< ...@@ -98,17 +92,55 @@ export class AdHocFilterField<
} }
return newPairs; 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() { render() {
const { pairs } = this.state; const { pairs } = this.state;
return ( return (
<> <>
{pairs.length < 1 && ( {pairs.length < 1 && addFilterButton(this.onAddFilter)}
<button className="gf-form-label gf-form-label--btn query-part" onClick={this.onAddFilter}>
<i className="fa fa-plus" />
</button>
)}
{pairs.map((pair, index) => { {pairs.map((pair, index) => {
const adHocKey = `adhoc-filter-${index}-${pair.key}-${pair.value}`; const adHocKey = `adhoc-filter-${index}-${pair.key}-${pair.value}`;
return ( return (
...@@ -129,11 +161,7 @@ export class AdHocFilterField< ...@@ -129,11 +161,7 @@ export class AdHocFilterField<
<i className="fa fa-minus" /> <i className="fa fa-minus" />
</button> </button>
)} )}
{index === pairs.length - 1 && ( {index === pairs.length - 1 && addFilterButton(this.onAddFilter)}
<button className="gf-form-label gf-form-label--btn" onClick={this.onAddFilter}>
<i className="fa fa-plus" />
</button>
)}
</div> </div>
); );
})} })}
......
...@@ -118,7 +118,13 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> { ...@@ -118,7 +118,13 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
</Cascader> </Cascader>
</div> </div>
<div className="flex-shrink-1 flex-flow-column-nowrap"> <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>
</div> </div>
); );
......
...@@ -182,14 +182,14 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO ...@@ -182,14 +182,14 @@ export default class InfluxDatasource extends DataSourceApi<InfluxQuery, InfluxO
return this._seriesQuery(interpolated, options).then(_.curry(this.responseParser.parse)(query)); return this._seriesQuery(interpolated, options).then(_.curry(this.responseParser.parse)(query));
} }
getTagKeys(options) { getTagKeys(options: any = {}) {
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database); const queryBuilder = new InfluxQueryBuilder({ measurement: options.measurement || '', tags: [] }, this.database);
const query = queryBuilder.buildExploreQuery('TAG_KEYS'); const query = queryBuilder.buildExploreQuery('TAG_KEYS');
return this.metricFindQuery(query, options); return this.metricFindQuery(query, options);
} }
getTagValues(options) { getTagValues(options: any = {}) {
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database); const queryBuilder = new InfluxQueryBuilder({ measurement: options.measurement || '', tags: [] }, this.database);
const query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key); const query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
return this.metricFindQuery(query, options); return this.metricFindQuery(query, options);
} }
......
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