mirror of
https://github.com/grafana/grafana.git
synced 2025-01-27 00:37:04 -06:00
InfluxDB: Update InfluxQL querybuilder to qualify identifiers (#62230)
This commit is contained in:
parent
bb798e24f3
commit
5bd2fac9c8
@ -78,7 +78,17 @@ func (query *Query) renderTags() []string {
|
||||
textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`))
|
||||
}
|
||||
|
||||
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue))
|
||||
escapedKey := fmt.Sprintf(`"%s"`, tag.Key)
|
||||
|
||||
if strings.HasSuffix(tag.Key, "::tag") {
|
||||
escapedKey = fmt.Sprintf(`"%s"::tag`, strings.TrimSuffix(tag.Key, "::tag"))
|
||||
}
|
||||
|
||||
if strings.HasSuffix(tag.Key, "::field") {
|
||||
escapedKey = fmt.Sprintf(`"%s"::field`, strings.TrimSuffix(tag.Key, "::field"))
|
||||
}
|
||||
|
||||
res = append(res, fmt.Sprintf(`%s%s %s %s`, str, escapedKey, tag.Operator, textValue))
|
||||
}
|
||||
|
||||
return res
|
||||
|
@ -98,10 +98,23 @@ func init() {
|
||||
}
|
||||
|
||||
func fieldRenderer(query *Query, queryContext *backend.QueryDataRequest, part *QueryPart, innerExpr string) string {
|
||||
if part.Params[0] == "*" {
|
||||
param := part.Params[0]
|
||||
|
||||
if param == "*" {
|
||||
return "*"
|
||||
}
|
||||
return fmt.Sprintf(`"%s"`, part.Params[0])
|
||||
|
||||
escapedParam := fmt.Sprintf(`"%s"`, param)
|
||||
|
||||
if strings.HasSuffix(param, "::tag") {
|
||||
escapedParam = fmt.Sprintf(`"%s"::tag`, strings.TrimSuffix(param, "::tag"))
|
||||
}
|
||||
|
||||
if strings.HasSuffix(param, "::field") {
|
||||
escapedParam = fmt.Sprintf(`"%s"::field`, strings.TrimSuffix(param, "::field"))
|
||||
}
|
||||
|
||||
return escapedParam
|
||||
}
|
||||
|
||||
func functionRenderer(query *Query, queryContext *backend.QueryDataRequest, part *QueryPart, innerExpr string) string {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
@ -10,11 +10,16 @@ import { Editor } from './Editor';
|
||||
|
||||
jest.mock('../../influxQLMetadataQuery', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
getFieldKeysForMeasurement: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(Promise.resolve(['free', 'total']))
|
||||
.mockReturnValueOnce(Promise.resolve([])),
|
||||
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']))
|
||||
// we respond by saying `cpu, host, device` are the real tags
|
||||
.mockReturnValueOnce(Promise.resolve(['cpu', 'host', 'device']))
|
||||
// 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
|
||||
@ -43,6 +48,7 @@ jest.mock('@grafana/runtime', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
(mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mockClear();
|
||||
(mockedMeta.getFieldKeysForMeasurement as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
const ONLY_TAGS = [
|
||||
@ -57,6 +63,12 @@ const ONLY_TAGS = [
|
||||
operator: '=',
|
||||
value: 'host2',
|
||||
},
|
||||
{
|
||||
condition: 'AND',
|
||||
key: 'device::tag',
|
||||
operator: '=',
|
||||
value: 'sdd',
|
||||
},
|
||||
];
|
||||
|
||||
const query: InfluxQuery = {
|
||||
@ -76,10 +88,22 @@ const query: InfluxQuery = {
|
||||
},
|
||||
{
|
||||
condition: 'AND',
|
||||
key: 'field1',
|
||||
key: 'device::tag',
|
||||
operator: '=',
|
||||
value: 'sdd',
|
||||
},
|
||||
{
|
||||
condition: 'AND',
|
||||
key: 'free',
|
||||
operator: '=',
|
||||
value: '45',
|
||||
},
|
||||
{
|
||||
condition: 'AND',
|
||||
key: 'total::field',
|
||||
operator: '=',
|
||||
value: '200',
|
||||
},
|
||||
],
|
||||
select: [
|
||||
[
|
||||
@ -101,17 +125,14 @@ describe('InfluxDB InfluxQL Visual Editor field-filtering', () => {
|
||||
} as unknown as InfluxDatasource;
|
||||
render(<Editor query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />);
|
||||
|
||||
await waitFor(() => {});
|
||||
|
||||
// when the editor-widget mounts, it calls getFieldKeysForMeasurement
|
||||
expect(mockedMeta.getFieldKeysForMeasurement).toHaveBeenCalledTimes(1);
|
||||
|
||||
// when the editor-widget mounts, it calls getTagKeysForMeasurementAndTags
|
||||
expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(1);
|
||||
|
||||
// we click the WHERE/cpu button
|
||||
await 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 userEvent.click(screen.getByRole('button', { name: 'host2' }));
|
||||
|
||||
|
@ -63,7 +63,7 @@ function withTemplateVariableOptions(optionsPromise: Promise<string[]>, filter?:
|
||||
// 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<string>): InfluxQueryTag[] {
|
||||
return parts.filter((t) => allTagKeys.has(t.key));
|
||||
return parts.filter((t) => t.key.endsWith('::tag') || allTagKeys.has(t.key + '::tag'));
|
||||
}
|
||||
|
||||
export const Editor = (props: Props): JSX.Element => {
|
||||
@ -76,10 +76,16 @@ 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);
|
||||
});
|
||||
const allTagKeys = useMemo(async () => {
|
||||
const tagKeys = (await getTagKeysForMeasurementAndTags(measurement, policy, [], datasource)).map(
|
||||
(tag) => `${tag}::tag`
|
||||
);
|
||||
|
||||
const fieldKeys = (await getFieldKeysForMeasurement(measurement || '', policy, datasource)).map(
|
||||
(field) => `${field}::field`
|
||||
);
|
||||
|
||||
return new Set([...tagKeys, ...fieldKeys]);
|
||||
}, [measurement, policy, datasource]);
|
||||
|
||||
const selectLists = useMemo(() => {
|
||||
@ -98,12 +104,14 @@ 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 () =>
|
||||
allTagKeys.then((keys) =>
|
||||
getTagKeysForMeasurementAndTags(measurement, policy, filterTags(query.tags ?? [], keys), datasource)
|
||||
);
|
||||
}, [measurement, policy, query.tags, datasource, allTagKeys]);
|
||||
const getTagKeys = useMemo(
|
||||
() => async () => {
|
||||
const selectedTagKeys = new Set(query.tags?.map((tag) => tag.key));
|
||||
|
||||
return [...(await allTagKeys)].filter((tagKey) => !selectedTagKeys.has(tagKey));
|
||||
},
|
||||
[query.tags, allTagKeys]
|
||||
);
|
||||
|
||||
const groupByList = useMemo(() => {
|
||||
const dynamicGroupByPartOptions = new Map([['tag_0', getTagKeys]]);
|
||||
|
@ -49,6 +49,11 @@ export async function getTagValues(
|
||||
datasource: InfluxDatasource
|
||||
): Promise<string[]> {
|
||||
const target = { tags, measurement, policy };
|
||||
|
||||
if (tagKey.endsWith('::field')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await runExploreQuery('TAG_VALUES', tagKey, undefined, target, datasource);
|
||||
return data.map((item) => item.text);
|
||||
}
|
||||
|
@ -170,7 +170,17 @@ export default class InfluxQueryModel {
|
||||
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
|
||||
}
|
||||
|
||||
return str + '"' + tag.key + '" ' + operator + ' ' + value;
|
||||
let escapedKey = `"${tag.key}"`;
|
||||
|
||||
if (tag.key.endsWith('::tag')) {
|
||||
escapedKey = `"${tag.key.slice(0, -5)}"::tag`;
|
||||
}
|
||||
|
||||
if (tag.key.endsWith('::field')) {
|
||||
escapedKey = `"${tag.key.slice(0, -7)}"::field`;
|
||||
}
|
||||
|
||||
return str + escapedKey + ' ' + operator + ' ' + value;
|
||||
}
|
||||
|
||||
getMeasurementAndPolicy(interpolate: any) {
|
||||
|
@ -25,7 +25,17 @@ function renderTagCondition(tag: { operator: any; value: string; condition: any;
|
||||
value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'";
|
||||
}
|
||||
|
||||
return str + '"' + tag.key + '" ' + operator + ' ' + value;
|
||||
let escapedKey = `"${tag.key}"`;
|
||||
|
||||
if (tag.key.endsWith('::tag')) {
|
||||
escapedKey = `"${tag.key.slice(0, -5)}"::tag`;
|
||||
}
|
||||
|
||||
if (tag.key.endsWith('::field')) {
|
||||
escapedKey = `"${tag.key.slice(0, -7)}"::field`;
|
||||
}
|
||||
|
||||
return str + escapedKey + ' ' + operator + ' ' + value;
|
||||
}
|
||||
|
||||
export class InfluxQueryBuilder {
|
||||
@ -83,7 +93,13 @@ export class InfluxQueryBuilder {
|
||||
}
|
||||
|
||||
if (withKey) {
|
||||
query += ' WITH KEY = "' + withKey + '"';
|
||||
let keyIdentifier = withKey;
|
||||
|
||||
if (keyIdentifier.endsWith('::tag')) {
|
||||
keyIdentifier = keyIdentifier.slice(0, -5);
|
||||
}
|
||||
|
||||
query += ' WITH KEY = "' + keyIdentifier + '"';
|
||||
}
|
||||
|
||||
if (this.target.tags && this.target.tags.length > 0) {
|
||||
|
@ -34,10 +34,23 @@ function aliasRenderer(part: { params: string[] }, innerExpr: string) {
|
||||
}
|
||||
|
||||
function fieldRenderer(part: { params: string[] }, innerExpr: any) {
|
||||
if (part.params[0] === '*') {
|
||||
const param = part.params[0];
|
||||
|
||||
if (param === '*') {
|
||||
return '*';
|
||||
}
|
||||
return '"' + part.params[0] + '"';
|
||||
|
||||
let escapedParam = `"${param}"`;
|
||||
|
||||
if (param.endsWith('::tag')) {
|
||||
escapedParam = `"${param.slice(0, -5)}"::tag`;
|
||||
}
|
||||
|
||||
if (param.endsWith('::field')) {
|
||||
escapedParam = `"${param.slice(0, -7)}"::field`;
|
||||
}
|
||||
|
||||
return escapedParam;
|
||||
}
|
||||
|
||||
function replaceAggregationAddStrategy(selectParts: any[], partModel: { def: { type: string } }) {
|
||||
|
Loading…
Reference in New Issue
Block a user