([]);
- const isInitialQuery = useMemo(() => {
+ const isInitialState = useMemo(() => {
// Initial query has all regular labels enabled and all parsed labels disabled
if (initialized && contextFilters.some((filter) => filter.fromParser === filter.enabled)) {
return false;
}
+ // if we include pipeline operations, we also want to enable the revert button
+ if (includePipelineOperations && logContextProvider.queryContainsValidPipelineStages(origQuery)) {
+ return false;
+ }
+
return true;
- }, [contextFilters, initialized]);
+ }, [contextFilters, includePipelineOperations, initialized, logContextProvider, origQuery]);
useEffect(() => {
if (!initialized) {
@@ -200,6 +236,10 @@ export function LokiContextUi(props: LokiContextUiProps) {
// Currently we support adding of parser and showing parsed labels only if there is 1 parser
const showParsedLabels = origQuery && isQueryWithParser(origQuery.expr).parserCount === 1 && parsedLabels.length > 0;
+ let queryExpr = logContextProvider.prepareExpression(
+ contextFilters.filter(({ enabled }) => enabled),
+ origQuery
+ );
return (
@@ -208,7 +248,7 @@ export function LokiContextUi(props: LokiContextUiProps) {
data-testid="revert-button"
icon="history-alt"
variant="secondary"
- disabled={isInitialQuery}
+ disabled={isInitialState}
onClick={(e) => {
reportInteraction('grafana_explore_logs_loki_log_context_reverted', {
logRowUid: row.uid,
@@ -222,6 +262,8 @@ export function LokiContextUi(props: LokiContextUiProps) {
});
// We are removing the preserved labels from local storage so we can preselect the labels in the UI
store.delete(LOKI_LOG_CONTEXT_PRESERVED_LABELS);
+ store.delete(SHOULD_INCLUDE_PIPELINE_OPERATIONS);
+ setIncludePipelineOperations(false);
}}
/>
@@ -242,15 +284,7 @@ export function LokiContextUi(props: LokiContextUiProps) {
{initialized ? (
<>
- enabled),
- origQuery
- )}
- className={styles.rawQuery}
- />
+
@@ -345,6 +379,37 @@ export function LokiContextUi(props: LokiContextUiProps) {
/>
>
)}
+ {logContextProvider.queryContainsValidPipelineStages(origQuery) && (
+
+
+ }
+ >
+ {
+ reportInteraction('grafana_explore_logs_loki_log_context_pipeline_toggled', {
+ logRowUid: row.uid,
+ action: e.currentTarget.checked ? 'enable' : 'disable',
+ });
+ store.set(SHOULD_INCLUDE_PIPELINE_OPERATIONS, e.currentTarget.checked);
+ setIncludePipelineOperations(e.currentTarget.checked);
+ if (runContextQuery) {
+ runContextQuery();
+ }
+ }}
+ />
+
+
+ )}
diff --git a/public/app/plugins/datasource/loki/modifyQuery.test.ts b/public/app/plugins/datasource/loki/modifyQuery.test.ts
index 608fc610992..d484fe6f5fc 100644
--- a/public/app/plugins/datasource/loki/modifyQuery.test.ts
+++ b/public/app/plugins/datasource/loki/modifyQuery.test.ts
@@ -1,8 +1,11 @@
+import { SyntaxNode } from '@lezer/common';
+
import {
addLabelFormatToQuery,
addLabelToQuery,
addNoPipelineErrorToQuery,
addParserToQuery,
+ NodePosition,
removeCommentsFromQuery,
} from './modifyQuery';
@@ -187,3 +190,59 @@ describe('removeCommentsFromQuery', () => {
expect(removeCommentsFromQuery(query)).toBe(expectedResult);
});
});
+
+describe('NodePosition', () => {
+ describe('contains', () => {
+ it('should return true if the position is contained within the current position', () => {
+ const position = new NodePosition(5, 10);
+ const containedPosition = new NodePosition(6, 9);
+ const result = position.contains(containedPosition);
+ expect(result).toBe(true);
+ });
+
+ it('should return false if the position is not contained within the current position', () => {
+ const position = new NodePosition(5, 10);
+ const outsidePosition = new NodePosition(11, 15);
+ const result = position.contains(outsidePosition);
+ expect(result).toBe(false);
+ });
+
+ it('should return true if the position is the same as the current position', () => {
+ const position = new NodePosition(5, 10);
+ const samePosition = new NodePosition(5, 10);
+ const result = position.contains(samePosition);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('getExpression', () => {
+ it('should return the substring of the query within the given position', () => {
+ const position = new NodePosition(7, 12);
+ const query = 'Hello, world!';
+ const result = position.getExpression(query);
+ expect(result).toBe('world');
+ });
+
+ it('should return an empty string if the position is out of range', () => {
+ const position = new NodePosition(15, 20);
+ const query = 'Hello, world!';
+ const result = position.getExpression(query);
+ expect(result).toBe('');
+ });
+ });
+
+ describe('fromNode', () => {
+ it('should create a new NodePosition instance from a SyntaxNode', () => {
+ const syntaxNode = {
+ from: 5,
+ to: 10,
+ type: 'identifier',
+ } as unknown as SyntaxNode;
+ const result = NodePosition.fromNode(syntaxNode);
+ expect(result).toBeInstanceOf(NodePosition);
+ expect(result.from).toBe(5);
+ expect(result.to).toBe(10);
+ expect(result.type).toBe('identifier');
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/loki/modifyQuery.ts b/public/app/plugins/datasource/loki/modifyQuery.ts
index 8f8ee3a880c..aebc105d4a8 100644
--- a/public/app/plugins/datasource/loki/modifyQuery.ts
+++ b/public/app/plugins/datasource/loki/modifyQuery.ts
@@ -1,4 +1,4 @@
-import { SyntaxNode } from '@lezer/common';
+import { NodeType, SyntaxNode } from '@lezer/common';
import { sortBy } from 'lodash';
import {
@@ -22,7 +22,29 @@ import { unescapeLabelValue } from './languageUtils';
import { LokiQueryModeller } from './querybuilder/LokiQueryModeller';
import { buildVisualQueryFromString } from './querybuilder/parsing';
-export type Position = { from: number; to: number };
+export class NodePosition {
+ from: number;
+ to: number;
+ type?: NodeType;
+
+ constructor(from: number, to: number, type?: NodeType) {
+ this.from = from;
+ this.to = to;
+ this.type = type;
+ }
+
+ static fromNode(node: SyntaxNode): NodePosition {
+ return new NodePosition(node.from, node.to, node.type);
+ }
+
+ contains(position: NodePosition): boolean {
+ return this.from <= position.from && this.to >= position.to;
+ }
+
+ getExpression(query: string): string {
+ return query.substring(this.from, this.to);
+ }
+}
/**
* Adds label filter to existing query. Useful for query modification for example for ad hoc filters.
*
@@ -139,13 +161,13 @@ export function removeCommentsFromQuery(query: string): string {
* selector.
* @param query
*/
-export function getStreamSelectorPositions(query: string): Position[] {
+export function getStreamSelectorPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
enter: ({ type, from, to }): false | void => {
if (type.id === Selector) {
- positions.push({ from, to });
+ positions.push(new NodePosition(from, to, type));
return false;
}
},
@@ -153,9 +175,9 @@ export function getStreamSelectorPositions(query: string): Position[] {
return positions;
}
-function getMatcherInStreamPositions(query: string): Position[] {
+function getMatcherInStreamPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
enter: ({ node }): false | void => {
if (node.type.id === Selector) {
@@ -170,13 +192,13 @@ function getMatcherInStreamPositions(query: string): Position[] {
* Parse the string and get all LabelParser positions in the query.
* @param query
*/
-export function getParserPositions(query: string): Position[] {
+export function getParserPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
enter: ({ type, from, to }): false | void => {
if (type.id === LabelParser || type.id === JsonExpressionParser) {
- positions.push({ from, to });
+ positions.push(new NodePosition(from, to, type));
return false;
}
},
@@ -188,13 +210,13 @@ export function getParserPositions(query: string): Position[] {
* Parse the string and get all LabelFilter positions in the query.
* @param query
*/
-export function getLabelFilterPositions(query: string): Position[] {
+export function getLabelFilterPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
enter: ({ type, from, to }): false | void => {
if (type.id === LabelFilter) {
- positions.push({ from, to });
+ positions.push(new NodePosition(from, to, type));
return false;
}
},
@@ -206,13 +228,13 @@ export function getLabelFilterPositions(query: string): Position[] {
* Parse the string and get all Line filter positions in the query.
* @param query
*/
-function getLineFiltersPositions(query: string): Position[] {
+function getLineFiltersPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
- enter: ({ type, node }): false | void => {
+ enter: ({ type, from, to }): false | void => {
if (type.id === LineFilters) {
- positions.push({ from: node.from, to: node.to });
+ positions.push(new NodePosition(from, to, type));
return false;
}
},
@@ -224,13 +246,13 @@ function getLineFiltersPositions(query: string): Position[] {
* Parse the string and get all Log query positions in the query.
* @param query
*/
-function getLogQueryPositions(query: string): Position[] {
+function getLogQueryPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
enter: ({ type, from, to, node }): false | void => {
if (type.id === LogExpr) {
- positions.push({ from, to });
+ positions.push(new NodePosition(from, to, type));
return false;
}
@@ -238,25 +260,25 @@ function getLogQueryPositions(query: string): Position[] {
if (type.id === LogRangeExpr) {
// Unfortunately, LogRangeExpr includes both log and non-log (e.g. Duration/Range/...) parts of query.
// We get position of all log-parts within LogRangeExpr: Selector, PipelineExpr and UnwrapExpr.
- const logPartsPositions: Position[] = [];
+ const logPartsPositions: NodePosition[] = [];
const selector = node.getChild(Selector);
if (selector) {
- logPartsPositions.push({ from: selector.from, to: selector.to });
+ logPartsPositions.push(NodePosition.fromNode(selector));
}
const pipeline = node.getChild(PipelineExpr);
if (pipeline) {
- logPartsPositions.push({ from: pipeline.from, to: pipeline.to });
+ logPartsPositions.push(NodePosition.fromNode(pipeline));
}
const unwrap = node.getChild(UnwrapExpr);
if (unwrap) {
- logPartsPositions.push({ from: unwrap.from, to: unwrap.to });
+ logPartsPositions.push(NodePosition.fromNode(unwrap));
}
// We sort them and then pick "from" from first position and "to" from last position.
const sorted = sortBy(logPartsPositions, (position) => position.to);
- positions.push({ from: sorted[0].from, to: sorted[sorted.length - 1].to });
+ positions.push(new NodePosition(sorted[0].from, sorted[sorted.length - 1].to));
return false;
}
},
@@ -277,7 +299,7 @@ export function toLabelFilter(key: string, value: string, operator: string): Que
*/
function addFilterToStreamSelector(
query: string,
- vectorSelectorPositions: Position[],
+ vectorSelectorPositions: NodePosition[],
filter: QueryBuilderLabelFilter
): string {
const modeller = new LokiQueryModeller();
@@ -313,7 +335,7 @@ function addFilterToStreamSelector(
*/
export function addFilterAsLabelFilter(
query: string,
- positionsToAddAfter: Position[],
+ positionsToAddAfter: NodePosition[],
filter: QueryBuilderLabelFilter
): string {
let newQuery = '';
@@ -349,7 +371,7 @@ export function addFilterAsLabelFilter(
* @param queryPartPositions
* @param parser
*/
-function addParser(query: string, queryPartPositions: Position[], parser: string): string {
+function addParser(query: string, queryPartPositions: NodePosition[], parser: string): string {
let newQuery = '';
let prev = 0;
@@ -376,7 +398,7 @@ function addParser(query: string, queryPartPositions: Position[], parser: string
*/
function addLabelFormat(
query: string,
- logQueryPositions: Position[],
+ logQueryPositions: NodePosition[],
labelFormat: { originalLabel: string; renameTo: string }
): string {
let newQuery = '';
@@ -405,13 +427,13 @@ export function addLineFilter(query: string): string {
return newQueryExpr;
}
-function getLineCommentPositions(query: string): Position[] {
+function getLineCommentPositions(query: string): NodePosition[] {
const tree = parser.parse(query);
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
tree.iterate({
enter: ({ type, from, to }): false | void => {
if (type.id === LineComment) {
- positions.push({ from, to });
+ positions.push(new NodePosition(from, to, type));
return false;
}
},
@@ -432,16 +454,16 @@ function labelExists(labels: QueryBuilderLabelFilter[], filter: QueryBuilderLabe
* Return the last position based on "to" property
* @param positions
*/
-export function findLastPosition(positions: Position[]): Position {
+export function findLastPosition(positions: NodePosition[]): NodePosition {
return positions.reduce((prev, current) => (prev.to > current.to ? prev : current));
}
-function getAllPositionsInNodeByType(query: string, node: SyntaxNode, type: number): Position[] {
+function getAllPositionsInNodeByType(query: string, node: SyntaxNode, type: number): NodePosition[] {
if (node.type.id === type) {
- return [{ from: node.from, to: node.to }];
+ return [NodePosition.fromNode(node)];
}
- const positions: Position[] = [];
+ const positions: NodePosition[] = [];
let pos = 0;
let child = node.childAfter(pos);
while (child) {
diff --git a/public/app/plugins/datasource/loki/queryUtils.test.ts b/public/app/plugins/datasource/loki/queryUtils.test.ts
index 34970a07e39..3480f164d62 100644
--- a/public/app/plugins/datasource/loki/queryUtils.test.ts
+++ b/public/app/plugins/datasource/loki/queryUtils.test.ts
@@ -1,3 +1,5 @@
+import { String } from '@grafana/lezer-logql';
+
import {
getHighlighterExpressionsFromQuery,
getLokiQueryType,
@@ -14,6 +16,7 @@ import {
isQueryPipelineErrorFiltering,
getLogQueryFromMetricsQuery,
getNormalizedLokiQuery,
+ getNodePositionsFromQuery,
} from './queryUtils';
import { LokiQuery, LokiQueryType } from './types';
@@ -416,3 +419,24 @@ describe('getLogQueryFromMetricsQuery', () => {
).toBe('{label="$var"} | logfmt | __error__=``');
});
});
+
+describe('getNodePositionsFromQuery', () => {
+ it('returns the right amount of positions without type', () => {
+ // LogQL, Expr, LogExpr, Selector, Matchers, Matcher, Identifier, Eq, String
+ expect(getNodePositionsFromQuery('{job="grafana"}').length).toBe(9);
+ });
+
+ it('returns the right position of a string in a stream selector', () => {
+ // LogQL, Expr, LogExpr, Selector, Matchers, Matcher, Identifier, Eq, String
+ const nodePositions = getNodePositionsFromQuery('{job="grafana"}', [String]);
+ expect(nodePositions.length).toBe(1);
+ expect(nodePositions[0].from).toBe(5);
+ expect(nodePositions[0].to).toBe(14);
+ });
+
+ it('returns an empty array with a wrong expr', () => {
+ // LogQL, Expr, LogExpr, Selector, Matchers, Matcher, Identifier, Eq, String
+ const nodePositions = getNodePositionsFromQuery('not loql', [String]);
+ expect(nodePositions.length).toBe(0);
+ });
+});
diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts
index e7aab3a1790..298aaafba7a 100644
--- a/public/app/plugins/datasource/loki/queryUtils.ts
+++ b/public/app/plugins/datasource/loki/queryUtils.ts
@@ -24,7 +24,7 @@ import { DataQuery } from '@grafana/schema';
import { ErrorId } from '../prometheus/querybuilder/shared/parsingUtils';
-import { getStreamSelectorPositions } from './modifyQuery';
+import { getStreamSelectorPositions, NodePosition } from './modifyQuery';
import { LokiQuery, LokiQueryType } from './types';
export function formatQuery(selector: string | undefined): string {
@@ -145,12 +145,12 @@ export function isQueryWithNode(query: string, nodeType: number): boolean {
return isQueryWithNode;
}
-export function getNodesFromQuery(query: string, nodeTypes: number[]): SyntaxNode[] {
+export function getNodesFromQuery(query: string, nodeTypes?: number[]): SyntaxNode[] {
const nodes: SyntaxNode[] = [];
const tree = parser.parse(query);
tree.iterate({
enter: (node): false | void => {
- if (nodeTypes.includes(node.type.id)) {
+ if (nodeTypes === undefined || nodeTypes.includes(node.type.id)) {
nodes.push(node.node);
}
},
@@ -158,6 +158,19 @@ export function getNodesFromQuery(query: string, nodeTypes: number[]): SyntaxNod
return nodes;
}
+export function getNodePositionsFromQuery(query: string, nodeTypes?: number[]): NodePosition[] {
+ const positions: NodePosition[] = [];
+ const tree = parser.parse(query);
+ tree.iterate({
+ enter: (node): false | void => {
+ if (nodeTypes === undefined || nodeTypes.includes(node.type.id)) {
+ positions.push(NodePosition.fromNode(node.node));
+ }
+ },
+ });
+ return positions;
+}
+
export function getNodeFromQuery(query: string, nodeType: number): SyntaxNode | undefined {
const nodes = getNodesFromQuery(query, [nodeType]);
return nodes.length > 0 ? nodes[0] : undefined;