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 {
|
return {
|
||||||
queryName: timeSeriesData.queryName,
|
queryName: timeSeriesData.queryName,
|
||||||
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
||||||
series: timeSeriesData.aggregations.flatMap((aggregation) =>
|
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
|
||||||
aggregation.series.map((series) => ({
|
aggregation.series.map((series) => ({
|
||||||
labels: series.labels
|
labels: series.labels
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
|
|||||||
@ -71,6 +71,7 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
|
|||||||
function createBaseSpec(
|
function createBaseSpec(
|
||||||
queryData: IBuilderQuery,
|
queryData: IBuilderQuery,
|
||||||
requestType: RequestType,
|
requestType: RequestType,
|
||||||
|
panelType?: PANEL_TYPES,
|
||||||
): BaseBuilderQuery {
|
): BaseBuilderQuery {
|
||||||
return {
|
return {
|
||||||
stepInterval: queryData.stepInterval,
|
stepInterval: queryData.stepInterval,
|
||||||
@ -90,9 +91,10 @@ function createBaseSpec(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
limit: isEmpty(queryData.limit)
|
limit:
|
||||||
? queryData?.pageSize ?? undefined
|
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||||
: queryData.limit ?? undefined,
|
? queryData.limit || queryData.pageSize || undefined
|
||||||
|
: queryData.limit || undefined,
|
||||||
offset: requestType === 'raw' ? queryData.offset : undefined,
|
offset: requestType === 'raw' ? queryData.offset : undefined,
|
||||||
order:
|
order:
|
||||||
queryData.orderBy.length > 0
|
queryData.orderBy.length > 0
|
||||||
@ -151,7 +153,7 @@ export function parseAggregations(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAggregation(
|
export function createAggregation(
|
||||||
queryData: any,
|
queryData: any,
|
||||||
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
|
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
|
||||||
if (queryData.dataSource === DataSource.METRICS) {
|
if (queryData.dataSource === DataSource.METRICS) {
|
||||||
@ -180,11 +182,12 @@ function createAggregation(
|
|||||||
function convertBuilderQueriesToV5(
|
function convertBuilderQueriesToV5(
|
||||||
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
requestType: RequestType,
|
requestType: RequestType,
|
||||||
|
panelType?: PANEL_TYPES,
|
||||||
): QueryEnvelope[] {
|
): QueryEnvelope[] {
|
||||||
return Object.entries(builderQueries).map(
|
return Object.entries(builderQueries).map(
|
||||||
([queryName, queryData]): QueryEnvelope => {
|
([queryName, queryData]): QueryEnvelope => {
|
||||||
const signal = getSignalType(queryData.dataSource);
|
const signal = getSignalType(queryData.dataSource);
|
||||||
const baseSpec = createBaseSpec(queryData, requestType);
|
const baseSpec = createBaseSpec(queryData, requestType, panelType);
|
||||||
let spec: QueryEnvelope['spec'];
|
let spec: QueryEnvelope['spec'];
|
||||||
|
|
||||||
const aggregations = createAggregation(queryData);
|
const aggregations = createAggregation(queryData);
|
||||||
@ -196,7 +199,6 @@ function convertBuilderQueriesToV5(
|
|||||||
signal: 'traces' as const,
|
signal: 'traces' as const,
|
||||||
...baseSpec,
|
...baseSpec,
|
||||||
aggregations: aggregations as TraceAggregation[],
|
aggregations: aggregations as TraceAggregation[],
|
||||||
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'logs':
|
case 'logs':
|
||||||
@ -205,7 +207,6 @@ function convertBuilderQueriesToV5(
|
|||||||
signal: 'logs' as const,
|
signal: 'logs' as const,
|
||||||
...baseSpec,
|
...baseSpec,
|
||||||
aggregations: aggregations as LogAggregation[],
|
aggregations: aggregations as LogAggregation[],
|
||||||
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'metrics':
|
case 'metrics':
|
||||||
@ -216,7 +217,6 @@ function convertBuilderQueriesToV5(
|
|||||||
...baseSpec,
|
...baseSpec,
|
||||||
aggregations: aggregations as MetricAggregation[],
|
aggregations: aggregations as MetricAggregation[],
|
||||||
// reduceTo: queryData.reduceTo,
|
// reduceTo: queryData.reduceTo,
|
||||||
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -321,6 +321,8 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
const requestType = mapPanelTypeToRequestType(graphType);
|
const requestType = mapPanelTypeToRequestType(graphType);
|
||||||
let queries: QueryEnvelope[] = [];
|
let queries: QueryEnvelope[] = [];
|
||||||
|
|
||||||
|
console.log('query', query);
|
||||||
|
|
||||||
switch (query.queryType) {
|
switch (query.queryType) {
|
||||||
case EQueryType.QUERY_BUILDER: {
|
case EQueryType.QUERY_BUILDER: {
|
||||||
const { queryData: data, queryFormulas } = query.builder;
|
const { queryData: data, queryFormulas } = query.builder;
|
||||||
@ -337,6 +339,7 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
const builderQueries = convertBuilderQueriesToV5(
|
const builderQueries = convertBuilderQueriesToV5(
|
||||||
currentQueryData.data,
|
currentQueryData.data,
|
||||||
requestType,
|
requestType,
|
||||||
|
graphType,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert formulas as separate query type
|
// Convert formulas as separate query type
|
||||||
|
|||||||
@ -101,12 +101,12 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
<QueryBuilderV2Provider>
|
<QueryBuilderV2Provider>
|
||||||
<div className="query-builder-v2">
|
<div className="query-builder-v2">
|
||||||
<div className="qb-content-container">
|
<div className="qb-content-container">
|
||||||
{currentQuery.builder.queryData.map((query, index) => (
|
{isListViewPanel && (
|
||||||
<QueryV2
|
<QueryV2
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
key={query.queryName}
|
key={currentQuery.builder.queryData[0].queryName}
|
||||||
index={index}
|
index={0}
|
||||||
query={query}
|
query={currentQuery.builder.queryData[0]}
|
||||||
filterConfigs={queryFilterConfigs}
|
filterConfigs={queryFilterConfigs}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
version={version}
|
version={version}
|
||||||
@ -115,7 +115,24 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
showOnlyWhereClause={showOnlyWhereClause}
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
isListViewPanel={isListViewPanel}
|
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 && (
|
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||||
<div className="qb-formulas-container">
|
<div className="qb-formulas-container">
|
||||||
|
|||||||
@ -97,6 +97,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
label="Seconds"
|
label="Seconds"
|
||||||
placeholder="Enter a number"
|
placeholder="Enter a number"
|
||||||
labelAfter
|
labelAfter
|
||||||
|
initialValue={query?.stepInterval ?? undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
autocompletion,
|
autocompletion,
|
||||||
closeCompletion,
|
closeCompletion,
|
||||||
|
Completion,
|
||||||
CompletionContext,
|
CompletionContext,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
CompletionResult,
|
CompletionResult,
|
||||||
@ -54,18 +55,18 @@ const havingOperators = [
|
|||||||
|
|
||||||
// Add common value suggestions
|
// Add common value suggestions
|
||||||
const commonValues = [
|
const commonValues = [
|
||||||
{ label: '0', value: '0' },
|
{ label: '0', value: '0 ' },
|
||||||
{ label: '1', value: '1' },
|
{ label: '1', value: '1 ' },
|
||||||
{ label: '5', value: '5' },
|
{ label: '5', value: '5 ' },
|
||||||
{ label: '10', value: '10' },
|
{ label: '10', value: '10 ' },
|
||||||
{ label: '50', value: '50' },
|
{ label: '50', value: '50 ' },
|
||||||
{ label: '100', value: '100' },
|
{ label: '100', value: '100 ' },
|
||||||
{ label: '1000', value: '1000' },
|
{ label: '1000', value: '1000 ' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const conjunctions = [
|
const conjunctions = [
|
||||||
{ label: 'AND', value: 'AND' },
|
{ label: 'AND', value: 'AND ' },
|
||||||
{ label: 'OR', value: 'OR' },
|
{ label: 'OR', value: 'OR ' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function HavingFilter({
|
function HavingFilter({
|
||||||
@ -143,109 +144,165 @@ function HavingFilter({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const havingAutocomplete = useMemo(
|
// Helper function for applying completion with space
|
||||||
() =>
|
const applyCompletionWithSpace = (
|
||||||
autocompletion({
|
view: EditorView,
|
||||||
override: [
|
completion: Completion,
|
||||||
(context: CompletionContext): CompletionResult | null => {
|
from: number,
|
||||||
const text = context.state.sliceDoc(0, context.pos);
|
to: number,
|
||||||
const trimmedText = text.trim();
|
): void => {
|
||||||
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
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
|
view.dispatch({
|
||||||
if (options.length === 0) {
|
changes: { from, to, insert: newText },
|
||||||
return {
|
selection: { anchor: newPos, head: newPos },
|
||||||
from: context.pos,
|
effects: EditorView.scrollIntoView(newPos),
|
||||||
options: [
|
});
|
||||||
{
|
};
|
||||||
label:
|
|
||||||
'No aggregation functions available. Please add aggregation functions first.',
|
|
||||||
type: 'text',
|
|
||||||
apply: (): boolean => true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show value suggestions after operator - this should take precedence
|
const havingAutocomplete = useMemo(() => {
|
||||||
if (isAfterOperator(tokens)) {
|
// Helper functions for applying completions
|
||||||
return {
|
const forceCompletion = (view: EditorView): void => {
|
||||||
from: context.pos,
|
setTimeout(() => {
|
||||||
options: [
|
if (view) {
|
||||||
...commonValues,
|
startCompletion(view);
|
||||||
{
|
}
|
||||||
label: 'Enter a custom number value',
|
}, 0);
|
||||||
type: 'text',
|
};
|
||||||
apply: (): boolean => true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggest key/operator pairs and ( for grouping
|
const applyValueCompletion = (
|
||||||
if (
|
view: EditorView,
|
||||||
tokens.length === 0 ||
|
completion: Completion,
|
||||||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value) ||
|
from: number,
|
||||||
tokens[tokens.length - 1] === '('
|
to: number,
|
||||||
) {
|
): void => {
|
||||||
return {
|
applyCompletionWithSpace(view, completion, from, to);
|
||||||
from: context.pos,
|
forceCompletion(view);
|
||||||
options,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show suggestions when typing
|
const applyOperatorCompletion = (
|
||||||
if (tokens.length > 0) {
|
view: EditorView,
|
||||||
const lastToken = tokens[tokens.length - 1];
|
completion: Completion,
|
||||||
const filteredOptions = options.filter((opt) =>
|
from: number,
|
||||||
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
|
to: number,
|
||||||
);
|
): void => {
|
||||||
if (filteredOptions.length > 0) {
|
const insertValue =
|
||||||
return {
|
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||||
from: context.pos - lastToken.length,
|
const insertWithSpace = `${insertValue} `;
|
||||||
options: filteredOptions,
|
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 (
|
return autocompletion({
|
||||||
if (
|
override: [
|
||||||
tokens.length > 0 &&
|
(context: CompletionContext): CompletionResult | null => {
|
||||||
isNumber(tokens[tokens.length - 1]) &&
|
const text = context.state.sliceDoc(0, context.pos);
|
||||||
text.endsWith(' ')
|
const trimmedText = text.trim();
|
||||||
) {
|
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
||||||
return {
|
|
||||||
from: context.pos,
|
|
||||||
options: conjunctions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggest conjunctions after a closing parenthesis and a space
|
// Handle empty state when no aggregation options are available
|
||||||
if (
|
if (options.length === 0) {
|
||||||
tokens.length > 0 &&
|
|
||||||
tokens[tokens.length - 1] === ')' &&
|
|
||||||
text.endsWith(' ')
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
from: context.pos,
|
|
||||||
options: conjunctions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show all options if no other condition matches
|
|
||||||
return {
|
return {
|
||||||
from: context.pos,
|
from: context.pos,
|
||||||
options,
|
options: [
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
'No aggregation functions available. Please add aggregation functions first.',
|
||||||
|
type: 'text',
|
||||||
|
apply: (): boolean => true,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
],
|
|
||||||
defaultKeymap: true,
|
// Show value suggestions after operator
|
||||||
closeOnBlur: true,
|
if (isAfterOperator(tokens)) {
|
||||||
maxRenderedOptions: 200,
|
return {
|
||||||
activateOnTyping: true,
|
from: context.pos,
|
||||||
}),
|
options: [
|
||||||
[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 (
|
return (
|
||||||
<div className="having-filter-container">
|
<div className="having-filter-container">
|
||||||
|
|||||||
@ -260,6 +260,7 @@ function QueryAddOns({
|
|||||||
query={query}
|
query={query}
|
||||||
onChange={handleChangeOrderByKeys}
|
onChange={handleChangeOrderByKeys}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
|
isNewQueryV2
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isListViewPanel && (
|
{!isListViewPanel && (
|
||||||
|
|||||||
@ -94,11 +94,11 @@
|
|||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
margin-top: -2px !important;
|
margin-top: 8px !important;
|
||||||
min-width: 400px !important;
|
min-width: 400px !important;
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 38px !important;
|
|
||||||
left: 0px !important;
|
left: 0px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--bg-slate-200, #1d212d);
|
border: 1px solid var(--bg-slate-200, #1d212d);
|
||||||
|
|||||||
@ -165,9 +165,15 @@ function QueryAggregationSelect({
|
|||||||
.split(',')
|
.split(',')
|
||||||
.map((arg) => arg.trim())
|
.map((arg) => arg.trim())
|
||||||
.filter((arg) => arg.length > 0);
|
.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);
|
setFunctionArgPairs(pairs);
|
||||||
setAggregationOptions(pairs);
|
setAggregationOptions(pairs);
|
||||||
@ -261,11 +267,19 @@ function QueryAggregationSelect({
|
|||||||
from: number,
|
from: number,
|
||||||
to: number,
|
to: number,
|
||||||
): void => {
|
): void => {
|
||||||
const isCount = op.value === TracesAggregatorOperator.COUNT;
|
const acceptsArgs = operatorArgMeta[op.value]?.acceptsArgs;
|
||||||
const insertText = isCount ? `${op.value}() ` : `${op.value}(`;
|
|
||||||
const cursorPos = isCount
|
let insertText: string;
|
||||||
? from + op.value.length + 3 // after 'count() '
|
let cursorPos: number;
|
||||||
: from + op.value.length + 1; // after 'operator('
|
|
||||||
|
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({
|
view.dispatch({
|
||||||
changes: { from, to, insert: insertText },
|
changes: { from, to, insert: insertText },
|
||||||
selection: { anchor: cursorPos },
|
selection: { anchor: cursorPos },
|
||||||
@ -293,10 +307,19 @@ function QueryAggregationSelect({
|
|||||||
from: number,
|
from: number,
|
||||||
to: number,
|
to: number,
|
||||||
): void => {
|
): 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({
|
view.dispatch({
|
||||||
changes: { from, to, insert: `${completion.label}) ` },
|
changes: { from, to, insert: insertText },
|
||||||
selection: { anchor: from + completion.label.length + 2 }, // Position cursor after ') '
|
selection: { anchor: cursorPos },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger next suggestions after a small delay
|
// Trigger next suggestions after a small delay
|
||||||
|
|||||||
@ -140,9 +140,10 @@ function QuerySearch({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setKeySuggestions([]);
|
||||||
fetchKeySuggestions();
|
fetchKeySuggestions();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [dataSource]);
|
||||||
|
|
||||||
// Add a state for tracking editing mode
|
// Add a state for tracking editing mode
|
||||||
const [editingMode, setEditingMode] = useState<
|
const [editingMode, setEditingMode] = useState<
|
||||||
|
|||||||
@ -8,11 +8,16 @@ import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearch
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import { Copy, Ellipsis, Trash } from 'lucide-react';
|
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 { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
convertAggregationToExpression,
|
||||||
|
convertFiltersToExpression,
|
||||||
|
convertHavingToExpression,
|
||||||
|
} from '../utils';
|
||||||
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
|
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
|
||||||
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
|
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
|
||||||
import QueryAddOns from './QueryAddOns/QueryAddOns';
|
import QueryAddOns from './QueryAddOns/QueryAddOns';
|
||||||
@ -49,6 +54,44 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
entityVersion: version,
|
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(() => {
|
const handleToggleDisableQuery = useCallback(() => {
|
||||||
handleChangeQueryData('disabled', !query.disabled);
|
handleChangeQueryData('disabled', !query.disabled);
|
||||||
}, [handleChangeQueryData, query]);
|
}, [handleChangeQueryData, query]);
|
||||||
@ -176,6 +219,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
<div className="qb-search-filter-container">
|
<div className="qb-search-filter-container">
|
||||||
<div className="query-search-container">
|
<div className="query-search-container">
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
queryData={query}
|
queryData={query}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
@ -196,6 +240,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
dataSource !== DataSource.METRICS && (
|
dataSource !== DataSource.METRICS && (
|
||||||
<QueryAggregation
|
<QueryAggregation
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
panelType={panelType || undefined}
|
panelType={panelType || undefined}
|
||||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||||
onChange={handleChangeAggregation}
|
onChange={handleChangeAggregation}
|
||||||
@ -208,6 +253,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
panelType={panelType}
|
panelType={panelType}
|
||||||
query={query}
|
query={query}
|
||||||
index={0}
|
index={0}
|
||||||
|
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
|
||||||
version="v4"
|
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',
|
destination = 'destination',
|
||||||
kindString = 'kindString',
|
kindString = 'kindString',
|
||||||
tab = 'tab',
|
tab = 'tab',
|
||||||
|
selectedExplorerView = 'selectedExplorerView',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,7 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
|
|||||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import {
|
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||||
ExplorerViews,
|
|
||||||
prepareQueryWithDefaultTimestamp,
|
|
||||||
} from 'pages/LogsExplorer/utils';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
@ -27,14 +24,15 @@ function LogExplorerQuerySection({
|
|||||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||||
|
|
||||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
const defaultValue = useMemo(() => {
|
const defaultValue = useMemo(
|
||||||
const updatedQuery = updateAllQueriesOperators(
|
() =>
|
||||||
initialQueriesMap.logs,
|
updateAllQueriesOperators(
|
||||||
PANEL_TYPES.LIST,
|
initialQueriesMap.logs,
|
||||||
DataSource.LOGS,
|
PANEL_TYPES.LIST,
|
||||||
);
|
DataSource.LOGS,
|
||||||
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
),
|
||||||
}, [updateAllQueriesOperators]);
|
[updateAllQueriesOperators],
|
||||||
|
);
|
||||||
|
|
||||||
useShareBuilderUrl(defaultValue);
|
useShareBuilderUrl(defaultValue);
|
||||||
|
|
||||||
|
|||||||
@ -138,7 +138,7 @@ function LogsExplorerViewsContainer({
|
|||||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||||
|
|
||||||
const [orderDirection, setOrderDirection] = useState<string>('asc');
|
const [orderDirection, setOrderDirection] = useState<string>('desc');
|
||||||
|
|
||||||
const listQuery = useMemo(() => {
|
const listQuery = useMemo(() => {
|
||||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
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[] =
|
const queryData: IBuilderQuery[] =
|
||||||
query.builder.queryData.length > 1
|
query.builder.queryData.length > 1
|
||||||
? query.builder.queryData
|
? query.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
...(selectedPanelType !== PANEL_TYPES.LIST ? { order: [] } : {}),
|
||||||
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
...(listQuery || initialQueryBuilderFormValues),
|
...(listQuery || initialQueryBuilderFormValues),
|
||||||
...paginateData,
|
...paginateData,
|
||||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||||
|
...(selectedPanelType === PANEL_TYPES.LIST
|
||||||
|
? { order: orderBy }
|
||||||
|
: { order: [] }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -352,7 +364,7 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
[listQuery, activeLogId],
|
[activeLogId, orderDirection, listQuery, selectedPanelType],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEndReached = useCallback(() => {
|
const handleEndReached = useCallback(() => {
|
||||||
@ -495,10 +507,19 @@ function LogsExplorerViewsContainer({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Store previous orderDirection to detect changes
|
||||||
|
const prevOrderDirectionRef = useRef(orderDirection);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const orderDirectionChanged =
|
||||||
|
prevOrderDirectionRef.current !== orderDirection &&
|
||||||
|
selectedPanelType === PANEL_TYPES.LIST;
|
||||||
|
prevOrderDirectionRef.current = orderDirection;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
requestData?.id !== stagedQuery?.id ||
|
requestData?.id !== stagedQuery?.id ||
|
||||||
currentMinTimeRef.current !== minTime
|
currentMinTimeRef.current !== minTime ||
|
||||||
|
orderDirectionChanged
|
||||||
) {
|
) {
|
||||||
// Recalculate global time when query changes i.e. stage and run query clicked
|
// Recalculate global time when query changes i.e. stage and run query clicked
|
||||||
if (
|
if (
|
||||||
@ -534,6 +555,8 @@ function LogsExplorerViewsContainer({
|
|||||||
dispatch,
|
dispatch,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
maxTime,
|
maxTime,
|
||||||
|
orderDirection,
|
||||||
|
selectedPanelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
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(
|
const exportDefaultQuery = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateAllQueriesOperators(
|
updateAllQueriesOperators(
|
||||||
@ -65,7 +75,7 @@ function Explorer(): JSX.Element {
|
|||||||
[currentQuery, updateAllQueriesOperators],
|
[currentQuery, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
useShareBuilderUrl(exportDefaultQuery);
|
useShareBuilderUrl(defaultQuery);
|
||||||
|
|
||||||
const handleExport = useCallback(
|
const handleExport = useCallback(
|
||||||
(
|
(
|
||||||
@ -132,7 +142,6 @@ function Explorer(): JSX.Element {
|
|||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
showFunctions={false}
|
showFunctions={false}
|
||||||
version="v3"
|
version="v3"
|
||||||
isListViewPanel
|
|
||||||
/>
|
/>
|
||||||
{/* TODO: Enable once we have resolved all related metrics issues */}
|
{/* TODO: Enable once we have resolved all related metrics issues */}
|
||||||
{/* <Button.Group className="explore-tabs">
|
{/* <Button.Group className="explore-tabs">
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export type OrderByFilterProps = {
|
|||||||
onChange: (values: OrderByPayload[]) => void;
|
onChange: (values: OrderByPayload[]) => void;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
entityVersion?: string;
|
entityVersion?: string;
|
||||||
|
isNewQueryV2?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OrderByFilterValue = {
|
export type OrderByFilterValue = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Select, Spin } from 'antd';
|
|||||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||||
|
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
@ -13,6 +14,7 @@ export function OrderByFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
entityVersion,
|
entityVersion,
|
||||||
|
isNewQueryV2 = false,
|
||||||
}: OrderByFilterProps): JSX.Element {
|
}: OrderByFilterProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
debouncedSearchText,
|
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 optionsData = useMemo(() => {
|
||||||
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
|
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
|
||||||
const groupByOptions = createOptions(query.groupBy);
|
const groupByOptions = createOptions(query.groupBy);
|
||||||
|
const aggregationOptionsFromParsed = createOptions(parsedAggregationOptions);
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
query.aggregateOperator === MetricAggregateOperator.NOOP
|
query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||||
? keyOptions
|
? keyOptions
|
||||||
: [...groupByOptions, ...aggregationOptions];
|
: [
|
||||||
|
...groupByOptions,
|
||||||
|
...(isNewQueryV2 ? aggregationOptionsFromParsed : aggregationOptions),
|
||||||
|
];
|
||||||
|
|
||||||
return generateOptions(options);
|
return generateOptions(options);
|
||||||
}, [
|
}, [
|
||||||
aggregationOptions,
|
|
||||||
createOptions,
|
createOptions,
|
||||||
data?.payload?.attributeKeys,
|
data?.payload?.attributeKeys,
|
||||||
generateOptions,
|
|
||||||
query.aggregateOperator,
|
|
||||||
query.groupBy,
|
query.groupBy,
|
||||||
|
query.aggregateOperator,
|
||||||
|
parsedAggregationOptions,
|
||||||
|
aggregationOptions,
|
||||||
|
generateOptions,
|
||||||
|
isNewQueryV2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isDisabledSelect =
|
const isDisabledSelect =
|
||||||
|
|||||||
@ -39,7 +39,9 @@ function QuerySection(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilderV2
|
<QueryBuilderV2
|
||||||
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
|
isListViewPanel={
|
||||||
|
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||||
|
}
|
||||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
panelType={panelTypes}
|
panelType={panelTypes}
|
||||||
|
|||||||
@ -58,8 +58,6 @@ export const useHandleExplorerTabChange = (): {
|
|||||||
currentQueryData?: ICurrentQueryData,
|
currentQueryData?: ICurrentQueryData,
|
||||||
redirectToUrl?: typeof ROUTES[keyof typeof ROUTES],
|
redirectToUrl?: typeof ROUTES[keyof typeof ROUTES],
|
||||||
) => {
|
) => {
|
||||||
console.log('hook - type', type);
|
|
||||||
|
|
||||||
const newPanelType = type as PANEL_TYPES;
|
const newPanelType = type as PANEL_TYPES;
|
||||||
|
|
||||||
if (newPanelType === panelType && !currentQueryData) return;
|
if (newPanelType === panelType && !currentQueryData) return;
|
||||||
|
|||||||
@ -22,9 +22,40 @@ import { isEmpty } from 'lodash-es';
|
|||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { prepareQueryRangePayload } from './prepareQueryRangePayload';
|
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(
|
export async function GetMetricQueryRange(
|
||||||
props: GetQueryResultsProps,
|
props: GetQueryResultsProps,
|
||||||
version: string,
|
version: string,
|
||||||
@ -35,6 +66,32 @@ export async function GetMetricQueryRange(
|
|||||||
let legendMap: Record<string, string>;
|
let legendMap: Record<string, string>;
|
||||||
let response: SuccessResponse<MetricRangePayloadProps>;
|
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) {
|
if (version === ENTITY_VERSION_V5) {
|
||||||
const v5Result = prepareQueryRangePayloadV5(props);
|
const v5Result = prepareQueryRangePayloadV5(props);
|
||||||
legendMap = v5Result.legendMap;
|
legendMap = v5Result.legendMap;
|
||||||
|
|||||||
@ -258,17 +258,78 @@ const transformColumnTitles = (
|
|||||||
return item;
|
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 getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
|
||||||
const dynamicColumns: DynamicColumns = [];
|
const dynamicColumns: DynamicColumns = [];
|
||||||
|
|
||||||
queryTableData.forEach((currentQuery) => {
|
queryTableData.forEach((currentQuery) => {
|
||||||
const { series, queryName, list } = currentQuery;
|
const { series, queryName, list, table } = currentQuery;
|
||||||
|
|
||||||
const currentStagedQuery = getQueryByName(
|
const currentStagedQuery = getQueryByName(
|
||||||
query,
|
query,
|
||||||
queryName,
|
queryName,
|
||||||
isFormula(queryName) ? 'queryFormulas' : 'queryData',
|
isFormula(queryName) ? 'queryFormulas' : 'queryData',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (list) {
|
if (list) {
|
||||||
list.forEach((listItem) => {
|
list.forEach((listItem) => {
|
||||||
Object.keys(listItem.data).forEach((label) => {
|
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) {
|
if (series) {
|
||||||
const isValuesColumnExist = series.some((item) => item.values.length > 0);
|
processSeriesColumns(
|
||||||
const isEveryValuesExist = series.every((item) => item.values.length > 0);
|
series,
|
||||||
|
currentStagedQuery,
|
||||||
if (isValuesColumnExist) {
|
dynamicColumns,
|
||||||
addOperatorFormulaColumns(
|
query.queryType,
|
||||||
currentStagedQuery,
|
currentQuery,
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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 fillColumnsData: FillColumnData = (queryTableData, cols) => {
|
||||||
const fields = cols.filter((item) => item.type === 'field');
|
const fields = cols.filter((item) => item.type === 'field');
|
||||||
const operators = cols.filter((item) => item.type === 'operator');
|
const operators = cols.filter((item) => item.type === 'operator');
|
||||||
@ -497,6 +603,8 @@ const fillColumnsData: FillColumnData = (queryTableData, cols) => {
|
|||||||
fillDataFromList(listItem, resultColumns);
|
fillDataFromList(listItem, resultColumns);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillDataFromTable(currentQuery, resultColumns);
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowsLength = resultColumns.length > 0 ? resultColumns[0].data.length : 0;
|
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 QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||||
import LogsExplorerViewsContainer from 'container/LogsExplorerViews';
|
import LogsExplorerViewsContainer from 'container/LogsExplorerViews';
|
||||||
@ -20,6 +21,7 @@ import { OptionsQuery } from 'container/OptionsMenu/types';
|
|||||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||||
import Toolbar from 'container/Toolbar/Toolbar';
|
import Toolbar from 'container/Toolbar/Toolbar';
|
||||||
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
@ -27,14 +29,24 @@ import { isEqual, isNull } from 'lodash-es';
|
|||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import {
|
||||||
|
getExplorerViewForPanelType,
|
||||||
|
getExplorerViewFromUrl,
|
||||||
|
} from 'utils/explorerUtils';
|
||||||
|
|
||||||
import { ExplorerViews } from './utils';
|
import { ExplorerViews } from './utils';
|
||||||
|
|
||||||
function LogsExplorer(): JSX.Element {
|
function LogsExplorer(): JSX.Element {
|
||||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(
|
const [searchParams] = useSearchParams();
|
||||||
ExplorerViews.LIST,
|
|
||||||
|
// Get panel type from URL
|
||||||
|
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
|
|
||||||
|
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||||
|
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||||
);
|
);
|
||||||
const { preferences, loading: preferencesLoading } = usePreferenceContext();
|
const { preferences, loading: preferencesLoading } = usePreferenceContext();
|
||||||
|
|
||||||
@ -48,6 +60,23 @@ function LogsExplorer(): JSX.Element {
|
|||||||
return true;
|
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 { handleRunQuery, handleSetConfig } = useQueryBuilder();
|
||||||
|
|
||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
|||||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||||
import ExportPanel from 'container/ExportPanel';
|
import ExportPanel from 'container/ExportPanel';
|
||||||
@ -22,6 +23,7 @@ import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/config
|
|||||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
import QuerySection from 'container/TracesExplorer/QuerySection';
|
||||||
import TableView from 'container/TracesExplorer/TableView';
|
import TableView from 'container/TracesExplorer/TableView';
|
||||||
import TracesView from 'container/TracesExplorer/TracesView';
|
import TracesView from 'container/TracesExplorer/TracesView';
|
||||||
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
@ -30,10 +32,15 @@ import { cloneDeep, isEmpty, set } from 'lodash-es';
|
|||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
|
import {
|
||||||
|
getExplorerViewForPanelType,
|
||||||
|
getExplorerViewFromUrl,
|
||||||
|
} from 'utils/explorerUtils';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
function TracesExplorer(): JSX.Element {
|
function TracesExplorer(): JSX.Element {
|
||||||
@ -55,13 +62,36 @@ function TracesExplorer(): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
ExplorerViews.LIST,
|
|
||||||
|
// Get panel type from URL
|
||||||
|
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
|
|
||||||
|
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||||
|
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
const { safeNavigate } = useSafeNavigate();
|
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(
|
const handleChangeSelectedView = useCallback(
|
||||||
(view: ExplorerViews): void => {
|
(view: ExplorerViews): void => {
|
||||||
if (selectedView === ExplorerViews.LIST) {
|
if (selectedView === ExplorerViews.LIST) {
|
||||||
@ -98,26 +128,15 @@ function TracesExplorer(): JSX.Element {
|
|||||||
return groupByCount > 0;
|
return groupByCount > 0;
|
||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
const defaultQuery = useMemo(() => {
|
const defaultQuery = useMemo(
|
||||||
const query = updateAllQueriesOperators(
|
() =>
|
||||||
initialQueriesMap.traces,
|
updateAllQueriesOperators(
|
||||||
PANEL_TYPES.LIST,
|
initialQueriesMap.traces,
|
||||||
DataSource.TRACES,
|
PANEL_TYPES.LIST,
|
||||||
);
|
DataSource.TRACES,
|
||||||
|
),
|
||||||
return {
|
[updateAllQueriesOperators],
|
||||||
...query,
|
);
|
||||||
builder: {
|
|
||||||
...query.builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...query.builder.queryData[0],
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [updateAllQueriesOperators]);
|
|
||||||
|
|
||||||
const exportDefaultQuery = useMemo(
|
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