mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-23 18:36:16 +00:00
fixes: includes fixes required in the new QB (#8675)
* fix: removed unused code for querycontext (#8674) * Update frontend/src/utils/queryValidationUtils.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * feat: added tooltips in metric aggregations * feat: enabled legend enhancement for explorer pages and alert page * feat: updated the error state in explorer pages with new APIError * fix: cloned panel query shows previous query (#8681) * fix: cloned panel query shows previous query * chore: removed comments * chore: added null check * fix: added fix for auto run query in dashboard panel + trace view issue --------- Co-authored-by: Abhi Kumar <abhikumar@Mac.lan> * feat: added new SubstituteVars api and enable v5 for creating new alerts (#8683) * feat: added new SubstituteVars api and enable v5 for creating new alerts * feat: add warning notification for query response * feat: fixed failing test case * fix: metric histogram UI config state in edit mode * fix: fixed table columns getting duplicate data (#8685) * fix: added fix for conversion of QB function to filter expression. (#8684) * fix: added fix for QB filters for functions * chore: minor fix --------- Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> * feat: query builder fixes and enhancement (#8692) * feat: legend format fixes around single and multiple aggregation * feat: fixed table unit and metric units * feat: add fallbacks to columnWidth and columnUnits for old-dashboards * feat: fixed metric edit issue and having filter suggestion duplications * feat: fix and cleanup functions across product for v5 * chore: add tooltips with links to documentation (#8676) * fix: added fix for query validation and empty query error (#8694) * fix: added fix for selected columns being empty in logs explorer (#8709) * feat: added columnUnit changes for old dashboard migrations (#8706) * fix: fixed keyfetching logic (#8712) * chore: lint fix * fix: fixed logs explorer test * feat: fix type checks --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: SagarRajput-7 <sagar@signoz.io> Co-authored-by: Abhi Kumar <abhikumar@Mac.lan> Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
parent
01202b5800
commit
3a2eab2019
34
frontend/src/api/dashboard/substitute_vars.ts
Normal file
34
frontend/src/api/dashboard/substitute_vars.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ApiV5Instance } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { QueryRangePayloadV5 } from 'api/v5/v5';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||||
|
|
||||||
|
interface ISubstituteVars {
|
||||||
|
compositeQuery: ICompositeMetricQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSubstituteVars = async (
|
||||||
|
props?: Partial<QueryRangePayloadV5>,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponseV2<ISubstituteVars>> => {
|
||||||
|
try {
|
||||||
|
const response = await ApiV5Instance.post<{ data: ISubstituteVars }>(
|
||||||
|
'/substitute_vars',
|
||||||
|
props,
|
||||||
|
{
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -28,14 +28,18 @@ function getColName(
|
|||||||
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
|
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
|
||||||
const isSingleAggregation = aggregationsCount === 1;
|
const isSingleAggregation = aggregationsCount === 1;
|
||||||
|
|
||||||
|
if (aggregationsCount > 0) {
|
||||||
// Single aggregation: Priority is alias > legend > expression
|
// Single aggregation: Priority is alias > legend > expression
|
||||||
if (isSingleAggregation) {
|
if (isSingleAggregation) {
|
||||||
return alias || legend || expression;
|
return alias || legend || expression || col.queryName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple aggregations: Each follows single rules BUT never shows legend
|
// Multiple aggregations: Each follows single rules BUT never shows legend
|
||||||
// Priority: alias > expression (legend is ignored for multiple aggregations)
|
// Priority: alias > expression (legend is ignored for multiple aggregations)
|
||||||
return alias || expression;
|
return alias || expression || col.queryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return legend || col.queryName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColId(
|
function getColId(
|
||||||
@ -48,9 +52,16 @@ function getColId(
|
|||||||
const aggregation =
|
const aggregation =
|
||||||
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
|
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
|
||||||
const expression = aggregation?.expression || '';
|
const expression = aggregation?.expression || '';
|
||||||
|
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
|
||||||
|
const isMultipleAggregations = aggregationsCount > 1;
|
||||||
|
|
||||||
|
if (isMultipleAggregations && expression) {
|
||||||
return `${col.queryName}.${expression}`;
|
return `${col.queryName}.${expression}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return col.queryName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts V5 TimeSeriesData to legacy format
|
* Converts V5 TimeSeriesData to legacy format
|
||||||
*/
|
*/
|
||||||
@ -374,6 +385,7 @@ export function convertV5ResponseToLegacy(
|
|||||||
data: {
|
data: {
|
||||||
resultType: 'scalar',
|
resultType: 'scalar',
|
||||||
result: webTables,
|
result: webTables,
|
||||||
|
warnings: v5Data?.data?.warnings || [],
|
||||||
},
|
},
|
||||||
warning: v5Data?.warning || undefined,
|
warning: v5Data?.warning || undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,10 +5,7 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
|||||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import {
|
import {
|
||||||
BaseBuilderQuery,
|
BaseBuilderQuery,
|
||||||
FieldContext,
|
FieldContext,
|
||||||
@ -30,6 +27,7 @@ import {
|
|||||||
} from 'types/api/v5/queryRange';
|
} from 'types/api/v5/queryRange';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
|
||||||
|
|
||||||
type PrepareQueryRangePayloadV5Result = {
|
type PrepareQueryRangePayloadV5Result = {
|
||||||
queryPayload: QueryRangePayloadV5;
|
queryPayload: QueryRangePayloadV5;
|
||||||
@ -123,17 +121,21 @@ function createBaseSpec(
|
|||||||
functions: isEmpty(queryData.functions)
|
functions: isEmpty(queryData.functions)
|
||||||
? undefined
|
? undefined
|
||||||
: queryData.functions.map(
|
: queryData.functions.map(
|
||||||
(func: QueryFunctionProps): QueryFunction => ({
|
(func: QueryFunction): QueryFunction => {
|
||||||
name: func.name as FunctionName,
|
// Normalize function name to handle case sensitivity
|
||||||
|
const normalizedName = normalizeFunctionName(func?.name);
|
||||||
|
return {
|
||||||
|
name: normalizedName as FunctionName,
|
||||||
args: isEmpty(func.namedArgs)
|
args: isEmpty(func.namedArgs)
|
||||||
? func.args.map((arg) => ({
|
? func.args?.map((arg) => ({
|
||||||
value: arg,
|
value: arg?.value,
|
||||||
}))
|
}))
|
||||||
: Object.entries(func.namedArgs).map(([name, value]) => ({
|
: Object.entries(func?.namedArgs || {}).map(([name, value]) => ({
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
})),
|
})),
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
),
|
),
|
||||||
selectFields: isEmpty(nonEmptySelectColumns)
|
selectFields: isEmpty(nonEmptySelectColumns)
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export interface NavigateToExplorerProps {
|
|||||||
endTime?: number;
|
endTime?: number;
|
||||||
sameTab?: boolean;
|
sameTab?: boolean;
|
||||||
shouldResolveQuery?: boolean;
|
shouldResolveQuery?: boolean;
|
||||||
|
widgetQuery?: Query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNavigateToExplorer(): (
|
export function useNavigateToExplorer(): (
|
||||||
@ -30,11 +31,17 @@ export function useNavigateToExplorer(): (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const prepareQuery = useCallback(
|
const prepareQuery = useCallback(
|
||||||
(selectedFilters: TagFilterItem[], dataSource: DataSource): Query => ({
|
(
|
||||||
...currentQuery,
|
selectedFilters: TagFilterItem[],
|
||||||
|
dataSource: DataSource,
|
||||||
|
query?: Query,
|
||||||
|
): Query => {
|
||||||
|
const widgetQuery = query || currentQuery;
|
||||||
|
return {
|
||||||
|
...widgetQuery,
|
||||||
builder: {
|
builder: {
|
||||||
...currentQuery.builder,
|
...widgetQuery.builder,
|
||||||
queryData: currentQuery.builder.queryData
|
queryData: widgetQuery.builder.queryData
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
dataSource,
|
dataSource,
|
||||||
@ -50,7 +57,8 @@ export function useNavigateToExplorer(): (
|
|||||||
.slice(0, 1),
|
.slice(0, 1),
|
||||||
queryFormulas: [],
|
queryFormulas: [],
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
[currentQuery],
|
[currentQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -67,6 +75,7 @@ export function useNavigateToExplorer(): (
|
|||||||
endTime,
|
endTime,
|
||||||
sameTab,
|
sameTab,
|
||||||
shouldResolveQuery,
|
shouldResolveQuery,
|
||||||
|
widgetQuery,
|
||||||
} = props;
|
} = props;
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
if (startTime && endTime) {
|
if (startTime && endTime) {
|
||||||
@ -77,7 +86,7 @@ export function useNavigateToExplorer(): (
|
|||||||
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
|
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
let preparedQuery = prepareQuery(filters, dataSource);
|
let preparedQuery = prepareQuery(filters, dataSource, widgetQuery);
|
||||||
|
|
||||||
if (shouldResolveQuery) {
|
if (shouldResolveQuery) {
|
||||||
await getUpdatedQuery({
|
await getUpdatedQuery({
|
||||||
|
|||||||
33
frontend/src/components/Common/Common.styles.scss
Normal file
33
frontend/src/components/Common/Common.styles.scss
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.error-state-container {
|
||||||
|
height: 240px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
.error-state-container-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.error-state-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state-additional-messages {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.error-state-additional-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
frontend/src/components/Common/ErrorStateComponent.tsx
Normal file
59
frontend/src/components/Common/ErrorStateComponent.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import './Common.styles.scss';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
import APIError from '../../types/api/error';
|
||||||
|
|
||||||
|
interface ErrorStateComponentProps {
|
||||||
|
message?: string;
|
||||||
|
error?: APIError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps: Partial<ErrorStateComponentProps> = {
|
||||||
|
message: undefined,
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ErrorStateComponent({
|
||||||
|
message,
|
||||||
|
error,
|
||||||
|
}: ErrorStateComponentProps): JSX.Element {
|
||||||
|
// Handle API Error object
|
||||||
|
if (error) {
|
||||||
|
const mainMessage = error.getErrorMessage();
|
||||||
|
const additionalErrors = error.getErrorDetails().error.errors || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="error-state-container">
|
||||||
|
<div className="error-state-container-content">
|
||||||
|
<Typography className="error-state-text">{mainMessage}</Typography>
|
||||||
|
{additionalErrors.length > 0 && (
|
||||||
|
<div className="error-state-additional-messages">
|
||||||
|
{additionalErrors.map((additionalError) => (
|
||||||
|
<Typography
|
||||||
|
key={`error-${additionalError.message}`}
|
||||||
|
className="error-state-additional-text"
|
||||||
|
>
|
||||||
|
• {additionalError.message}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle simple string message (backwards compatibility)
|
||||||
|
return (
|
||||||
|
<div className="error-state-container">
|
||||||
|
<div className="error-state-container-content">
|
||||||
|
<Typography className="error-state-text">{message}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorStateComponent.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default ErrorStateComponent;
|
||||||
@ -1,4 +1,11 @@
|
|||||||
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
// Types for the context state
|
// Types for the context state
|
||||||
export type AggregationOption = { func: string; arg: string };
|
export type AggregationOption = { func: string; arg: string };
|
||||||
@ -6,8 +13,12 @@ export type AggregationOption = { func: string; arg: string };
|
|||||||
interface QueryBuilderV2ContextType {
|
interface QueryBuilderV2ContextType {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
setSearchText: (text: string) => void;
|
setSearchText: (text: string) => void;
|
||||||
aggregationOptions: AggregationOption[];
|
aggregationOptionsMap: Record<string, AggregationOption[]>;
|
||||||
setAggregationOptions: (options: AggregationOption[]) => void;
|
setAggregationOptions: (
|
||||||
|
queryName: string,
|
||||||
|
options: AggregationOption[],
|
||||||
|
) => void;
|
||||||
|
getAggregationOptions: (queryName: string) => AggregationOption[];
|
||||||
aggregationInterval: string;
|
aggregationInterval: string;
|
||||||
setAggregationInterval: (interval: string) => void;
|
setAggregationInterval: (interval: string) => void;
|
||||||
queryAddValues: any; // Replace 'any' with a more specific type if available
|
queryAddValues: any; // Replace 'any' with a more specific type if available
|
||||||
@ -24,26 +35,50 @@ export function QueryBuilderV2Provider({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [aggregationOptions, setAggregationOptions] = useState<
|
const [aggregationOptionsMap, setAggregationOptionsMap] = useState<
|
||||||
AggregationOption[]
|
Record<string, AggregationOption[]>
|
||||||
>([]);
|
>({});
|
||||||
const [aggregationInterval, setAggregationInterval] = useState('');
|
const [aggregationInterval, setAggregationInterval] = useState('');
|
||||||
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
|
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
|
||||||
|
|
||||||
|
const setAggregationOptions = useCallback(
|
||||||
|
(queryName: string, options: AggregationOption[]): void => {
|
||||||
|
setAggregationOptionsMap((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[queryName]: options,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAggregationOptions = useCallback(
|
||||||
|
(queryName: string): AggregationOption[] =>
|
||||||
|
aggregationOptionsMap[queryName] || [],
|
||||||
|
[aggregationOptionsMap],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilderV2Context.Provider
|
<QueryBuilderV2Context.Provider
|
||||||
value={useMemo(
|
value={useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
searchText,
|
searchText,
|
||||||
setSearchText,
|
setSearchText,
|
||||||
aggregationOptions,
|
aggregationOptionsMap,
|
||||||
setAggregationOptions,
|
setAggregationOptions,
|
||||||
|
getAggregationOptions,
|
||||||
aggregationInterval,
|
aggregationInterval,
|
||||||
setAggregationInterval,
|
setAggregationInterval,
|
||||||
queryAddValues,
|
queryAddValues,
|
||||||
setQueryAddValues,
|
setQueryAddValues,
|
||||||
}),
|
}),
|
||||||
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
|
[
|
||||||
|
searchText,
|
||||||
|
aggregationOptionsMap,
|
||||||
|
aggregationInterval,
|
||||||
|
queryAddValues,
|
||||||
|
getAggregationOptions,
|
||||||
|
setAggregationOptions,
|
||||||
|
],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
|||||||
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
|
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
|
||||||
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
|
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import { Info } from 'lucide-react';
|
|
||||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||||
@ -50,17 +49,17 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAggregationOptions([
|
setAggregationOptions(query.queryName, [
|
||||||
{
|
{
|
||||||
func: queryAggregation.spaceAggregation || 'count',
|
func: queryAggregation.spaceAggregation || 'count',
|
||||||
arg: queryAggregation.metricName || '',
|
arg: queryAggregation.metricName || '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
queryAggregation.spaceAggregation,
|
queryAggregation.spaceAggregation,
|
||||||
queryAggregation.metricName,
|
queryAggregation.metricName,
|
||||||
setAggregationOptions,
|
query.queryName,
|
||||||
query,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleChangeGroupByKeys = useCallback(
|
const handleChangeGroupByKeys = useCallback(
|
||||||
@ -100,12 +99,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
<div className="metrics-time-aggregation-section">
|
<div className="metrics-time-aggregation-section">
|
||||||
<div className="metrics-aggregation-section-content">
|
<div className="metrics-aggregation-section-content">
|
||||||
<div className="metrics-aggregation-section-content-item">
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
Learn more about temporal aggregation
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="metrics-aggregation-section-content-item-label main-label">
|
<div className="metrics-aggregation-section-content-item-label main-label">
|
||||||
AGGREGATE BY TIME{' '}
|
AGGREGATE WITHIN TIME SERIES{' '}
|
||||||
<Tooltip title="AGGREGATE BY TIME">
|
|
||||||
<Info size={12} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<div className="metrics-aggregation-section-content-item-value">
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
<OperatorsSelect
|
<OperatorsSelect
|
||||||
value={queryAggregation.timeAggregation || ''}
|
value={queryAggregation.timeAggregation || ''}
|
||||||
@ -118,9 +127,30 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
|
|
||||||
{showAggregationInterval && (
|
{showAggregationInterval && (
|
||||||
<div className="metrics-aggregation-section-content-item">
|
<div className="metrics-aggregation-section-content-item">
|
||||||
<div className="metrics-aggregation-section-content-item-label">
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
Set the time interval for aggregation
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
Learn about step intervals
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="metrics-aggregation-section-content-item-label"
|
||||||
|
style={{ cursor: 'help' }}
|
||||||
|
>
|
||||||
every
|
every
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<div className="metrics-aggregation-section-content-item-value">
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
@ -138,12 +168,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
<div className="metrics-space-aggregation-section">
|
<div className="metrics-space-aggregation-section">
|
||||||
<div className="metrics-aggregation-section-content">
|
<div className="metrics-aggregation-section-content">
|
||||||
<div className="metrics-aggregation-section-content-item">
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
Learn more about spatial aggregation
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="metrics-aggregation-section-content-item-label main-label">
|
<div className="metrics-aggregation-section-content-item-label main-label">
|
||||||
AGGREGATE LABELS
|
AGGREGATE ACROSS TIME SERIES
|
||||||
<Tooltip title="AGGREGATE LABELS">
|
|
||||||
<Info size={12} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<div className="metrics-aggregation-section-content-item-value">
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
<SpaceAggregationOptions
|
<SpaceAggregationOptions
|
||||||
panelType={panelType}
|
panelType={panelType}
|
||||||
@ -208,9 +248,30 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="metrics-aggregation-section-content-item">
|
<div className="metrics-aggregation-section-content-item">
|
||||||
<div className="metrics-aggregation-section-content-item-label">
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
Set the time interval for aggregation
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
Learn about step intervals
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="metrics-aggregation-section-content-item-label"
|
||||||
|
style={{ cursor: 'help' }}
|
||||||
|
>
|
||||||
every
|
every
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<div className="metrics-aggregation-section-content-item-value">
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
|
|||||||
@ -22,7 +22,11 @@ export const MetricsSelect = memo(function MetricsSelect({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-select-container">
|
<div className="metrics-select-container">
|
||||||
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
|
<AggregatorFilter
|
||||||
|
onChange={handleChangeAggregatorAttribute}
|
||||||
|
query={query}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -95,7 +95,8 @@ function HavingFilter({
|
|||||||
queryData: IBuilderQuery;
|
queryData: IBuilderQuery;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { aggregationOptions } = useQueryBuilderV2Context();
|
const { getAggregationOptions } = useQueryBuilderV2Context();
|
||||||
|
const aggregationOptions = getAggregationOptions(queryData.queryName);
|
||||||
const having = queryData?.having as Having;
|
const having = queryData?.having as Having;
|
||||||
const [input, setInput] = useState(having?.expression || '');
|
const [input, setInput] = useState(having?.expression || '');
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
/* eslint-disable react/require-default-props */
|
||||||
import './QueryAddOns.styles.scss';
|
import './QueryAddOns.styles.scss';
|
||||||
|
|
||||||
import { Button, Radio, RadioChangeEvent } from 'antd';
|
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
|
||||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
|
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
|
||||||
@ -9,7 +10,7 @@ import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/Re
|
|||||||
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 { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { BarChart2, ChevronUp, ScrollText } from 'lucide-react';
|
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||||
@ -21,6 +22,8 @@ interface AddOn {
|
|||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
description?: string;
|
||||||
|
docLink?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADD_ONS_KEYS = {
|
const ADD_ONS_KEYS = {
|
||||||
@ -36,26 +39,45 @@ const ADD_ONS = [
|
|||||||
icon: <BarChart2 size={14} />,
|
icon: <BarChart2 size={14} />,
|
||||||
label: 'Group By',
|
label: 'Group By',
|
||||||
key: 'group_by',
|
key: 'group_by',
|
||||||
|
description:
|
||||||
|
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
|
||||||
|
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ScrollText size={14} />,
|
icon: <ScrollText size={14} />,
|
||||||
label: 'Having',
|
label: 'Having',
|
||||||
key: 'having',
|
key: 'having',
|
||||||
|
description:
|
||||||
|
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
|
||||||
|
docLink:
|
||||||
|
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ScrollText size={14} />,
|
icon: <ScrollText size={14} />,
|
||||||
label: 'Order By',
|
label: 'Order By',
|
||||||
key: 'order_by',
|
key: 'order_by',
|
||||||
|
description:
|
||||||
|
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
|
||||||
|
docLink:
|
||||||
|
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ScrollText size={14} />,
|
icon: <ScrollText size={14} />,
|
||||||
label: 'Limit',
|
label: 'Limit',
|
||||||
key: 'limit',
|
key: 'limit',
|
||||||
|
description:
|
||||||
|
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
|
||||||
|
docLink:
|
||||||
|
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ScrollText size={14} />,
|
icon: <ScrollText size={14} />,
|
||||||
label: 'Legend format',
|
label: 'Legend format',
|
||||||
key: 'legend_format',
|
key: 'legend_format',
|
||||||
|
description:
|
||||||
|
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
|
||||||
|
docLink:
|
||||||
|
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -63,8 +85,58 @@ const REDUCE_TO = {
|
|||||||
icon: <ScrollText size={14} />,
|
icon: <ScrollText size={14} />,
|
||||||
label: 'Reduce to',
|
label: 'Reduce to',
|
||||||
key: 'reduce_to',
|
key: 'reduce_to',
|
||||||
|
description:
|
||||||
|
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
|
||||||
|
docLink:
|
||||||
|
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Custom tooltip content component
|
||||||
|
function TooltipContent({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
docLink,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
docLink?: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px',
|
||||||
|
maxWidth: '300px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong style={{ fontSize: '14px' }}>{label}</strong>
|
||||||
|
{description && (
|
||||||
|
<span style={{ fontSize: '12px', lineHeight: '1.5' }}>{description}</span>
|
||||||
|
)}
|
||||||
|
{docLink && (
|
||||||
|
<a
|
||||||
|
href={docLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e): void => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
color: '#4096ff',
|
||||||
|
fontSize: '12px',
|
||||||
|
marginTop: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<ExternalLink size={12} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function QueryAddOns({
|
function QueryAddOns({
|
||||||
query,
|
query,
|
||||||
version,
|
version,
|
||||||
@ -212,7 +284,21 @@ function QueryAddOns({
|
|||||||
{selectedViews.find((view) => view.key === 'group_by') && (
|
{selectedViews.find((view) => view.key === 'group_by') && (
|
||||||
<div className="add-on-content">
|
<div className="add-on-content">
|
||||||
<div className="periscope-input-with-label">
|
<div className="periscope-input-with-label">
|
||||||
<div className="label">Group By</div>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<TooltipContent
|
||||||
|
label="Group By"
|
||||||
|
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
|
||||||
|
docLink="https://signoz.io/docs/userguide/query-builder-v5/#grouping"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
>
|
||||||
|
<div className="label" style={{ cursor: 'help' }}>
|
||||||
|
Group By
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<GroupByFilter
|
<GroupByFilter
|
||||||
disabled={
|
disabled={
|
||||||
@ -234,7 +320,21 @@ function QueryAddOns({
|
|||||||
{selectedViews.find((view) => view.key === 'having') && (
|
{selectedViews.find((view) => view.key === 'having') && (
|
||||||
<div className="add-on-content">
|
<div className="add-on-content">
|
||||||
<div className="periscope-input-with-label">
|
<div className="periscope-input-with-label">
|
||||||
<div className="label">Having</div>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<TooltipContent
|
||||||
|
label="Having"
|
||||||
|
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
|
||||||
|
docLink="https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
>
|
||||||
|
<div className="label" style={{ cursor: 'help' }}>
|
||||||
|
Having
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<HavingFilter
|
<HavingFilter
|
||||||
onClose={(): void => {
|
onClose={(): void => {
|
||||||
@ -266,7 +366,21 @@ function QueryAddOns({
|
|||||||
{selectedViews.find((view) => view.key === 'order_by') && (
|
{selectedViews.find((view) => view.key === 'order_by') && (
|
||||||
<div className="add-on-content">
|
<div className="add-on-content">
|
||||||
<div className="periscope-input-with-label">
|
<div className="periscope-input-with-label">
|
||||||
<div className="label">Order By</div>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<TooltipContent
|
||||||
|
label="Order By"
|
||||||
|
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
|
||||||
|
docLink="https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
>
|
||||||
|
<div className="label" style={{ cursor: 'help' }}>
|
||||||
|
Order By
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<OrderByFilter
|
<OrderByFilter
|
||||||
entityVersion={version}
|
entityVersion={version}
|
||||||
@ -290,7 +404,21 @@ function QueryAddOns({
|
|||||||
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
||||||
<div className="add-on-content">
|
<div className="add-on-content">
|
||||||
<div className="periscope-input-with-label">
|
<div className="periscope-input-with-label">
|
||||||
<div className="label">Reduce to</div>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<TooltipContent
|
||||||
|
label="Reduce to"
|
||||||
|
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
|
||||||
|
docLink="https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
>
|
||||||
|
<div className="label" style={{ cursor: 'help' }}>
|
||||||
|
Reduce to
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
|
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
|
||||||
</div>
|
</div>
|
||||||
@ -330,8 +458,19 @@ function QueryAddOns({
|
|||||||
value={selectedViews}
|
value={selectedViews}
|
||||||
>
|
>
|
||||||
{addOns.map((addOn) => (
|
{addOns.map((addOn) => (
|
||||||
|
<Tooltip
|
||||||
|
key={addOn.key}
|
||||||
|
title={
|
||||||
|
<TooltipContent
|
||||||
|
label={addOn.label}
|
||||||
|
description={addOn.description}
|
||||||
|
docLink={addOn.docLink}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
>
|
||||||
<Radio.Button
|
<Radio.Button
|
||||||
key={addOn.label}
|
|
||||||
className={
|
className={
|
||||||
selectedViews.find((view) => view.key === addOn.key)
|
selectedViews.find((view) => view.key === addOn.key)
|
||||||
? 'selected-view tab'
|
? 'selected-view tab'
|
||||||
@ -344,6 +483,7 @@ function QueryAddOns({
|
|||||||
{addOn.label}
|
{addOn.label}
|
||||||
</div>
|
</div>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import './QueryAggregation.styles.scss';
|
import './QueryAggregation.styles.scss';
|
||||||
|
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@ -53,7 +54,31 @@ function QueryAggregationOptions({
|
|||||||
|
|
||||||
{showAggregationInterval && (
|
{showAggregationInterval && (
|
||||||
<div className="query-aggregation-interval">
|
<div className="query-aggregation-interval">
|
||||||
<div className="query-aggregation-interval-label">every</div>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
Set the time interval for aggregation
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
Learn about step intervals
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="metrics-aggregation-section-content-item-label"
|
||||||
|
style={{ cursor: 'help' }}
|
||||||
|
>
|
||||||
|
every
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<div className="query-aggregation-interval-input-container">
|
<div className="query-aggregation-interval-input-container">
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
initialValue={
|
initialValue={
|
||||||
|
|||||||
@ -27,13 +27,13 @@ import CodeMirror, {
|
|||||||
ViewPlugin,
|
ViewPlugin,
|
||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
} from '@uiw/react-codemirror';
|
} from '@uiw/react-codemirror';
|
||||||
import { Button, Popover } from 'antd';
|
import { Button, Popover, Tooltip } from 'antd';
|
||||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||||
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
|
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
|
||||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { TriangleAlert } from 'lucide-react';
|
import { Info, TriangleAlert } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@ -263,7 +263,7 @@ function QueryAggregationSelect({
|
|||||||
|
|
||||||
setValidationError(validateAggregations());
|
setValidationError(validateAggregations());
|
||||||
setFunctionArgPairs(pairs);
|
setFunctionArgPairs(pairs);
|
||||||
setAggregationOptions(pairs);
|
setAggregationOptions(queryData.queryName, pairs);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [input, maxAggregations, validFunctions]);
|
}, [input, maxAggregations, validFunctions]);
|
||||||
|
|
||||||
@ -639,6 +639,50 @@ function QueryAggregationSelect({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
Aggregation functions:
|
||||||
|
<br />
|
||||||
|
<span style={{ fontSize: '12px', lineHeight: '1.4' }}>
|
||||||
|
• <strong>count</strong> - number of occurrences
|
||||||
|
<br />• <strong>sum/avg</strong> - sum/average of values
|
||||||
|
<br />• <strong>min/max</strong> - minimum/maximum value
|
||||||
|
<br />• <strong>p50/p90/p99</strong> - percentiles
|
||||||
|
<br />• <strong>count_distinct</strong> - unique values
|
||||||
|
<br />• <strong>rate</strong> - per-interval rate
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/query-builder-v5/#core-aggregation-functions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
View documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '8px', // Match the error icon's top position
|
||||||
|
right: validationError ? '40px' : '8px', // Move left when error icon is shown
|
||||||
|
cursor: 'help',
|
||||||
|
zIndex: 10,
|
||||||
|
transition: 'right 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Info
|
||||||
|
size={14}
|
||||||
|
style={{ opacity: 0.9, color: isDarkMode ? '#ffffff' : '#000000' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{validationError && (
|
{validationError && (
|
||||||
<div className="query-aggregation-error-container">
|
<div className="query-aggregation-error-container">
|
||||||
<Popover
|
<Popover
|
||||||
|
|||||||
@ -12,22 +12,7 @@ export default function QueryFooter({
|
|||||||
<div className="qb-footer">
|
<div className="qb-footer">
|
||||||
<div className="qb-footer-container">
|
<div className="qb-footer-container">
|
||||||
<div className="qb-add-new-query">
|
<div className="qb-add-new-query">
|
||||||
<Tooltip
|
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
|
||||||
title={
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
Add New Query
|
|
||||||
<Typography.Link
|
|
||||||
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
|
|
||||||
target="_blank"
|
|
||||||
style={{ textDecoration: 'underline' }}
|
|
||||||
>
|
|
||||||
{' '}
|
|
||||||
<br />
|
|
||||||
Learn more
|
|
||||||
</Typography.Link>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
className="add-new-query-button periscope-btn secondary"
|
className="add-new-query-button periscope-btn secondary"
|
||||||
type="text"
|
type="text"
|
||||||
@ -43,7 +28,7 @@ export default function QueryFooter({
|
|||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
Add New Formula
|
Add New Formula
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
|
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style={{ textDecoration: 'underline' }}
|
style={{ textDecoration: 'underline' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -16,12 +16,13 @@ import { Color } from '@signozhq/design-tokens';
|
|||||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
||||||
import { Button, Card, Collapse, Popover, Tag } from 'antd';
|
import { Button, Card, Collapse, Popover, Tag, Tooltip } from 'antd';
|
||||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import {
|
import {
|
||||||
negationQueryOperatorSuggestions,
|
negationQueryOperatorSuggestions,
|
||||||
|
OPERATORS,
|
||||||
QUERY_BUILDER_KEY_TYPES,
|
QUERY_BUILDER_KEY_TYPES,
|
||||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||||
queryOperatorSuggestions,
|
queryOperatorSuggestions,
|
||||||
@ -30,7 +31,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { debounce, isNull } from 'lodash-es';
|
import { debounce, isNull } from 'lodash-es';
|
||||||
import { TriangleAlert } from 'lucide-react';
|
import { Info, TriangleAlert } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IDetailedError,
|
IDetailedError,
|
||||||
@ -40,11 +41,11 @@ import {
|
|||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { validateQuery } from 'utils/antlrQueryUtils';
|
|
||||||
import {
|
import {
|
||||||
getCurrentValueIndexAtCursor,
|
getCurrentValueIndexAtCursor,
|
||||||
getQueryContextAtCursor,
|
getQueryContextAtCursor,
|
||||||
} from 'utils/queryContextUtils';
|
} from 'utils/queryContextUtils';
|
||||||
|
import { validateQuery } from 'utils/queryValidationUtils';
|
||||||
import { unquote } from 'utils/stringUtils';
|
import { unquote } from 'utils/stringUtils';
|
||||||
|
|
||||||
import { queryExamples } from './constants';
|
import { queryExamples } from './constants';
|
||||||
@ -88,10 +89,7 @@ function QuerySearch({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
|
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
|
||||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([
|
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||||
{ label: 'error', type: 'value' },
|
|
||||||
{ label: 'frontend', type: 'value' },
|
|
||||||
]);
|
|
||||||
const [activeKey, setActiveKey] = useState<string>('');
|
const [activeKey, setActiveKey] = useState<string>('');
|
||||||
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
||||||
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
|
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
|
||||||
@ -114,9 +112,27 @@ function QuerySearch({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track if the query was changed externally (from queryData) vs internally (user input)
|
||||||
|
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||||
|
const [lastExternalQuery, setLastExternalQuery] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuery(queryData.filter?.expression || '');
|
const newQuery = queryData.filter?.expression || '';
|
||||||
}, [queryData.filter?.expression]);
|
// Only mark as external change if the query actually changed from external source
|
||||||
|
if (newQuery !== lastExternalQuery) {
|
||||||
|
setQuery(newQuery);
|
||||||
|
setIsExternalQueryChange(true);
|
||||||
|
setLastExternalQuery(newQuery);
|
||||||
|
}
|
||||||
|
}, [queryData.filter?.expression, lastExternalQuery]);
|
||||||
|
|
||||||
|
// Validate query when it changes externally (from queryData)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isExternalQueryChange && query) {
|
||||||
|
handleQueryValidation(query);
|
||||||
|
setIsExternalQueryChange(false);
|
||||||
|
}
|
||||||
|
}, [isExternalQueryChange, query]);
|
||||||
|
|
||||||
const [keySuggestions, setKeySuggestions] = useState<
|
const [keySuggestions, setKeySuggestions] = useState<
|
||||||
QueryKeyDataSuggestionsProps[] | null
|
QueryKeyDataSuggestionsProps[] | null
|
||||||
@ -127,7 +143,6 @@ function QuerySearch({
|
|||||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const [isCompleteKeysList, setIsCompleteKeysList] = useState(false);
|
|
||||||
const [
|
const [
|
||||||
isFetchingCompleteValuesList,
|
isFetchingCompleteValuesList,
|
||||||
setIsFetchingCompleteValuesList,
|
setIsFetchingCompleteValuesList,
|
||||||
@ -170,8 +185,28 @@ function QuerySearch({
|
|||||||
500,
|
500,
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchKeySuggestions = async (searchText?: string): Promise<void> => {
|
const toggleSuggestions = useCallback(
|
||||||
if (dataSource === DataSource.METRICS && !queryData.aggregateAttribute?.key) {
|
(timeout?: number) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
if (isFocused) {
|
||||||
|
startCompletion(editorRef.current);
|
||||||
|
} else {
|
||||||
|
closeCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
return (): void => clearTimeout(timeoutId);
|
||||||
|
},
|
||||||
|
[isFocused],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchKeySuggestions = useCallback(
|
||||||
|
async (searchText?: string): Promise<void> => {
|
||||||
|
if (
|
||||||
|
dataSource === DataSource.METRICS &&
|
||||||
|
!queryData.aggregateAttribute?.key
|
||||||
|
) {
|
||||||
setKeySuggestions([]);
|
setKeySuggestions([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,7 +217,7 @@ function QuerySearch({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.data) {
|
if (response.data.data) {
|
||||||
const { complete, keys } = response.data.data;
|
const { keys } = response.data.data;
|
||||||
const options = generateOptions(keys);
|
const options = generateOptions(keys);
|
||||||
// Use a Map to deduplicate by label and preserve order: new options take precedence
|
// Use a Map to deduplicate by label and preserve order: new options take precedence
|
||||||
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
|
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
|
||||||
@ -193,13 +228,30 @@ function QuerySearch({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setKeySuggestions(Array.from(merged.values()));
|
setKeySuggestions(Array.from(merged.values()));
|
||||||
setIsCompleteKeysList(complete);
|
|
||||||
|
// Force reopen the completion if editor is available and focused
|
||||||
|
if (editorRef.current) {
|
||||||
|
toggleSuggestions(10);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
dataSource,
|
||||||
|
debouncedMetricName,
|
||||||
|
keySuggestions,
|
||||||
|
toggleSuggestions,
|
||||||
|
queryData.aggregateAttribute?.key,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedFetchKeySuggestions = useMemo(
|
||||||
|
() => debounce(fetchKeySuggestions, 300),
|
||||||
|
[fetchKeySuggestions],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKeySuggestions([]);
|
setKeySuggestions([]);
|
||||||
fetchKeySuggestions();
|
debouncedFetchKeySuggestions();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [dataSource, debouncedMetricName]);
|
}, [dataSource, debouncedMetricName]);
|
||||||
|
|
||||||
@ -310,6 +362,11 @@ function QuerySearch({
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Force reopen the completion if editor is available and focused
|
||||||
|
if (editorRef.current) {
|
||||||
|
toggleSuggestions(10);
|
||||||
|
}
|
||||||
|
|
||||||
const sanitizedSearchText = searchText ? searchText?.trim() : '';
|
const sanitizedSearchText = searchText ? searchText?.trim() : '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -382,13 +439,9 @@ function QuerySearch({
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force reopen the completion if editor is available
|
// Force reopen the completion if editor is available and focused
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
setTimeout(() => {
|
toggleSuggestions(10);
|
||||||
if (isMountedRef.current && editorRef.current) {
|
|
||||||
startCompletion(editorRef.current);
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -408,7 +461,8 @@ function QuerySearch({
|
|||||||
setIsFetchingCompleteValuesList(false);
|
setIsFetchingCompleteValuesList(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeKey, dataSource, isLoadingSuggestions],
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[activeKey, dataSource, isFocused],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedFetchValueSuggestions = useMemo(
|
const debouncedFetchValueSuggestions = useMemo(
|
||||||
@ -468,14 +522,13 @@ function QuerySearch({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleQueryChange = useCallback(async (newQuery: string) => {
|
|
||||||
setQuery(newQuery);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleChange = (value: string): void => {
|
const handleChange = (value: string): void => {
|
||||||
setQuery(value);
|
setQuery(value);
|
||||||
handleQueryChange(value);
|
|
||||||
onChange(value);
|
onChange(value);
|
||||||
|
// Mark as internal change to avoid triggering external validation
|
||||||
|
setIsExternalQueryChange(false);
|
||||||
|
// Update lastExternalQuery to prevent external validation trigger
|
||||||
|
setLastExternalQuery(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
const handleBlur = (): void => {
|
||||||
@ -483,24 +536,27 @@ function QuerySearch({
|
|||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
if (query) {
|
() => (): void => {
|
||||||
handleQueryValidation(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
if (debouncedFetchValueSuggestions) {
|
if (debouncedFetchValueSuggestions) {
|
||||||
debouncedFetchValueSuggestions.cancel();
|
debouncedFetchValueSuggestions.cancel();
|
||||||
}
|
}
|
||||||
};
|
if (debouncedFetchKeySuggestions) {
|
||||||
|
debouncedFetchKeySuggestions.cancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const handleExampleClick = (exampleQuery: string): void => {
|
const handleExampleClick = (exampleQuery: string): void => {
|
||||||
// If there's an existing query, append the example with AND
|
// If there's an existing query, append the example with AND
|
||||||
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
|
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
|
||||||
setQuery(newQuery);
|
setQuery(newQuery);
|
||||||
handleQueryChange(newQuery);
|
// Mark as internal change to avoid triggering external validation
|
||||||
|
setIsExternalQueryChange(false);
|
||||||
|
// Update lastExternalQuery to prevent external validation trigger
|
||||||
|
setLastExternalQuery(newQuery);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to render a badge for the current context mode
|
// Helper function to render a badge for the current context mode
|
||||||
@ -743,16 +799,14 @@ function QuerySearch({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (queryContext.isInKey) {
|
if (queryContext.isInKey) {
|
||||||
const searchText = word?.text.toLowerCase() ?? '';
|
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||||
|
|
||||||
options = (keySuggestions || []).filter((option) =>
|
options = (keySuggestions || []).filter((option) =>
|
||||||
option.label.toLowerCase().includes(searchText),
|
option.label.toLowerCase().includes(searchText),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isCompleteKeysList && options.length === 0) {
|
if (options.length === 0 && lastKeyRef.current !== searchText) {
|
||||||
setTimeout(() => {
|
debouncedFetchKeySuggestions(searchText);
|
||||||
fetchKeySuggestions(searchText);
|
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have previous pairs, we can prioritize keys that haven't been used yet
|
// If we have previous pairs, we can prioritize keys that haven't been used yet
|
||||||
@ -827,12 +881,32 @@ function QuerySearch({
|
|||||||
QUERY_BUILDER_KEY_TYPES.STRING
|
QUERY_BUILDER_KEY_TYPES.STRING
|
||||||
].includes(op.label),
|
].includes(op.label),
|
||||||
)
|
)
|
||||||
.map((op) => ({
|
.map((op) => {
|
||||||
|
if (op.label === OPERATORS['=']) {
|
||||||
|
return {
|
||||||
...op,
|
...op,
|
||||||
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
|
boost: 200,
|
||||||
? 100
|
};
|
||||||
: 0,
|
}
|
||||||
}));
|
if (
|
||||||
|
[
|
||||||
|
OPERATORS['!='],
|
||||||
|
OPERATORS.LIKE,
|
||||||
|
OPERATORS.ILIKE,
|
||||||
|
OPERATORS.CONTAINS,
|
||||||
|
OPERATORS.IN,
|
||||||
|
].includes(op.label)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...op,
|
||||||
|
boost: 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...op,
|
||||||
|
boost: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
} else if (keyType === QUERY_BUILDER_KEY_TYPES.BOOLEAN) {
|
} else if (keyType === QUERY_BUILDER_KEY_TYPES.BOOLEAN) {
|
||||||
// Prioritize boolean operators
|
// Prioritize boolean operators
|
||||||
options = options
|
options = options
|
||||||
@ -841,10 +915,24 @@ function QuerySearch({
|
|||||||
QUERY_BUILDER_KEY_TYPES.BOOLEAN
|
QUERY_BUILDER_KEY_TYPES.BOOLEAN
|
||||||
].includes(op.label),
|
].includes(op.label),
|
||||||
)
|
)
|
||||||
.map((op) => ({
|
.map((op) => {
|
||||||
|
if (op.label === OPERATORS['=']) {
|
||||||
|
return {
|
||||||
...op,
|
...op,
|
||||||
boost: ['=', '!='].includes(op.label) ? 100 : 0,
|
boost: 200,
|
||||||
}));
|
};
|
||||||
|
}
|
||||||
|
if (op.label === OPERATORS['!=']) {
|
||||||
|
return {
|
||||||
|
...op,
|
||||||
|
boost: 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...op,
|
||||||
|
boost: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1034,26 +1122,15 @@ function QuerySearch({
|
|||||||
|
|
||||||
// Effect to handle focus state and trigger suggestions
|
// Effect to handle focus state and trigger suggestions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current) {
|
const clearTimeout = toggleSuggestions(10);
|
||||||
if (!isFocused) {
|
return (): void => clearTimeout();
|
||||||
closeCompletion(editorRef.current);
|
}, [isFocused, toggleSuggestions]);
|
||||||
} else {
|
|
||||||
startCompletion(editorRef.current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isFocused]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!queryContext) return;
|
if (!queryContext) return;
|
||||||
|
|
||||||
// Trigger suggestions based on context
|
// Trigger suggestions based on context
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
// Small delay to ensure the context is fully updated
|
toggleSuggestions(10);
|
||||||
setTimeout(() => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
startCompletion(editorRef.current);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle value suggestions for value context
|
// Handle value suggestions for value context
|
||||||
@ -1066,7 +1143,28 @@ function QuerySearch({
|
|||||||
fetchValueSuggestions({ key });
|
fetchValueSuggestions({ key });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [queryContext, activeKey, isLoadingSuggestions, fetchValueSuggestions]);
|
}, [
|
||||||
|
queryContext,
|
||||||
|
toggleSuggestions,
|
||||||
|
isLoadingSuggestions,
|
||||||
|
activeKey,
|
||||||
|
fetchValueSuggestions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getTooltipContent = (): JSX.Element => (
|
||||||
|
<div>
|
||||||
|
Need help with search syntax?
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/search-syntax/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
View documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="code-mirror-where-clause">
|
<div className="code-mirror-where-clause">
|
||||||
@ -1109,6 +1207,31 @@ function QuerySearch({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="query-where-clause-editor-container">
|
<div className="query-where-clause-editor-container">
|
||||||
|
<Tooltip title={getTooltipContent()} placement="left">
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/search-syntax/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: validation.isValid === false && query ? 40 : 8, // Move left when error shown
|
||||||
|
cursor: 'help',
|
||||||
|
zIndex: 10,
|
||||||
|
transition: 'right 0.2s ease',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
}}
|
||||||
|
onClick={(e): void => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Info
|
||||||
|
size={14}
|
||||||
|
style={{ opacity: 0.9, color: isDarkMode ? '#ffffff' : '#000000' }}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={query}
|
value={query}
|
||||||
theme={isDarkMode ? copilot : githubLight}
|
theme={isDarkMode ? copilot : githubLight}
|
||||||
@ -1167,7 +1290,7 @@ function QuerySearch({
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
]}
|
]}
|
||||||
placeholder="Enter your filter query (e.g., status = 'error' AND service = 'frontend')"
|
placeholder="Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')"
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: false,
|
lineNumbers: false,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { EQueryType } from 'types/common/dashboard';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||||
import { unquote } from 'utils/stringUtils';
|
import { unquote } from 'utils/stringUtils';
|
||||||
|
import { isFunctionOperator } from 'utils/tokenUtils';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,6 +87,10 @@ export const convertFiltersToExpression = (
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFunctionOperator(op)) {
|
||||||
|
return `${op}(${key.key}, ${value})`;
|
||||||
|
}
|
||||||
|
|
||||||
const formattedValue = formatValueForExpression(value, op);
|
const formattedValue = formatValueForExpression(value, op);
|
||||||
return `${key.key} ${op} ${formattedValue}`;
|
return `${key.key} ${op} ${formattedValue}`;
|
||||||
})
|
})
|
||||||
@ -539,45 +544,18 @@ export const convertAggregationToExpression = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQueryTitles = (currentQuery: Query): string[] => {
|
|
||||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
|
||||||
const queryTitles: string[] = [];
|
|
||||||
|
|
||||||
// Handle builder queries with multiple aggregations
|
|
||||||
currentQuery.builder.queryData.forEach((q) => {
|
|
||||||
const aggregationCount = q.aggregations?.length || 1;
|
|
||||||
|
|
||||||
if (aggregationCount > 1) {
|
|
||||||
// If multiple aggregations, create titles like A.0, A.1, A.2
|
|
||||||
for (let i = 0; i < aggregationCount; i++) {
|
|
||||||
queryTitles.push(`${q.queryName}.${i}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Single aggregation, just use query name
|
|
||||||
queryTitles.push(q.queryName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle formulas (they don't have aggregations, so just use query name)
|
|
||||||
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
|
|
||||||
|
|
||||||
return [...queryTitles, ...formulas];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
|
|
||||||
return currentQuery.clickhouse_sql.map((q) => q.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentQuery.promql.map((q) => q.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getColId(
|
function getColId(
|
||||||
queryName: string,
|
queryName: string,
|
||||||
aggregation: { alias?: string; expression?: string },
|
aggregation: { alias?: string; expression?: string },
|
||||||
|
isMultipleAggregations: boolean,
|
||||||
): string {
|
): string {
|
||||||
|
if (isMultipleAggregations && aggregation.expression) {
|
||||||
return `${queryName}.${aggregation.expression}`;
|
return `${queryName}.${aggregation.expression}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return queryName;
|
||||||
|
}
|
||||||
|
|
||||||
// function to give you label value for query name taking multiaggregation into account
|
// function to give you label value for query name taking multiaggregation into account
|
||||||
export function getQueryLabelWithAggregation(
|
export function getQueryLabelWithAggregation(
|
||||||
queryData: IBuilderQuery[],
|
queryData: IBuilderQuery[],
|
||||||
@ -599,7 +577,7 @@ export function getQueryLabelWithAggregation(
|
|||||||
const isMultipleAggregations = aggregations.length > 1;
|
const isMultipleAggregations = aggregations.length > 1;
|
||||||
|
|
||||||
aggregations.forEach((agg: any, index: number) => {
|
aggregations.forEach((agg: any, index: number) => {
|
||||||
const columnId = getColId(queryName, agg);
|
const columnId = getColId(queryName, agg, isMultipleAggregations);
|
||||||
|
|
||||||
// For display purposes, show the aggregation index for multiple aggregations
|
// For display purposes, show the aggregation index for multiple aggregations
|
||||||
const displayLabel = isMultipleAggregations
|
const displayLabel = isMultipleAggregations
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Table } from 'antd';
|
|||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { getColumnWidth, RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { debounce, set } from 'lodash-es';
|
import { debounce, set } from 'lodash-es';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
@ -110,12 +110,15 @@ function ResizeTable({
|
|||||||
// Apply stored column widths from widget configuration
|
// Apply stored column widths from widget configuration
|
||||||
const columnsWithStoredWidths = columns.map((col) => {
|
const columnsWithStoredWidths = columns.map((col) => {
|
||||||
const dataIndex = (col as RowData).dataIndex as string;
|
const dataIndex = (col as RowData).dataIndex as string;
|
||||||
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
|
if (dataIndex && columnWidths) {
|
||||||
|
const width = getColumnWidth(dataIndex, columnWidths);
|
||||||
|
if (width) {
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
width: columnWidths[dataIndex], // Apply stored width
|
width, // Apply stored width
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return col;
|
return col;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,12 @@ export const OPERATORS = {
|
|||||||
'<': '<',
|
'<': '<',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const QUERY_BUILDER_FUNCTIONS = {
|
||||||
|
HAS: 'has',
|
||||||
|
HASANY: 'hasAny',
|
||||||
|
HASALL: 'hasAll',
|
||||||
|
};
|
||||||
|
|
||||||
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
|
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -76,3 +82,15 @@ export const queryOperatorSuggestions = [
|
|||||||
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
|
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
|
||||||
...negationQueryOperatorSuggestions,
|
...negationQueryOperatorSuggestions,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function negateOperator(operatorOrFunction: string): string {
|
||||||
|
// Special cases for equals/not equals
|
||||||
|
if (operatorOrFunction === OPERATORS['=']) {
|
||||||
|
return OPERATORS['!='];
|
||||||
|
}
|
||||||
|
if (operatorOrFunction === OPERATORS['!=']) {
|
||||||
|
return OPERATORS['='];
|
||||||
|
}
|
||||||
|
// For all other operators and functions, add NOT in front
|
||||||
|
return `${OPERATORS.NOT} ${operatorOrFunction}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import {
|
import {
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
initialQueryPromQLData,
|
initialQueryPromQLData,
|
||||||
@ -28,7 +28,7 @@ const defaultAnnotations = {
|
|||||||
|
|
||||||
export const alertDefaults: AlertDef = {
|
export const alertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@ -62,7 +62,7 @@ export const alertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const anamolyAlertDefaults: AlertDef = {
|
export const anamolyAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
@ -107,7 +107,7 @@ export const anamolyAlertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const logAlertDefaults: AlertDef = {
|
export const logAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.LOGS_BASED_ALERT,
|
alertType: AlertTypes.LOGS_BASED_ALERT,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@ -139,7 +139,7 @@ export const logAlertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const traceAlertDefaults: AlertDef = {
|
export const traceAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.TRACES_BASED_ALERT,
|
alertType: AlertTypes.TRACES_BASED_ALERT,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@ -171,7 +171,7 @@ export const traceAlertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const exceptionAlertDefaults: AlertDef = {
|
export const exceptionAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Form, Row } from 'antd';
|
import { Form, Row } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
@ -71,14 +71,14 @@ function CreateRules(): JSX.Element {
|
|||||||
case AlertTypes.ANOMALY_BASED_ALERT:
|
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||||
setInitValues({
|
setInitValues({
|
||||||
...anamolyAlertDefaults,
|
...anamolyAlertDefaults,
|
||||||
version: version || ENTITY_VERSION_V4,
|
version: version || ENTITY_VERSION_V5,
|
||||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setInitValues({
|
setInitValues({
|
||||||
...alertDefaults,
|
...alertDefaults,
|
||||||
version: version || ENTITY_VERSION_V4,
|
version: version || ENTITY_VERSION_V5,
|
||||||
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
|
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { FeatureKeys } from 'constants/features';
|
|||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||||
|
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
@ -37,6 +38,7 @@ import { UpdateTimeInterval } from 'store/actions';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Warning } from 'types/api';
|
import { Warning } from 'types/api';
|
||||||
import { AlertDef } from 'types/api/alerts/def';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
@ -83,6 +85,7 @@ function ChartPreview({
|
|||||||
const threshold = alertDef?.condition.target || 0;
|
const threshold = alertDef?.condition.target || 0;
|
||||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
@ -174,6 +177,19 @@ function ChartPreview({
|
|||||||
setMaxTimeScale(endTime);
|
setMaxTimeScale(endTime);
|
||||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
||||||
|
|
||||||
|
// Initialize graph visibility from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryResponse?.data?.payload?.data?.result) {
|
||||||
|
const {
|
||||||
|
graphVisibilityStates: localStoredVisibilityState,
|
||||||
|
} = getLocalStorageGraphVisibilityState({
|
||||||
|
apiResponse: queryResponse.data.payload.data.result,
|
||||||
|
name: 'alert-chart-preview',
|
||||||
|
});
|
||||||
|
setGraphVisibility(localStoredVisibilityState);
|
||||||
|
}
|
||||||
|
}, [queryResponse?.data?.payload?.data?.result]);
|
||||||
|
|
||||||
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
||||||
const sortedSeriesData = getSortedSeriesData(
|
const sortedSeriesData = getSortedSeriesData(
|
||||||
queryResponse.data?.payload.data.result,
|
queryResponse.data?.payload.data.result,
|
||||||
@ -260,6 +276,10 @@ function ChartPreview({
|
|||||||
timezone: timezone.value,
|
timezone: timezone.value,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
query: query || currentQuery,
|
query: query || currentQuery,
|
||||||
|
graphsVisibilityStates: graphVisibility,
|
||||||
|
setGraphsVisibilityStates: setGraphVisibility,
|
||||||
|
enhancedLegend: true,
|
||||||
|
legendPosition: LegendPosition.BOTTOM,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
@ -277,6 +297,7 @@ function ChartPreview({
|
|||||||
timezone.value,
|
timezone.value,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
query,
|
query,
|
||||||
|
graphVisibility,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -37,11 +37,8 @@ import {
|
|||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import {
|
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||||
Query,
|
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -182,12 +179,17 @@ function FormAlertRules({
|
|||||||
setDetectionMethod(value);
|
setDetectionMethod(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
|
const updateFunctions = (data: IBuilderQuery): QueryFunction[] => {
|
||||||
const anomalyFunction = {
|
const anomalyFunction: QueryFunction = {
|
||||||
name: 'anomaly',
|
name: 'anomaly' as any,
|
||||||
args: [],
|
args: [
|
||||||
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
|
{
|
||||||
|
name: 'z_score_threshold',
|
||||||
|
value: alertDef.condition.target || 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const functions = data.functions || [];
|
const functions = data.functions || [];
|
||||||
|
|
||||||
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { Props } from 'types/api/dashboard/update';
|
import { Props } from 'types/api/dashboard/update';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
@ -183,9 +184,19 @@ function WidgetGraphComponent({
|
|||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clonedWidget = updatedDashboard.data?.data?.widgets?.find(
|
||||||
|
(w) => w.id === uuid,
|
||||||
|
) as Widgets;
|
||||||
|
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
graphType: widget?.panelTypes,
|
[QueryParams.graphType]: clonedWidget?.panelTypes,
|
||||||
widgetId: uuid,
|
[QueryParams.widgetId]: uuid,
|
||||||
|
...(clonedWidget?.query && {
|
||||||
|
[QueryParams.compositeQuery]: encodeURIComponent(
|
||||||
|
JSON.stringify(clonedWidget.query),
|
||||||
|
),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
|
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -235,6 +235,7 @@ export const handleGraphClick = async ({
|
|||||||
? customTracesTimeRange?.end
|
? customTracesTimeRange?.end
|
||||||
: xValue + (stepInterval ?? 60),
|
: xValue + (stepInterval ?? 60),
|
||||||
shouldResolveQuery: true,
|
shouldResolveQuery: true,
|
||||||
|
widgetQuery: widget?.query,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
|
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
|
||||||
|
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
|
||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
@ -32,7 +32,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
|||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const queryRangeMutation = useMutation(getQueryRangeFormat);
|
const queryRangeMutation = useMutation(getSubstituteVars);
|
||||||
|
|
||||||
const getUpdatedQuery = useCallback(
|
const getUpdatedQuery = useCallback(
|
||||||
async ({
|
async ({
|
||||||
@ -40,7 +40,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
|||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
}: UseUpdatedQueryOptions): Promise<Query> => {
|
}: UseUpdatedQueryOptions): Promise<Query> => {
|
||||||
// Prepare query payload with resolved variables
|
// Prepare query payload with resolved variables
|
||||||
const { queryPayload } = prepareQueryRangePayload({
|
const { queryPayload } = prepareQueryRangePayloadV5({
|
||||||
query: widgetConfig.query,
|
query: widgetConfig.query,
|
||||||
graphType: getGraphType(widgetConfig.panelTypes),
|
graphType: getGraphType(widgetConfig.panelTypes),
|
||||||
selectedTime: widgetConfig.timePreferance,
|
selectedTime: widgetConfig.timePreferance,
|
||||||
@ -53,7 +53,10 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
|||||||
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
|
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
|
||||||
|
|
||||||
// Map query data from API response
|
// Map query data from API response
|
||||||
return mapQueryDataFromApi(queryResult.compositeQuery, widgetConfig?.query);
|
return mapQueryDataFromApi(
|
||||||
|
queryResult.data.compositeQuery,
|
||||||
|
widgetConfig?.query,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[globalSelectedInterval, queryRangeMutation],
|
[globalSelectedInterval, queryRangeMutation],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { ColumnType } from 'antd/es/table';
|
|||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { QueryTable } from 'container/QueryTable';
|
import { QueryTable } from 'container/QueryTable';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { getColumnUnit, RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { cloneDeep, get, isEmpty } from 'lodash-es';
|
import { cloneDeep, get, isEmpty } from 'lodash-es';
|
||||||
import { Compass } from 'lucide-react';
|
import { Compass } from 'lucide-react';
|
||||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||||
@ -84,10 +84,11 @@ function GridTableComponent({
|
|||||||
(val): RowData => {
|
(val): RowData => {
|
||||||
const newValue = { ...val };
|
const newValue = { ...val };
|
||||||
Object.keys(val).forEach((k) => {
|
Object.keys(val).forEach((k) => {
|
||||||
if (columnUnits[k]) {
|
const unit = getColumnUnit(k, columnUnits);
|
||||||
|
if (unit) {
|
||||||
// the check below takes care of not adding units for rows that have n/a or null values
|
// the check below takes care of not adding units for rows that have n/a or null values
|
||||||
if (val[k] !== 'n/a' && val[k] !== null) {
|
if (val[k] !== 'n/a' && val[k] !== null) {
|
||||||
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
|
newValue[k] = getYAxisFormattedValue(String(val[k]), unit);
|
||||||
} else if (val[k] === null) {
|
} else if (val[k] === null) {
|
||||||
newValue[k] = 'n/a';
|
newValue[k] = 'n/a';
|
||||||
}
|
}
|
||||||
@ -121,7 +122,8 @@ function GridTableComponent({
|
|||||||
render: (text: string, ...rest: any): ReactNode => {
|
render: (text: string, ...rest: any): ReactNode => {
|
||||||
let textForThreshold = text;
|
let textForThreshold = text;
|
||||||
const dataIndex = (e as ColumnType<RowData>)?.dataIndex || e.title;
|
const dataIndex = (e as ColumnType<RowData>)?.dataIndex || e.title;
|
||||||
if (columnUnits && columnUnits?.[dataIndex as string]) {
|
const unit = getColumnUnit(dataIndex as string, columnUnits || {});
|
||||||
|
if (unit) {
|
||||||
textForThreshold = rest[0][`${dataIndex}_without_unit`];
|
textForThreshold = rest[0][`${dataIndex}_without_unit`];
|
||||||
}
|
}
|
||||||
const isNumber = !Number.isNaN(Number(textForThreshold));
|
const isNumber = !Number.isNaN(Number(textForThreshold));
|
||||||
@ -131,7 +133,7 @@ function GridTableComponent({
|
|||||||
thresholds,
|
thresholds,
|
||||||
dataIndex as string,
|
dataIndex as string,
|
||||||
Number(textForThreshold),
|
Number(textForThreshold),
|
||||||
columnUnits?.[dataIndex as string],
|
unit,
|
||||||
);
|
);
|
||||||
|
|
||||||
const idx = thresholds.findIndex(
|
const idx = thresholds.findIndex(
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import { SettingOutlined } from '@ant-design/icons';
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
import { Dropdown, MenuProps } from 'antd';
|
import { Dropdown, MenuProps } from 'antd';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import {
|
||||||
|
negateOperator,
|
||||||
|
OPERATORS,
|
||||||
|
QUERY_BUILDER_FUNCTIONS,
|
||||||
|
} from 'constants/antlrQueryConstants';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
|
|
||||||
import { TitleWrapper } from './BodyTitleRenderer.styles';
|
import { TitleWrapper } from './BodyTitleRenderer.styles';
|
||||||
@ -29,7 +33,9 @@ function BodyTitleRenderer({
|
|||||||
getDataTypes(value),
|
getDataTypes(value),
|
||||||
),
|
),
|
||||||
`${value}`,
|
`${value}`,
|
||||||
isFilterIn ? OPERATORS.HAS : OPERATORS.NHAS,
|
isFilterIn
|
||||||
|
? QUERY_BUILDER_FUNCTIONS.HAS
|
||||||
|
: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
|
||||||
true,
|
true,
|
||||||
parentIsArray ? getDataTypes([value]) : getDataTypes(value),
|
parentIsArray ? getDataTypes([value]) : getDataTypes(value),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -82,6 +82,48 @@ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
|
|||||||
useGetExplorerQueryRange: jest.fn(),
|
useGetExplorerQueryRange: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock ErrorStateComponent to handle APIError properly
|
||||||
|
jest.mock(
|
||||||
|
'components/Common/ErrorStateComponent',
|
||||||
|
() =>
|
||||||
|
function MockErrorStateComponent({ error, message }: any): JSX.Element {
|
||||||
|
if (error) {
|
||||||
|
// Mock the getErrorMessage and getErrorDetails methods
|
||||||
|
const getErrorMessage = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue(
|
||||||
|
error.error?.message ||
|
||||||
|
'Something went wrong. Please try again or contact support.',
|
||||||
|
);
|
||||||
|
const getErrorDetails = jest.fn().mockReturnValue(error);
|
||||||
|
|
||||||
|
// Add the methods to the error object
|
||||||
|
const errorWithMethods = {
|
||||||
|
...error,
|
||||||
|
getErrorMessage,
|
||||||
|
getErrorDetails,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="error-state-component">
|
||||||
|
<div>{errorWithMethods.getErrorMessage()}</div>
|
||||||
|
{errorWithMethods.getErrorDetails().error?.errors?.map((err: any) => (
|
||||||
|
<div key={`error-${err.message}`}>• {err.message}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="error-state-component">
|
||||||
|
<div>
|
||||||
|
{message || 'Something went wrong. Please try again or contact support.'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
jest.mock('hooks/useSafeNavigate', () => ({
|
jest.mock('hooks/useSafeNavigate', () => ({
|
||||||
useSafeNavigate: (): any => ({
|
useSafeNavigate: (): any => ({
|
||||||
safeNavigate: jest.fn(),
|
safeNavigate: jest.fn(),
|
||||||
|
|||||||
@ -239,10 +239,7 @@ function QuerySection({
|
|||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
<TextToolTip
|
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||||
text="This will temporarily save the current query and graph state. This will persist across tab change"
|
|
||||||
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
loading={queryResponse.isFetching}
|
loading={queryResponse.isFetching}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import './ColumnUnitSelector.styles.scss';
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
||||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { Dispatch, SetStateAction, useCallback, useEffect } from 'react';
|
||||||
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||||
@ -31,12 +32,49 @@ export function ColumnUnitSelector(
|
|||||||
[setColumnUnits],
|
[setColumnUnits],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getValues = (value: string): string => {
|
||||||
|
const currentValue = columnUnits[value];
|
||||||
|
if (currentValue) {
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if base query has value, return it
|
||||||
|
const baseQuery = value.split('.')[0];
|
||||||
|
|
||||||
|
if (columnUnits[baseQuery]) {
|
||||||
|
return columnUnits[baseQuery];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have value as base query i.e. value = B, but the columnUnit have let say B.count(): 'h' then we need to return B.count()
|
||||||
|
// get the queryName B.count() from the columnUnits keys based on the B that we have (first match - 0th aggregationIndex)
|
||||||
|
const newQueryWithExpression = Object.keys(columnUnits).find(
|
||||||
|
(key) =>
|
||||||
|
key.startsWith(baseQuery) &&
|
||||||
|
!isEmpty(aggregationQueries.find((query) => query.value === key)),
|
||||||
|
);
|
||||||
|
if (newQueryWithExpression) {
|
||||||
|
return columnUnits[newQueryWithExpression];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newColumnUnits = aggregationQueries.reduce((acc, query) => {
|
||||||
|
acc[query.value] = getValues(query.value);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
setColumnUnits(newColumnUnits);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [aggregationQueries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="column-unit-selector">
|
<section className="column-unit-selector">
|
||||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||||
{aggregationQueries.map(({ value, label }) => (
|
{aggregationQueries.map(({ value, label }) => (
|
||||||
<YAxisUnitSelector
|
<YAxisUnitSelector
|
||||||
defaultValue={columnUnits[value]}
|
defaultValue={columnUnits[value]}
|
||||||
|
value={columnUnits[value] || ''}
|
||||||
onSelect={(unitValue: string): void =>
|
onSelect={(unitValue: string): void =>
|
||||||
handleColumnUnitSelect(value, unitValue)
|
handleColumnUnitSelect(value, unitValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Button, Collapse, ColorPicker, Tooltip, Typography } from 'antd';
|
|||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||||
import getLabelName from 'lib/getLabelName';
|
import getLabelName from 'lib/getLabelName';
|
||||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
import { Palette } from 'lucide-react';
|
import { Palette } from 'lucide-react';
|
||||||
@ -69,7 +70,11 @@ function LegendColors({
|
|||||||
const legendLabels = useMemo(() => {
|
const legendLabels = useMemo(() => {
|
||||||
if (queryResponse?.data?.payload?.data?.result) {
|
if (queryResponse?.data?.payload?.data?.result) {
|
||||||
return queryResponse.data.payload.data.result.map((item: any) =>
|
return queryResponse.data.payload.data.result.map((item: any) =>
|
||||||
|
getLegend(
|
||||||
|
item,
|
||||||
|
currentQuery,
|
||||||
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
|
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { unitOptions } from 'container/NewWidget/utils';
|
import { unitOptions } from 'container/NewWidget/utils';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { getColumnUnit } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { Check, Pencil, Trash2, X } from 'lucide-react';
|
import { Check, Pencil, Trash2, X } from 'lucide-react';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||||
@ -197,7 +198,11 @@ function Threshold({
|
|||||||
const isInvalidUnitComparison = useMemo(
|
const isInvalidUnitComparison = useMemo(
|
||||||
() =>
|
() =>
|
||||||
unit !== 'none' &&
|
unit !== 'none' &&
|
||||||
convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null,
|
convertUnit(
|
||||||
|
value,
|
||||||
|
unit,
|
||||||
|
getColumnUnit(tableSelectedOption, columnUnits || {}),
|
||||||
|
) === null,
|
||||||
[unit, value, columnUnits, tableSelectedOption],
|
[unit, value, columnUnits, tableSelectedOption],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -312,7 +317,9 @@ function Threshold({
|
|||||||
{isEditMode ? (
|
{isEditMode ? (
|
||||||
<Select
|
<Select
|
||||||
defaultValue={unit}
|
defaultValue={unit}
|
||||||
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
|
options={unitOptions(
|
||||||
|
getColumnUnit(tableSelectedOption, columnUnits || {}) || '',
|
||||||
|
)}
|
||||||
onChange={handleUnitChange}
|
onChange={handleUnitChange}
|
||||||
showSearch
|
showSearch
|
||||||
className="unit-selection"
|
className="unit-selection"
|
||||||
@ -351,7 +358,7 @@ function Threshold({
|
|||||||
{isInvalidUnitComparison && (
|
{isInvalidUnitComparison && (
|
||||||
<Typography.Text className="invalid-unit">
|
<Typography.Text className="invalid-unit">
|
||||||
Threshold unit ({unit}) is not valid in comparison with the column unit (
|
Threshold unit ({unit}) is not valid in comparison with the column unit (
|
||||||
{columnUnits?.[tableSelectedOption] || 'none'})
|
{getColumnUnit(tableSelectedOption, columnUnits || {}) || 'none'})
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
|
|||||||
@ -16,11 +16,13 @@ const findCategoryByName = (
|
|||||||
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
|
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
|
||||||
function YAxisUnitSelector({
|
function YAxisUnitSelector({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
value,
|
||||||
onSelect,
|
onSelect,
|
||||||
fieldLabel,
|
fieldLabel,
|
||||||
handleClear,
|
handleClear,
|
||||||
}: {
|
}: {
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
|
value: string;
|
||||||
onSelect: OnSelectType;
|
onSelect: OnSelectType;
|
||||||
fieldLabel: string;
|
fieldLabel: string;
|
||||||
handleClear?: () => void;
|
handleClear?: () => void;
|
||||||
@ -40,6 +42,7 @@ function YAxisUnitSelector({
|
|||||||
options={options}
|
options={options}
|
||||||
allowClear
|
allowClear
|
||||||
defaultValue={findCategoryById(defaultValue)?.name}
|
defaultValue={findCategoryById(defaultValue)?.name}
|
||||||
|
value={findCategoryById(value)?.name || ''}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
onSelect={onSelectHandler}
|
onSelect={onSelectHandler}
|
||||||
filterOption={(inputValue, option): boolean => {
|
filterOption={(inputValue, option): boolean => {
|
||||||
|
|||||||
@ -339,6 +339,7 @@ function RightContainer({
|
|||||||
<YAxisUnitSelector
|
<YAxisUnitSelector
|
||||||
defaultValue={yAxisUnit}
|
defaultValue={yAxisUnit}
|
||||||
onSelect={setYAxisUnit}
|
onSelect={setYAxisUnit}
|
||||||
|
value={yAxisUnit || ''}
|
||||||
fieldLabel={
|
fieldLabel={
|
||||||
selectedGraphType === PanelDisplay.VALUE ||
|
selectedGraphType === PanelDisplay.VALUE ||
|
||||||
selectedGraphType === PanelDisplay.PIE
|
selectedGraphType === PanelDisplay.PIE
|
||||||
|
|||||||
@ -16,10 +16,8 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import {
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { DataSourceDropdown } from '..';
|
import { DataSourceDropdown } from '..';
|
||||||
@ -36,7 +34,7 @@ interface QBEntityOptionsProps {
|
|||||||
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
|
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
|
||||||
onToggleVisibility: () => void;
|
onToggleVisibility: () => void;
|
||||||
onCollapseEntity: () => void;
|
onCollapseEntity: () => void;
|
||||||
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
onQueryFunctionsUpdates?: (functions: QueryFunction[]) => void;
|
||||||
showDeleteButton?: boolean;
|
showDeleteButton?: boolean;
|
||||||
showCloneOption?: boolean;
|
showCloneOption?: boolean;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
|
|||||||
@ -9,15 +9,14 @@ import {
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { debounce, isNil } from 'lodash-es';
|
import { debounce, isNil } from 'lodash-es';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import {
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||||
|
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
|
||||||
|
|
||||||
interface FunctionProps {
|
interface FunctionProps {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
funcData: QueryFunctionProps;
|
funcData: QueryFunction;
|
||||||
index: any;
|
index: any;
|
||||||
handleUpdateFunctionArgs: any;
|
handleUpdateFunctionArgs: any;
|
||||||
handleUpdateFunctionName: any;
|
handleUpdateFunctionName: any;
|
||||||
@ -33,17 +32,19 @@ export default function Function({
|
|||||||
handleDeleteFunction,
|
handleDeleteFunction,
|
||||||
}: FunctionProps): JSX.Element {
|
}: FunctionProps): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { showInput, disabled } = queryFunctionsTypesConfig[funcData.name];
|
// Normalize function name to handle backend response case sensitivity
|
||||||
|
const normalizedFunctionName = normalizeFunctionName(funcData.name);
|
||||||
|
const { showInput, disabled } = queryFunctionsTypesConfig[
|
||||||
|
normalizedFunctionName
|
||||||
|
];
|
||||||
|
|
||||||
let functionValue;
|
let functionValue;
|
||||||
|
|
||||||
const hasValue = !isNil(
|
const hasValue = !isNil(funcData.args?.[0]?.value);
|
||||||
funcData.args && funcData.args.length > 0 && funcData.args[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasValue) {
|
if (hasValue) {
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
functionValue = funcData.args[0];
|
functionValue = funcData.args?.[0]?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedhandleUpdateFunctionArgs = debounce(
|
const debouncedhandleUpdateFunctionArgs = debounce(
|
||||||
@ -57,9 +58,10 @@ export default function Function({
|
|||||||
? logsQueryFunctionOptions
|
? logsQueryFunctionOptions
|
||||||
: metricQueryFunctionOptions;
|
: metricQueryFunctionOptions;
|
||||||
|
|
||||||
const disableRemoveFunction = funcData.name === QueryFunctionsTypes.ANOMALY;
|
const disableRemoveFunction =
|
||||||
|
normalizedFunctionName === QueryFunctionsTypes.ANOMALY;
|
||||||
|
|
||||||
if (funcData.name === QueryFunctionsTypes.ANOMALY) {
|
if (normalizedFunctionName === QueryFunctionsTypes.ANOMALY) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
@ -68,7 +70,7 @@ export default function Function({
|
|||||||
<Flex className="query-function">
|
<Flex className="query-function">
|
||||||
<Select
|
<Select
|
||||||
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
|
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
|
||||||
value={funcData.name}
|
value={normalizedFunctionName}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ minWidth: '100px' }}
|
style={{ minWidth: '100px' }}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
|
|||||||
@ -6,29 +6,28 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { cloneDeep, pullAt } from 'lodash-es';
|
import { cloneDeep, pullAt } from 'lodash-es';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||||
|
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
|
||||||
|
|
||||||
import Function from './Function';
|
import Function from './Function';
|
||||||
import { toFloat64 } from './utils';
|
import { toFloat64 } from './utils';
|
||||||
|
|
||||||
const defaultMetricFunctionStruct: QueryFunctionProps = {
|
const defaultMetricFunctionStruct: QueryFunction = {
|
||||||
name: QueryFunctionsTypes.CUTOFF_MIN,
|
name: QueryFunctionsTypes.CUTOFF_MIN as any,
|
||||||
args: [],
|
args: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultLogFunctionStruct: QueryFunctionProps = {
|
const defaultLogFunctionStruct: QueryFunction = {
|
||||||
name: QueryFunctionsTypes.TIME_SHIFT,
|
name: QueryFunctionsTypes.TIME_SHIFT as any,
|
||||||
args: [],
|
args: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface QueryFunctionsProps {
|
interface QueryFunctionsProps {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
queryFunctions: QueryFunctionProps[];
|
queryFunctions: QueryFunction[];
|
||||||
onChange: (functions: QueryFunctionProps[]) => void;
|
onChange: (functions: QueryFunction[]) => void;
|
||||||
maxFunctions: number;
|
maxFunctions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,8 +86,11 @@ export default function QueryFunctions({
|
|||||||
onChange,
|
onChange,
|
||||||
maxFunctions = 3,
|
maxFunctions = 3,
|
||||||
}: QueryFunctionsProps): JSX.Element {
|
}: QueryFunctionsProps): JSX.Element {
|
||||||
const [functions, setFunctions] = useState<QueryFunctionProps[]>(
|
const [functions, setFunctions] = useState<QueryFunction[]>(
|
||||||
queryFunctions,
|
queryFunctions.map((func) => ({
|
||||||
|
...func,
|
||||||
|
name: normalizeFunctionName(func.name) as any,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@ -127,7 +129,7 @@ export default function QueryFunctions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteFunction = (
|
const handleDeleteFunction = (
|
||||||
queryFunction: QueryFunctionProps,
|
queryFunction: QueryFunction,
|
||||||
index: number,
|
index: number,
|
||||||
): void => {
|
): void => {
|
||||||
const clonedFunctions = cloneDeep(functions);
|
const clonedFunctions = cloneDeep(functions);
|
||||||
@ -138,21 +140,23 @@ export default function QueryFunctions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateFunctionName = (
|
const handleUpdateFunctionName = (
|
||||||
func: QueryFunctionProps,
|
func: QueryFunction,
|
||||||
index: number,
|
index: number,
|
||||||
value: string,
|
value: string,
|
||||||
): void => {
|
): void => {
|
||||||
const updateFunctions = cloneDeep(functions);
|
const updateFunctions = cloneDeep(functions);
|
||||||
|
|
||||||
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
|
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
|
||||||
updateFunctions[index].name = value;
|
// Normalize function name from backend response to match frontend expectations
|
||||||
|
const normalizedValue = normalizeFunctionName(value);
|
||||||
|
updateFunctions[index].name = normalizedValue as any;
|
||||||
setFunctions(updateFunctions);
|
setFunctions(updateFunctions);
|
||||||
onChange(updateFunctions);
|
onChange(updateFunctions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateFunctionArgs = (
|
const handleUpdateFunctionArgs = (
|
||||||
func: QueryFunctionProps,
|
func: QueryFunction,
|
||||||
index: number,
|
index: number,
|
||||||
value: string,
|
value: string,
|
||||||
): void => {
|
): void => {
|
||||||
@ -160,11 +164,12 @@ export default function QueryFunctions({
|
|||||||
|
|
||||||
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
|
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
|
||||||
updateFunctions[index].args = [
|
updateFunctions[index].args = [
|
||||||
// timeShift expects a float64 value, so we convert the string to a number
|
{
|
||||||
// For other functions, we keep the value as a string
|
value:
|
||||||
updateFunctions[index].name === QueryFunctionsTypes.TIME_SHIFT
|
updateFunctions[index].name === QueryFunctionsTypes.TIME_SHIFT
|
||||||
? toFloat64(value)
|
? toFloat64(value)
|
||||||
: value,
|
: value,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
setFunctions(updateFunctions);
|
setFunctions(updateFunctions);
|
||||||
onChange(updateFunctions);
|
onChange(updateFunctions);
|
||||||
|
|||||||
@ -7,4 +7,5 @@ export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
|||||||
onChange: (value: BaseAutocompleteData) => void;
|
onChange: (value: BaseAutocompleteData) => void;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onSelect?: (value: BaseAutocompleteData) => void;
|
onSelect?: (value: BaseAutocompleteData) => void;
|
||||||
|
index?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
|||||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||||
import { getAutocompleteValueAndType } from 'lib/newQueryBuilder/getAutocompleteValueAndType';
|
import { getAutocompleteValueAndType } from 'lib/newQueryBuilder/getAutocompleteValueAndType';
|
||||||
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from 'react-query';
|
import { useQuery, useQueryClient } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
@ -24,7 +24,6 @@ import { MetricAggregation } from 'types/api/v5/queryRange';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { ExtendedSelectOption } from 'types/common/select';
|
import { ExtendedSelectOption } from 'types/common/select';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
|
||||||
|
|
||||||
import { removePrefix } from '../GroupByFilter/utils';
|
import { removePrefix } from '../GroupByFilter/utils';
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
@ -38,10 +37,10 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
index,
|
||||||
}: AgregatorFilterProps): JSX.Element {
|
}: AgregatorFilterProps): JSX.Element {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
|
||||||
|
|
||||||
// this function is only relevant for metrics and now operators are part of aggregations
|
// this function is only relevant for metrics and now operators are part of aggregations
|
||||||
const queryAggregation = useMemo(
|
const queryAggregation = useMemo(
|
||||||
@ -49,6 +48,10 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
[query.aggregations],
|
[query.aggregations],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState<string>(
|
||||||
|
(query.aggregations?.[0] as MetricAggregation)?.metricName || '',
|
||||||
|
);
|
||||||
|
|
||||||
const debouncedSearchText = useMemo(() => {
|
const debouncedSearchText = useMemo(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
||||||
const [_, value] = getAutocompleteValueAndType(searchText);
|
const [_, value] = getAutocompleteValueAndType(searchText);
|
||||||
@ -57,12 +60,13 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
|
|
||||||
const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY);
|
const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY);
|
||||||
const { isFetching } = useQuery(
|
const { isFetching, data: aggregateAttributeData } = useQuery(
|
||||||
[
|
[
|
||||||
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||||
debouncedValue,
|
debouncedValue,
|
||||||
queryAggregation.timeAggregation,
|
queryAggregation.timeAggregation,
|
||||||
query.dataSource,
|
query.dataSource,
|
||||||
|
index,
|
||||||
],
|
],
|
||||||
async () =>
|
async () =>
|
||||||
getAggregateAttribute({
|
getAggregateAttribute({
|
||||||
@ -108,13 +112,49 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle edit mode: update aggregateAttribute type when data is available
|
||||||
|
useEffect(() => {
|
||||||
|
const metricName = queryAggregation?.metricName;
|
||||||
|
const hasAggregateAttributeType = query.aggregateAttribute?.type;
|
||||||
|
|
||||||
|
// Check if we're in edit mode and have data from the existing query
|
||||||
|
// Also ensure this is for the correct query by checking the metric name matches
|
||||||
|
if (
|
||||||
|
query.dataSource === DataSource.METRICS &&
|
||||||
|
metricName &&
|
||||||
|
!hasAggregateAttributeType &&
|
||||||
|
aggregateAttributeData?.payload?.attributeKeys &&
|
||||||
|
// Only update if the data contains the metric we're looking for
|
||||||
|
aggregateAttributeData.payload.attributeKeys.some(
|
||||||
|
(item) => item.key === metricName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const metricData = aggregateAttributeData.payload.attributeKeys.find(
|
||||||
|
(item) => item.key === metricName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metricData) {
|
||||||
|
// Update the aggregateAttribute with the fetched type information
|
||||||
|
onChange(metricData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
query.dataSource,
|
||||||
|
queryAggregation?.metricName,
|
||||||
|
query.aggregateAttribute?.type,
|
||||||
|
aggregateAttributeData,
|
||||||
|
onChange,
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSearchText = useCallback((text: string): void => {
|
const handleSearchText = useCallback((text: string): void => {
|
||||||
setSearchText(text);
|
setSearchText(text);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const placeholder: string =
|
const placeholder: string =
|
||||||
query.dataSource === DataSource.METRICS
|
query.dataSource === DataSource.METRICS
|
||||||
? `${transformToUpperCase(query.dataSource)} name`
|
? `Search metric name`
|
||||||
: 'Aggregate attribute';
|
: 'Aggregate attribute';
|
||||||
|
|
||||||
const getAttributesData = useCallback(
|
const getAttributesData = useCallback(
|
||||||
@ -124,12 +164,14 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
debouncedValue,
|
debouncedValue,
|
||||||
queryAggregation.timeAggregation,
|
queryAggregation.timeAggregation,
|
||||||
query.dataSource,
|
query.dataSource,
|
||||||
|
index,
|
||||||
])?.payload?.attributeKeys || [],
|
])?.payload?.attributeKeys || [],
|
||||||
[
|
[
|
||||||
debouncedValue,
|
debouncedValue,
|
||||||
queryAggregation.timeAggregation,
|
queryAggregation.timeAggregation,
|
||||||
query.dataSource,
|
query.dataSource,
|
||||||
queryClient,
|
queryClient,
|
||||||
|
index,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -140,6 +182,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
searchText,
|
searchText,
|
||||||
queryAggregation.timeAggregation,
|
queryAggregation.timeAggregation,
|
||||||
query.dataSource,
|
query.dataSource,
|
||||||
|
index,
|
||||||
],
|
],
|
||||||
async () =>
|
async () =>
|
||||||
getAggregateAttribute({
|
getAggregateAttribute({
|
||||||
@ -155,6 +198,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
query.dataSource,
|
query.dataSource,
|
||||||
queryClient,
|
queryClient,
|
||||||
searchText,
|
searchText,
|
||||||
|
index,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleChangeCustomValue = useCallback(
|
const handleChangeCustomValue = useCallback(
|
||||||
|
|||||||
@ -195,6 +195,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
data-testid="group-by"
|
data-testid="group-by"
|
||||||
|
placeholder={localValues.length === 0 ? 'Everything (no breakdown)' : ''}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
|||||||
import Uplot from 'components/Uplot';
|
import Uplot from 'components/Uplot';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||||
|
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||||
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
|
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
|
||||||
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
|
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
|
||||||
@ -36,6 +37,7 @@ import { useLocation } from 'react-router-dom';
|
|||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { SuccessResponse, Warning } from 'types/api';
|
import { SuccessResponse, Warning } from 'types/api';
|
||||||
|
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@ -76,6 +78,7 @@ function TimeSeriesView({
|
|||||||
|
|
||||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -89,6 +92,19 @@ function TimeSeriesView({
|
|||||||
setMaxTimeScale(endTime);
|
setMaxTimeScale(endTime);
|
||||||
}, [maxTime, minTime, globalSelectedInterval, data]);
|
}, [maxTime, minTime, globalSelectedInterval, data]);
|
||||||
|
|
||||||
|
// Initialize graph visibility from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.payload?.data?.result) {
|
||||||
|
const {
|
||||||
|
graphVisibilityStates: localStoredVisibilityState,
|
||||||
|
} = getLocalStorageGraphVisibilityState({
|
||||||
|
apiResponse: data.payload.data.result,
|
||||||
|
name: 'time-series-explorer',
|
||||||
|
});
|
||||||
|
setGraphVisibility(localStoredVisibilityState);
|
||||||
|
}
|
||||||
|
}, [data?.payload?.data?.result]);
|
||||||
|
|
||||||
const onDragSelect = useCallback(
|
const onDragSelect = useCallback(
|
||||||
(start: number, end: number): void => {
|
(start: number, end: number): void => {
|
||||||
const startTimestamp = Math.trunc(start);
|
const startTimestamp = Math.trunc(start);
|
||||||
@ -162,6 +178,7 @@ function TimeSeriesView({
|
|||||||
const { timezone } = useTimezone();
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const chartOptions = getUPlotChartOptions({
|
const chartOptions = getUPlotChartOptions({
|
||||||
|
id: 'time-series-explorer',
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
yAxisUnit: yAxisUnit || '',
|
yAxisUnit: yAxisUnit || '',
|
||||||
apiResponse: data?.payload,
|
apiResponse: data?.payload,
|
||||||
@ -179,6 +196,10 @@ function TimeSeriesView({
|
|||||||
timezone: timezone.value,
|
timezone: timezone.value,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
query: currentQuery,
|
query: currentQuery,
|
||||||
|
graphsVisibilityStates: graphVisibility,
|
||||||
|
setGraphsVisibilityStates: setGraphVisibility,
|
||||||
|
enhancedLegend: true,
|
||||||
|
legendPosition: LegendPosition.BOTTOM,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -36,7 +36,6 @@ function TableView({
|
|||||||
dataSource: 'traces',
|
dataSource: 'traces',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// ENTITY_VERSION_V4,
|
|
||||||
ENTITY_VERSION_V5,
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
|
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
|
||||||
|
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
@ -25,7 +25,7 @@ const useCreateAlerts = (
|
|||||||
caller?: string,
|
caller?: string,
|
||||||
thresholds?: ThresholdProps[],
|
thresholds?: ThresholdProps[],
|
||||||
): VoidFunction => {
|
): VoidFunction => {
|
||||||
const queryRangeMutation = useMutation(getQueryRangeFormat);
|
const queryRangeMutation = useMutation(getSubstituteVars);
|
||||||
|
|
||||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -57,7 +57,7 @@ const useCreateAlerts = (
|
|||||||
queryType: widget.query.queryType,
|
queryType: widget.query.queryType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { queryPayload } = prepareQueryRangePayload({
|
const { queryPayload } = prepareQueryRangePayloadV5({
|
||||||
query: widget.query,
|
query: widget.query,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
graphType: getGraphType(widget.panelTypes),
|
graphType: getGraphType(widget.panelTypes),
|
||||||
@ -68,7 +68,7 @@ const useCreateAlerts = (
|
|||||||
queryRangeMutation.mutate(queryPayload, {
|
queryRangeMutation.mutate(queryPayload, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const updatedQuery = mapQueryDataFromApi(
|
const updatedQuery = mapQueryDataFromApi(
|
||||||
data.compositeQuery,
|
data.data.compositeQuery,
|
||||||
widget?.query,
|
widget?.query,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,9 +76,7 @@ const useCreateAlerts = (
|
|||||||
QueryParams.compositeQuery
|
QueryParams.compositeQuery
|
||||||
}=${encodeURIComponent(JSON.stringify(updatedQuery))}&${
|
}=${encodeURIComponent(JSON.stringify(updatedQuery))}&${
|
||||||
QueryParams.panelTypes
|
QueryParams.panelTypes
|
||||||
}=${widget.panelTypes}&version=${
|
}=${widget.panelTypes}&version=${ENTITY_VERSION_V5}`;
|
||||||
selectedDashboard?.data.version || DEFAULT_ENTITY_VERSION
|
|
||||||
}`;
|
|
||||||
|
|
||||||
history.push(url, {
|
history.push(url, {
|
||||||
thresholds,
|
thresholds,
|
||||||
|
|||||||
@ -30,10 +30,10 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import {
|
import {
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import {
|
import {
|
||||||
MetricAggregation,
|
MetricAggregation,
|
||||||
|
QueryFunction,
|
||||||
SpaceAggregation,
|
SpaceAggregation,
|
||||||
TimeAggregation,
|
TimeAggregation,
|
||||||
} from 'types/api/v5/queryRange';
|
} from 'types/api/v5/queryRange';
|
||||||
@ -138,7 +138,6 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
timeAggregation: value as TimeAggregation,
|
timeAggregation: value as TimeAggregation,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
having: [],
|
|
||||||
limit: null,
|
limit: null,
|
||||||
...(shouldResetAggregateAttribute
|
...(shouldResetAggregateAttribute
|
||||||
? { aggregateAttribute: initialAutocompleteData }
|
? { aggregateAttribute: initialAutocompleteData }
|
||||||
@ -217,7 +216,6 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
const newQuery: IBuilderQuery = {
|
const newQuery: IBuilderQuery = {
|
||||||
...query,
|
...query,
|
||||||
aggregateAttribute: value,
|
aggregateAttribute: value,
|
||||||
having: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -416,7 +414,7 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleQueryFunctionsUpdates = useCallback(
|
const handleQueryFunctionsUpdates = useCallback(
|
||||||
(functions: QueryFunctionProps[]): void => {
|
(functions: QueryFunction[]): void => {
|
||||||
const newQuery: IBuilderQuery = {
|
const newQuery: IBuilderQuery = {
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -72,6 +72,70 @@ const getQueryDataSource = (
|
|||||||
return queryItem?.dataSource || null;
|
return queryItem?.dataSource || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLegendForSingleAggregation = (
|
||||||
|
queryData: QueryData,
|
||||||
|
payloadQuery: Query,
|
||||||
|
aggregationAlias: string,
|
||||||
|
aggregationExpression: string,
|
||||||
|
labelName: string,
|
||||||
|
singleAggregation: boolean,
|
||||||
|
) => {
|
||||||
|
// Find the corresponding query in payloadQuery
|
||||||
|
const queryItem = payloadQuery.builder?.queryData.find(
|
||||||
|
(query) => query.queryName === queryData.queryName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const legend = queryItem?.legend;
|
||||||
|
// Check if groupBy exists and has items
|
||||||
|
const hasGroupBy = queryItem?.groupBy && queryItem.groupBy.length > 0;
|
||||||
|
|
||||||
|
if (hasGroupBy) {
|
||||||
|
if (singleAggregation) {
|
||||||
|
return labelName;
|
||||||
|
} else {
|
||||||
|
return `${aggregationAlias || aggregationExpression}-${labelName}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (singleAggregation) {
|
||||||
|
return aggregationAlias || legend || aggregationExpression;
|
||||||
|
} else {
|
||||||
|
return aggregationAlias || aggregationExpression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLegendForMultipleAggregations = (
|
||||||
|
queryData: QueryData,
|
||||||
|
payloadQuery: Query,
|
||||||
|
aggregationAlias: string,
|
||||||
|
aggregationExpression: string,
|
||||||
|
labelName: string,
|
||||||
|
singleAggregation: boolean,
|
||||||
|
) => {
|
||||||
|
// Find the corresponding query in payloadQuery
|
||||||
|
const queryItem = payloadQuery.builder?.queryData.find(
|
||||||
|
(query) => query.queryName === queryData.queryName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const legend = queryItem?.legend;
|
||||||
|
// Check if groupBy exists and has items
|
||||||
|
const hasGroupBy = queryItem?.groupBy && queryItem.groupBy.length > 0;
|
||||||
|
|
||||||
|
if (hasGroupBy) {
|
||||||
|
if (singleAggregation) {
|
||||||
|
return labelName;
|
||||||
|
} else {
|
||||||
|
return `${aggregationAlias || aggregationExpression}-${labelName}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (singleAggregation) {
|
||||||
|
return aggregationAlias || labelName || aggregationExpression;
|
||||||
|
} else {
|
||||||
|
return `${aggregationAlias || aggregationExpression}-${labelName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getLegend = (
|
export const getLegend = (
|
||||||
queryData: QueryData,
|
queryData: QueryData,
|
||||||
payloadQuery: Query,
|
payloadQuery: Query,
|
||||||
@ -91,21 +155,34 @@ export const getLegend = (
|
|||||||
const aggregation =
|
const aggregation =
|
||||||
aggregationPerQuery?.[metaData?.queryName]?.[metaData?.index];
|
aggregationPerQuery?.[metaData?.queryName]?.[metaData?.index];
|
||||||
|
|
||||||
const aggregationName = aggregation?.alias || aggregation?.expression || '';
|
const aggregationAlias = aggregation?.alias || '';
|
||||||
|
const aggregationExpression = aggregation?.expression || '';
|
||||||
|
|
||||||
// Check if there's only one total query (queryData + queryFormulas)
|
// Check if there's only one total query (queryData)
|
||||||
const totalQueries =
|
const singleQuery = payloadQuery?.builder?.queryData?.length === 1;
|
||||||
(payloadQuery?.builder?.queryData?.length || 0) +
|
const singleAggregation =
|
||||||
(payloadQuery?.builder?.queryFormulas?.length || 0);
|
aggregationPerQuery?.[metaData?.queryName]?.length === 1;
|
||||||
const showSingleAggregationName =
|
|
||||||
totalQueries === 1 && labelName === metaData?.queryName;
|
|
||||||
|
|
||||||
if (aggregationName) {
|
if (aggregationAlias || aggregationExpression) {
|
||||||
return showSingleAggregationName
|
return singleQuery
|
||||||
? aggregationName
|
? getLegendForSingleAggregation(
|
||||||
: `${aggregationName}-${labelName}`;
|
queryData,
|
||||||
|
payloadQuery,
|
||||||
|
aggregationAlias,
|
||||||
|
aggregationExpression,
|
||||||
|
labelName,
|
||||||
|
singleAggregation,
|
||||||
|
)
|
||||||
|
: getLegendForMultipleAggregations(
|
||||||
|
queryData,
|
||||||
|
payloadQuery,
|
||||||
|
aggregationAlias,
|
||||||
|
aggregationExpression,
|
||||||
|
labelName,
|
||||||
|
singleAggregation,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return labelName || metaData?.queryName;
|
return labelName || metaData?.queryName || queryData.queryName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function GetMetricQueryRange(
|
export async function GetMetricQueryRange(
|
||||||
@ -151,6 +228,7 @@ export async function GetMetricQueryRange(
|
|||||||
warning: undefined,
|
warning: undefined,
|
||||||
},
|
},
|
||||||
params: props,
|
params: props,
|
||||||
|
warnings: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,6 +256,7 @@ export async function GetMetricQueryRange(
|
|||||||
},
|
},
|
||||||
warning: undefined,
|
warning: undefined,
|
||||||
params: props,
|
params: props,
|
||||||
|
warnings: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -237,37 +237,6 @@ const addOperatorFormulaColumns = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformColumnTitles = (
|
|
||||||
dynamicColumns: DynamicColumns,
|
|
||||||
): DynamicColumns =>
|
|
||||||
dynamicColumns.map((item) => {
|
|
||||||
if (isFormula(item.field as string)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sameValues = dynamicColumns.filter(
|
|
||||||
(column) => column.title === item.title,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sameValues.length > 1) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
dataIndex: `${item.title} - ${get(
|
|
||||||
item.query,
|
|
||||||
'queryName',
|
|
||||||
get(item.query, 'name', ''),
|
|
||||||
)}`,
|
|
||||||
title: `${item.title} - ${get(
|
|
||||||
item.query,
|
|
||||||
'queryName',
|
|
||||||
get(item.query, 'name', ''),
|
|
||||||
)}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
const processTableColumns = (
|
const processTableColumns = (
|
||||||
table: NonNullable<QueryDataV3['table']>,
|
table: NonNullable<QueryDataV3['table']>,
|
||||||
currentStagedQuery:
|
currentStagedQuery:
|
||||||
@ -369,7 +338,7 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return transformColumnTitles(dynamicColumns);
|
return dynamicColumns;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillEmptyRowCells = (
|
const fillEmptyRowCells = (
|
||||||
@ -634,9 +603,9 @@ const generateData = (
|
|||||||
|
|
||||||
for (let i = 0; i < rowsLength; i += 1) {
|
for (let i = 0; i < rowsLength; i += 1) {
|
||||||
const rowData: RowData = dynamicColumns.reduce((acc, item) => {
|
const rowData: RowData = dynamicColumns.reduce((acc, item) => {
|
||||||
const { dataIndex } = item;
|
const { dataIndex, id } = item;
|
||||||
|
|
||||||
acc[dataIndex] = item.data[i];
|
acc[id || dataIndex] = item.data[i];
|
||||||
acc.key = uuid();
|
acc.key = uuid();
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
@ -655,25 +624,22 @@ const generateTableColumns = (
|
|||||||
const columns: ColumnsType<RowData> = dynamicColumns.reduce<
|
const columns: ColumnsType<RowData> = dynamicColumns.reduce<
|
||||||
ColumnsType<RowData>
|
ColumnsType<RowData>
|
||||||
>((acc, item) => {
|
>((acc, item) => {
|
||||||
|
const dataIndex = item.id || item.dataIndex;
|
||||||
const column: ColumnType<RowData> = {
|
const column: ColumnType<RowData> = {
|
||||||
dataIndex: item.dataIndex,
|
dataIndex,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
width: QUERY_TABLE_CONFIG.width,
|
width: QUERY_TABLE_CONFIG.width,
|
||||||
render: renderColumnCell && renderColumnCell[item.dataIndex],
|
render: renderColumnCell && renderColumnCell[dataIndex],
|
||||||
sorter: (a: RowData, b: RowData): number => {
|
sorter: (a: RowData, b: RowData): number => {
|
||||||
const valueA = Number(
|
const valueA = Number(a[`${dataIndex}_without_unit`] ?? a[dataIndex]);
|
||||||
a[`${item.dataIndex}_without_unit`] ?? a[item.dataIndex],
|
const valueB = Number(b[`${dataIndex}_without_unit`] ?? b[dataIndex]);
|
||||||
);
|
|
||||||
const valueB = Number(
|
|
||||||
b[`${item.dataIndex}_without_unit`] ?? b[item.dataIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isNaN(valueA) && !isNaN(valueB)) {
|
if (!isNaN(valueA) && !isNaN(valueB)) {
|
||||||
return valueA - valueB;
|
return valueA - valueB;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((a[item.dataIndex] as string) || '').localeCompare(
|
return ((a[dataIndex] as string) || '').localeCompare(
|
||||||
(b[item.dataIndex] as string) || '',
|
(b[dataIndex] as string) || '',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -684,6 +650,70 @@ const generateTableColumns = (
|
|||||||
return columns;
|
return columns;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate column unit with fallback logic
|
||||||
|
* New syntax: queryName.expression -> unit
|
||||||
|
* Old syntax: queryName -> unit (fallback)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - New syntax: "A.count()" -> looks for "A.count()" first, then falls back to "A"
|
||||||
|
* - Old syntax: "A" -> looks for "A" directly
|
||||||
|
* - Mixed: "A.avg(test)" -> looks for "A.avg(test)" first, then falls back to "A"
|
||||||
|
*
|
||||||
|
* @param columnKey - The column identifier (could be queryName.expression or queryName)
|
||||||
|
* @param columnUnits - The column units mapping
|
||||||
|
* @returns The unit string or undefined if not found
|
||||||
|
*/
|
||||||
|
export const getColumnUnit = (
|
||||||
|
columnKey: string,
|
||||||
|
columnUnits: Record<string, string>,
|
||||||
|
): string | undefined => {
|
||||||
|
// First try the exact match (new syntax: queryName.expression)
|
||||||
|
if (columnUnits[columnKey]) {
|
||||||
|
return columnUnits[columnKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old syntax: extract queryName from queryName.expression
|
||||||
|
if (columnKey.includes('.')) {
|
||||||
|
const queryName = columnKey.split('.')[0];
|
||||||
|
return columnUnits[queryName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate column width with fallback logic
|
||||||
|
* New syntax: queryName.expression -> width
|
||||||
|
* Old syntax: queryName -> width (fallback)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - New syntax: "A.count()" -> looks for "A.count()" first, then falls back to "A"
|
||||||
|
* - Old syntax: "A" -> looks for "A" directly
|
||||||
|
* - Mixed: "A.avg(test)" -> looks for "A.avg(test)" first, then falls back to "A"
|
||||||
|
*
|
||||||
|
* @param columnKey - The column identifier (could be queryName.expression or queryName)
|
||||||
|
* @param columnWidths - The column widths mapping
|
||||||
|
* @returns The width number or undefined if not found
|
||||||
|
*/
|
||||||
|
export const getColumnWidth = (
|
||||||
|
columnKey: string,
|
||||||
|
columnWidths: Record<string, number>,
|
||||||
|
): number | undefined => {
|
||||||
|
// First try the exact match (new syntax: queryName.expression)
|
||||||
|
if (columnWidths[columnKey]) {
|
||||||
|
return columnWidths[columnKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old syntax: extract queryName from queryName.expression
|
||||||
|
if (columnKey.includes('.')) {
|
||||||
|
const queryName = columnKey.split('.')[0];
|
||||||
|
return columnWidths[queryName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({
|
export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({
|
||||||
query,
|
query,
|
||||||
queryTableData,
|
queryTableData,
|
||||||
|
|||||||
@ -60,9 +60,9 @@ const getSeries = ({
|
|||||||
: baseLabelName;
|
: baseLabelName;
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
colorMapping?.[label] ||
|
colorMapping?.[label || ''] ||
|
||||||
generateColor(
|
generateColor(
|
||||||
label,
|
label || '',
|
||||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -252,7 +252,7 @@ describe('Logs Explorer Tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const queries = queryAllByText(
|
const queries = queryAllByText(
|
||||||
"Enter your filter query (e.g., status = 'error' AND service = 'frontend')",
|
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
|
||||||
);
|
);
|
||||||
expect(queries.length).toBe(1);
|
expect(queries.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -899,9 +899,22 @@ export function QueryBuilderProvider({
|
|||||||
[currentQuery, queryType, maxTime, minTime, redirectWithQueryBuilderData],
|
[currentQuery, queryType, maxTime, minTime, redirectWithQueryBuilderData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname !== currentPathnameRef.current) {
|
||||||
|
currentPathnameRef.current = location.pathname;
|
||||||
|
|
||||||
|
setStagedQuery(null);
|
||||||
|
// reset the last used query to 0 when navigating away from the page
|
||||||
|
setLastUsedQuery(0);
|
||||||
|
}
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
// Separate useEffect to handle initQueryBuilderData after pathname changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!compositeQueryParam) return;
|
if (!compositeQueryParam) return;
|
||||||
|
|
||||||
|
// Only run initQueryBuilderData if we're not in the middle of a pathname change
|
||||||
|
if (location.pathname === currentPathnameRef.current) {
|
||||||
if (stagedQuery && stagedQuery.id === compositeQueryParam.id) {
|
if (stagedQuery && stagedQuery.id === compositeQueryParam.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -916,11 +929,13 @@ export function QueryBuilderProvider({
|
|||||||
} else {
|
} else {
|
||||||
initQueryBuilderData(compositeQueryParam);
|
initQueryBuilderData(compositeQueryParam);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
initQueryBuilderData,
|
initQueryBuilderData,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
compositeQueryParam,
|
compositeQueryParam,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
|
location.pathname,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const resetQuery = (newCurrentQuery?: QueryState): void => {
|
const resetQuery = (newCurrentQuery?: QueryState): void => {
|
||||||
@ -932,16 +947,6 @@ export function QueryBuilderProvider({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (location.pathname !== currentPathnameRef.current) {
|
|
||||||
currentPathnameRef.current = location.pathname;
|
|
||||||
|
|
||||||
setStagedQuery(null);
|
|
||||||
// reset the last used query to 0 when navigating away from the page
|
|
||||||
setLastUsedQuery(0);
|
|
||||||
}
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
const handleOnUnitsChange = useCallback(
|
const handleOnUnitsChange = useCallback(
|
||||||
(unit: string) => {
|
(unit: string) => {
|
||||||
setCurrentQuery((prevState) => ({
|
setCurrentQuery((prevState) => ({
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||||
import { defaultSelectedColumns as defaultTracesSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
import { defaultSelectedColumns as defaultTracesSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
||||||
@ -31,6 +32,16 @@ export function usePreferenceSync({
|
|||||||
setSavedViewPreferences,
|
setSavedViewPreferences,
|
||||||
] = useState<Preferences | null>(null);
|
] = useState<Preferences | null>(null);
|
||||||
|
|
||||||
|
const updateExtraDataSelectColumns = (
|
||||||
|
columns: TelemetryFieldKey[],
|
||||||
|
): TelemetryFieldKey[] | null => {
|
||||||
|
if (!columns) return null;
|
||||||
|
return columns.map((column) => ({
|
||||||
|
...column,
|
||||||
|
name: column.name ?? column.key,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const extraData = viewsData?.data?.data?.find(
|
const extraData = viewsData?.data?.data?.find(
|
||||||
(view) => view.id === savedViewId,
|
(view) => view.id === savedViewId,
|
||||||
@ -40,7 +51,9 @@ export function usePreferenceSync({
|
|||||||
let columns: TelemetryFieldKey[] = [];
|
let columns: TelemetryFieldKey[] = [];
|
||||||
let formatting: FormattingOptions | undefined;
|
let formatting: FormattingOptions | undefined;
|
||||||
if (dataSource === DataSource.LOGS) {
|
if (dataSource === DataSource.LOGS) {
|
||||||
columns = parsedExtraData?.selectColumns || defaultLogsSelectedColumns;
|
columns =
|
||||||
|
updateExtraDataSelectColumns(parsedExtraData?.selectColumns) ||
|
||||||
|
defaultLogsSelectedColumns;
|
||||||
formatting = {
|
formatting = {
|
||||||
maxLines: parsedExtraData?.maxLines ?? 2,
|
maxLines: parsedExtraData?.maxLines ?? 2,
|
||||||
format: parsedExtraData?.format ?? 'table',
|
format: parsedExtraData?.format ?? 'table',
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export interface MetricRangePayloadProps {
|
|||||||
result: QueryData[];
|
result: QueryData[];
|
||||||
resultType: string;
|
resultType: string;
|
||||||
newResult: MetricRangePayloadV3;
|
newResult: MetricRangePayloadV3;
|
||||||
|
warnings?: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ export interface MetricRangePayloadV3 {
|
|||||||
data: {
|
data: {
|
||||||
result: QueryDataV3[];
|
result: QueryDataV3[];
|
||||||
resultType: string;
|
resultType: string;
|
||||||
|
warnings?: string[];
|
||||||
};
|
};
|
||||||
warning?: Warning;
|
warning?: Warning;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
Having as HavingV5,
|
Having as HavingV5,
|
||||||
LogAggregation,
|
LogAggregation,
|
||||||
MetricAggregation,
|
MetricAggregation,
|
||||||
|
QueryFunction,
|
||||||
TraceAggregation,
|
TraceAggregation,
|
||||||
} from '../v5/queryRange';
|
} from '../v5/queryRange';
|
||||||
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
||||||
@ -71,7 +72,7 @@ export type IBuilderQuery = {
|
|||||||
timeAggregation?: string;
|
timeAggregation?: string;
|
||||||
spaceAggregation?: string;
|
spaceAggregation?: string;
|
||||||
temporality?: string;
|
temporality?: string;
|
||||||
functions: QueryFunctionProps[];
|
functions: QueryFunction[];
|
||||||
filter?: Filter;
|
filter?: Filter;
|
||||||
filters?: TagFilter;
|
filters?: TagFilter;
|
||||||
groupBy: BaseAutocompleteData[];
|
groupBy: BaseAutocompleteData[];
|
||||||
|
|||||||
@ -127,6 +127,7 @@ export interface VariableItem {
|
|||||||
|
|
||||||
export interface TelemetryFieldKey {
|
export interface TelemetryFieldKey {
|
||||||
name: string;
|
name: string;
|
||||||
|
key?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
signal?: SignalType;
|
signal?: SignalType;
|
||||||
@ -170,6 +171,7 @@ export interface FunctionArg {
|
|||||||
export interface QueryFunction {
|
export interface QueryFunction {
|
||||||
name: FunctionName;
|
name: FunctionName;
|
||||||
args?: FunctionArg[];
|
args?: FunctionArg[];
|
||||||
|
namedArgs?: Record<string, string | number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Aggregation Types =====================
|
// ===================== Aggregation Types =====================
|
||||||
@ -418,7 +420,7 @@ export type QueryRangeDataV5 =
|
|||||||
|
|
||||||
export interface QueryRangeResponseV5 {
|
export interface QueryRangeResponseV5 {
|
||||||
type: RequestType;
|
type: RequestType;
|
||||||
data: QueryRangeDataV5;
|
data: QueryRangeDataV5 & { warnings?: string[] };
|
||||||
meta: ExecStats;
|
meta: ExecStats;
|
||||||
warning?: Warning;
|
warning?: Warning;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import {
|
import {
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
QueryFunctionProps,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import {
|
import {
|
||||||
BaseBuilderQuery,
|
BaseBuilderQuery,
|
||||||
LogBuilderQuery,
|
LogBuilderQuery,
|
||||||
MetricBuilderQuery,
|
MetricBuilderQuery,
|
||||||
|
QueryFunction,
|
||||||
TraceBuilderQuery,
|
TraceBuilderQuery,
|
||||||
} from 'types/api/v5/queryRange';
|
} from 'types/api/v5/queryRange';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@ -63,6 +63,6 @@ export type UseQueryOperations = (
|
|||||||
handleDeleteQuery: () => void;
|
handleDeleteQuery: () => void;
|
||||||
handleChangeQueryData: HandleChangeQueryData;
|
handleChangeQueryData: HandleChangeQueryData;
|
||||||
handleChangeFormulaData: HandleChangeFormulaData;
|
handleChangeFormulaData: HandleChangeFormulaData;
|
||||||
handleQueryFunctionsUpdates: (functions: QueryFunctionProps[]) => void;
|
handleQueryFunctionsUpdates: (functions: QueryFunction[]) => void;
|
||||||
listOfAdditionalFormulaFilters: string[];
|
listOfAdditionalFormulaFilters: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,895 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-collapsible-if */
|
|
||||||
/* eslint-disable no-continue */
|
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
|
||||||
import { CharStreams, CommonTokenStream } from 'antlr4';
|
|
||||||
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
|
||||||
import FilterQueryParser from 'parser/FilterQueryParser';
|
|
||||||
import {
|
|
||||||
IDetailedError,
|
|
||||||
IQueryContext,
|
|
||||||
IToken,
|
|
||||||
IValidationResult,
|
|
||||||
} from 'types/antlrQueryTypes';
|
|
||||||
|
|
||||||
// Custom error listener to capture ANTLR errors
|
|
||||||
class QueryErrorListener {
|
|
||||||
private errors: IDetailedError[] = [];
|
|
||||||
|
|
||||||
syntaxError(
|
|
||||||
_recognizer: any,
|
|
||||||
offendingSymbol: any,
|
|
||||||
line: number,
|
|
||||||
column: number,
|
|
||||||
msg: string,
|
|
||||||
): void {
|
|
||||||
// For unterminated quotes, we only want to show one error
|
|
||||||
if (this.hasUnterminatedQuoteError() && msg.includes('expecting')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error: IDetailedError = {
|
|
||||||
message: msg,
|
|
||||||
line,
|
|
||||||
column,
|
|
||||||
offendingSymbol: offendingSymbol?.text || String(offendingSymbol),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract expected tokens if available
|
|
||||||
if (msg.includes('expecting')) {
|
|
||||||
const expectedTokens = msg
|
|
||||||
.split('expecting')[1]
|
|
||||||
.trim()
|
|
||||||
.split(',')
|
|
||||||
.map((token) => token.trim());
|
|
||||||
error.expectedTokens = expectedTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a duplicate error (same location and similar message)
|
|
||||||
const isDuplicate = this.errors.some(
|
|
||||||
(e) =>
|
|
||||||
e.line === line &&
|
|
||||||
e.column === column &&
|
|
||||||
this.isSimilarError(e.message, msg),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
this.errors.push(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasUnterminatedQuoteError(): boolean {
|
|
||||||
return this.errors.some(
|
|
||||||
(error) =>
|
|
||||||
error.message.includes('unterminated') ||
|
|
||||||
(error.message.includes('missing') && error.message.includes("'")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSimilarError = (msg1: string, msg2: string): boolean => {
|
|
||||||
// Consider errors similar if they're for the same core issue
|
|
||||||
const normalize = (msg: string): string =>
|
|
||||||
msg.toLowerCase().replace(/['"`]/g, 'quote').replace(/\s+/g, ' ').trim();
|
|
||||||
|
|
||||||
return normalize(msg1) === normalize(msg2);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
reportAmbiguity = (): void => {};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
reportAttemptingFullContext = (): void => {};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
reportContextSensitivity = (): void => {};
|
|
||||||
|
|
||||||
getErrors(): IDetailedError[] {
|
|
||||||
return this.errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasErrors(): boolean {
|
|
||||||
return this.errors.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedErrors(): string[] {
|
|
||||||
return this.errors.map((error) => {
|
|
||||||
const {
|
|
||||||
offendingSymbol,
|
|
||||||
expectedTokens,
|
|
||||||
message: errorMessage,
|
|
||||||
line,
|
|
||||||
column,
|
|
||||||
} = error;
|
|
||||||
|
|
||||||
let message = `Line ${line}:${column} - ${errorMessage}`;
|
|
||||||
|
|
||||||
if (offendingSymbol && offendingSymbol !== 'undefined') {
|
|
||||||
message += `\n Symbol: '${offendingSymbol}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expectedTokens && expectedTokens.length > 0) {
|
|
||||||
message += `\n Expected: ${expectedTokens.join(', ')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validateQuery = (query: string): IValidationResult => {
|
|
||||||
// Empty query is considered invalid
|
|
||||||
if (!query.trim()) {
|
|
||||||
return {
|
|
||||||
isValid: true,
|
|
||||||
message: 'Query is empty',
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const errorListener = new QueryErrorListener();
|
|
||||||
const inputStream = CharStreams.fromString(query);
|
|
||||||
|
|
||||||
// Setup lexer
|
|
||||||
const lexer = new FilterQueryLexer(inputStream);
|
|
||||||
lexer.removeErrorListeners(); // Remove default error listeners
|
|
||||||
lexer.addErrorListener(errorListener);
|
|
||||||
|
|
||||||
// Setup parser
|
|
||||||
const tokenStream = new CommonTokenStream(lexer);
|
|
||||||
const parser = new FilterQueryParser(tokenStream);
|
|
||||||
parser.removeErrorListeners(); // Remove default error listeners
|
|
||||||
parser.addErrorListener(errorListener);
|
|
||||||
|
|
||||||
// Try parsing
|
|
||||||
parser.query();
|
|
||||||
|
|
||||||
// Check if any errors were captured
|
|
||||||
if (errorListener.hasErrors()) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
message: 'Query syntax error',
|
|
||||||
errors: errorListener.getErrors(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: true,
|
|
||||||
message: 'Query is valid!',
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : 'Invalid query syntax';
|
|
||||||
|
|
||||||
const detailedError: IDetailedError = {
|
|
||||||
message: errorMessage,
|
|
||||||
line: 0,
|
|
||||||
column: 0,
|
|
||||||
offendingSymbol: '',
|
|
||||||
expectedTokens: [],
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
message: 'Invalid query syntax',
|
|
||||||
errors: [detailedError],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to find key-operator-value triplets in token stream
|
|
||||||
export function findKeyOperatorValueTriplet(
|
|
||||||
allTokens: IToken[],
|
|
||||||
currentToken: IToken,
|
|
||||||
isInKey: boolean,
|
|
||||||
isInOperator: boolean,
|
|
||||||
isInValue: boolean,
|
|
||||||
): { keyToken?: string; operatorToken?: string; valueToken?: string } {
|
|
||||||
// Find current token index in allTokens
|
|
||||||
let currentTokenIndex = -1;
|
|
||||||
for (let i = 0; i < allTokens.length; i++) {
|
|
||||||
if (
|
|
||||||
allTokens[i].start === currentToken.start &&
|
|
||||||
allTokens[i].stop === currentToken.stop &&
|
|
||||||
allTokens[i].type === currentToken.type
|
|
||||||
) {
|
|
||||||
currentTokenIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTokenIndex === -1) return {};
|
|
||||||
|
|
||||||
// Initialize result with empty object
|
|
||||||
const result: {
|
|
||||||
keyToken?: string;
|
|
||||||
operatorToken?: string;
|
|
||||||
valueToken?: string;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
if (isInKey) {
|
|
||||||
// When in key context, we only know the key
|
|
||||||
result.keyToken = currentToken.text;
|
|
||||||
} else if (isInOperator) {
|
|
||||||
// When in operator context, we know the operator and can find the preceding key
|
|
||||||
result.operatorToken = currentToken.text;
|
|
||||||
|
|
||||||
// Look backward for key
|
|
||||||
for (let i = currentTokenIndex - 1; i >= 0; i--) {
|
|
||||||
const token = allTokens[i];
|
|
||||||
// Skip whitespace and other hidden channel tokens
|
|
||||||
if (token.channel !== 0) continue;
|
|
||||||
|
|
||||||
if (token.type === FilterQueryLexer.KEY) {
|
|
||||||
result.keyToken = token.text;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isInValue) {
|
|
||||||
// When in value context, we know the value and can find the preceding operator and key
|
|
||||||
result.valueToken = currentToken.text;
|
|
||||||
|
|
||||||
let foundOperator = false;
|
|
||||||
|
|
||||||
// Look backward for operator and key
|
|
||||||
for (let i = currentTokenIndex - 1; i >= 0; i--) {
|
|
||||||
const token = allTokens[i];
|
|
||||||
// Skip whitespace and other hidden channel tokens
|
|
||||||
if (token.channel !== 0) continue;
|
|
||||||
|
|
||||||
// If we haven't found an operator yet, check for operator
|
|
||||||
if (
|
|
||||||
!foundOperator &&
|
|
||||||
[
|
|
||||||
FilterQueryLexer.EQUALS,
|
|
||||||
FilterQueryLexer.NOT_EQUALS,
|
|
||||||
FilterQueryLexer.NEQ,
|
|
||||||
FilterQueryLexer.LT,
|
|
||||||
FilterQueryLexer.LE,
|
|
||||||
FilterQueryLexer.GT,
|
|
||||||
FilterQueryLexer.GE,
|
|
||||||
FilterQueryLexer.LIKE,
|
|
||||||
// FilterQueryLexer.NOT_LIKE,
|
|
||||||
FilterQueryLexer.ILIKE,
|
|
||||||
// FilterQueryLexer.NOT_ILIKE,
|
|
||||||
FilterQueryLexer.BETWEEN,
|
|
||||||
FilterQueryLexer.EXISTS,
|
|
||||||
FilterQueryLexer.REGEXP,
|
|
||||||
FilterQueryLexer.CONTAINS,
|
|
||||||
FilterQueryLexer.IN,
|
|
||||||
FilterQueryLexer.NOT,
|
|
||||||
].includes(token.type)
|
|
||||||
) {
|
|
||||||
result.operatorToken = token.text;
|
|
||||||
foundOperator = true;
|
|
||||||
}
|
|
||||||
// If we already found an operator and this is a key, record it
|
|
||||||
else if (foundOperator && token.type === FilterQueryLexer.KEY) {
|
|
||||||
result.keyToken = token.text;
|
|
||||||
break; // We found our triplet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getQueryContextAtCursor(
|
|
||||||
query: string,
|
|
||||||
cursorIndex: number,
|
|
||||||
): IQueryContext {
|
|
||||||
try {
|
|
||||||
// Create input stream and lexer
|
|
||||||
const input = query || '';
|
|
||||||
const chars = CharStreams.fromString(input);
|
|
||||||
const lexer = new FilterQueryLexer(chars);
|
|
||||||
|
|
||||||
// Create token stream and force token generation
|
|
||||||
const tokenStream = new CommonTokenStream(lexer);
|
|
||||||
tokenStream.fill();
|
|
||||||
|
|
||||||
// Get all tokens including whitespace
|
|
||||||
const allTokens = tokenStream.tokens as IToken[];
|
|
||||||
|
|
||||||
// Find exact token at cursor, including whitespace
|
|
||||||
let exactToken: IToken | null = null;
|
|
||||||
let previousToken: IToken | null = null;
|
|
||||||
let nextToken: IToken | null = null;
|
|
||||||
|
|
||||||
// Handle cursor at the very end of input
|
|
||||||
if (cursorIndex === input.length && allTokens.length > 0) {
|
|
||||||
const lastRealToken = allTokens
|
|
||||||
.filter((t) => t.type !== FilterQueryLexer.EOF)
|
|
||||||
.pop();
|
|
||||||
if (lastRealToken) {
|
|
||||||
exactToken = lastRealToken;
|
|
||||||
previousToken =
|
|
||||||
allTokens.filter((t) => t.stop < lastRealToken.start).pop() || null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal token search
|
|
||||||
for (let i = 0; i < allTokens.length; i++) {
|
|
||||||
const token = allTokens[i];
|
|
||||||
// Skip EOF token in normal search
|
|
||||||
if (token.type === FilterQueryLexer.EOF) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if cursor is within token bounds (inclusive)
|
|
||||||
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
|
|
||||||
exactToken = token;
|
|
||||||
previousToken = i > 0 ? allTokens[i - 1] : null;
|
|
||||||
nextToken = i < allTokens.length - 1 ? allTokens[i + 1] : null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cursor is between tokens, find surrounding tokens
|
|
||||||
if (!exactToken) {
|
|
||||||
for (let i = 0; i < allTokens.length - 1; i++) {
|
|
||||||
const current = allTokens[i];
|
|
||||||
const next = allTokens[i + 1];
|
|
||||||
if (current.type === FilterQueryLexer.EOF) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (next.type === FilterQueryLexer.EOF) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.stop + 1 < cursorIndex && cursorIndex < next.start) {
|
|
||||||
previousToken = current;
|
|
||||||
nextToken = next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the context based on cursor position and surrounding tokens
|
|
||||||
let currentToken: IToken | null = null;
|
|
||||||
|
|
||||||
if (exactToken) {
|
|
||||||
// If cursor is in a non-whitespace token, use that
|
|
||||||
if (exactToken.channel === 0) {
|
|
||||||
currentToken = exactToken;
|
|
||||||
} else {
|
|
||||||
// If in whitespace, use the previous non-whitespace token
|
|
||||||
currentToken = previousToken?.channel === 0 ? previousToken : nextToken;
|
|
||||||
}
|
|
||||||
} else if (previousToken?.channel === 0) {
|
|
||||||
// If between tokens, prefer the previous non-whitespace token
|
|
||||||
currentToken = previousToken;
|
|
||||||
} else if (nextToken?.channel === 0) {
|
|
||||||
// Otherwise use the next non-whitespace token
|
|
||||||
currentToken = nextToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still no token (empty query or all whitespace), return default context
|
|
||||||
if (!currentToken) {
|
|
||||||
// Handle transitions based on spaces and current state
|
|
||||||
if (query.trim() === '') {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: true, // Default to key context when input is empty
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if the current token is a conjunction (AND or OR)
|
|
||||||
const isInConjunction = [FilterQueryLexer.AND, FilterQueryLexer.OR].includes(
|
|
||||||
currentToken.type,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine if the current token is a parenthesis or bracket
|
|
||||||
const isInParenthesis = [
|
|
||||||
FilterQueryLexer.LPAREN,
|
|
||||||
FilterQueryLexer.RPAREN,
|
|
||||||
FilterQueryLexer.LBRACK,
|
|
||||||
FilterQueryLexer.RBRACK,
|
|
||||||
].includes(currentToken.type);
|
|
||||||
|
|
||||||
// Determine the context based on the token type
|
|
||||||
const isInValue = [
|
|
||||||
FilterQueryLexer.QUOTED_TEXT,
|
|
||||||
FilterQueryLexer.NUMBER,
|
|
||||||
FilterQueryLexer.BOOL,
|
|
||||||
].includes(currentToken.type);
|
|
||||||
|
|
||||||
const isInKey = currentToken.type === FilterQueryLexer.KEY;
|
|
||||||
|
|
||||||
const isInNegation = currentToken.type === FilterQueryLexer.NOT;
|
|
||||||
|
|
||||||
const isInOperator = [
|
|
||||||
FilterQueryLexer.EQUALS,
|
|
||||||
FilterQueryLexer.NOT_EQUALS,
|
|
||||||
FilterQueryLexer.NEQ,
|
|
||||||
FilterQueryLexer.LT,
|
|
||||||
FilterQueryLexer.LE,
|
|
||||||
FilterQueryLexer.GT,
|
|
||||||
FilterQueryLexer.GE,
|
|
||||||
FilterQueryLexer.LIKE,
|
|
||||||
// FilterQueryLexer.NOT_LIKE,
|
|
||||||
FilterQueryLexer.ILIKE,
|
|
||||||
// FilterQueryLexer.NOT_ILIKE,
|
|
||||||
FilterQueryLexer.BETWEEN,
|
|
||||||
FilterQueryLexer.EXISTS,
|
|
||||||
FilterQueryLexer.REGEXP,
|
|
||||||
FilterQueryLexer.CONTAINS,
|
|
||||||
FilterQueryLexer.IN,
|
|
||||||
FilterQueryLexer.NOT,
|
|
||||||
].includes(currentToken.type);
|
|
||||||
|
|
||||||
const isInFunction = [
|
|
||||||
FilterQueryLexer.HAS,
|
|
||||||
FilterQueryLexer.HASANY,
|
|
||||||
FilterQueryLexer.HASALL,
|
|
||||||
// FilterQueryLexer.HASNONE,
|
|
||||||
].includes(currentToken.type);
|
|
||||||
|
|
||||||
// Get the context-related tokens (key, operator, value)
|
|
||||||
const relationTokens = findKeyOperatorValueTriplet(
|
|
||||||
allTokens,
|
|
||||||
currentToken,
|
|
||||||
isInKey,
|
|
||||||
isInOperator,
|
|
||||||
isInValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle transitions based on spaces
|
|
||||||
// When a user adds a space after a token, change the context accordingly
|
|
||||||
if (
|
|
||||||
currentToken &&
|
|
||||||
cursorIndex === currentToken.stop + 2 &&
|
|
||||||
query[currentToken.stop + 1] === ' '
|
|
||||||
) {
|
|
||||||
// User added a space right after this token
|
|
||||||
|
|
||||||
if (isInKey) {
|
|
||||||
// After a key + space, we should be in operator context
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: true,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInOperator) {
|
|
||||||
// After an operator + space, we should be in value context
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: true,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInValue) {
|
|
||||||
// After a value + space, we should be in conjunction context
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInConjunction) {
|
|
||||||
// After a conjunction + space, we should be in key context again
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInKey: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInParenthesis) {
|
|
||||||
// After a parenthesis/bracket + space, determine context based on which bracket
|
|
||||||
if (currentToken.type === FilterQueryLexer.LPAREN) {
|
|
||||||
// After an opening parenthesis + space, we should be in key context
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInKey: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentToken.type === FilterQueryLexer.RPAREN ||
|
|
||||||
currentToken.type === FilterQueryLexer.RBRACK
|
|
||||||
) {
|
|
||||||
// After a closing parenthesis/bracket + space, we should be in conjunction context
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken.type === FilterQueryLexer.LBRACK) {
|
|
||||||
// After an opening bracket + space, we should be in value context (for arrays)
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: true,
|
|
||||||
isInNegation: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add logic for context detection that works for both forward and backward navigation
|
|
||||||
// This handles both cases: when user is typing forward and when they're moving backward
|
|
||||||
if (previousToken && nextToken) {
|
|
||||||
// Determine context based on token sequence pattern
|
|
||||||
|
|
||||||
// Key -> Operator -> Value -> Conjunction pattern detection
|
|
||||||
if (isInKey && nextToken.type === FilterQueryLexer.EQUALS) {
|
|
||||||
// When cursor is on a key and next token is an operator
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: true,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInNegation && nextToken.type === FilterQueryLexer.NOT) {
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isInOperator &&
|
|
||||||
previousToken.type === FilterQueryLexer.KEY &&
|
|
||||||
(nextToken.type === FilterQueryLexer.QUOTED_TEXT ||
|
|
||||||
nextToken.type === FilterQueryLexer.NUMBER ||
|
|
||||||
nextToken.type === FilterQueryLexer.BOOL)
|
|
||||||
) {
|
|
||||||
// When cursor is on an operator between a key and value
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: true,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isInValue &&
|
|
||||||
previousToken.type !== FilterQueryLexer.AND &&
|
|
||||||
previousToken.type !== FilterQueryLexer.OR &&
|
|
||||||
(nextToken.type === FilterQueryLexer.AND ||
|
|
||||||
nextToken.type === FilterQueryLexer.OR)
|
|
||||||
) {
|
|
||||||
// When cursor is on a value and next token is a conjunction
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: true,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isInConjunction &&
|
|
||||||
(previousToken.type === FilterQueryLexer.QUOTED_TEXT ||
|
|
||||||
previousToken.type === FilterQueryLexer.NUMBER ||
|
|
||||||
previousToken.type === FilterQueryLexer.BOOL) &&
|
|
||||||
nextToken.type === FilterQueryLexer.KEY
|
|
||||||
) {
|
|
||||||
// When cursor is on a conjunction between a value and a key
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're in between tokens (no exact token match), use next token type to determine context
|
|
||||||
if (!exactToken && nextToken) {
|
|
||||||
if (nextToken.type === FilterQueryLexer.KEY) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: true,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextToken.type === FilterQueryLexer.NOT) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
FilterQueryLexer.EQUALS,
|
|
||||||
FilterQueryLexer.NOT_EQUALS,
|
|
||||||
FilterQueryLexer.GT,
|
|
||||||
FilterQueryLexer.LT,
|
|
||||||
FilterQueryLexer.GE,
|
|
||||||
FilterQueryLexer.LE,
|
|
||||||
].includes(nextToken.type)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: true,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
FilterQueryLexer.QUOTED_TEXT,
|
|
||||||
FilterQueryLexer.NUMBER,
|
|
||||||
FilterQueryLexer.BOOL,
|
|
||||||
].includes(nextToken.type)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInNegation: false,
|
|
||||||
isInValue: true,
|
|
||||||
isInKey: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([FilterQueryLexer.AND, FilterQueryLexer.OR].includes(nextToken.type)) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add case for parentheses and brackets
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
FilterQueryLexer.LPAREN,
|
|
||||||
FilterQueryLexer.RPAREN,
|
|
||||||
FilterQueryLexer.LBRACK,
|
|
||||||
FilterQueryLexer.RBRACK,
|
|
||||||
].includes(nextToken.type)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: true,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to default context detection based on current token
|
|
||||||
return {
|
|
||||||
tokenType: currentToken.type,
|
|
||||||
text: currentToken.text,
|
|
||||||
start: currentToken.start,
|
|
||||||
stop: currentToken.stop,
|
|
||||||
currentToken: currentToken.text,
|
|
||||||
isInValue,
|
|
||||||
isInKey,
|
|
||||||
isInNegation,
|
|
||||||
isInOperator,
|
|
||||||
isInFunction,
|
|
||||||
isInConjunction,
|
|
||||||
isInParenthesis,
|
|
||||||
...relationTokens, // Include related tokens
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getQueryContextAtCursor:', error);
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInValue: false,
|
|
||||||
isInKey: false,
|
|
||||||
isInNegation: false,
|
|
||||||
isInOperator: false,
|
|
||||||
isInFunction: false,
|
|
||||||
isInConjunction: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -31,6 +31,7 @@ export const convertBuilderQueryToIBuilderQuery = (
|
|||||||
|
|
||||||
const result: IBuilderQuery = ({
|
const result: IBuilderQuery = ({
|
||||||
...builderQuery,
|
...builderQuery,
|
||||||
|
queryName: builderQuery.name,
|
||||||
dataSource,
|
dataSource,
|
||||||
legend: builderQuery.legend,
|
legend: builderQuery.legend,
|
||||||
groupBy: builderQuery.groupBy?.map((group) => ({
|
groupBy: builderQuery.groupBy?.map((group) => ({
|
||||||
|
|||||||
86
frontend/src/utils/functionNameNormalizer.test.ts
Normal file
86
frontend/src/utils/functionNameNormalizer.test.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { normalizeFunctionName } from './functionNameNormalizer';
|
||||||
|
|
||||||
|
describe('functionNameNormalizer', () => {
|
||||||
|
describe('normalizeFunctionName', () => {
|
||||||
|
it('should normalize timeshift to timeShift', () => {
|
||||||
|
expect(normalizeFunctionName('timeshift')).toBe(
|
||||||
|
QueryFunctionsTypes.TIME_SHIFT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize TIMESHIFT to timeShift', () => {
|
||||||
|
expect(normalizeFunctionName('TIMESHIFT')).toBe(
|
||||||
|
QueryFunctionsTypes.TIME_SHIFT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize TimeShift to timeShift', () => {
|
||||||
|
expect(normalizeFunctionName('TimeShift')).toBe(
|
||||||
|
QueryFunctionsTypes.TIME_SHIFT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original value if no normalization needed', () => {
|
||||||
|
expect(normalizeFunctionName('timeShift')).toBe('timeShift');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unknown function names', () => {
|
||||||
|
expect(normalizeFunctionName('unknownFunction')).toBe('unknownFunction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize other function names', () => {
|
||||||
|
expect(normalizeFunctionName('cutoffmin')).toBe(
|
||||||
|
QueryFunctionsTypes.CUTOFF_MIN,
|
||||||
|
);
|
||||||
|
expect(normalizeFunctionName('cutoffmax')).toBe(
|
||||||
|
QueryFunctionsTypes.CUTOFF_MAX,
|
||||||
|
);
|
||||||
|
expect(normalizeFunctionName('clampmin')).toBe(
|
||||||
|
QueryFunctionsTypes.CLAMP_MIN,
|
||||||
|
);
|
||||||
|
expect(normalizeFunctionName('clampmax')).toBe(
|
||||||
|
QueryFunctionsTypes.CLAMP_MAX,
|
||||||
|
);
|
||||||
|
expect(normalizeFunctionName('absolut')).toBe(QueryFunctionsTypes.ABSOLUTE);
|
||||||
|
expect(normalizeFunctionName('runningdiff')).toBe(
|
||||||
|
QueryFunctionsTypes.RUNNING_DIFF,
|
||||||
|
);
|
||||||
|
expect(normalizeFunctionName('log2')).toBe(QueryFunctionsTypes.LOG_2);
|
||||||
|
expect(normalizeFunctionName('log10')).toBe(QueryFunctionsTypes.LOG_10);
|
||||||
|
expect(normalizeFunctionName('cumulativesum')).toBe(
|
||||||
|
QueryFunctionsTypes.CUMULATIVE_SUM,
|
||||||
|
);
|
||||||
|
expect(normalizeFunctionName('ewma3')).toBe(QueryFunctionsTypes.EWMA_3);
|
||||||
|
expect(normalizeFunctionName('ewma5')).toBe(QueryFunctionsTypes.EWMA_5);
|
||||||
|
expect(normalizeFunctionName('ewma7')).toBe(QueryFunctionsTypes.EWMA_7);
|
||||||
|
expect(normalizeFunctionName('median3')).toBe(QueryFunctionsTypes.MEDIAN_3);
|
||||||
|
expect(normalizeFunctionName('median5')).toBe(QueryFunctionsTypes.MEDIAN_5);
|
||||||
|
expect(normalizeFunctionName('median7')).toBe(QueryFunctionsTypes.MEDIAN_7);
|
||||||
|
expect(normalizeFunctionName('anomaly')).toBe(QueryFunctionsTypes.ANOMALY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('function argument handling', () => {
|
||||||
|
it('should handle string arguments correctly', () => {
|
||||||
|
const func = {
|
||||||
|
name: 'timeshift',
|
||||||
|
args: ['5m'],
|
||||||
|
};
|
||||||
|
const normalizedName = normalizeFunctionName(func.name);
|
||||||
|
expect(normalizedName).toBe(QueryFunctionsTypes.TIME_SHIFT);
|
||||||
|
expect(func.args[0]).toBe('5m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle numeric arguments correctly', () => {
|
||||||
|
const func = {
|
||||||
|
name: 'cutoffmin',
|
||||||
|
args: [100],
|
||||||
|
};
|
||||||
|
const normalizedName = normalizeFunctionName(func.name);
|
||||||
|
expect(normalizedName).toBe(QueryFunctionsTypes.CUTOFF_MIN);
|
||||||
|
expect(func.args[0]).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
37
frontend/src/utils/functionNameNormalizer.ts
Normal file
37
frontend/src/utils/functionNameNormalizer.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes function names from backend responses to match frontend expectations
|
||||||
|
* Backend returns lowercase function names (e.g., 'timeshift') while frontend expects camelCase (e.g., 'timeShift')
|
||||||
|
*/
|
||||||
|
export const normalizeFunctionName = (functionName: string): string => {
|
||||||
|
// Create a mapping from lowercase to expected camelCase function names
|
||||||
|
const functionNameMap: Record<string, string> = {
|
||||||
|
// Time shift function
|
||||||
|
timeshift: QueryFunctionsTypes.TIME_SHIFT,
|
||||||
|
|
||||||
|
// Other functions that might have case sensitivity issues
|
||||||
|
cutoffmin: QueryFunctionsTypes.CUTOFF_MIN,
|
||||||
|
cutoffmax: QueryFunctionsTypes.CUTOFF_MAX,
|
||||||
|
clampmin: QueryFunctionsTypes.CLAMP_MIN,
|
||||||
|
clampmax: QueryFunctionsTypes.CLAMP_MAX,
|
||||||
|
absolut: QueryFunctionsTypes.ABSOLUTE,
|
||||||
|
runningdiff: QueryFunctionsTypes.RUNNING_DIFF,
|
||||||
|
log2: QueryFunctionsTypes.LOG_2,
|
||||||
|
log10: QueryFunctionsTypes.LOG_10,
|
||||||
|
cumulativesum: QueryFunctionsTypes.CUMULATIVE_SUM,
|
||||||
|
ewma3: QueryFunctionsTypes.EWMA_3,
|
||||||
|
ewma5: QueryFunctionsTypes.EWMA_5,
|
||||||
|
ewma7: QueryFunctionsTypes.EWMA_7,
|
||||||
|
median3: QueryFunctionsTypes.MEDIAN_3,
|
||||||
|
median5: QueryFunctionsTypes.MEDIAN_5,
|
||||||
|
median7: QueryFunctionsTypes.MEDIAN_7,
|
||||||
|
anomaly: QueryFunctionsTypes.ANOMALY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to lowercase for case-insensitive matching
|
||||||
|
const normalizedName = functionName.toLowerCase();
|
||||||
|
|
||||||
|
// Return the mapped function name or the original if no mapping exists
|
||||||
|
return functionNameMap[normalizedName] || functionName;
|
||||||
|
};
|
||||||
171
frontend/src/utils/queryValidationUtils.ts
Normal file
171
frontend/src/utils/queryValidationUtils.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
/* eslint-disable sonarjs/no-collapsible-if */
|
||||||
|
/* eslint-disable no-continue */
|
||||||
|
import { CharStreams, CommonTokenStream } from 'antlr4';
|
||||||
|
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
||||||
|
import FilterQueryParser from 'parser/FilterQueryParser';
|
||||||
|
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
|
||||||
|
|
||||||
|
// Custom error listener to capture ANTLR errors
|
||||||
|
class QueryErrorListener {
|
||||||
|
private errors: IDetailedError[] = [];
|
||||||
|
|
||||||
|
syntaxError(
|
||||||
|
_recognizer: any,
|
||||||
|
offendingSymbol: any,
|
||||||
|
line: number,
|
||||||
|
column: number,
|
||||||
|
msg: string,
|
||||||
|
): void {
|
||||||
|
// For unterminated quotes, we only want to show one error
|
||||||
|
if (this.hasUnterminatedQuoteError() && msg.includes('expecting')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error: IDetailedError = {
|
||||||
|
message: msg,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
offendingSymbol: offendingSymbol?.text || String(offendingSymbol),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract expected tokens if available
|
||||||
|
if (msg.includes('expecting')) {
|
||||||
|
const expectedTokens = msg
|
||||||
|
.split('expecting')[1]
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.map((token) => token.trim());
|
||||||
|
error.expectedTokens = expectedTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a duplicate error (same location and similar message)
|
||||||
|
const isDuplicate = this.errors.some(
|
||||||
|
(e) =>
|
||||||
|
e.line === line &&
|
||||||
|
e.column === column &&
|
||||||
|
this.isSimilarError(e.message, msg),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
this.errors.push(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasUnterminatedQuoteError(): boolean {
|
||||||
|
return this.errors.some(
|
||||||
|
(error) =>
|
||||||
|
error.message.includes('unterminated') ||
|
||||||
|
(error.message.includes('missing') && error.message.includes("'")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSimilarError = (msg1: string, msg2: string): boolean => {
|
||||||
|
// Consider errors similar if they're for the same core issue
|
||||||
|
const normalize = (msg: string): string =>
|
||||||
|
msg.toLowerCase().replace(/['"`]/g, 'quote').replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
return normalize(msg1) === normalize(msg2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
reportAmbiguity = (): void => {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
reportAttemptingFullContext = (): void => {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
reportContextSensitivity = (): void => {};
|
||||||
|
|
||||||
|
getErrors(): IDetailedError[] {
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasErrors(): boolean {
|
||||||
|
return this.errors.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormattedErrors(): string[] {
|
||||||
|
return this.errors.map((error) => {
|
||||||
|
const {
|
||||||
|
offendingSymbol,
|
||||||
|
expectedTokens,
|
||||||
|
message: errorMessage,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
} = error;
|
||||||
|
|
||||||
|
let message = `Line ${line}:${column} - ${errorMessage}`;
|
||||||
|
|
||||||
|
if (offendingSymbol && offendingSymbol !== 'undefined') {
|
||||||
|
message += `\n Symbol: '${offendingSymbol}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedTokens && expectedTokens.length > 0) {
|
||||||
|
message += `\n Expected: ${expectedTokens.join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateQuery = (query: string): IValidationResult => {
|
||||||
|
// Empty query is considered valid
|
||||||
|
if (!query.trim()) {
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
message: 'Query is empty',
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorListener = new QueryErrorListener();
|
||||||
|
const inputStream = CharStreams.fromString(query);
|
||||||
|
|
||||||
|
// Setup lexer
|
||||||
|
const lexer = new FilterQueryLexer(inputStream);
|
||||||
|
lexer.removeErrorListeners(); // Remove default error listeners
|
||||||
|
lexer.addErrorListener(errorListener);
|
||||||
|
|
||||||
|
// Setup parser
|
||||||
|
const tokenStream = new CommonTokenStream(lexer);
|
||||||
|
const parser = new FilterQueryParser(tokenStream);
|
||||||
|
parser.removeErrorListeners(); // Remove default error listeners
|
||||||
|
parser.addErrorListener(errorListener);
|
||||||
|
|
||||||
|
// Try parsing
|
||||||
|
parser.query();
|
||||||
|
|
||||||
|
// Check if any errors were captured
|
||||||
|
if (errorListener.hasErrors()) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: 'Query syntax error',
|
||||||
|
errors: errorListener.getErrors(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
message: 'Query is valid!',
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Invalid query syntax';
|
||||||
|
|
||||||
|
const detailedError: IDetailedError = {
|
||||||
|
message: errorMessage,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
offendingSymbol: '',
|
||||||
|
expectedTokens: [],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: 'Invalid query syntax',
|
||||||
|
errors: [detailedError],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,8 @@
|
|||||||
import { NON_VALUE_OPERATORS } from 'constants/antlrQueryConstants';
|
import {
|
||||||
|
NON_VALUE_OPERATORS,
|
||||||
|
OPERATORS,
|
||||||
|
QUERY_BUILDER_FUNCTIONS,
|
||||||
|
} from 'constants/antlrQueryConstants';
|
||||||
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
||||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||||
|
|
||||||
@ -91,3 +95,21 @@ export function isQueryPairComplete(queryPair: Partial<IQueryPair>): boolean {
|
|||||||
// For other operators, we need a value as well
|
// For other operators, we need a value as well
|
||||||
return Boolean(queryPair.key && queryPair.operator && queryPair.value);
|
return Boolean(queryPair.key && queryPair.operator && queryPair.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFunctionOperator(operator: string): boolean {
|
||||||
|
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
|
||||||
|
|
||||||
|
const sanitizedOperator = operator.trim();
|
||||||
|
// Check if it's a direct function operator
|
||||||
|
if (functionOperators.includes(sanitizedOperator)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a NOT function operator (e.g., "NOT has")
|
||||||
|
if (sanitizedOperator.toUpperCase().startsWith(OPERATORS.NOT)) {
|
||||||
|
const operatorWithoutNot = sanitizedOperator.substring(4).toLowerCase();
|
||||||
|
return functionOperators.includes(operatorWithoutNot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user