diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx new file mode 100644 index 00000000000..238fd61102f --- /dev/null +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tags.test.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { InfluxQuery } from '../../types'; +import InfluxDatasource from '../../datasource'; +import { render, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Editor } from './Editor'; +import * as mockedMeta from '../../influxQLMetadataQuery'; + +jest.mock('../../influxQLMetadataQuery', () => { + return { + getTagKeysForMeasurementAndTags: jest + .fn() + // first time we are called when the widget mounts, + // we respond by saying `cpu, host` are the real tags + .mockReturnValueOnce(Promise.resolve(['cpu', 'host'])) + // afterwards we will be called once when we click + // on a tag-key in the WHERE section. + // it does not matter what we return, as long as it is + // promise-of-a-list-of-strings + .mockReturnValueOnce(Promise.resolve([])), + getTagValues: jest + .fn() + // it does not matter what we return, as long as it is + // promise-of-a-list-of-strings + .mockReturnValueOnce(Promise.resolve([])), + getAllMeasurementsForTags: jest + .fn() + // it does not matter what we return, as long as it is + // promise-of-a-list-of-strings + .mockReturnValueOnce(Promise.resolve([])), + }; +}); + +beforeEach(() => { + (mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mockClear(); +}); + +const ONLY_TAGS = [ + { + key: 'cpu', + operator: '=', + value: 'cpu1', + }, + { + condition: 'AND', + key: 'host', + operator: '=', + value: 'host2', + }, +]; + +const query: InfluxQuery = { + refId: 'A', + policy: 'default', + tags: [ + { + key: 'cpu', + operator: '=', + value: 'cpu1', + }, + { + condition: 'AND', + key: 'host', + operator: '=', + value: 'host2', + }, + { + condition: 'AND', + key: 'field1', + operator: '=', + value: '45', + }, + ], + select: [ + [ + { + type: 'field', + params: ['usage_idle'], + }, + ], + ], + measurement: 'cpudata', +}; + +describe('InfluxDB InfluxQL Visual Editor field-filtering', () => { + it('should not send fields in tag-structures to metadata queries', async () => { + const onChange = jest.fn(); + const onRunQuery = jest.fn(); + const datasource: InfluxDatasource = ({ + metricFindQuery: () => Promise.resolve([]), + } as unknown) as InfluxDatasource; + render(); + + // when the editor-widget mounts, it calls getTagKeysForMeasurementAndTags + expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(1); + + // we click the WHERE/cpu button + await act(async () => { + userEvent.click(screen.getByRole('button', { name: 'cpu' })); + }); + + // and verify getTagKeysForMeasurementAndTags was called again, + // and in the tags-param we did not receive the `field1` part. + expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(2); + expect((mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mock.calls[1][2]).toStrictEqual(ONLY_TAGS); + + // now we click on the WHERE/host2 button + await act(async () => { + userEvent.click(screen.getByRole('button', { name: 'host2' })); + }); + + // verify `getTagValues` was called once, and in the tags-param we did not receive `field1` + expect(mockedMeta.getTagValues).toHaveBeenCalledTimes(1); + expect((mockedMeta.getTagValues as jest.Mock).mock.calls[0][3]).toStrictEqual(ONLY_TAGS); + + // now we click on the FROM/cpudata button + await act(async () => { + userEvent.click(screen.getByRole('button', { name: 'cpudata' })); + }); + + // verify `getTagValues` was called once, and in the tags-param we did not receive `field1` + expect(mockedMeta.getAllMeasurementsForTags).toHaveBeenCalledTimes(1); + expect((mockedMeta.getAllMeasurementsForTags as jest.Mock).mock.calls[0][1]).toStrictEqual(ONLY_TAGS); + }); +}); diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx index d88f6e329b3..b1a12faf7c5 100644 --- a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.test.tsx @@ -36,7 +36,9 @@ jest.mock('./Seg', () => { function assertEditor(query: InfluxQuery, textContent: string) { const onChange = jest.fn(); const onRunQuery = jest.fn(); - const datasource: InfluxDatasource = {} as InfluxDatasource; + const datasource: InfluxDatasource = ({ + metricFindQuery: () => Promise.resolve([]), + } as unknown) as InfluxDatasource; const { container } = render( ); diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx index 7e99de7161d..c32be940678 100644 --- a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Editor.tsx @@ -53,6 +53,12 @@ function withTemplateVariableOptions(optionsPromise: Promise): Promise return optionsPromise.then((options) => [...getTemplateVariableOptions(), ...options]); } +// it is possible to add fields into the `InfluxQueryTag` structures, and they do work, +// but in some cases, when we do metadata queries, we have to remove them from the queries. +function filterTags(parts: InfluxQueryTag[], allTagKeys: Set): InfluxQueryTag[] { + return parts.filter((t) => allTagKeys.has(t.key)); +} + export const Editor = (props: Props): JSX.Element => { const uniqueId = useUniqueId(); const formatAsId = `influxdb-qe-format-as-${uniqueId}`; @@ -63,6 +69,12 @@ export const Editor = (props: Props): JSX.Element => { const { datasource } = props; const { measurement, policy } = query; + const allTagKeys = useMemo(() => { + return getTagKeysForMeasurementAndTags(measurement, policy, [], datasource).then((tags) => { + return new Set(tags); + }); + }, [measurement, policy, datasource]); + const selectLists = useMemo(() => { const dynamicSelectPartOptions = new Map([ [ @@ -80,8 +92,11 @@ export const Editor = (props: Props): JSX.Element => { // the following function is not complicated enough to memoize, but it's result // is used in both memoized and un-memoized parts, so we have no choice const getTagKeys = useMemo(() => { - return () => getTagKeysForMeasurementAndTags(measurement, policy, query.tags ?? [], datasource); - }, [measurement, policy, query.tags, datasource]); + return () => + allTagKeys.then((keys) => + getTagKeysForMeasurementAndTags(measurement, policy, filterTags(query.tags ?? [], keys), datasource) + ); + }, [measurement, policy, query.tags, datasource, allTagKeys]); const groupByList = useMemo(() => { const dynamicGroupByPartOptions = new Map([['tag_0', getTagKeys]]); @@ -118,7 +133,13 @@ export const Editor = (props: Props): JSX.Element => { getPolicyOptions={() => getAllPolicies(datasource)} getMeasurementOptions={(filter) => withTemplateVariableOptions( - getAllMeasurementsForTags(filter === '' ? undefined : filter, query.tags ?? [], datasource) + allTagKeys.then((keys) => + getAllMeasurementsForTags( + filter === '' ? undefined : filter, + filterTags(query.tags ?? [], keys), + datasource + ) + ) ) } onChange={handleFromSectionChange} @@ -131,7 +152,11 @@ export const Editor = (props: Props): JSX.Element => { onChange={handleTagsSectionChange} getTagKeyOptions={getTagKeys} getTagValueOptions={(key: string) => - withTemplateVariableOptions(getTagValues(key, measurement, policy, query.tags ?? [], datasource)) + withTemplateVariableOptions( + allTagKeys.then((keys) => + getTagValues(key, measurement, policy, filterTags(query.tags ?? [], keys), datasource) + ) + ) } />