mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
Query builder misc - fixes (#8295)
* feat: trace and logs explorer fixes * fix: ui fixes * fix: handle multi arg aggregation * feat: explorer pages fixes * feat: added fixes for order by for datasource * feat: metric order by issue * feat: support for paneltype selectedview tab switch * feat: qb v2 compatiblity with url's composite query * feat: conversion fixes * feat: where clause and aggregation fix --------- Co-authored-by: Yunus M <myounis.ar@live.com>
This commit is contained in:
parent
62c56d2150
commit
6d1d48e156
@ -21,7 +21,7 @@ function convertTimeSeriesData(
|
||||
return {
|
||||
queryName: timeSeriesData.queryName,
|
||||
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
||||
series: timeSeriesData.aggregations.flatMap((aggregation) =>
|
||||
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
|
||||
aggregation.series.map((series) => ({
|
||||
labels: series.labels
|
||||
? Object.fromEntries(
|
||||
|
||||
@ -71,6 +71,7 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
|
||||
function createBaseSpec(
|
||||
queryData: IBuilderQuery,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): BaseBuilderQuery {
|
||||
return {
|
||||
stepInterval: queryData.stepInterval,
|
||||
@ -90,9 +91,10 @@ function createBaseSpec(
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit: isEmpty(queryData.limit)
|
||||
? queryData?.pageSize ?? undefined
|
||||
: queryData.limit ?? undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
? queryData.limit || queryData.pageSize || undefined
|
||||
: queryData.limit || undefined,
|
||||
offset: requestType === 'raw' ? queryData.offset : undefined,
|
||||
order:
|
||||
queryData.orderBy.length > 0
|
||||
@ -151,7 +153,7 @@ export function parseAggregations(
|
||||
return result;
|
||||
}
|
||||
|
||||
function createAggregation(
|
||||
export function createAggregation(
|
||||
queryData: any,
|
||||
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
|
||||
if (queryData.dataSource === DataSource.METRICS) {
|
||||
@ -180,11 +182,12 @@ function createAggregation(
|
||||
function convertBuilderQueriesToV5(
|
||||
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(builderQueries).map(
|
||||
([queryName, queryData]): QueryEnvelope => {
|
||||
const signal = getSignalType(queryData.dataSource);
|
||||
const baseSpec = createBaseSpec(queryData, requestType);
|
||||
const baseSpec = createBaseSpec(queryData, requestType, panelType);
|
||||
let spec: QueryEnvelope['spec'];
|
||||
|
||||
const aggregations = createAggregation(queryData);
|
||||
@ -196,7 +199,6 @@ function convertBuilderQueriesToV5(
|
||||
signal: 'traces' as const,
|
||||
...baseSpec,
|
||||
aggregations: aggregations as TraceAggregation[],
|
||||
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
|
||||
};
|
||||
break;
|
||||
case 'logs':
|
||||
@ -205,7 +207,6 @@ function convertBuilderQueriesToV5(
|
||||
signal: 'logs' as const,
|
||||
...baseSpec,
|
||||
aggregations: aggregations as LogAggregation[],
|
||||
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
|
||||
};
|
||||
break;
|
||||
case 'metrics':
|
||||
@ -216,7 +217,6 @@ function convertBuilderQueriesToV5(
|
||||
...baseSpec,
|
||||
aggregations: aggregations as MetricAggregation[],
|
||||
// reduceTo: queryData.reduceTo,
|
||||
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
|
||||
};
|
||||
break;
|
||||
}
|
||||
@ -321,6 +321,8 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
|
||||
console.log('query', query);
|
||||
|
||||
switch (query.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas } = query.builder;
|
||||
@ -337,6 +339,7 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
const builderQueries = convertBuilderQueriesToV5(
|
||||
currentQueryData.data,
|
||||
requestType,
|
||||
graphType,
|
||||
);
|
||||
|
||||
// Convert formulas as separate query type
|
||||
|
||||
@ -101,12 +101,12 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
{currentQuery.builder.queryData.map((query, index) => (
|
||||
{isListViewPanel && (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={query.queryName}
|
||||
index={index}
|
||||
query={query}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
index={0}
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
@ -115,7 +115,24 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
|
||||
{!isListViewPanel &&
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={query.queryName}
|
||||
index={index}
|
||||
query={query}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
|
||||
@ -97,6 +97,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
label="Seconds"
|
||||
placeholder="Enter a number"
|
||||
labelAfter
|
||||
initialValue={query?.stepInterval ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
Completion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
@ -54,18 +55,18 @@ const havingOperators = [
|
||||
|
||||
// Add common value suggestions
|
||||
const commonValues = [
|
||||
{ label: '0', value: '0' },
|
||||
{ label: '1', value: '1' },
|
||||
{ label: '5', value: '5' },
|
||||
{ label: '10', value: '10' },
|
||||
{ label: '50', value: '50' },
|
||||
{ label: '100', value: '100' },
|
||||
{ label: '1000', value: '1000' },
|
||||
{ label: '0', value: '0 ' },
|
||||
{ label: '1', value: '1 ' },
|
||||
{ label: '5', value: '5 ' },
|
||||
{ label: '10', value: '10 ' },
|
||||
{ label: '50', value: '50 ' },
|
||||
{ label: '100', value: '100 ' },
|
||||
{ label: '1000', value: '1000 ' },
|
||||
];
|
||||
|
||||
const conjunctions = [
|
||||
{ label: 'AND', value: 'AND' },
|
||||
{ label: 'OR', value: 'OR' },
|
||||
{ label: 'AND', value: 'AND ' },
|
||||
{ label: 'OR', value: 'OR ' },
|
||||
];
|
||||
|
||||
function HavingFilter({
|
||||
@ -143,109 +144,165 @@ function HavingFilter({
|
||||
});
|
||||
};
|
||||
|
||||
const havingAutocomplete = useMemo(
|
||||
() =>
|
||||
autocompletion({
|
||||
override: [
|
||||
(context: CompletionContext): CompletionResult | null => {
|
||||
const text = context.state.sliceDoc(0, context.pos);
|
||||
const trimmedText = text.trim();
|
||||
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
||||
// Helper function for applying completion with space
|
||||
const applyCompletionWithSpace = (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const insertValue =
|
||||
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||
const newText = `${insertValue} `;
|
||||
const newPos = from + newText.length;
|
||||
|
||||
// Handle empty state when no aggregation options are available
|
||||
if (options.length === 0) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [
|
||||
{
|
||||
label:
|
||||
'No aggregation functions available. Please add aggregation functions first.',
|
||||
type: 'text',
|
||||
apply: (): boolean => true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: newText },
|
||||
selection: { anchor: newPos, head: newPos },
|
||||
effects: EditorView.scrollIntoView(newPos),
|
||||
});
|
||||
};
|
||||
|
||||
// Show value suggestions after operator - this should take precedence
|
||||
if (isAfterOperator(tokens)) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [
|
||||
...commonValues,
|
||||
{
|
||||
label: 'Enter a custom number value',
|
||||
type: 'text',
|
||||
apply: (): boolean => true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const havingAutocomplete = useMemo(() => {
|
||||
// Helper functions for applying completions
|
||||
const forceCompletion = (view: EditorView): void => {
|
||||
setTimeout(() => {
|
||||
if (view) {
|
||||
startCompletion(view);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Suggest key/operator pairs and ( for grouping
|
||||
if (
|
||||
tokens.length === 0 ||
|
||||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value) ||
|
||||
tokens[tokens.length - 1] === '('
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options,
|
||||
};
|
||||
}
|
||||
const applyValueCompletion = (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
applyCompletionWithSpace(view, completion, from, to);
|
||||
forceCompletion(view);
|
||||
};
|
||||
|
||||
// Show suggestions when typing
|
||||
if (tokens.length > 0) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
const filteredOptions = options.filter((opt) =>
|
||||
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
|
||||
);
|
||||
if (filteredOptions.length > 0) {
|
||||
return {
|
||||
from: context.pos - lastToken.length,
|
||||
options: filteredOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
const applyOperatorCompletion = (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const insertValue =
|
||||
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||
const insertWithSpace = `${insertValue} `;
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertWithSpace },
|
||||
selection: { anchor: from + insertWithSpace.length },
|
||||
});
|
||||
forceCompletion(view);
|
||||
};
|
||||
|
||||
// Suggest ) for grouping after a value and a space, if there are unmatched (
|
||||
if (
|
||||
tokens.length > 0 &&
|
||||
isNumber(tokens[tokens.length - 1]) &&
|
||||
text.endsWith(' ')
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: conjunctions,
|
||||
};
|
||||
}
|
||||
return autocompletion({
|
||||
override: [
|
||||
(context: CompletionContext): CompletionResult | null => {
|
||||
const text = context.state.sliceDoc(0, context.pos);
|
||||
const trimmedText = text.trim();
|
||||
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
||||
|
||||
// Suggest conjunctions after a closing parenthesis and a space
|
||||
if (
|
||||
tokens.length > 0 &&
|
||||
tokens[tokens.length - 1] === ')' &&
|
||||
text.endsWith(' ')
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: conjunctions,
|
||||
};
|
||||
}
|
||||
|
||||
// Show all options if no other condition matches
|
||||
// Handle empty state when no aggregation options are available
|
||||
if (options.length === 0) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options,
|
||||
options: [
|
||||
{
|
||||
label:
|
||||
'No aggregation functions available. Please add aggregation functions first.',
|
||||
type: 'text',
|
||||
apply: (): boolean => true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
],
|
||||
defaultKeymap: true,
|
||||
closeOnBlur: true,
|
||||
maxRenderedOptions: 200,
|
||||
activateOnTyping: true,
|
||||
}),
|
||||
[options],
|
||||
);
|
||||
}
|
||||
|
||||
// Show value suggestions after operator
|
||||
if (isAfterOperator(tokens)) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [
|
||||
...commonValues.map((value) => ({
|
||||
...value,
|
||||
apply: applyValueCompletion,
|
||||
})),
|
||||
{
|
||||
label: 'Enter a custom number value',
|
||||
type: 'text',
|
||||
apply: applyValueCompletion,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Suggest key/operator pairs and ( for grouping
|
||||
if (
|
||||
tokens.length === 0 ||
|
||||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
|
||||
tokens[tokens.length - 1] === '('
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: options.map((opt) => ({
|
||||
...opt,
|
||||
apply: applyOperatorCompletion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show suggestions when typing
|
||||
if (tokens.length > 0) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
const filteredOptions = options.filter((opt) =>
|
||||
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
|
||||
);
|
||||
if (filteredOptions.length > 0) {
|
||||
return {
|
||||
from: context.pos - lastToken.length,
|
||||
options: filteredOptions.map((opt) => ({
|
||||
...opt,
|
||||
apply: applyOperatorCompletion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Suggest conjunctions after a value and a space
|
||||
if (
|
||||
tokens.length > 0 &&
|
||||
(isNumber(tokens[tokens.length - 1]) ||
|
||||
tokens[tokens.length - 1] === ')') &&
|
||||
text.endsWith(' ')
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: conjunctions.map((conj) => ({
|
||||
...conj,
|
||||
apply: applyValueCompletion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show all options if no other condition matches
|
||||
return {
|
||||
from: context.pos,
|
||||
options: options.map((opt) => ({
|
||||
...opt,
|
||||
apply: applyOperatorCompletion,
|
||||
})),
|
||||
};
|
||||
},
|
||||
],
|
||||
defaultKeymap: true,
|
||||
closeOnBlur: true,
|
||||
maxRenderedOptions: 200,
|
||||
activateOnTyping: true,
|
||||
});
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<div className="having-filter-container">
|
||||
|
||||
@ -260,6 +260,7 @@ function QueryAddOns({
|
||||
query={query}
|
||||
onChange={handleChangeOrderByKeys}
|
||||
isListViewPanel={isListViewPanel}
|
||||
isNewQueryV2
|
||||
/>
|
||||
</div>
|
||||
{!isListViewPanel && (
|
||||
|
||||
@ -94,11 +94,11 @@
|
||||
border-radius: 2px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
margin-top: -2px !important;
|
||||
margin-top: 8px !important;
|
||||
min-width: 400px !important;
|
||||
position: absolute !important;
|
||||
top: 38px !important;
|
||||
left: 0px !important;
|
||||
width: 100% !important;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-200, #1d212d);
|
||||
|
||||
@ -165,9 +165,15 @@ function QueryAggregationSelect({
|
||||
.split(',')
|
||||
.map((arg) => arg.trim())
|
||||
.filter((arg) => arg.length > 0);
|
||||
args.forEach((arg) => {
|
||||
pairs.push({ func, arg });
|
||||
});
|
||||
|
||||
if (args.length === 0) {
|
||||
// For functions with no arguments, add a pair with empty string as arg
|
||||
pairs.push({ func, arg: '' });
|
||||
} else {
|
||||
args.forEach((arg) => {
|
||||
pairs.push({ func, arg });
|
||||
});
|
||||
}
|
||||
}
|
||||
setFunctionArgPairs(pairs);
|
||||
setAggregationOptions(pairs);
|
||||
@ -261,11 +267,19 @@ function QueryAggregationSelect({
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const isCount = op.value === TracesAggregatorOperator.COUNT;
|
||||
const insertText = isCount ? `${op.value}() ` : `${op.value}(`;
|
||||
const cursorPos = isCount
|
||||
? from + op.value.length + 3 // after 'count() '
|
||||
: from + op.value.length + 1; // after 'operator('
|
||||
const acceptsArgs = operatorArgMeta[op.value]?.acceptsArgs;
|
||||
|
||||
let insertText: string;
|
||||
let cursorPos: number;
|
||||
|
||||
if (!acceptsArgs) {
|
||||
insertText = `${op.value}() `;
|
||||
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
} else {
|
||||
insertText = `${op.value}(`;
|
||||
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertText },
|
||||
selection: { anchor: cursorPos },
|
||||
@ -293,10 +307,19 @@ function QueryAggregationSelect({
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
// Insert the selected key followed by ') '
|
||||
const text = view.state.sliceDoc(0, from);
|
||||
const funcName = getFunctionContextAtCursor(text, from);
|
||||
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
|
||||
|
||||
// Insert the selected key followed by either a comma or closing parenthesis
|
||||
const insertText = multiple
|
||||
? `${completion.label},`
|
||||
: `${completion.label}) `;
|
||||
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: `${completion.label}) ` },
|
||||
selection: { anchor: from + completion.label.length + 2 }, // Position cursor after ') '
|
||||
changes: { from, to, insert: insertText },
|
||||
selection: { anchor: cursorPos },
|
||||
});
|
||||
|
||||
// Trigger next suggestions after a small delay
|
||||
|
||||
@ -140,9 +140,10 @@ function QuerySearch({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setKeySuggestions([]);
|
||||
fetchKeySuggestions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [dataSource]);
|
||||
|
||||
// Add a state for tracking editing mode
|
||||
const [editingMode, setEditingMode] = useState<
|
||||
|
||||
@ -8,11 +8,16 @@ import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearch
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Copy, Ellipsis, Trash } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
convertAggregationToExpression,
|
||||
convertFiltersToExpression,
|
||||
convertHavingToExpression,
|
||||
} from '../utils';
|
||||
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
|
||||
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
|
||||
import QueryAddOns from './QueryAddOns/QueryAddOns';
|
||||
@ -49,6 +54,44 @@ export const QueryV2 = memo(function QueryV2({
|
||||
entityVersion: version,
|
||||
});
|
||||
|
||||
// Convert old format to new format and update query when component mounts or query changes
|
||||
const performQueryConversions = useCallback(() => {
|
||||
// Convert filters if needed
|
||||
if (query.filters?.items?.length > 0 && !query.filter?.expression) {
|
||||
const convertedFilter = convertFiltersToExpression(query.filters);
|
||||
handleChangeQueryData('filter', convertedFilter);
|
||||
}
|
||||
|
||||
// Convert having if needed
|
||||
if (query.having?.length > 0 && !query.havingExpression?.expression) {
|
||||
const convertedHaving = convertHavingToExpression(query.having);
|
||||
handleChangeQueryData('havingExpression', convertedHaving);
|
||||
}
|
||||
|
||||
// Convert aggregation if needed
|
||||
if (!query.aggregations && query.aggregateOperator) {
|
||||
const convertedAggregation = convertAggregationToExpression(
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute,
|
||||
query.dataSource,
|
||||
query.timeAggregation,
|
||||
query.spaceAggregation,
|
||||
) as any; // Type assertion to handle union type
|
||||
handleChangeQueryData('aggregations', convertedAggregation);
|
||||
}
|
||||
}, [query, handleChangeQueryData]);
|
||||
|
||||
useEffect(() => {
|
||||
const needsConversion =
|
||||
(query.filters?.items?.length > 0 && !query.filter?.expression) ||
|
||||
(query.having?.length > 0 && !query.havingExpression?.expression) ||
|
||||
(!query.aggregations && query.aggregateOperator);
|
||||
|
||||
if (needsConversion) {
|
||||
performQueryConversions();
|
||||
}
|
||||
}, [performQueryConversions, query]);
|
||||
|
||||
const handleToggleDisableQuery = useCallback(() => {
|
||||
handleChangeQueryData('disabled', !query.disabled);
|
||||
}, [handleChangeQueryData, query]);
|
||||
@ -176,6 +219,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
@ -196,6 +240,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
dataSource !== DataSource.METRICS && (
|
||||
<QueryAggregation
|
||||
dataSource={dataSource}
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
panelType={panelType || undefined}
|
||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||
onChange={handleChangeAggregation}
|
||||
@ -208,6 +253,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
panelType={panelType}
|
||||
query={query}
|
||||
index={0}
|
||||
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
|
||||
version="v4"
|
||||
/>
|
||||
)}
|
||||
|
||||
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Having, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogAggregation,
|
||||
MetricAggregation,
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
/**
|
||||
* Check if an operator requires array values (like IN, NOT IN)
|
||||
* @param operator - The operator to check
|
||||
* @returns True if the operator requires array values
|
||||
*/
|
||||
const isArrayOperator = (operator: string): boolean => {
|
||||
const arrayOperators = ['in', 'nin', 'IN', 'NOT IN'];
|
||||
return arrayOperators.includes(operator);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a value for the expression string
|
||||
* @param value - The value to format
|
||||
* @param operator - The operator being used (to determine if array is needed)
|
||||
* @returns Formatted value string
|
||||
*/
|
||||
const formatValueForExpression = (
|
||||
value: string[] | string | number | boolean,
|
||||
operator?: string,
|
||||
): string => {
|
||||
// For IN operators, ensure value is always an array
|
||||
if (isArrayOperator(operator || '')) {
|
||||
const arrayValue = Array.isArray(value) ? value : [value];
|
||||
return `[${arrayValue
|
||||
.map((v) =>
|
||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||
)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Handle array values (e.g., for IN operations)
|
||||
return `[${value
|
||||
.map((v) =>
|
||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||
)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Add single quotes around all string values and escape internal single quotes
|
||||
return `'${value.replace(/'/g, "\\'")}'`;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export const convertFiltersToExpression = (
|
||||
filters: TagFilter,
|
||||
): { expression: string } => {
|
||||
if (!filters?.items || filters.items.length === 0) {
|
||||
return { expression: '' };
|
||||
}
|
||||
|
||||
const expressions = filters.items
|
||||
.map((filter) => {
|
||||
const { key, op, value } = filter;
|
||||
|
||||
// Skip if key is not defined
|
||||
if (!key?.key) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formattedValue = formatValueForExpression(value, op);
|
||||
return `${key.key} ${op} ${formattedValue}`;
|
||||
})
|
||||
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||
|
||||
return {
|
||||
expression: expressions.join(' AND '),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert old having format to new having format
|
||||
* @param having - Array of old having objects with columnName, op, and value
|
||||
* @returns New having format with expression string
|
||||
*/
|
||||
export const convertHavingToExpression = (
|
||||
having: Having[],
|
||||
): { expression: string } => {
|
||||
if (!having || having.length === 0) {
|
||||
return { expression: '' };
|
||||
}
|
||||
|
||||
const expressions = having
|
||||
.map((havingItem) => {
|
||||
const { columnName, op, value } = havingItem;
|
||||
|
||||
// Skip if columnName is not defined
|
||||
if (!columnName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Format value based on its type
|
||||
let formattedValue: string;
|
||||
if (Array.isArray(value)) {
|
||||
// For array values, format as [val1, val2, ...]
|
||||
formattedValue = `[${value.join(', ')}]`;
|
||||
} else {
|
||||
// For single values, just convert to string
|
||||
formattedValue = String(value);
|
||||
}
|
||||
|
||||
return `${columnName} ${op} ${formattedValue}`;
|
||||
})
|
||||
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||
|
||||
return {
|
||||
expression: expressions.join(' AND '),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert old aggregation format to new aggregation format
|
||||
* @param aggregateOperator - The aggregate operator (e.g., 'sum', 'count', 'avg')
|
||||
* @param aggregateAttribute - The attribute to aggregate
|
||||
* @param dataSource - The data source type
|
||||
* @param timeAggregation - Time aggregation for metrics (optional)
|
||||
* @param spaceAggregation - Space aggregation for metrics (optional)
|
||||
* @param alias - Optional alias for the aggregation
|
||||
* @returns New aggregation format based on data source
|
||||
*
|
||||
*/
|
||||
export const convertAggregationToExpression = (
|
||||
aggregateOperator: string,
|
||||
aggregateAttribute: BaseAutocompleteData,
|
||||
dataSource: DataSource,
|
||||
timeAggregation?: string,
|
||||
spaceAggregation?: string,
|
||||
alias?: string,
|
||||
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||
// Skip if no operator or attribute key
|
||||
if (!aggregateOperator) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Replace noop with count as default
|
||||
const normalizedOperator =
|
||||
aggregateOperator === 'noop' ? 'count' : aggregateOperator;
|
||||
const normalizedTimeAggregation =
|
||||
timeAggregation === 'noop' ? 'count' : timeAggregation;
|
||||
const normalizedSpaceAggregation =
|
||||
spaceAggregation === 'noop' ? 'count' : spaceAggregation;
|
||||
|
||||
// For metrics, use the MetricAggregation format
|
||||
if (dataSource === DataSource.METRICS) {
|
||||
return [
|
||||
{
|
||||
metricName: aggregateAttribute.key,
|
||||
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
||||
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
||||
} as MetricAggregation,
|
||||
];
|
||||
}
|
||||
|
||||
// For traces and logs, use expression format
|
||||
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return [
|
||||
{
|
||||
expression,
|
||||
...(alias && { alias }),
|
||||
} as TraceAggregation,
|
||||
];
|
||||
}
|
||||
|
||||
// For logs
|
||||
return [
|
||||
{
|
||||
expression,
|
||||
...(alias && { alias }),
|
||||
} as LogAggregation,
|
||||
];
|
||||
};
|
||||
@ -47,4 +47,5 @@ export enum QueryParams {
|
||||
destination = 'destination',
|
||||
kindString = 'kindString',
|
||||
tab = 'tab',
|
||||
selectedExplorerView = 'selectedExplorerView',
|
||||
}
|
||||
|
||||
@ -12,10 +12,7 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
ExplorerViews,
|
||||
prepareQueryWithDefaultTimestamp,
|
||||
} from 'pages/LogsExplorer/utils';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@ -27,14 +24,15 @@ function LogExplorerQuerySection({
|
||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const defaultValue = useMemo(() => {
|
||||
const updatedQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
||||
}, [updateAllQueriesOperators]);
|
||||
const defaultValue = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(defaultValue);
|
||||
|
||||
|
||||
@ -138,7 +138,7 @@ function LogsExplorerViewsContainer({
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
|
||||
const [orderDirection, setOrderDirection] = useState<string>('asc');
|
||||
const [orderDirection, setOrderDirection] = useState<string>('desc');
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
@ -331,14 +331,26 @@ function LogsExplorerViewsContainer({
|
||||
};
|
||||
}
|
||||
|
||||
// Create orderBy array based on orderDirection
|
||||
const orderBy = [
|
||||
{ columnName: 'timestamp', order: orderDirection },
|
||||
{ columnName: 'id', order: orderDirection },
|
||||
];
|
||||
|
||||
const queryData: IBuilderQuery[] =
|
||||
query.builder.queryData.length > 1
|
||||
? query.builder.queryData
|
||||
? query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
...(selectedPanelType !== PANEL_TYPES.LIST ? { order: [] } : {}),
|
||||
}))
|
||||
: [
|
||||
{
|
||||
...(listQuery || initialQueryBuilderFormValues),
|
||||
...paginateData,
|
||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||
...(selectedPanelType === PANEL_TYPES.LIST
|
||||
? { order: orderBy }
|
||||
: { order: [] }),
|
||||
},
|
||||
];
|
||||
|
||||
@ -352,7 +364,7 @@ function LogsExplorerViewsContainer({
|
||||
|
||||
return data;
|
||||
},
|
||||
[listQuery, activeLogId],
|
||||
[activeLogId, orderDirection, listQuery, selectedPanelType],
|
||||
);
|
||||
|
||||
const handleEndReached = useCallback(() => {
|
||||
@ -495,10 +507,19 @@ function LogsExplorerViewsContainer({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
// Store previous orderDirection to detect changes
|
||||
const prevOrderDirectionRef = useRef(orderDirection);
|
||||
|
||||
useEffect(() => {
|
||||
const orderDirectionChanged =
|
||||
prevOrderDirectionRef.current !== orderDirection &&
|
||||
selectedPanelType === PANEL_TYPES.LIST;
|
||||
prevOrderDirectionRef.current = orderDirection;
|
||||
|
||||
if (
|
||||
requestData?.id !== stagedQuery?.id ||
|
||||
currentMinTimeRef.current !== minTime
|
||||
currentMinTimeRef.current !== minTime ||
|
||||
orderDirectionChanged
|
||||
) {
|
||||
// Recalculate global time when query changes i.e. stage and run query clicked
|
||||
if (
|
||||
@ -534,6 +555,8 @@ function LogsExplorerViewsContainer({
|
||||
dispatch,
|
||||
selectedTime,
|
||||
maxTime,
|
||||
orderDirection,
|
||||
selectedPanelType,
|
||||
]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
|
||||
@ -55,6 +55,16 @@ function Explorer(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap[DataSource.METRICS],
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.METRICS,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
@ -65,7 +75,7 @@ function Explorer(): JSX.Element {
|
||||
[currentQuery, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(exportDefaultQuery);
|
||||
useShareBuilderUrl(defaultQuery);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(
|
||||
@ -132,7 +142,6 @@ function Explorer(): JSX.Element {
|
||||
queryComponents={queryComponents}
|
||||
showFunctions={false}
|
||||
version="v3"
|
||||
isListViewPanel
|
||||
/>
|
||||
{/* TODO: Enable once we have resolved all related metrics issues */}
|
||||
{/* <Button.Group className="explore-tabs">
|
||||
|
||||
@ -8,6 +8,7 @@ export type OrderByFilterProps = {
|
||||
onChange: (values: OrderByPayload[]) => void;
|
||||
isListViewPanel?: boolean;
|
||||
entityVersion?: string;
|
||||
isNewQueryV2?: boolean;
|
||||
};
|
||||
|
||||
export type OrderByFilterValue = {
|
||||
|
||||
@ -2,6 +2,7 @@ import { Select, Spin } from 'antd';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useMemo } from 'react';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
@ -13,6 +14,7 @@ export function OrderByFilter({
|
||||
onChange,
|
||||
isListViewPanel = false,
|
||||
entityVersion,
|
||||
isNewQueryV2 = false,
|
||||
}: OrderByFilterProps): JSX.Element {
|
||||
const {
|
||||
debouncedSearchText,
|
||||
@ -37,22 +39,35 @@ export function OrderByFilter({
|
||||
},
|
||||
);
|
||||
|
||||
// Get parsed aggregation options using createAggregation only for QueryV2
|
||||
const parsedAggregationOptions = useMemo(
|
||||
() => (isNewQueryV2 ? getParsedAggregationOptionsForOrderBy(query) : []),
|
||||
[query, isNewQueryV2],
|
||||
);
|
||||
|
||||
const optionsData = useMemo(() => {
|
||||
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
|
||||
const groupByOptions = createOptions(query.groupBy);
|
||||
const aggregationOptionsFromParsed = createOptions(parsedAggregationOptions);
|
||||
|
||||
const options =
|
||||
query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||
? keyOptions
|
||||
: [...groupByOptions, ...aggregationOptions];
|
||||
: [
|
||||
...groupByOptions,
|
||||
...(isNewQueryV2 ? aggregationOptionsFromParsed : aggregationOptions),
|
||||
];
|
||||
|
||||
return generateOptions(options);
|
||||
}, [
|
||||
aggregationOptions,
|
||||
createOptions,
|
||||
data?.payload?.attributeKeys,
|
||||
generateOptions,
|
||||
query.aggregateOperator,
|
||||
query.groupBy,
|
||||
query.aggregateOperator,
|
||||
parsedAggregationOptions,
|
||||
aggregationOptions,
|
||||
generateOptions,
|
||||
isNewQueryV2,
|
||||
]);
|
||||
|
||||
const isDisabledSelect =
|
||||
|
||||
@ -39,7 +39,9 @@ function QuerySection(): JSX.Element {
|
||||
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
|
||||
isListViewPanel={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
|
||||
@ -58,8 +58,6 @@ export const useHandleExplorerTabChange = (): {
|
||||
currentQueryData?: ICurrentQueryData,
|
||||
redirectToUrl?: typeof ROUTES[keyof typeof ROUTES],
|
||||
) => {
|
||||
console.log('hook - type', type);
|
||||
|
||||
const newPanelType = type as PANEL_TYPES;
|
||||
|
||||
if (newPanelType === panelType && !currentQueryData) return;
|
||||
|
||||
@ -22,9 +22,40 @@ import { isEmpty } from 'lodash-es';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { prepareQueryRangePayload } from './prepareQueryRangePayload';
|
||||
|
||||
/**
|
||||
* Validates if metric name is available for METRICS data source
|
||||
*/
|
||||
function validateMetricNameForMetricsDataSource(query: Query): boolean {
|
||||
if (query.queryType !== 'builder') {
|
||||
return true; // Non-builder queries don't need this validation
|
||||
}
|
||||
|
||||
const { queryData } = query.builder;
|
||||
|
||||
// Check if any METRICS data source queries exist
|
||||
const metricsQueries = queryData.filter(
|
||||
(queryItem) => queryItem.dataSource === DataSource.METRICS,
|
||||
);
|
||||
|
||||
// If no METRICS queries, validation passes
|
||||
if (metricsQueries.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if ALL METRICS queries are missing metric names
|
||||
const allMetricsQueriesMissingNames = metricsQueries.every((queryItem) => {
|
||||
const metricName = queryItem.aggregateAttribute?.key;
|
||||
return !metricName || metricName.trim() === '';
|
||||
});
|
||||
|
||||
// Return false only if ALL METRICS queries are missing metric names
|
||||
return !allMetricsQueriesMissingNames;
|
||||
}
|
||||
|
||||
export async function GetMetricQueryRange(
|
||||
props: GetQueryResultsProps,
|
||||
version: string,
|
||||
@ -35,6 +66,32 @@ export async function GetMetricQueryRange(
|
||||
let legendMap: Record<string, string>;
|
||||
let response: SuccessResponse<MetricRangePayloadProps>;
|
||||
|
||||
// Validate metric name for METRICS data source before making the API call
|
||||
if (
|
||||
version === ENTITY_VERSION_V5 &&
|
||||
!validateMetricNameForMetricsDataSource(props.query)
|
||||
) {
|
||||
// Return empty response to avoid 400 error when metric name is missing
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Metric name is required for metrics data source',
|
||||
payload: {
|
||||
data: {
|
||||
result: [],
|
||||
resultType: '',
|
||||
newResult: {
|
||||
data: {
|
||||
result: [],
|
||||
resultType: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: props,
|
||||
};
|
||||
}
|
||||
|
||||
if (version === ENTITY_VERSION_V5) {
|
||||
const v5Result = prepareQueryRangePayloadV5(props);
|
||||
legendMap = v5Result.legendMap;
|
||||
|
||||
@ -258,17 +258,78 @@ const transformColumnTitles = (
|
||||
return item;
|
||||
});
|
||||
|
||||
const processTableColumns = (
|
||||
table: NonNullable<QueryDataV3['table']>,
|
||||
currentStagedQuery:
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery,
|
||||
dynamicColumns: DynamicColumns,
|
||||
queryType: EQueryType,
|
||||
): void => {
|
||||
table.columns.forEach((column) => {
|
||||
if (column.isValueColumn) {
|
||||
// For value columns, add as operator/formula column
|
||||
addOperatorFormulaColumns(
|
||||
currentStagedQuery,
|
||||
dynamicColumns,
|
||||
queryType,
|
||||
column.name,
|
||||
);
|
||||
} else {
|
||||
// For non-value columns, add as field/label column
|
||||
addLabels(currentStagedQuery, column.name, dynamicColumns);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const processSeriesColumns = (
|
||||
series: NonNullable<QueryDataV3['series']>,
|
||||
currentStagedQuery:
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery,
|
||||
dynamicColumns: DynamicColumns,
|
||||
queryType: EQueryType,
|
||||
currentQuery: QueryDataV3,
|
||||
): void => {
|
||||
const isValuesColumnExist = series.some((item) => item.values.length > 0);
|
||||
const isEveryValuesExist = series.every((item) => item.values.length > 0);
|
||||
|
||||
if (isValuesColumnExist) {
|
||||
addOperatorFormulaColumns(
|
||||
currentStagedQuery,
|
||||
dynamicColumns,
|
||||
queryType,
|
||||
isEveryValuesExist ? undefined : get(currentStagedQuery, 'queryName', ''),
|
||||
);
|
||||
}
|
||||
|
||||
series.forEach((seria) => {
|
||||
seria.labelsArray?.forEach((lab) => {
|
||||
Object.keys(lab).forEach((label) => {
|
||||
if (label === currentQuery?.queryName) return;
|
||||
|
||||
addLabels(currentStagedQuery, label, dynamicColumns);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
|
||||
const dynamicColumns: DynamicColumns = [];
|
||||
|
||||
queryTableData.forEach((currentQuery) => {
|
||||
const { series, queryName, list } = currentQuery;
|
||||
const { series, queryName, list, table } = currentQuery;
|
||||
|
||||
const currentStagedQuery = getQueryByName(
|
||||
query,
|
||||
queryName,
|
||||
isFormula(queryName) ? 'queryFormulas' : 'queryData',
|
||||
);
|
||||
|
||||
if (list) {
|
||||
list.forEach((listItem) => {
|
||||
Object.keys(listItem.data).forEach((label) => {
|
||||
@ -277,28 +338,23 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (table) {
|
||||
processTableColumns(
|
||||
table,
|
||||
currentStagedQuery,
|
||||
dynamicColumns,
|
||||
query.queryType,
|
||||
);
|
||||
}
|
||||
|
||||
if (series) {
|
||||
const isValuesColumnExist = series.some((item) => item.values.length > 0);
|
||||
const isEveryValuesExist = series.every((item) => item.values.length > 0);
|
||||
|
||||
if (isValuesColumnExist) {
|
||||
addOperatorFormulaColumns(
|
||||
currentStagedQuery,
|
||||
dynamicColumns,
|
||||
query.queryType,
|
||||
isEveryValuesExist ? undefined : get(currentStagedQuery, 'queryName', ''),
|
||||
);
|
||||
}
|
||||
|
||||
series.forEach((seria) => {
|
||||
seria.labelsArray?.forEach((lab) => {
|
||||
Object.keys(lab).forEach((label) => {
|
||||
if (label === currentQuery?.queryName) return;
|
||||
|
||||
addLabels(currentStagedQuery, label, dynamicColumns);
|
||||
});
|
||||
});
|
||||
});
|
||||
processSeriesColumns(
|
||||
series,
|
||||
currentStagedQuery,
|
||||
dynamicColumns,
|
||||
query.queryType,
|
||||
currentQuery,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -474,6 +530,56 @@ const fillDataFromList = (
|
||||
});
|
||||
};
|
||||
|
||||
const processTableRowValue = (value: any, column: DynamicColumn): void => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
if (isObject(value)) {
|
||||
column.data.push(JSON.stringify(value));
|
||||
} else if (typeof value === 'number' || !isNaN(Number(value))) {
|
||||
column.data.push(Number(value));
|
||||
} else {
|
||||
column.data.push(value.toString());
|
||||
}
|
||||
} else {
|
||||
column.data.push('N/A');
|
||||
}
|
||||
};
|
||||
|
||||
const fillDataFromTable = (
|
||||
currentQuery: QueryDataV3,
|
||||
columns: DynamicColumns,
|
||||
): void => {
|
||||
const { table } = currentQuery;
|
||||
|
||||
if (!table || !table.rows) return;
|
||||
|
||||
table.rows.forEach((row) => {
|
||||
const unusedColumnsKeys = new Set<keyof RowData>(
|
||||
columns.map((item) => item.field),
|
||||
);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const rowData = row.data;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(rowData, column.field)) {
|
||||
const value = rowData[column.field];
|
||||
processTableRowValue(value, column);
|
||||
unusedColumnsKeys.delete(column.field);
|
||||
} else {
|
||||
column.data.push('N/A');
|
||||
unusedColumnsKeys.delete(column.field);
|
||||
}
|
||||
});
|
||||
|
||||
// Fill any remaining unused columns with N/A
|
||||
unusedColumnsKeys.forEach((key) => {
|
||||
const unusedCol = columns.find((item) => item.field === key);
|
||||
if (unusedCol) {
|
||||
unusedCol.data.push('N/A');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const fillColumnsData: FillColumnData = (queryTableData, cols) => {
|
||||
const fields = cols.filter((item) => item.type === 'field');
|
||||
const operators = cols.filter((item) => item.type === 'operator');
|
||||
@ -497,6 +603,8 @@ const fillColumnsData: FillColumnData = (queryTableData, cols) => {
|
||||
fillDataFromList(listItem, resultColumns);
|
||||
});
|
||||
}
|
||||
|
||||
fillDataFromTable(currentQuery, resultColumns);
|
||||
});
|
||||
|
||||
const rowsLength = resultColumns.length > 0 ? resultColumns[0].data.length : 0;
|
||||
|
||||
@ -8,6 +8,7 @@ import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||
import LogsExplorerViewsContainer from 'container/LogsExplorerViews';
|
||||
@ -20,6 +21,7 @@ import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
@ -27,14 +29,24 @@ import { isEqual, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import {
|
||||
getExplorerViewForPanelType,
|
||||
getExplorerViewFromUrl,
|
||||
} from 'utils/explorerUtils';
|
||||
|
||||
import { ExplorerViews } from './utils';
|
||||
|
||||
function LogsExplorer(): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(
|
||||
ExplorerViews.LIST,
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||
);
|
||||
const { preferences, loading: preferencesLoading } = usePreferenceContext();
|
||||
|
||||
@ -48,6 +60,23 @@ function LogsExplorer(): JSX.Element {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update selected view when panel type from URL changes
|
||||
useEffect(() => {
|
||||
if (panelTypesFromUrl) {
|
||||
const newView = getExplorerViewForPanelType(panelTypesFromUrl);
|
||||
if (newView && newView !== selectedView) {
|
||||
setSelectedView(newView);
|
||||
}
|
||||
}
|
||||
}, [panelTypesFromUrl, selectedView]);
|
||||
|
||||
// Update URL when selectedView changes (without triggering re-renders)
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set(QueryParams.selectedExplorerView, selectedView);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}, [selectedView]);
|
||||
|
||||
const { handleRunQuery, handleSetConfig } = useQueryBuilder();
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
@ -9,6 +9,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import ExportPanel from 'container/ExportPanel';
|
||||
@ -22,6 +23,7 @@ import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/config
|
||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
||||
import TableView from 'container/TracesExplorer/TableView';
|
||||
import TracesView from 'container/TracesExplorer/TracesView';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
@ -30,10 +32,15 @@ import { cloneDeep, isEmpty, set } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import {
|
||||
getExplorerViewForPanelType,
|
||||
getExplorerViewFromUrl,
|
||||
} from 'utils/explorerUtils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
function TracesExplorer(): JSX.Element {
|
||||
@ -55,13 +62,36 @@ function TracesExplorer(): JSX.Element {
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(
|
||||
ExplorerViews.LIST,
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||
);
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
// Update selected view when panel type from URL changes
|
||||
useEffect(() => {
|
||||
if (panelTypesFromUrl) {
|
||||
const newView = getExplorerViewForPanelType(panelTypesFromUrl);
|
||||
if (newView && newView !== selectedView) {
|
||||
setSelectedView(newView);
|
||||
}
|
||||
}
|
||||
}, [panelTypesFromUrl, selectedView]);
|
||||
|
||||
// Update URL when selectedView changes
|
||||
useEffect(() => {
|
||||
setSearchParams((prev: URLSearchParams) => {
|
||||
prev.set(QueryParams.selectedExplorerView, selectedView);
|
||||
return prev;
|
||||
});
|
||||
}, [selectedView, setSearchParams]);
|
||||
|
||||
const handleChangeSelectedView = useCallback(
|
||||
(view: ExplorerViews): void => {
|
||||
if (selectedView === ExplorerViews.LIST) {
|
||||
@ -98,26 +128,15 @@ function TracesExplorer(): JSX.Element {
|
||||
return groupByCount > 0;
|
||||
}, [currentQuery]);
|
||||
|
||||
const defaultQuery = useMemo(() => {
|
||||
const query = updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: [
|
||||
{
|
||||
...query.builder.queryData[0],
|
||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [updateAllQueriesOperators]);
|
||||
const defaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
|
||||
139
frontend/src/utils/aggregationConverter.ts
Normal file
139
frontend/src/utils/aggregationConverter.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
LogAggregation,
|
||||
MetricAggregation,
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* Converts QueryV2 aggregations to BaseAutocompleteData format
|
||||
* for compatibility with existing OrderByFilter component
|
||||
*/
|
||||
export function convertAggregationsToBaseAutocompleteData(
|
||||
aggregations:
|
||||
| TraceAggregation[]
|
||||
| LogAggregation[]
|
||||
| MetricAggregation[]
|
||||
| undefined,
|
||||
dataSource: DataSource,
|
||||
metricName?: string,
|
||||
spaceAggregation?: string,
|
||||
): BaseAutocompleteData[] {
|
||||
// If no aggregations provided, return default based on data source
|
||||
if (!aggregations || aggregations.length === 0) {
|
||||
switch (dataSource) {
|
||||
case DataSource.METRICS:
|
||||
return [
|
||||
{
|
||||
id: uuid(),
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: false,
|
||||
type: '',
|
||||
isJSON: false,
|
||||
key: `${spaceAggregation || 'avg'}(${metricName || 'metric'})`,
|
||||
},
|
||||
];
|
||||
case DataSource.TRACES:
|
||||
case DataSource.LOGS:
|
||||
default:
|
||||
return [
|
||||
{
|
||||
id: uuid(),
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: false,
|
||||
type: '',
|
||||
isJSON: false,
|
||||
key: 'count()',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return aggregations.map((agg) => {
|
||||
if ('expression' in agg) {
|
||||
// TraceAggregation or LogAggregation
|
||||
const { expression } = agg;
|
||||
const alias = 'alias' in agg ? agg.alias : '';
|
||||
const displayKey = alias || expression;
|
||||
|
||||
return {
|
||||
id: uuid(),
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: false,
|
||||
type: '',
|
||||
isJSON: false,
|
||||
key: displayKey,
|
||||
};
|
||||
}
|
||||
// MetricAggregation
|
||||
const {
|
||||
metricName: aggMetricName,
|
||||
spaceAggregation: aggSpaceAggregation,
|
||||
} = agg;
|
||||
const displayKey = `${aggSpaceAggregation}(${aggMetricName})`;
|
||||
|
||||
return {
|
||||
id: uuid(),
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: false,
|
||||
type: '',
|
||||
isJSON: false,
|
||||
key: displayKey,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get aggregation options for OrderByFilter
|
||||
* This creates BaseAutocompleteData that can be used with the existing OrderByFilter
|
||||
*/
|
||||
export function getAggregationOptionsForOrderBy(query: {
|
||||
aggregations?: TraceAggregation[] | LogAggregation[] | MetricAggregation[];
|
||||
dataSource: DataSource;
|
||||
aggregateAttribute?: { key: string };
|
||||
spaceAggregation?: string;
|
||||
}): BaseAutocompleteData[] {
|
||||
const {
|
||||
aggregations,
|
||||
dataSource,
|
||||
aggregateAttribute,
|
||||
spaceAggregation,
|
||||
} = query;
|
||||
|
||||
return convertAggregationsToBaseAutocompleteData(
|
||||
aggregations,
|
||||
dataSource,
|
||||
aggregateAttribute?.key,
|
||||
spaceAggregation,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced function that uses createAggregation to parse aggregations first
|
||||
* then converts them to BaseAutocompleteData format for OrderByFilter
|
||||
*/
|
||||
export function getParsedAggregationOptionsForOrderBy(query: {
|
||||
aggregations?: TraceAggregation[] | LogAggregation[] | MetricAggregation[];
|
||||
dataSource: DataSource;
|
||||
aggregateAttribute?: { key: string };
|
||||
spaceAggregation?: string;
|
||||
timeAggregation?: string;
|
||||
temporality?: string;
|
||||
}): BaseAutocompleteData[] {
|
||||
// First, use createAggregation to parse the aggregations
|
||||
const parsedAggregations = createAggregation(query);
|
||||
|
||||
// Then convert the parsed aggregations to BaseAutocompleteData format
|
||||
return convertAggregationsToBaseAutocompleteData(
|
||||
parsedAggregations,
|
||||
query.dataSource,
|
||||
query.aggregateAttribute?.key,
|
||||
query.spaceAggregation,
|
||||
);
|
||||
}
|
||||
45
frontend/src/utils/explorerUtils.ts
Normal file
45
frontend/src/utils/explorerUtils.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
|
||||
// Mapping between panel types and explorer views
|
||||
export const panelTypeToExplorerView: Record<PANEL_TYPES, ExplorerViews> = {
|
||||
[PANEL_TYPES.LIST]: ExplorerViews.LIST,
|
||||
[PANEL_TYPES.TIME_SERIES]: ExplorerViews.TIMESERIES,
|
||||
[PANEL_TYPES.TRACE]: ExplorerViews.TRACE,
|
||||
[PANEL_TYPES.TABLE]: ExplorerViews.TABLE,
|
||||
[PANEL_TYPES.VALUE]: ExplorerViews.TIMESERIES,
|
||||
[PANEL_TYPES.BAR]: ExplorerViews.TIMESERIES,
|
||||
[PANEL_TYPES.PIE]: ExplorerViews.TIMESERIES,
|
||||
[PANEL_TYPES.HISTOGRAM]: ExplorerViews.TIMESERIES,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: ExplorerViews.LIST,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the explorer view based on panel type from URL or saved view
|
||||
* @param searchParams - URL search parameters
|
||||
* @param panelTypesFromUrl - Panel type extracted from URL
|
||||
* @returns The appropriate ExplorerViews value
|
||||
*/
|
||||
export const getExplorerViewFromUrl = (
|
||||
searchParams: URLSearchParams,
|
||||
panelTypesFromUrl: PANEL_TYPES | null,
|
||||
): ExplorerViews => {
|
||||
const savedView = searchParams.get(QueryParams.selectedExplorerView);
|
||||
if (savedView) {
|
||||
return savedView as ExplorerViews;
|
||||
}
|
||||
|
||||
// If no saved view, use panel type from URL to determine the view
|
||||
const urlPanelType = panelTypesFromUrl || PANEL_TYPES.LIST;
|
||||
return panelTypeToExplorerView[urlPanelType];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the explorer view for a given panel type
|
||||
* @param panelType - The panel type
|
||||
* @returns The corresponding ExplorerViews value
|
||||
*/
|
||||
export const getExplorerViewForPanelType = (
|
||||
panelType: PANEL_TYPES,
|
||||
): ExplorerViews => panelTypeToExplorerView[panelType];
|
||||
Loading…
x
Reference in New Issue
Block a user