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
This commit is contained in:
@@ -28,17 +28,18 @@ function getColName(
|
||||
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
|
||||
const isSingleAggregation = aggregationsCount === 1;
|
||||
|
||||
if (aggregationsCount === 0) {
|
||||
return legend || col.queryName;
|
||||
}
|
||||
// Single aggregation: Priority is alias > legend > expression
|
||||
if (isSingleAggregation) {
|
||||
return alias || legend || expression;
|
||||
if (aggregationsCount > 0) {
|
||||
// Single aggregation: Priority is alias > legend > expression
|
||||
if (isSingleAggregation) {
|
||||
return alias || legend || expression || col.queryName;
|
||||
}
|
||||
|
||||
// Multiple aggregations: Each follows single rules BUT never shows legend
|
||||
// Priority: alias > expression (legend is ignored for multiple aggregations)
|
||||
return alias || expression || col.queryName;
|
||||
}
|
||||
|
||||
// Multiple aggregations: Each follows single rules BUT never shows legend
|
||||
// Priority: alias > expression (legend is ignored for multiple aggregations)
|
||||
return alias || expression;
|
||||
return legend || col.queryName;
|
||||
}
|
||||
|
||||
function getColId(
|
||||
@@ -51,7 +52,12 @@ function getColId(
|
||||
const aggregation =
|
||||
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
|
||||
const expression = aggregation?.expression || '';
|
||||
return expression ? `${col.queryName}.${expression}` : col.queryName;
|
||||
|
||||
if (aggregationPerQuery?.[col.queryName]?.length > 0 && expression) {
|
||||
return `${col.queryName}.${expression}`;
|
||||
}
|
||||
|
||||
return col.queryName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,6 +376,7 @@ export function convertV5ResponseToLegacy(
|
||||
legendMap,
|
||||
aggregationPerQuery,
|
||||
);
|
||||
|
||||
return {
|
||||
...v5Response,
|
||||
payload: {
|
||||
|
||||
@@ -5,10 +5,7 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
@@ -30,6 +27,7 @@ import {
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
|
||||
|
||||
type PrepareQueryRangePayloadV5Result = {
|
||||
queryPayload: QueryRangePayloadV5;
|
||||
@@ -123,17 +121,21 @@ function createBaseSpec(
|
||||
functions: isEmpty(queryData.functions)
|
||||
? undefined
|
||||
: queryData.functions.map(
|
||||
(func: QueryFunctionProps): QueryFunction => ({
|
||||
name: func.name as FunctionName,
|
||||
args: isEmpty(func.namedArgs)
|
||||
? func.args.map((arg) => ({
|
||||
value: arg,
|
||||
}))
|
||||
: Object.entries(func.namedArgs).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
}),
|
||||
(func: QueryFunction): QueryFunction => {
|
||||
// Normalize function name to handle case sensitivity
|
||||
const normalizedName = normalizeFunctionName(func?.name);
|
||||
return {
|
||||
name: normalizedName as FunctionName,
|
||||
args: isEmpty(func.namedArgs)
|
||||
? func.args?.map((arg) => ({
|
||||
value: arg?.value,
|
||||
}))
|
||||
: Object.entries(func?.namedArgs || {}).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
};
|
||||
},
|
||||
),
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
|
||||
@@ -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
|
||||
export type AggregationOption = { func: string; arg: string };
|
||||
@@ -6,8 +13,12 @@ export type AggregationOption = { func: string; arg: string };
|
||||
interface QueryBuilderV2ContextType {
|
||||
searchText: string;
|
||||
setSearchText: (text: string) => void;
|
||||
aggregationOptions: AggregationOption[];
|
||||
setAggregationOptions: (options: AggregationOption[]) => void;
|
||||
aggregationOptionsMap: Record<string, AggregationOption[]>;
|
||||
setAggregationOptions: (
|
||||
queryName: string,
|
||||
options: AggregationOption[],
|
||||
) => void;
|
||||
getAggregationOptions: (queryName: string) => AggregationOption[];
|
||||
aggregationInterval: string;
|
||||
setAggregationInterval: (interval: string) => void;
|
||||
queryAddValues: any; // Replace 'any' with a more specific type if available
|
||||
@@ -24,26 +35,50 @@ export function QueryBuilderV2Provider({
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [aggregationOptions, setAggregationOptions] = useState<
|
||||
AggregationOption[]
|
||||
>([]);
|
||||
const [aggregationOptionsMap, setAggregationOptionsMap] = useState<
|
||||
Record<string, AggregationOption[]>
|
||||
>({});
|
||||
const [aggregationInterval, setAggregationInterval] = useState('');
|
||||
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 (
|
||||
<QueryBuilderV2Context.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
searchText,
|
||||
setSearchText,
|
||||
aggregationOptions,
|
||||
aggregationOptionsMap,
|
||||
setAggregationOptions,
|
||||
getAggregationOptions,
|
||||
aggregationInterval,
|
||||
setAggregationInterval,
|
||||
queryAddValues,
|
||||
setQueryAddValues,
|
||||
}),
|
||||
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
|
||||
[
|
||||
searchText,
|
||||
aggregationOptionsMap,
|
||||
aggregationInterval,
|
||||
queryAddValues,
|
||||
getAggregationOptions,
|
||||
setAggregationOptions,
|
||||
],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -45,24 +45,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
);
|
||||
|
||||
const isHistogram = useMemo(
|
||||
() =>
|
||||
query.aggregateAttribute?.type === ATTRIBUTE_TYPES.HISTOGRAM ||
|
||||
queryAggregation.metricName?.endsWith('.bucket'),
|
||||
[query.aggregateAttribute?.type, queryAggregation.metricName],
|
||||
() => query.aggregateAttribute?.type === ATTRIBUTE_TYPES.HISTOGRAM,
|
||||
[query.aggregateAttribute?.type],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAggregationOptions([
|
||||
setAggregationOptions(query.queryName, [
|
||||
{
|
||||
func: queryAggregation.spaceAggregation || 'count',
|
||||
arg: queryAggregation.metricName || '',
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
queryAggregation.spaceAggregation,
|
||||
queryAggregation.metricName,
|
||||
setAggregationOptions,
|
||||
query,
|
||||
query.queryName,
|
||||
]);
|
||||
|
||||
const handleChangeGroupByKeys = useCallback(
|
||||
|
||||
@@ -22,7 +22,11 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
|
||||
return (
|
||||
<div className="metrics-select-container">
|
||||
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
|
||||
<AggregatorFilter
|
||||
onChange={handleChangeAggregatorAttribute}
|
||||
query={query}
|
||||
index={index}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -95,7 +95,8 @@ function HavingFilter({
|
||||
queryData: IBuilderQuery;
|
||||
}): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { aggregationOptions } = useQueryBuilderV2Context();
|
||||
const { getAggregationOptions } = useQueryBuilderV2Context();
|
||||
const aggregationOptions = getAggregationOptions(queryData.queryName);
|
||||
const having = queryData?.having as Having;
|
||||
const [input, setInput] = useState(having?.expression || '');
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ function QueryAggregationSelect({
|
||||
|
||||
setValidationError(validateAggregations());
|
||||
setFunctionArgPairs(pairs);
|
||||
setAggregationOptions(pairs);
|
||||
setAggregationOptions(queryData.queryName, pairs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [input, maxAggregations, validFunctions]);
|
||||
|
||||
|
||||
@@ -544,45 +544,15 @@ 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(
|
||||
queryName: string,
|
||||
aggregation: { alias?: string; expression?: string },
|
||||
): string {
|
||||
return aggregation.expression
|
||||
? `${queryName}.${aggregation.expression}`
|
||||
: queryName;
|
||||
if (aggregation.expression) {
|
||||
return `${queryName}.${aggregation.expression}`;
|
||||
}
|
||||
|
||||
return queryName;
|
||||
}
|
||||
|
||||
// function to give you label value for query name taking multiaggregation into account
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Table } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import cx from 'classnames';
|
||||
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 { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
@@ -110,11 +110,14 @@ function ResizeTable({
|
||||
// Apply stored column widths from widget configuration
|
||||
const columnsWithStoredWidths = columns.map((col) => {
|
||||
const dataIndex = (col as RowData).dataIndex as string;
|
||||
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
|
||||
return {
|
||||
...col,
|
||||
width: columnWidths[dataIndex], // Apply stored width
|
||||
};
|
||||
if (dataIndex && columnWidths) {
|
||||
const width = getColumnWidth(dataIndex, columnWidths);
|
||||
if (width) {
|
||||
return {
|
||||
...col,
|
||||
width, // Apply stored width
|
||||
};
|
||||
}
|
||||
}
|
||||
return col;
|
||||
});
|
||||
|
||||
@@ -37,11 +37,8 @@ import {
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -182,12 +179,17 @@ function FormAlertRules({
|
||||
setDetectionMethod(value);
|
||||
};
|
||||
|
||||
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
|
||||
const anomalyFunction = {
|
||||
name: 'anomaly',
|
||||
args: [],
|
||||
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
|
||||
const updateFunctions = (data: IBuilderQuery): QueryFunction[] => {
|
||||
const anomalyFunction: QueryFunction = {
|
||||
name: 'anomaly' as any,
|
||||
args: [
|
||||
{
|
||||
name: 'z_score_threshold',
|
||||
value: alertDef.condition.target || 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const functions = data.functions || [];
|
||||
|
||||
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ColumnType } from 'antd/es/table';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { Events } from 'constants/events';
|
||||
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 { Compass } from 'lucide-react';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
@@ -84,10 +84,11 @@ function GridTableComponent({
|
||||
(val): RowData => {
|
||||
const newValue = { ...val };
|
||||
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
|
||||
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) {
|
||||
newValue[k] = 'n/a';
|
||||
}
|
||||
@@ -121,7 +122,8 @@ function GridTableComponent({
|
||||
render: (text: string, ...rest: any): ReactNode => {
|
||||
let textForThreshold = text;
|
||||
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`];
|
||||
}
|
||||
const isNumber = !Number.isNaN(Number(textForThreshold));
|
||||
@@ -131,7 +133,7 @@ function GridTableComponent({
|
||||
thresholds,
|
||||
dataIndex as string,
|
||||
Number(textForThreshold),
|
||||
columnUnits?.[dataIndex as string],
|
||||
unit,
|
||||
);
|
||||
|
||||
const idx = thresholds.findIndex(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { unitOptions } from 'container/NewWidget/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getColumnUnit } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { Check, Pencil, Trash2, X } from 'lucide-react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||
@@ -197,7 +198,11 @@ function Threshold({
|
||||
const isInvalidUnitComparison = useMemo(
|
||||
() =>
|
||||
unit !== 'none' &&
|
||||
convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null,
|
||||
convertUnit(
|
||||
value,
|
||||
unit,
|
||||
getColumnUnit(tableSelectedOption, columnUnits || {}),
|
||||
) === null,
|
||||
[unit, value, columnUnits, tableSelectedOption],
|
||||
);
|
||||
|
||||
@@ -312,7 +317,9 @@ function Threshold({
|
||||
{isEditMode ? (
|
||||
<Select
|
||||
defaultValue={unit}
|
||||
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
|
||||
options={unitOptions(
|
||||
getColumnUnit(tableSelectedOption, columnUnits || {}) || '',
|
||||
)}
|
||||
onChange={handleUnitChange}
|
||||
showSearch
|
||||
className="unit-selection"
|
||||
@@ -351,7 +358,7 @@ function Threshold({
|
||||
{isInvalidUnitComparison && (
|
||||
<Typography.Text className="invalid-unit">
|
||||
Threshold unit ({unit}) is not valid in comparison with the column unit (
|
||||
{columnUnits?.[tableSelectedOption] || 'none'})
|
||||
{getColumnUnit(tableSelectedOption, columnUnits || {}) || 'none'})
|
||||
</Typography.Text>
|
||||
)}
|
||||
{isEditMode && (
|
||||
|
||||
@@ -16,10 +16,8 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { DataSourceDropdown } from '..';
|
||||
@@ -36,7 +34,7 @@ interface QBEntityOptionsProps {
|
||||
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
|
||||
onToggleVisibility: () => void;
|
||||
onCollapseEntity: () => void;
|
||||
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
||||
onQueryFunctionsUpdates?: (functions: QueryFunction[]) => void;
|
||||
showDeleteButton?: boolean;
|
||||
showCloneOption?: boolean;
|
||||
isListViewPanel?: boolean;
|
||||
|
||||
@@ -9,15 +9,14 @@ import {
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { debounce, isNil } from 'lodash-es';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
|
||||
|
||||
interface FunctionProps {
|
||||
query: IBuilderQuery;
|
||||
funcData: QueryFunctionProps;
|
||||
funcData: QueryFunction;
|
||||
index: any;
|
||||
handleUpdateFunctionArgs: any;
|
||||
handleUpdateFunctionName: any;
|
||||
@@ -33,17 +32,19 @@ export default function Function({
|
||||
handleDeleteFunction,
|
||||
}: FunctionProps): JSX.Element {
|
||||
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;
|
||||
|
||||
const hasValue = !isNil(
|
||||
funcData.args && funcData.args.length > 0 && funcData.args[0],
|
||||
);
|
||||
const hasValue = !isNil(funcData.args?.[0]?.value);
|
||||
|
||||
if (hasValue) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
functionValue = funcData.args[0];
|
||||
functionValue = funcData.args?.[0]?.value;
|
||||
}
|
||||
|
||||
const debouncedhandleUpdateFunctionArgs = debounce(
|
||||
@@ -57,9 +58,10 @@ export default function Function({
|
||||
? logsQueryFunctionOptions
|
||||
: 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
|
||||
return <></>;
|
||||
}
|
||||
@@ -68,7 +70,7 @@ export default function Function({
|
||||
<Flex className="query-function">
|
||||
<Select
|
||||
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
|
||||
value={funcData.name}
|
||||
value={normalizedFunctionName}
|
||||
disabled={disabled}
|
||||
style={{ minWidth: '100px' }}
|
||||
onChange={(value): void => {
|
||||
|
||||
@@ -6,29 +6,28 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { cloneDeep, pullAt } from 'lodash-es';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
|
||||
|
||||
import Function from './Function';
|
||||
import { toFloat64 } from './utils';
|
||||
|
||||
const defaultMetricFunctionStruct: QueryFunctionProps = {
|
||||
name: QueryFunctionsTypes.CUTOFF_MIN,
|
||||
const defaultMetricFunctionStruct: QueryFunction = {
|
||||
name: QueryFunctionsTypes.CUTOFF_MIN as any,
|
||||
args: [],
|
||||
};
|
||||
|
||||
const defaultLogFunctionStruct: QueryFunctionProps = {
|
||||
name: QueryFunctionsTypes.TIME_SHIFT,
|
||||
const defaultLogFunctionStruct: QueryFunction = {
|
||||
name: QueryFunctionsTypes.TIME_SHIFT as any,
|
||||
args: [],
|
||||
};
|
||||
|
||||
interface QueryFunctionsProps {
|
||||
query: IBuilderQuery;
|
||||
queryFunctions: QueryFunctionProps[];
|
||||
onChange: (functions: QueryFunctionProps[]) => void;
|
||||
queryFunctions: QueryFunction[];
|
||||
onChange: (functions: QueryFunction[]) => void;
|
||||
maxFunctions: number;
|
||||
}
|
||||
|
||||
@@ -87,8 +86,11 @@ export default function QueryFunctions({
|
||||
onChange,
|
||||
maxFunctions = 3,
|
||||
}: QueryFunctionsProps): JSX.Element {
|
||||
const [functions, setFunctions] = useState<QueryFunctionProps[]>(
|
||||
queryFunctions,
|
||||
const [functions, setFunctions] = useState<QueryFunction[]>(
|
||||
queryFunctions.map((func) => ({
|
||||
...func,
|
||||
name: normalizeFunctionName(func.name) as any,
|
||||
})),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -127,7 +129,7 @@ export default function QueryFunctions({
|
||||
};
|
||||
|
||||
const handleDeleteFunction = (
|
||||
queryFunction: QueryFunctionProps,
|
||||
queryFunction: QueryFunction,
|
||||
index: number,
|
||||
): void => {
|
||||
const clonedFunctions = cloneDeep(functions);
|
||||
@@ -138,21 +140,23 @@ export default function QueryFunctions({
|
||||
};
|
||||
|
||||
const handleUpdateFunctionName = (
|
||||
func: QueryFunctionProps,
|
||||
func: QueryFunction,
|
||||
index: number,
|
||||
value: string,
|
||||
): void => {
|
||||
const updateFunctions = cloneDeep(functions);
|
||||
|
||||
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);
|
||||
onChange(updateFunctions);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateFunctionArgs = (
|
||||
func: QueryFunctionProps,
|
||||
func: QueryFunction,
|
||||
index: number,
|
||||
value: string,
|
||||
): void => {
|
||||
@@ -160,11 +164,12 @@ export default function QueryFunctions({
|
||||
|
||||
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
|
||||
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
|
||||
updateFunctions[index].name === QueryFunctionsTypes.TIME_SHIFT
|
||||
? toFloat64(value)
|
||||
: value,
|
||||
{
|
||||
value:
|
||||
updateFunctions[index].name === QueryFunctionsTypes.TIME_SHIFT
|
||||
? toFloat64(value)
|
||||
: value,
|
||||
},
|
||||
];
|
||||
setFunctions(updateFunctions);
|
||||
onChange(updateFunctions);
|
||||
|
||||
@@ -7,4 +7,5 @@ export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
||||
onChange: (value: BaseAutocompleteData) => void;
|
||||
defaultValue?: string;
|
||||
onSelect?: (value: BaseAutocompleteData) => void;
|
||||
index?: number;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import { getAutocompleteValueAndType } from 'lib/newQueryBuilder/getAutocompleteValueAndType';
|
||||
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 { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
@@ -38,6 +38,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
onChange,
|
||||
defaultValue,
|
||||
onSelect,
|
||||
index,
|
||||
}: AgregatorFilterProps): JSX.Element {
|
||||
const queryClient = useQueryClient();
|
||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||
@@ -57,12 +58,13 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
}, [searchText]);
|
||||
|
||||
const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY);
|
||||
const { isFetching } = useQuery(
|
||||
const { isFetching, data: aggregateAttributeData } = useQuery(
|
||||
[
|
||||
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||
debouncedValue,
|
||||
queryAggregation.timeAggregation,
|
||||
query.dataSource,
|
||||
index,
|
||||
],
|
||||
async () =>
|
||||
getAggregateAttribute({
|
||||
@@ -108,6 +110,42 @@ 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 => {
|
||||
setSearchText(text);
|
||||
}, []);
|
||||
@@ -124,12 +162,14 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
debouncedValue,
|
||||
queryAggregation.timeAggregation,
|
||||
query.dataSource,
|
||||
index,
|
||||
])?.payload?.attributeKeys || [],
|
||||
[
|
||||
debouncedValue,
|
||||
queryAggregation.timeAggregation,
|
||||
query.dataSource,
|
||||
queryClient,
|
||||
index,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -140,6 +180,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
searchText,
|
||||
queryAggregation.timeAggregation,
|
||||
query.dataSource,
|
||||
index,
|
||||
],
|
||||
async () =>
|
||||
getAggregateAttribute({
|
||||
@@ -155,6 +196,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
query.dataSource,
|
||||
queryClient,
|
||||
searchText,
|
||||
index,
|
||||
]);
|
||||
|
||||
const handleChangeCustomValue = useCallback(
|
||||
|
||||
@@ -30,10 +30,10 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
MetricAggregation,
|
||||
QueryFunction,
|
||||
SpaceAggregation,
|
||||
TimeAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
@@ -138,7 +138,6 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
timeAggregation: value as TimeAggregation,
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
limit: null,
|
||||
...(shouldResetAggregateAttribute
|
||||
? { aggregateAttribute: initialAutocompleteData }
|
||||
@@ -217,7 +216,6 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
const newQuery: IBuilderQuery = {
|
||||
...query,
|
||||
aggregateAttribute: value,
|
||||
having: [],
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -416,7 +414,7 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
);
|
||||
|
||||
const handleQueryFunctionsUpdates = useCallback(
|
||||
(functions: QueryFunctionProps[]): void => {
|
||||
(functions: QueryFunction[]): void => {
|
||||
const newQuery: IBuilderQuery = {
|
||||
...query,
|
||||
};
|
||||
|
||||
@@ -72,6 +72,70 @@ const getQueryDataSource = (
|
||||
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 = (
|
||||
queryData: QueryData,
|
||||
payloadQuery: Query,
|
||||
@@ -91,21 +155,34 @@ export const getLegend = (
|
||||
const aggregation =
|
||||
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)
|
||||
const totalQueries =
|
||||
(payloadQuery?.builder?.queryData?.length || 0) +
|
||||
(payloadQuery?.builder?.queryFormulas?.length || 0);
|
||||
const showSingleAggregationName =
|
||||
totalQueries === 1 && labelName === metaData?.queryName;
|
||||
// Check if there's only one total query (queryData)
|
||||
const singleQuery = payloadQuery?.builder?.queryData?.length === 1;
|
||||
const singleAggregation =
|
||||
aggregationPerQuery?.[metaData?.queryName]?.length === 1;
|
||||
|
||||
if (aggregationName) {
|
||||
return showSingleAggregationName
|
||||
? aggregationName
|
||||
: `${aggregationName}-${labelName}`;
|
||||
if (aggregationAlias || aggregationExpression) {
|
||||
return singleQuery
|
||||
? getLegendForSingleAggregation(
|
||||
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(
|
||||
|
||||
@@ -650,6 +650,70 @@ const generateTableColumns = (
|
||||
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 = ({
|
||||
query,
|
||||
queryTableData,
|
||||
|
||||
@@ -60,9 +60,9 @@ const getSeries = ({
|
||||
: baseLabelName;
|
||||
|
||||
const color =
|
||||
colorMapping?.[label] ||
|
||||
colorMapping?.[label || ''] ||
|
||||
generateColor(
|
||||
label,
|
||||
label || '',
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Having as HavingV5,
|
||||
LogAggregation,
|
||||
MetricAggregation,
|
||||
QueryFunction,
|
||||
TraceAggregation,
|
||||
} from '../v5/queryRange';
|
||||
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
||||
@@ -71,7 +72,7 @@ export type IBuilderQuery = {
|
||||
timeAggregation?: string;
|
||||
spaceAggregation?: string;
|
||||
temporality?: string;
|
||||
functions: QueryFunctionProps[];
|
||||
functions: QueryFunction[];
|
||||
filter?: Filter;
|
||||
filters?: TagFilter;
|
||||
groupBy: BaseAutocompleteData[];
|
||||
|
||||
@@ -168,6 +168,7 @@ export interface FunctionArg {
|
||||
export interface QueryFunction {
|
||||
name: FunctionName;
|
||||
args?: FunctionArg[];
|
||||
namedArgs?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
// ===================== Aggregation Types =====================
|
||||
|
||||
@@ -4,12 +4,12 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
LogBuilderQuery,
|
||||
MetricBuilderQuery,
|
||||
QueryFunction,
|
||||
TraceBuilderQuery,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -63,6 +63,6 @@ export type UseQueryOperations = (
|
||||
handleDeleteQuery: () => void;
|
||||
handleChangeQueryData: HandleChangeQueryData;
|
||||
handleChangeFormulaData: HandleChangeFormulaData;
|
||||
handleQueryFunctionsUpdates: (functions: QueryFunctionProps[]) => void;
|
||||
handleQueryFunctionsUpdates: (functions: QueryFunction[]) => void;
|
||||
listOfAdditionalFormulaFilters: string[];
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user