mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			663 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			663 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import { SyntaxNode } from '@lezer/common';
 | |
| import { BinModifiers, OnOrIgnoring } from '@prometheus-io/lezer-promql';
 | |
| 
 | |
| import {
 | |
|   And,
 | |
|   BinOpExpr,
 | |
|   Bool,
 | |
|   By,
 | |
|   ConvOp,
 | |
|   Decolorize,
 | |
|   DistinctFilter,
 | |
|   DistinctLabel,
 | |
|   Filter,
 | |
|   FilterOp,
 | |
|   Grouping,
 | |
|   GroupingLabelList,
 | |
|   GroupingLabels,
 | |
|   Identifier,
 | |
|   Ip,
 | |
|   IpLabelFilter,
 | |
|   Json,
 | |
|   JsonExpression,
 | |
|   JsonExpressionParser,
 | |
|   LabelFilter,
 | |
|   LabelFormatMatcher,
 | |
|   LabelParser,
 | |
|   LineFilter,
 | |
|   LineFormatExpr,
 | |
|   LogRangeExpr,
 | |
|   Matcher,
 | |
|   MetricExpr,
 | |
|   Number as NumberLezer,
 | |
|   On,
 | |
|   Or,
 | |
|   parser,
 | |
|   Range,
 | |
|   RangeAggregationExpr,
 | |
|   RangeOp,
 | |
|   String,
 | |
|   UnitFilter,
 | |
|   Unwrap,
 | |
|   UnwrapExpr,
 | |
|   VectorAggregationExpr,
 | |
|   VectorOp,
 | |
|   Without,
 | |
| } from '@grafana/lezer-logql';
 | |
| 
 | |
| import {
 | |
|   ErrorId,
 | |
|   getAllByType,
 | |
|   getLeftMostChild,
 | |
|   getString,
 | |
|   makeBinOp,
 | |
|   makeError,
 | |
|   replaceVariables,
 | |
| } from '../../prometheus/querybuilder/shared/parsingUtils';
 | |
| import {
 | |
|   QueryBuilderLabelFilter,
 | |
|   QueryBuilderOperation,
 | |
|   QueryBuilderOperationParamValue,
 | |
| } from '../../prometheus/querybuilder/shared/types';
 | |
| 
 | |
| import { binaryScalarDefs } from './binaryScalarOperations';
 | |
| import { checkParamsAreValid, getDefinitionById } from './operations';
 | |
| import { LokiOperationId, LokiVisualQuery, LokiVisualQueryBinary } from './types';
 | |
| 
 | |
| interface Context {
 | |
|   query: LokiVisualQuery;
 | |
|   errors: ParsingError[];
 | |
| }
 | |
| 
 | |
| interface ParsingError {
 | |
|   text: string;
 | |
|   from?: number;
 | |
|   to?: number;
 | |
|   parentType?: string;
 | |
| }
 | |
| 
 | |
| export function buildVisualQueryFromString(expr: string): Context {
 | |
|   const replacedExpr = replaceVariables(expr);
 | |
|   const tree = parser.parse(replacedExpr);
 | |
|   const node = tree.topNode;
 | |
| 
 | |
|   // This will be modified in the handleExpression
 | |
|   const visQuery: LokiVisualQuery = {
 | |
|     labels: [],
 | |
|     operations: [],
 | |
|   };
 | |
| 
 | |
|   const context: Context = {
 | |
|     query: visQuery,
 | |
|     errors: [],
 | |
|   };
 | |
| 
 | |
|   try {
 | |
|     handleExpression(replacedExpr, node, context);
 | |
|   } catch (err) {
 | |
|     // Not ideal to log it here, but otherwise we would lose the stack trace.
 | |
|     console.error(err);
 | |
|     if (err instanceof Error) {
 | |
|       context.errors.push({
 | |
|         text: err.message,
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // If we have empty query, we want to reset errors
 | |
|   if (isEmptyQuery(context.query)) {
 | |
|     context.errors = [];
 | |
|   }
 | |
|   return context;
 | |
| }
 | |
| 
 | |
| export function handleExpression(expr: string, node: SyntaxNode, context: Context) {
 | |
|   const visQuery = context.query;
 | |
|   switch (node.type.id) {
 | |
|     case Matcher: {
 | |
|       visQuery.labels.push(getLabel(expr, node));
 | |
|       const err = node.getChild(ErrorId);
 | |
|       if (err) {
 | |
|         context.errors.push(makeError(expr, err));
 | |
|       }
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case LineFilter: {
 | |
|       const { operation, error } = getLineFilter(expr, node);
 | |
|       if (operation) {
 | |
|         visQuery.operations.push(operation);
 | |
|       }
 | |
|       // Show error for query patterns not supported in visual query builder
 | |
|       if (error) {
 | |
|         context.errors.push(createNotSupportedError(expr, node, error));
 | |
|       }
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case LabelParser: {
 | |
|       visQuery.operations.push(getLabelParser(expr, node));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case LabelFilter: {
 | |
|       const { operation, error } = getLabelFilter(expr, node);
 | |
|       if (operation) {
 | |
|         visQuery.operations.push(operation);
 | |
|       }
 | |
|       // Show error for query patterns not supported in visual query builder
 | |
|       if (error) {
 | |
|         context.errors.push(createNotSupportedError(expr, node, error));
 | |
|       }
 | |
|       break;
 | |
|     }
 | |
|     case JsonExpressionParser: {
 | |
|       visQuery.operations.push(getJsonExpressionParser(expr, node));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case LineFormatExpr: {
 | |
|       visQuery.operations.push(getLineFormat(expr, node));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case LabelFormatMatcher: {
 | |
|       visQuery.operations.push(getLabelFormat(expr, node));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case UnwrapExpr: {
 | |
|       const { operation, error } = handleUnwrapExpr(expr, node, context);
 | |
|       if (operation) {
 | |
|         visQuery.operations.push(operation);
 | |
|       }
 | |
|       // Show error for query patterns not supported in visual query builder
 | |
|       if (error) {
 | |
|         context.errors.push(createNotSupportedError(expr, node, error));
 | |
|       }
 | |
| 
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case Decolorize: {
 | |
|       visQuery.operations.push(getDecolorize());
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case RangeAggregationExpr: {
 | |
|       visQuery.operations.push(handleRangeAggregation(expr, node, context));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case VectorAggregationExpr: {
 | |
|       visQuery.operations.push(handleVectorAggregation(expr, node, context));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case BinOpExpr: {
 | |
|       handleBinary(expr, node, context);
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case ErrorId: {
 | |
|       if (isIntervalVariableError(node)) {
 | |
|         break;
 | |
|       }
 | |
|       context.errors.push(makeError(expr, node));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     case DistinctFilter: {
 | |
|       visQuery.operations.push(handleDistinctFilter(expr, node, context));
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     default: {
 | |
|       // Any other nodes we just ignore and go to its children. This should be fine as there are lots of wrapper
 | |
|       // nodes that can be skipped.
 | |
|       // TODO: there are probably cases where we will just skip nodes we don't support and we should be able to
 | |
|       //  detect those and report back.
 | |
|       let child = node.firstChild;
 | |
|       while (child) {
 | |
|         handleExpression(expr, child, context);
 | |
|         child = child.nextSibling;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter {
 | |
|   const labelNode = node.getChild(Identifier);
 | |
|   const label = getString(expr, labelNode);
 | |
|   const op = getString(expr, labelNode!.nextSibling);
 | |
|   let value = getString(expr, node.getChild(String));
 | |
|   // `value` is wrapped in double quotes, so we need to remove them. As a value can contain double quotes, we can't use RegEx here.
 | |
|   value = value.substring(1, value.length - 1);
 | |
| 
 | |
|   return {
 | |
|     label,
 | |
|     op,
 | |
|     value,
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getLineFilter(expr: string, node: SyntaxNode): { operation?: QueryBuilderOperation; error?: string } {
 | |
|   const filter = getString(expr, node.getChild(Filter));
 | |
|   const filterExpr = handleQuotes(getString(expr, node.getChild(String)));
 | |
|   const ipLineFilter = node.getChild(FilterOp)?.getChild(Ip);
 | |
| 
 | |
|   if (ipLineFilter) {
 | |
|     return {
 | |
|       operation: {
 | |
|         id: LokiOperationId.LineFilterIpMatches,
 | |
|         params: [filter, filterExpr],
 | |
|       },
 | |
|     };
 | |
|   }
 | |
|   const mapFilter: Record<string, LokiOperationId> = {
 | |
|     '|=': LokiOperationId.LineContains,
 | |
|     '!=': LokiOperationId.LineContainsNot,
 | |
|     '|~': LokiOperationId.LineMatchesRegex,
 | |
|     '!~': LokiOperationId.LineMatchesRegexNot,
 | |
|   };
 | |
| 
 | |
|   return {
 | |
|     operation: {
 | |
|       id: mapFilter[filter],
 | |
|       params: [filterExpr],
 | |
|     },
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getLabelParser(expr: string, node: SyntaxNode): QueryBuilderOperation {
 | |
|   const parserNode = node.firstChild;
 | |
|   const parser = getString(expr, parserNode);
 | |
| 
 | |
|   const string = handleQuotes(getString(expr, node.getChild(String)));
 | |
|   let params: QueryBuilderOperationParamValue[] = !!string ? [string] : [];
 | |
|   const opDef = getDefinitionById(parser);
 | |
|   if (opDef && !checkParamsAreValid(opDef, params)) {
 | |
|     params = opDef?.defaultParams || [];
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     id: parser,
 | |
|     params,
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getJsonExpressionParser(expr: string, node: SyntaxNode): QueryBuilderOperation {
 | |
|   const parserNode = node.getChild(Json);
 | |
|   const parser = getString(expr, parserNode);
 | |
| 
 | |
|   const params = [...getAllByType(expr, node, JsonExpression)];
 | |
|   return {
 | |
|     id: parser,
 | |
|     params,
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getLabelFilter(expr: string, node: SyntaxNode): { operation?: QueryBuilderOperation; error?: string } {
 | |
|   // Check for nodes not supported in visual builder and return error
 | |
|   if (node.getChild(Or) || node.getChild(And) || node.getChild('Comma')) {
 | |
|     return {
 | |
|       error: 'Label filter with comma, "and", "or" not supported in query builder',
 | |
|     };
 | |
|   }
 | |
|   if (node.firstChild!.type.id === IpLabelFilter) {
 | |
|     const ipLabelFilter = node.firstChild;
 | |
|     const label = ipLabelFilter?.getChild(Identifier);
 | |
|     const op = label?.nextSibling;
 | |
|     const value = ipLabelFilter?.getChild(String);
 | |
|     const valueString = handleQuotes(getString(expr, value));
 | |
| 
 | |
|     return {
 | |
|       operation: {
 | |
|         id: LokiOperationId.LabelFilterIpMatches,
 | |
|         params: [getString(expr, label), getString(expr, op), valueString],
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   const id = LokiOperationId.LabelFilter;
 | |
|   if (node.firstChild!.type.id === UnitFilter) {
 | |
|     const filter = node.firstChild!.firstChild;
 | |
|     const label = filter!.firstChild;
 | |
|     const op = label!.nextSibling;
 | |
|     const value = op!.nextSibling;
 | |
|     const valueString = handleQuotes(getString(expr, value));
 | |
| 
 | |
|     return {
 | |
|       operation: {
 | |
|         id,
 | |
|         params: [getString(expr, label), getString(expr, op), valueString],
 | |
|       },
 | |
|     };
 | |
|   }
 | |
|   // In this case it is Matcher or NumberFilter
 | |
|   const filter = node.firstChild;
 | |
|   const label = filter!.firstChild;
 | |
|   const op = label!.nextSibling;
 | |
|   const value = op!.nextSibling;
 | |
|   const params = [getString(expr, label), getString(expr, op), handleQuotes(getString(expr, value))];
 | |
| 
 | |
|   // Special case of pipe filtering - no errors
 | |
|   if (params.join('') === `__error__=`) {
 | |
|     return {
 | |
|       operation: {
 | |
|         id: LokiOperationId.LabelFilterNoErrors,
 | |
|         params: [],
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     operation: {
 | |
|       id,
 | |
|       params,
 | |
|     },
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getLineFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
 | |
|   const id = LokiOperationId.LineFormat;
 | |
|   const string = handleQuotes(getString(expr, node.getChild(String)));
 | |
| 
 | |
|   return {
 | |
|     id,
 | |
|     params: [string],
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getLabelFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
 | |
|   const id = LokiOperationId.LabelFormat;
 | |
|   const renameTo = node.getChild(Identifier);
 | |
|   const op = renameTo!.nextSibling;
 | |
|   const originalLabel = op!.nextSibling;
 | |
| 
 | |
|   return {
 | |
|     id,
 | |
|     params: [getString(expr, originalLabel), handleQuotes(getString(expr, renameTo))],
 | |
|   };
 | |
| }
 | |
| 
 | |
| function getDecolorize(): QueryBuilderOperation {
 | |
|   const id = LokiOperationId.Decolorize;
 | |
| 
 | |
|   return {
 | |
|     id,
 | |
|     params: [],
 | |
|   };
 | |
| }
 | |
| 
 | |
| function handleUnwrapExpr(
 | |
|   expr: string,
 | |
|   node: SyntaxNode,
 | |
|   context: Context
 | |
| ): { operation?: QueryBuilderOperation; error?: string } {
 | |
|   const unwrapExprChild = node.getChild(UnwrapExpr);
 | |
|   const labelFilterChild = node.getChild(LabelFilter);
 | |
|   const unwrapChild = node.getChild(Unwrap);
 | |
| 
 | |
|   if (unwrapExprChild) {
 | |
|     handleExpression(expr, unwrapExprChild, context);
 | |
|   }
 | |
| 
 | |
|   if (labelFilterChild) {
 | |
|     handleExpression(expr, labelFilterChild, context);
 | |
|   }
 | |
| 
 | |
|   if (unwrapChild) {
 | |
|     if (unwrapChild.nextSibling?.type.id === ConvOp) {
 | |
|       const convOp = unwrapChild.nextSibling;
 | |
|       const identifier = convOp.nextSibling;
 | |
|       return {
 | |
|         operation: {
 | |
|           id: LokiOperationId.Unwrap,
 | |
|           params: [getString(expr, identifier), getString(expr, convOp)],
 | |
|         },
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       operation: {
 | |
|         id: LokiOperationId.Unwrap,
 | |
|         params: [getString(expr, unwrapChild?.nextSibling), ''],
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   return {};
 | |
| }
 | |
| 
 | |
| function handleRangeAggregation(expr: string, node: SyntaxNode, context: Context) {
 | |
|   const nameNode = node.getChild(RangeOp);
 | |
|   const funcName = getString(expr, nameNode);
 | |
|   const number = node.getChild(NumberLezer);
 | |
|   const logExpr = node.getChild(LogRangeExpr);
 | |
|   const params = number !== null && number !== undefined ? [getString(expr, number)] : [];
 | |
|   const range = logExpr?.getChild(Range);
 | |
|   const rangeValue = range ? getString(expr, range) : null;
 | |
| 
 | |
|   if (rangeValue) {
 | |
|     params.unshift(rangeValue.substring(1, rangeValue.length - 1));
 | |
|   }
 | |
| 
 | |
|   const op = {
 | |
|     id: funcName,
 | |
|     params,
 | |
|   };
 | |
| 
 | |
|   if (logExpr) {
 | |
|     handleExpression(expr, logExpr, context);
 | |
|   }
 | |
| 
 | |
|   return op;
 | |
| }
 | |
| 
 | |
| function handleVectorAggregation(expr: string, node: SyntaxNode, context: Context) {
 | |
|   const nameNode = node.getChild(VectorOp);
 | |
|   let funcName = getString(expr, nameNode);
 | |
| 
 | |
|   const grouping = node.getChild(Grouping);
 | |
|   const params = [];
 | |
| 
 | |
|   const numberNode = node.getChild(NumberLezer);
 | |
| 
 | |
|   if (numberNode) {
 | |
|     params.push(Number(getString(expr, numberNode)));
 | |
|   }
 | |
| 
 | |
|   if (grouping) {
 | |
|     const byModifier = grouping.getChild(By);
 | |
|     if (byModifier && funcName) {
 | |
|       funcName = `__${funcName}_by`;
 | |
|     }
 | |
| 
 | |
|     const withoutModifier = grouping.getChild(Without);
 | |
|     if (withoutModifier) {
 | |
|       funcName = `__${funcName}_without`;
 | |
|     }
 | |
| 
 | |
|     params.push(...getAllByType(expr, grouping, Identifier));
 | |
|   }
 | |
| 
 | |
|   const metricExpr = node.getChild(MetricExpr);
 | |
|   const op: QueryBuilderOperation = { id: funcName, params };
 | |
| 
 | |
|   if (metricExpr) {
 | |
|     handleExpression(expr, metricExpr, context);
 | |
|   }
 | |
| 
 | |
|   return op;
 | |
| }
 | |
| 
 | |
| const operatorToOpName = binaryScalarDefs.reduce<Record<string, { id: string; comparison?: boolean }>>((acc, def) => {
 | |
|   acc[def.sign] = {
 | |
|     id: def.id,
 | |
|     comparison: def.comparison,
 | |
|   };
 | |
|   return acc;
 | |
| }, {});
 | |
| 
 | |
| /**
 | |
|  * Right now binary expressions can be represented in 2 way in visual query. As additional operation in case it is
 | |
|  * just operation with scalar or it creates a binaryQuery when it's 2 queries.
 | |
|  * @param expr
 | |
|  * @param node
 | |
|  * @param context
 | |
|  */
 | |
| function handleBinary(expr: string, node: SyntaxNode, context: Context) {
 | |
|   const visQuery = context.query;
 | |
|   const left = node.firstChild!;
 | |
|   const op = getString(expr, left.nextSibling);
 | |
|   const binModifier = getBinaryModifier(expr, node.getChild(BinModifiers));
 | |
| 
 | |
|   const right = node.lastChild!;
 | |
| 
 | |
|   const opDef = operatorToOpName[op];
 | |
| 
 | |
|   const leftNumber = getLastChildWithSelector(left, 'MetricExpr.LiteralExpr.Number');
 | |
|   const rightNumber = getLastChildWithSelector(right, 'MetricExpr.LiteralExpr.Number');
 | |
| 
 | |
|   const rightBinary = right.getChild(BinOpExpr);
 | |
| 
 | |
|   if (leftNumber) {
 | |
|     // TODO: this should be already handled in case parent is binary expression as it has to be added to parent
 | |
|     //  if query starts with a number that isn't handled now.
 | |
|   } else {
 | |
|     // If this is binary we don't really know if there is a query or just chained scalars. So
 | |
|     // we have to traverse a bit deeper to know
 | |
|     handleExpression(expr, left, context);
 | |
|   }
 | |
| 
 | |
|   if (rightNumber) {
 | |
|     visQuery.operations.push(makeBinOp(opDef, expr, right, !!binModifier?.isBool));
 | |
|   } else if (rightBinary) {
 | |
|     // Due to the way binary ops are parsed we can get a binary operation on the right that starts with a number which
 | |
|     // is a factor for a current binary operation. So we have to add it as an operation now.
 | |
|     const leftMostChild = getLeftMostChild(right);
 | |
|     if (leftMostChild?.name === 'Number') {
 | |
|       visQuery.operations.push(makeBinOp(opDef, expr, leftMostChild, !!binModifier?.isBool));
 | |
|     }
 | |
| 
 | |
|     // If we added the first number literal as operation here we still can continue and handle the rest as the first
 | |
|     // number will be just skipped.
 | |
|     handleExpression(expr, right, context);
 | |
|   } else {
 | |
|     visQuery.binaryQueries = visQuery.binaryQueries || [];
 | |
|     const binQuery: LokiVisualQueryBinary = {
 | |
|       operator: op,
 | |
|       query: {
 | |
|         labels: [],
 | |
|         operations: [],
 | |
|       },
 | |
|     };
 | |
|     if (binModifier?.isMatcher) {
 | |
|       binQuery.vectorMatchesType = binModifier.matchType;
 | |
|       binQuery.vectorMatches = binModifier.matches;
 | |
|     }
 | |
|     visQuery.binaryQueries.push(binQuery);
 | |
|     handleExpression(expr, right, {
 | |
|       query: binQuery.query,
 | |
|       errors: context.errors,
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getBinaryModifier(
 | |
|   expr: string,
 | |
|   node: SyntaxNode | null
 | |
| ):
 | |
|   | { isBool: true; isMatcher: false }
 | |
|   | { isBool: false; isMatcher: true; matches: string; matchType: 'ignoring' | 'on' }
 | |
|   | undefined {
 | |
|   if (!node) {
 | |
|     return undefined;
 | |
|   }
 | |
|   if (node.getChild(Bool)) {
 | |
|     return { isBool: true, isMatcher: false };
 | |
|   } else {
 | |
|     const matcher = node.getChild(OnOrIgnoring);
 | |
|     if (!matcher) {
 | |
|       // Not sure what this could be, maybe should be an error.
 | |
|       return undefined;
 | |
|     }
 | |
|     const labels = getString(expr, matcher.getChild(GroupingLabels)?.getChild(GroupingLabelList));
 | |
|     return {
 | |
|       isMatcher: true,
 | |
|       isBool: false,
 | |
|       matches: labels,
 | |
|       matchType: matcher.getChild(On) ? 'on' : 'ignoring',
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| function isIntervalVariableError(node: SyntaxNode) {
 | |
|   return node?.parent?.type.id === Range;
 | |
| }
 | |
| 
 | |
| export function handleQuotes(string: string) {
 | |
|   if (string[0] === `"` && string[string.length - 1] === `"`) {
 | |
|     return string
 | |
|       .substring(1, string.length - 1)
 | |
|       .replace(/\\"/g, '"')
 | |
|       .replace(/\\\\/g, '\\');
 | |
|   }
 | |
|   return string.replace(/`/g, '');
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Simple helper to traverse the syntax tree. Instead of node.getChild('foo')?.getChild('bar')?.getChild('baz') you
 | |
|  * can write getChildWithSelector(node, 'foo.bar.baz')
 | |
|  * @param node
 | |
|  * @param selector
 | |
|  */
 | |
| function getLastChildWithSelector(node: SyntaxNode, selector: string) {
 | |
|   let child: SyntaxNode | null = node;
 | |
|   const children = selector.split('.');
 | |
|   for (const s of children) {
 | |
|     child = child.getChild(s);
 | |
|     if (!child) {
 | |
|       return null;
 | |
|     }
 | |
|   }
 | |
|   return child;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Helper function to enrich error text with information that visual query builder doesn't support that logQL
 | |
|  * @param expr
 | |
|  * @param node
 | |
|  * @param error
 | |
|  */
 | |
| function createNotSupportedError(expr: string, node: SyntaxNode, error: string) {
 | |
|   const err = makeError(expr, node);
 | |
|   err.text = `${error}: ${err.text}`;
 | |
|   return err;
 | |
| }
 | |
| 
 | |
| function isEmptyQuery(query: LokiVisualQuery) {
 | |
|   if (query.labels.length === 0 && query.operations.length === 0) {
 | |
|     return true;
 | |
|   }
 | |
|   return false;
 | |
| }
 | |
| 
 | |
| function handleDistinctFilter(expr: string, node: SyntaxNode, context: Context): QueryBuilderOperation {
 | |
|   const labels: string[] = [];
 | |
|   let exploringNode = node.getChild(DistinctLabel);
 | |
|   while (exploringNode) {
 | |
|     const label = getString(expr, exploringNode.getChild(Identifier));
 | |
|     if (label) {
 | |
|       labels.push(label);
 | |
|     }
 | |
|     exploringNode = exploringNode?.getChild(DistinctLabel);
 | |
|   }
 | |
|   labels.reverse();
 | |
|   return {
 | |
|     id: LokiOperationId.Distinct,
 | |
|     params: labels,
 | |
|   };
 | |
| }
 |