Compare commits

..

14 Commits

Author SHA1 Message Date
Yunus M
c2f00bbb34 chore(deps): bump requests from 2.32.3 to 2.32.4 in /tests/integration 2025-08-07 23:37:36 +05:30
Yunus M
b5098e00a3 fix: logs explorer - should have atleast 1 column, discard empty key columns (#8740) 2025-08-07 20:17:34 +05:30
Abhi kumar
20dc561bfe fix: added fix for query becoming empty on time change (#8739) 2025-08-07 19:42:07 +05:30
Nityananda Gohain
99bbb87738 chore: add option to ignore data skipping indices (#8738)
* chore: add option to ignore data skipping indices

* fix: update example
2025-08-07 13:21:17 +00:00
Vikrant Gupta
f1ce93171c feat(telemetrymeter): add support for telemetry meter (#8667)
* feat(telemetry/meter): added base setup for telemetry meter signal

* feat(telemetry/meter): added metadata setup for meter

* feat(telemetry/meter): fix stmnt builder tests

* feat(telemetry/meter): test query range API fixes

* feat(telemetry/meter): improve error messages

* feat(telemetrymeter): step interval improvements

* feat(telemetrymeter): metadata changes and aggregate attribute changes

* feat(telemetrymeter): metadata changes and aggregate attribute changes

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): cleanup the types

* feat(telemetrymeter): introduce source for query

* feat(telemetrymeter): better naming for source in metadata

* feat(telemetrymeter): added quick filters for meter explorer

* feat(telemetrymeter): incorporate the new changes to stmnt builder

* feat(telemetrymeter): add the statement builder for the ranged cache queries

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): update the quick filters to use meter
2025-08-07 16:50:37 +05:30
Srikanth Chekuri
92794389d6 fix: limit keys for empty search key (#8728) 2025-08-07 00:34:44 +05:30
Srikanth Chekuri
bd02848623 chore: add sql migration for dashboards, alerts, and saved views (#8642)
## 📄 Summary

To reliably migrate the alerts and dashboards, we need access to the telemetrystore to fetch some metadata and while doing migration, I need to log some stuff to fix stuff later.

Key changes:
- Modified the migration to include telemetrystore and a logging provider (open to using a standard logger instead)
- To avoid the previous issues with imported dashboards failing during migration, I've ensured that imported JSON files are automatically transformed when migration is active
- Implemented detailed logic to handle dashboard migration cleanly and prevent unnecessary errors
- Separated the core migration logic from SQL migration code, as users from the dot metrics migration requested shareable code snippets for local migrations. This modular approach allows others to easily reuse the migration functionality.

Known: I didn't register the migration yet in this PR, and will not merge this yet, so please review with that in mid.
2025-08-06 23:05:39 +05:30
Abhi kumar
b5016b061b fix: added fix for key suggestions (#8727) 2025-08-06 11:48:43 +00:00
Abhi kumar
c308e8668c fix: added fix for query addon lightmode ui (#8725) 2025-08-06 16:21:35 +05:30
SagarRajput-7
41ee4176ad fix: fixed metric aggregation and value retention inconsistency in edit mode (#8718) 2025-08-06 13:55:16 +05:30
Abhi kumar
994663110d fix: added fix for query suggestions position (#8719)
* fix: added fix for query suggestions position

* chore: added box-shadows in the dropdowns
2025-08-06 12:48:07 +05:30
Abhi kumar
3a2eab2019 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>
2025-08-06 00:16:20 +05:30
SagarRajput-7
01202b5800 feat: created new error content plugin for QB v5 (#8700)
* feat: created new error content plugin for QB v5

* feat: added warning popover content for QB v5 feature

* feat: icon change for warning

* feat: added warning to QB v5 components

* feat: fixed type error

* feat: fix test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-08-05 23:45:39 +05:30
Amlan Kumar Nandy
2901e052ae chore: improve metrics explorer empty state (#8711) 2025-08-05 20:45:21 +07:00
215 changed files with 6703 additions and 4965 deletions

View File

@@ -121,6 +121,7 @@ telemetrystore:
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
max_result_rows: 0
ignore_data_skipping_indices: ""
##################### Prometheus #####################
prometheus:

View 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>);
}
};

View File

@@ -2,7 +2,7 @@ import { ApiV3Instance, ApiV4Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { ErrorResponse, SuccessResponse, Warning } from 'types/api';
import {
MetricRangePayloadV3,
QueryRangePayload,
@@ -13,7 +13,9 @@ export const getMetricsQueryRange = async (
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
): Promise<
(SuccessResponse<MetricRangePayloadV3> & { warning?: Warning }) | ErrorResponse
> => {
try {
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, {

View File

@@ -1,5 +1,5 @@
import { cloneDeep, isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
DistributionData,
@@ -28,14 +28,18 @@ function getColName(
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
const isSingleAggregation = aggregationsCount === 1;
// 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(
@@ -48,7 +52,14 @@ function getColId(
const aggregation =
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
const expression = aggregation?.expression || '';
return `${col.queryName}.${expression}`;
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
const isMultipleAggregations = aggregationsCount > 1;
if (isMultipleAggregations && expression) {
return `${col.queryName}.${expression}`;
}
return col.queryName;
}
/**
@@ -341,7 +352,7 @@ export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> {
): SuccessResponse<MetricRangePayloadV3> & { warning?: Warning } {
const { payload, params } = v5Response;
const v5Data = payload?.data;
@@ -367,14 +378,18 @@ export function convertV5ResponseToLegacy(
legendMap,
aggregationPerQuery,
);
return {
...v5Response,
payload: {
data: {
resultType: 'scalar',
result: webTables,
warnings: v5Data?.data?.warning || [],
},
warning: v5Data?.warning || undefined,
},
warning: v5Data?.warning || undefined,
};
}
@@ -390,6 +405,7 @@ export function convertV5ResponseToLegacy(
...v5Response,
payload: {
data: convertedData,
warning: v5Response.payload?.data?.warning || undefined,
},
};

View File

@@ -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

View File

@@ -19,6 +19,7 @@ export interface NavigateToExplorerProps {
endTime?: number;
sameTab?: boolean;
shouldResolveQuery?: boolean;
widgetQuery?: Query;
}
export function useNavigateToExplorer(): (
@@ -30,27 +31,34 @@ export function useNavigateToExplorer(): (
);
const prepareQuery = useCallback(
(selectedFilters: TagFilterItem[], dataSource: DataSource): Query => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData
.map((item) => ({
...item,
dataSource,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,
}))
.slice(0, 1),
queryFormulas: [],
},
}),
(
selectedFilters: TagFilterItem[],
dataSource: DataSource,
query?: Query,
): Query => {
const widgetQuery = query || currentQuery;
return {
...widgetQuery,
builder: {
...widgetQuery.builder,
queryData: widgetQuery.builder.queryData
.map((item) => ({
...item,
dataSource,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,
}))
.slice(0, 1),
queryFormulas: [],
},
};
},
[currentQuery],
);
@@ -67,6 +75,7 @@ export function useNavigateToExplorer(): (
endTime,
sameTab,
shouldResolveQuery,
widgetQuery,
} = props;
const urlParams = new URLSearchParams();
if (startTime && endTime) {
@@ -77,7 +86,7 @@ export function useNavigateToExplorer(): (
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
}
let preparedQuery = prepareQuery(filters, dataSource);
let preparedQuery = prepareQuery(filters, dataSource, widgetQuery);
if (shouldResolveQuery) {
await getUpdatedQuery({

View 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;
}
}
}
}

View 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;

View File

@@ -0,0 +1,79 @@
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { ReactNode } from 'react';
import APIError from 'types/api/error';
interface ErrorInPlaceProps {
/** The error object to display */
error: APIError;
/** Custom class name */
className?: string;
/** Custom style */
style?: React.CSSProperties;
/** Whether to show a border */
bordered?: boolean;
/** Background color */
background?: string;
/** Padding */
padding?: string | number;
/** Height - defaults to 100% to take available space */
height?: string | number;
/** Width - defaults to 100% to take available space */
width?: string | number;
/** Custom content instead of ErrorContent */
children?: ReactNode;
}
/**
* ErrorInPlace - A component that renders error content directly in the available space
* of its parent container. Perfect for displaying errors in widgets, cards, or any
* container where you want the error to take up the full available space.
*
* @example
* <ErrorInPlace error={error} />
*
* @example
* <ErrorInPlace error={error} bordered background="#f5f5f5" padding={16} />
*/
function ErrorInPlace({
error,
className = '',
style,
bordered = false,
background,
padding = 16,
height = '100%',
width = '100%',
children,
}: ErrorInPlaceProps): JSX.Element {
const containerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
width,
height,
padding: typeof padding === 'number' ? `${padding}px` : padding,
backgroundColor: background,
border: bordered ? '1px solid var(--bg-slate-400, #374151)' : 'none',
borderRadius: bordered ? '4px' : '0',
overflow: 'auto',
...style,
};
return (
<div className={`error-in-place ${className}`.trim()} style={containerStyle}>
{children || <ErrorContent error={error} />}
</div>
);
}
ErrorInPlace.defaultProps = {
className: undefined,
style: undefined,
bordered: undefined,
background: undefined,
padding: undefined,
height: undefined,
width: undefined,
children: undefined,
};
export default ErrorInPlace;

View File

@@ -0,0 +1,33 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Popover, PopoverProps } from 'antd';
import { ReactNode } from 'react';
interface ErrorPopoverProps extends Omit<PopoverProps, 'content'> {
/** Content to display in the popover */
content: ReactNode;
/** Element that triggers the popover */
children: ReactNode;
}
/**
* ErrorPopover - A clean wrapper around Ant Design's Popover
* that provides a simple interface for displaying content in a popover.
*
* @example
* <ErrorPopover content={<ErrorContent error={error} />}>
* <CircleX />
* </ErrorPopover>
*/
function ErrorPopover({
content,
children,
...popoverProps
}: ErrorPopoverProps): JSX.Element {
return (
<Popover content={content} {...popoverProps}>
{children}
</Popover>
);
}
export default ErrorPopover;

View File

@@ -169,7 +169,6 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

@@ -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}

View File

@@ -7,7 +7,6 @@ import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Info } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -50,17 +49,17 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
);
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(
@@ -100,12 +99,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-time-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<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">
AGGREGATE WITHIN TIME SERIES{' '}
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={queryAggregation.timeAggregation || ''}
@@ -118,9 +127,30 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-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="metrics-aggregation-section-content-item-value">
<InputWithLabel
@@ -138,12 +168,22 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<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">
AGGREGATE ACROSS TIME SERIES
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<SpaceAggregationOptions
panelType={panelType}
@@ -208,9 +248,30 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-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="metrics-aggregation-section-content-item-value">
<InputWithLabel

View File

@@ -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>
);
});

View File

@@ -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 || '');

View File

@@ -111,17 +111,13 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: 38px !important;
top: calc(100% + 6px) !important;
left: 0px !important;
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
@@ -129,7 +125,9 @@
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
ul {
width: 100% !important;
@@ -165,7 +163,6 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -330,16 +327,18 @@
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
}
}
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/require-default-props */
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent } from 'antd';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
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 { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
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 { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -21,6 +22,8 @@ interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
description?: string;
docLink?: string;
}
const ADD_ONS_KEYS = {
@@ -36,26 +39,45 @@ const ADD_ONS = [
icon: <BarChart2 size={14} />,
label: '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} />,
label: '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} />,
label: '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} />,
label: '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} />,
label: '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} />,
label: '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({
query,
version,
@@ -212,7 +284,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<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">
<GroupByFilter
disabled={
@@ -234,7 +320,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<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">
<HavingFilter
onClose={(): void => {
@@ -266,7 +366,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<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">
<OrderByFilter
entityVersion={version}
@@ -290,7 +404,21 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<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">
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
@@ -330,20 +458,32 @@ function QueryAddOns({
value={selectedViews}
>
{addOns.map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
description={addOn.description}
docLink={addOn.docLink}
/>
}
value={addOn}
placement="top"
mouseEnterDelay={0.5}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
<Radio.Button
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
</Tooltip>
))}
</Radio.Group>
</div>

View File

@@ -63,17 +63,14 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: 8px !important;
min-width: 400px !important;
position: absolute !important;
top: calc(100% + 6px) !important;
left: 0px !important;
width: 100% !important;
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
@@ -81,6 +78,7 @@
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
ul {
@@ -269,19 +267,17 @@
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
color: var(--bg-ink-300) !important;
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
font-weight: 600 !important;
}
}
}

View File

@@ -1,5 +1,6 @@
import './QueryAggregation.styles.scss';
import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
@@ -53,7 +54,31 @@ function QueryAggregationOptions({
{showAggregationInterval && (
<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">
<InputWithLabel
initialValue={

View File

@@ -27,13 +27,13 @@ import CodeMirror, {
ViewPlugin,
ViewUpdate,
} from '@uiw/react-codemirror';
import { Button, Popover } from 'antd';
import { Button, Popover, Tooltip } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
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 { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -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]);
@@ -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 && (
<div className="query-aggregation-error-container">
<Popover

View File

@@ -12,22 +12,7 @@ export default function QueryFooter({
<div className="qb-footer">
<div className="qb-footer-container">
<div className="qb-add-new-query">
<Tooltip
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>
}
>
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
@@ -43,7 +28,7 @@ export default function QueryFooter({
<div style={{ textAlign: 'center' }}>
Add New Formula
<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"
style={{ textDecoration: 'underline' }}
>

View File

@@ -48,7 +48,7 @@
.cm-editor {
border-radius: 2px;
overflow: hidden;
// overflow: hidden;
background-color: transparent !important;
&:focus-within {
@@ -75,11 +75,11 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
min-width: 400px !important;
position: relative !important;
top: 0px !important;
position: absolute !important;
top: calc(100% + 6px) !important;
left: 0px !important;
right: 0px !important;
border-radius: 4px;
border: 0px;
@@ -91,6 +91,8 @@
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
border: 1px solid var(--bg-slate-200);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
ul {
width: 100% !important;
@@ -571,9 +573,9 @@
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 0px;
border: 1px solid var(--bg-vanilla-300);
backdrop-filter: blur(20px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
@@ -583,7 +585,7 @@
&:hover,
&[aria-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
font-weight: 600;
font-weight: 600 !important;
}
}
}

View File

@@ -16,12 +16,13 @@ import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
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 { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import cx from 'classnames';
import {
negationQueryOperatorSuggestions,
OPERATORS,
QUERY_BUILDER_KEY_TYPES,
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
queryOperatorSuggestions,
@@ -30,7 +31,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
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 {
IDetailedError,
@@ -40,11 +41,11 @@ import {
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
import { validateQuery } from 'utils/antlrQueryUtils';
import {
getCurrentValueIndexAtCursor,
getQueryContextAtCursor,
} from 'utils/queryContextUtils';
import { validateQuery } from 'utils/queryValidationUtils';
import { unquote } from 'utils/stringUtils';
import { queryExamples } from './constants';
@@ -88,10 +89,7 @@ function QuerySearch({
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([
{ label: 'error', type: 'value' },
{ label: 'frontend', type: 'value' },
]);
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
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(() => {
setQuery(queryData.filter?.expression || '');
}, [queryData.filter?.expression]);
const newQuery = 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<
QueryKeyDataSuggestionsProps[] | null
@@ -127,7 +143,6 @@ function QuerySearch({
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [isCompleteKeysList, setIsCompleteKeysList] = useState(false);
const [
isFetchingCompleteValuesList,
setIsFetchingCompleteValuesList,
@@ -138,6 +153,7 @@ function QuerySearch({
// Reference to the editor view for programmatic autocompletion
const editorRef = useRef<EditorView | null>(null);
const lastKeyRef = useRef<string>('');
const lastFetchedKeyRef = useRef<string>('');
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
@@ -170,36 +186,76 @@ function QuerySearch({
500,
);
const fetchKeySuggestions = async (searchText?: string): Promise<void> => {
if (dataSource === DataSource.METRICS && !queryData.aggregateAttribute?.key) {
setKeySuggestions([]);
return;
}
const response = await getKeySuggestions({
signal: dataSource,
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
});
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
if (response.data.data) {
const { complete, keys } = response.data.data;
const options = generateOptions(keys);
// Use a Map to deduplicate by label and preserve order: new options take precedence
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(opt.label, opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(opt.label)) merged.set(opt.label, opt);
});
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const fetchKeySuggestions = useCallback(
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key
) {
setKeySuggestions([]);
return;
}
setKeySuggestions(Array.from(merged.values()));
setIsCompleteKeysList(complete);
}
};
lastFetchedKeyRef.current = searchText || '';
const response = await getKeySuggestions({
signal: dataSource,
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
});
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
// Use a Map to deduplicate by label and preserve order: new options take precedence
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(opt.label, opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(opt.label)) merged.set(opt.label, opt);
});
}
setKeySuggestions(Array.from(merged.values()));
// 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(() => {
setKeySuggestions([]);
fetchKeySuggestions();
debouncedFetchKeySuggestions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource, debouncedMetricName]);
@@ -310,6 +366,11 @@ function QuerySearch({
},
]);
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
toggleSuggestions(10);
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
try {
@@ -382,13 +443,9 @@ function QuerySearch({
]);
}
// Force reopen the completion if editor is available
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
setTimeout(() => {
if (isMountedRef.current && editorRef.current) {
startCompletion(editorRef.current);
}
}, 10);
toggleSuggestions(10);
}
}
} catch (error) {
@@ -408,7 +465,8 @@ function QuerySearch({
setIsFetchingCompleteValuesList(false);
}
},
[activeKey, dataSource, isLoadingSuggestions],
// eslint-disable-next-line react-hooks/exhaustive-deps
[activeKey, dataSource, isFocused],
);
const debouncedFetchValueSuggestions = useMemo(
@@ -468,14 +526,13 @@ function QuerySearch({
}
}, []);
const handleQueryChange = useCallback(async (newQuery: string) => {
setQuery(newQuery);
}, []);
const handleChange = (value: string): void => {
setQuery(value);
handleQueryChange(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 => {
@@ -483,24 +540,27 @@ function QuerySearch({
setIsFocused(false);
};
useEffect(() => {
if (query) {
handleQueryValidation(query);
}
return (): void => {
useEffect(
() => (): void => {
if (debouncedFetchValueSuggestions) {
debouncedFetchValueSuggestions.cancel();
}
};
if (debouncedFetchKeySuggestions) {
debouncedFetchKeySuggestions.cancel();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
[],
);
const handleExampleClick = (exampleQuery: string): void => {
// If there's an existing query, append the example with AND
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
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
@@ -743,16 +803,14 @@ function QuerySearch({
}
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase() ?? '';
const searchText = word?.text.toLowerCase().trim() ?? '';
options = (keySuggestions || []).filter((option) =>
option.label.toLowerCase().includes(searchText),
);
if (!isCompleteKeysList && options.length === 0) {
setTimeout(() => {
fetchKeySuggestions(searchText);
}, 300);
if (options.length === 0 && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}
// If we have previous pairs, we can prioritize keys that haven't been used yet
@@ -827,12 +885,32 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.STRING
].includes(op.label),
)
.map((op) => ({
...op,
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
? 100
: 0,
}));
.map((op) => {
if (op.label === OPERATORS['=']) {
return {
...op,
boost: 200,
};
}
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) {
// Prioritize boolean operators
options = options
@@ -841,10 +919,24 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.BOOLEAN
].includes(op.label),
)
.map((op) => ({
...op,
boost: ['=', '!='].includes(op.label) ? 100 : 0,
}));
.map((op) => {
if (op.label === OPERATORS['=']) {
return {
...op,
boost: 200,
};
}
if (op.label === OPERATORS['!=']) {
return {
...op,
boost: 100,
};
}
return {
...op,
boost: 0,
};
});
}
}
}
@@ -1034,26 +1126,15 @@ function QuerySearch({
// Effect to handle focus state and trigger suggestions
useEffect(() => {
if (editorRef.current) {
if (!isFocused) {
closeCompletion(editorRef.current);
} else {
startCompletion(editorRef.current);
}
}
}, [isFocused]);
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
useEffect(() => {
if (!queryContext) return;
// Trigger suggestions based on context
if (editorRef.current) {
// Small delay to ensure the context is fully updated
setTimeout(() => {
if (editorRef.current) {
startCompletion(editorRef.current);
}
}, 50);
toggleSuggestions(10);
}
// Handle value suggestions for value context
@@ -1066,7 +1147,28 @@ function QuerySearch({
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 (
<div className="code-mirror-where-clause">
@@ -1109,6 +1211,31 @@ function QuerySearch({
)}
<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
value={query}
theme={isDarkMode ? copilot : githubLight}
@@ -1167,7 +1294,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={{
lineNumbers: false,
}}

View File

@@ -21,6 +21,7 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { isFunctionOperator } from 'utils/tokenUtils';
import { v4 as uuid } from 'uuid';
/**
@@ -86,6 +87,10 @@ export const convertFiltersToExpression = (
return '';
}
if (isFunctionOperator(op)) {
return `${op}(${key.key}, ${value})`;
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
@@ -539,43 +544,16 @@ 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 },
isMultipleAggregations: boolean,
): string {
return `${queryName}.${aggregation.expression}`;
if (isMultipleAggregations && aggregation.expression) {
return `${queryName}.${aggregation.expression}`;
}
return queryName;
}
// function to give you label value for query name taking multiaggregation into account
@@ -599,7 +577,7 @@ export function getQueryLabelWithAggregation(
const isMultipleAggregations = aggregations.length > 1;
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
const displayLabel = isMultipleAggregations

View File

@@ -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;
});

View File

@@ -0,0 +1,208 @@
.warning-content {
display: flex;
flex-direction: column;
// === SECTION: Summary (Top)
&__summary-section {
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--bg-slate-400);
}
&__summary {
display: flex;
justify-content: space-between;
padding: 16px;
}
&__summary-left {
display: flex;
align-items: baseline;
gap: 8px;
}
&__summary-text {
display: flex;
flex-direction: column;
gap: 6px;
}
&__warning-code {
color: var(--bg-vanilla-100);
margin: 0;
font-size: 16px;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
}
&__warning-message {
margin: 0;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&__docs-button {
display: flex;
align-items: center;
gap: 6px;
padding: 9px 12.5px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.12px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: none;
}
&__message-badge {
display: flex;
align-items: center;
gap: 12px;
padding: 0px 16px 16px;
.key-value-label {
width: fit-content;
border-color: var(--bg-slate-400);
border-radius: 20px;
overflow: hidden;
&__key {
padding-left: 8px;
padding-right: 8px;
}
&__value {
padding-right: 10px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
pointer-events: none;
}
}
&-label {
display: flex;
align-items: center;
gap: 6px;
&-dot {
height: 6px;
width: 6px;
background: var(--bg-sakura-500);
border-radius: 50%;
}
&-text {
color: var(--bg-vanilla-100);
font-size: 10px;
font-weight: 500;
line-height: 18px; /* 180% */
letter-spacing: 0.5px;
}
}
&-line {
flex: 1;
height: 8px;
background-image: radial-gradient(circle, #444c63 1px, transparent 2px);
background-size: 8px 11px;
background-position: top left;
padding: 6px;
}
}
// === SECTION: Message List (Bottom)
&__message-list-container {
position: relative;
}
&__message-list {
margin: 0;
padding: 0;
list-style: none;
max-height: 275px;
}
&__message-item {
position: relative;
margin-bottom: 4px;
color: var(--bg-vanilla-400);
font-family: Geist Mono;
font-size: 12px;
font-weight: 400;
line-height: 18px;
color: var(--bg-vanilla-400);
padding: 3px 12px;
padding-left: 26px;
}
&__message-item::before {
font-family: unset;
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 4px;
border-radius: 50px;
background: var(--bg-slate-400);
}
&__scroll-hint {
position: absolute;
bottom: 10px;
left: 0px;
right: 0px;
margin: auto;
width: fit-content;
display: inline-flex;
padding: 4px 12px 4px 10px;
justify-content: center;
align-items: center;
gap: 3px;
background: var(--bg-slate-200);
border-radius: 20px;
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
0px 66px 18px 0px rgba(0, 0, 0, 0.01), 0px 37px 22px 0px rgba(0, 0, 0, 0.03),
0px 17px 17px 0px rgba(0, 0, 0, 0.04), 0px 4px 9px 0px rgba(0, 0, 0, 0.04);
}
&__scroll-hint-text {
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
}
.lightMode {
.warning-content {
&__warning-code {
color: var(--bg-ink-100);
}
&__warning-message {
color: var(--bg-ink-400);
}
&__message-item {
color: var(--bg-ink-400);
}
&__message-badge {
&-label-text {
color: var(--bg-ink-400);
}
.key-value-label__value {
color: var(--bg-ink-400);
}
}
&__docs-button {
background: var(--bg-vanilla-100);
color: var(--bg-ink-100);
}
}
}

View File

@@ -0,0 +1,142 @@
/* eslint-disable react/jsx-props-no-spreading */
import './WarningPopover.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, PopoverProps } from 'antd';
import ErrorIcon from 'assets/Error';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { BookOpenText, ChevronsDown, TriangleAlert } from 'lucide-react';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { ReactNode } from 'react';
import { Warning } from 'types/api';
interface WarningContentProps {
warning: Warning;
}
export function WarningContent({ warning }: WarningContentProps): JSX.Element {
const {
url: warningUrl,
warnings: warningMessages,
code: warningCode,
message: warningMessage,
} = warning || {};
if (!warning) {
return <div />;
}
return (
<section className="warning-content">
{/* Summary Header */}
<section className="warning-content__summary-section">
<header className="warning-content__summary">
<div className="warning-content__summary-left">
<div className="warning-content__icon-wrapper">
<ErrorIcon />
</div>
<div className="warning-content__summary-text">
<h2 className="warning-content__warning-code">{warningCode}</h2>
<p className="warning-content__warning-message">{warningMessage}</p>
</div>
</div>
{warningUrl && (
<div className="warning-content__summary-right">
<Button
type="default"
className="warning-content__docs-button"
href={warningUrl}
target="_blank"
data-testid="warning-docs-button"
>
<BookOpenText size={14} />
Open Docs
</Button>
</div>
)}
</header>
{warningMessages?.length > 0 && (
<div className="warning-content__message-badge">
<KeyValueLabel
badgeKey={
<div className="warning-content__message-badge-label">
<div className="warning-content__message-badge-label-dot" />
<div className="warning-content__message-badge-label-text">
MESSAGES
</div>
</div>
}
badgeValue={warningMessages.length.toString()}
/>
<div className="warning-content__message-badge-line" />
</div>
)}
</section>
{/* Detailed Messages */}
<section className="warning-content__messages-section">
<div className="warning-content__message-list-container">
<OverlayScrollbar>
<ul className="warning-content__message-list">
{warningMessages?.map((warning) => (
<li className="warning-content__message-item" key={warning.message}>
{warning.message}
</li>
))}
</ul>
</OverlayScrollbar>
{warningMessages?.length > 10 && (
<div className="warning-content__scroll-hint">
<ChevronsDown
size={16}
color={Color.BG_VANILLA_100}
className="warning-content__scroll-hint-icon"
/>
<span className="warning-content__scroll-hint-text">
Scroll for more
</span>
</div>
)}
</div>
</section>
</section>
);
}
interface WarningPopoverProps extends PopoverProps {
children?: ReactNode;
warningData: Warning;
}
function WarningPopover({
children,
warningData,
...popoverProps
}: WarningPopoverProps): JSX.Element {
return (
<Popover
content={<WarningContent warning={warningData} />}
overlayStyle={{ padding: 0, maxWidth: '600px' }}
overlayInnerStyle={{ padding: 0 }}
autoAdjustOverflow
{...popoverProps}
>
{children || (
<TriangleAlert
size={16}
style={{ cursor: 'pointer' }}
color={Color.BG_AMBER_500}
/>
)}
</Popover>
);
}
WarningPopover.defaultProps = {
children: undefined,
};
export default WarningPopover;

View File

@@ -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];
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -76,3 +82,15 @@ export const queryOperatorSuggestions = [
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
...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}`;
}

View File

@@ -46,7 +46,6 @@ export enum QueryParams {
msgSystem = 'msgSystem',
destination = 'destination',
kindString = 'kindString',
summaryFilters = 'summaryFilters',
tab = 'tab',
thresholds = 'thresholds',
selectedExplorerView = 'selectedExplorerView',

View File

@@ -1,4 +1,4 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import {
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
@@ -28,7 +28,7 @@ const defaultAnnotations = {
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {
@@ -62,7 +62,7 @@ export const alertDefaults: AlertDef = {
export const anamolyAlertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
condition: {
compositeQuery: {
@@ -107,7 +107,7 @@ export const anamolyAlertDefaults: AlertDef = {
export const logAlertDefaults: AlertDef = {
alertType: AlertTypes.LOGS_BASED_ALERT,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {
@@ -139,7 +139,7 @@ export const logAlertDefaults: AlertDef = {
export const traceAlertDefaults: AlertDef = {
alertType: AlertTypes.TRACES_BASED_ALERT,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {
@@ -171,7 +171,7 @@ export const traceAlertDefaults: AlertDef = {
export const exceptionAlertDefaults: AlertDef = {
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
condition: {
compositeQuery: {
builderQueries: {

View File

@@ -1,6 +1,6 @@
import { Form, Row } from 'antd';
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 FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
@@ -71,14 +71,14 @@ function CreateRules(): JSX.Element {
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...anamolyAlertDefaults,
version: version || ENTITY_VERSION_V4,
version: version || ENTITY_VERSION_V5,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
break;
default:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V4,
version: version || ENTITY_VERSION_V5,
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
});
}

View File

@@ -2,6 +2,12 @@
height: 57vh;
width: 100%;
.chart-preview-header {
display: flex;
align-items: center;
gap: 8px;
}
.threshold-alert-uplot-chart-container {
height: calc(100% - 24px);
}

View File

@@ -1,12 +1,14 @@
import './ChartPreview.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import Spinner from 'components/Spinner';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
@@ -26,6 +28,7 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -34,7 +37,10 @@ import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { AlertDef } from 'types/api/alerts/def';
import { LegendPosition } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -44,7 +50,7 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import { ChartContainer, FailedMessageContainer } from './styles';
import { ChartContainer } from './styles';
import { getThresholdLabel } from './utils';
export interface ChartPreviewProps {
@@ -80,6 +86,7 @@ function ChartPreview({
const threshold = alertDef?.condition.target || 0;
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
@@ -171,6 +178,19 @@ function ChartPreview({
setMaxTimeScale(endTime);
}, [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) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
@@ -257,6 +277,10 @@ function ChartPreview({
timezone: timezone.value,
currentQuery,
query: query || currentQuery,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition: LegendPosition.BOTTOM,
}),
[
yAxisUnit,
@@ -274,6 +298,7 @@ function ChartPreview({
timezone.value,
currentQuery,
query,
graphVisibility,
],
);
@@ -289,20 +314,23 @@ function ChartPreview({
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
const isWarning = !isEmpty(queryResponse.data?.warning);
return (
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>
{headline}
<div className="chart-preview-header">
{headline}
{isWarning && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>
<div className="threshold-alert-uplot-chart-container">
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
<ErrorInPlace error={queryResponse.error as APIError} />
)}
{chartDataAvailable && !isAnomalyDetectionAlert && (

View File

@@ -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) {
@@ -238,8 +240,18 @@ function FormAlertRules({
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(queryData);
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
// Only update if functions actually changed to avoid resetting aggregateAttribute
const currentFunctions = queryData.functions || [];
const functionsChanged = !isEqual(currentFunctions, updatedFunctions);
if (functionsChanged) {
const updatedQueryData = {
...queryData,
functions: updatedFunctions,
};
handleSetQueryData(index, updatedQueryData);
}
}
};

View File

@@ -4,9 +4,7 @@
overflow-y: hidden;
.full-view-header-container {
display: flex;
flex-direction: column;
gap: 16px;
height: 40px;
}
.graph-container {

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './WidgetFullView.styles.scss';
import {
@@ -9,23 +8,18 @@ import {
import { Button, Input, Spin } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useChartMutable } from 'hooks/useChartMutable';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -58,7 +52,6 @@ function FullView({
onClickHandler,
customOnDragSelect,
setCurrentGraphRef,
enableDrillDown = false,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedTime } = useSelector<
@@ -70,7 +63,6 @@ function FullView({
const location = useLocation();
const fullViewRef = useRef<HTMLDivElement>(null);
const { handleRunQuery } = useQueryBuilder();
useEffect(() => {
setCurrentGraphRef(fullViewRef);
@@ -122,13 +114,6 @@ function FullView({
};
});
const { dashboardEditView, handleResetQuery, showResetQuery } = useDrilldown({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
});
useEffect(() => {
setRequestData((prev) => ({
...prev,
@@ -219,115 +204,71 @@ function FullView({
return (
<div className="full-view-container">
<OverlayScrollbar>
<>
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{enableDrillDown && (
<div className="drildown-options-container">
{showResetQuery && (
<Button type="link" onClick={handleResetQuery}>
Reset Query
</Button>
)}
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}
onClick={(): void => {
if (dashboardEditView) {
safeNavigate(dashboardEditView);
}
}}
>
Switch to Edit Mode
</Button>
</div>
)}
<div className="time-container">
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
)}
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
/>
<Button
style={{
marginLeft: '4px',
}}
onClick={(): void => {
response.refetch();
}}
type="primary"
icon={<SyncOutlined />}
/>
</div>
</TimeContainer>
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
)}
{enableDrillDown && (
<>
<QueryBuilderV2
panelType={widget.panelTypes}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={widget.panelTypes === PANEL_TYPES.LIST}
// filterConfigs={filterConfigs}
// queryComponents={queryComponents}
/>
<RightToolbarActions
onStageRunQuery={(): void => {
handleRunQuery(true, true);
}}
/>
</>
)}
</div>
<div
className={cx('graph-container', {
disabled: isDashboardLocked,
'height-widget':
widget?.mergeAllActiveQueries || widget?.stackedBarChart,
'list-graph-container': isListView,
})}
ref={fullViewRef}
>
<GraphContainer
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
/>
<Button
style={{
height: isListView ? '100%' : '90%',
marginLeft: '4px',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
{isTablePanel && (
<Input
addonBefore={<SearchOutlined size={14} />}
className="global-search"
placeholder="Search..."
allowClear
key={widget.id}
onChange={(e): void => {
setSearchTerm(e.target.value || '');
}}
/>
)}
<PanelWrapper
queryResponse={response}
widget={widget}
setRequestData={setRequestData}
isFullViewMode
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={customOnDragSelect ?? onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
onClickHandler={onClickHandler}
enableDrillDown={enableDrillDown}
/>
</GraphContainer>
</div>
</>
</OverlayScrollbar>
onClick={(): void => {
response.refetch();
}}
type="primary"
icon={<SyncOutlined />}
/>
</TimeContainer>
)}
</div>
<div
className={cx('graph-container', {
disabled: isDashboardLocked,
'height-widget': widget?.mergeAllActiveQueries || widget?.stackedBarChart,
'list-graph-container': isListView,
})}
ref={fullViewRef}
>
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
{isTablePanel && (
<Input
addonBefore={<SearchOutlined size={14} />}
className="global-search"
placeholder="Search..."
allowClear
key={widget.id}
onChange={(e): void => {
setSearchTerm(e.target.value || '');
}}
/>
)}
<PanelWrapper
queryResponse={response}
widget={widget}
setRequestData={setRequestData}
isFullViewMode
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={customOnDragSelect ?? onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
onClickHandler={onClickHandler}
/>
</GraphContainer>
</div>
</div>
);
}

View File

@@ -18,7 +18,6 @@ export const NotFoundContainer = styled.div`
export const TimeContainer = styled.div<Props>`
display: flex;
justify-content: flex-end;
gap: 16px;
align-items: center;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
@@ -26,10 +25,6 @@ export const TimeContainer = styled.div<Props>`
margin-bottom: 1rem;
`
: css``}
.time-container {
display: flex;
}
`;
export const GraphContainer = styled.div<GraphContainerProps>`

View File

@@ -59,7 +59,6 @@ export interface FullViewProps {
isDependedDataLoaded?: boolean;
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
setCurrentGraphRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
enableDrillDown?: boolean;
}
export interface GraphManagerProps extends UplotProps {

View File

@@ -1,84 +0,0 @@
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
export interface DrilldownQueryProps {
widget: Widgets;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
enableDrillDown: boolean;
selectedDashboard: Dashboard | undefined;
}
export interface UseDrilldownReturn {
dashboardEditView: string;
handleResetQuery: () => void;
showResetQuery: boolean;
}
const useDrilldown = ({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
}: DrilldownQueryProps): UseDrilldownReturn => {
const isMounted = useRef(false);
const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
useEffect(() => {
if (enableDrillDown && !!compositeQuery) {
setRequestData((prev) => ({
...prev,
query: compositeQuery,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentQuery, compositeQuery]);
// update composite query with widget query if composite query is not present in url.
// Composite query should be in the url if switch to edit mode is clicked or drilldown happens from dashboard.
useEffect(() => {
if (enableDrillDown && !isMounted.current) {
redirectWithQueryBuilderData(compositeQuery || widget.query);
}
isMounted.current = true;
}, [widget, enableDrillDown, compositeQuery, redirectWithQueryBuilderData]);
const dashboardEditView = selectedDashboard?.id
? generateExportToDashboardLink({
query: currentQuery,
panelType: widget.panelTypes,
dashboardId: selectedDashboard?.id || '',
widgetId: widget.id,
})
: '';
const showResetQuery = useMemo(
() =>
JSON.stringify(widget.query?.builder) !==
JSON.stringify(compositeQuery?.builder),
[widget.query, compositeQuery],
);
const handleResetQuery = useCallback((): void => {
redirectWithQueryBuilderData(widget.query);
}, [redirectWithQueryBuilderData, widget.query]);
return {
dashboardEditView,
handleResetQuery,
showResetQuery,
};
};
export default useDrilldown;

View File

@@ -30,6 +30,7 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
@@ -45,7 +46,6 @@ import { getLocalStorageGraphVisibilityState, handleGraphClick } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
version,
threshold,
headerMenuList,
@@ -62,7 +62,6 @@ function WidgetGraphComponent({
customErrorMessage,
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@@ -185,9 +184,19 @@ function WidgetGraphComponent({
notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.',
});
const clonedWidget = updatedDashboard.data?.data?.widgets?.find(
(w) => w.id === uuid,
) as Widgets;
const queryParams = {
graphType: widget?.panelTypes,
widgetId: uuid,
[QueryParams.graphType]: clonedWidget?.panelTypes,
[QueryParams.widgetId]: uuid,
...(clonedWidget?.query && {
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(clonedWidget.query),
),
}),
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
},
@@ -227,7 +236,6 @@ function WidgetGraphComponent({
const onToggleModelHandler = (): void => {
const existingSearchParams = new URLSearchParams(search);
existingSearchParams.delete(QueryParams.expandedWidgetId);
existingSearchParams.delete(QueryParams.compositeQuery);
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
if (queryResponse.data?.payload) {
const {
@@ -356,7 +364,6 @@ function WidgetGraphComponent({
onClickHandler={onClickHandler ?? graphClickHandler}
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
enableDrillDown={enableDrillDown}
/>
</Modal>
@@ -369,7 +376,6 @@ function WidgetGraphComponent({
onDelete={handleOnDelete}
onClone={onCloneHandler}
queryResponse={queryResponse}
errorMessage={errorMessage}
threshold={threshold}
headerMenuList={headerMenuList}
isWarning={isWarning}
@@ -408,7 +414,6 @@ function WidgetGraphComponent({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customOnRowClick={customOnRowClick}
enableDrillDown={enableDrillDown}
/>
</div>
)}
@@ -421,7 +426,6 @@ WidgetGraphComponent.defaultProps = {
setLayout: undefined,
onClickHandler: undefined,
customTimeRangeWindowForCoRelation: undefined,
enableDrillDown: false,
};
export default WidgetGraphComponent;

View File

@@ -53,7 +53,6 @@ function GridCardGraph({
customTimeRange,
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@@ -268,7 +267,6 @@ function GridCardGraph({
getGraphData?.(data?.payload?.data);
setDashboardQueryRangeCalled(true);
},
showErrorModal: false,
},
);
@@ -303,7 +301,7 @@ function GridCardGraph({
widget={widget}
queryResponse={queryResponse}
errorMessage={errorMessage}
isWarning={false}
isWarning={!isEmpty(queryResponse.data?.warning)}
version={version}
threshold={threshold}
headerMenuList={menuList}
@@ -319,7 +317,6 @@ function GridCardGraph({
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
customOnRowClick={customOnRowClick}
customTimeRangeWindowForCoRelation={customTimeRangeWindowForCoRelation}
enableDrillDown={enableDrillDown}
/>
)}
</div>
@@ -335,7 +332,6 @@ GridCardGraph.defaultProps = {
version: 'v3',
analyticsEvent: undefined,
customTimeRangeWindowForCoRelation: undefined,
enableDrillDown: false,
};
export default memo(GridCardGraph);

View File

@@ -41,7 +41,6 @@ export interface WidgetGraphComponentProps {
customErrorMessage?: string;
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
enableDrillDown?: boolean;
}
export interface GridCardGraphProps {
@@ -70,7 +69,6 @@ export interface GridCardGraphProps {
};
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
enableDrillDown?: boolean;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -235,6 +235,7 @@ export const handleGraphClick = async ({
? customTracesTimeRange?.end
: xValue + (stepInterval ?? 60),
shouldResolveQuery: true,
widgetQuery: widget?.query,
}),
}));

View File

@@ -53,12 +53,11 @@ import { WidgetRowHeader } from './WidgetRow';
interface GraphLayoutProps {
handle: FullScreenHandle;
enableDrillDown?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { handle, enableDrillDown = false } = props;
const { handle } = props;
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
@@ -585,7 +584,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
version={ENTITY_VERSION_V5}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}
enableDrillDown={enableDrillDown}
/>
</Card>
</CardContainer>
@@ -672,7 +670,3 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
}
export default GraphLayout;
GraphLayout.defaultProps = {
enableDrillDown: false,
};

View File

@@ -10,10 +10,13 @@ import {
InfoCircleOutlined,
MoreOutlined,
SearchOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
import Spinner from 'components/Spinner';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
@@ -28,11 +31,12 @@ import { unparse } from 'papaparse';
import { useAppContext } from 'providers/App/App';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponse, Warning } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
import { errorTooltipPosition } from './config';
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
import { MenuItem } from './types';
import { generateMenuList, isTWidgetOptions } from './utils';
@@ -45,9 +49,11 @@ interface IWidgetHeaderProps {
onClone?: VoidFunction;
parentHover: boolean;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
SuccessResponse<MetricRangePayloadProps, unknown> & {
warning?: Warning;
},
Error
>;
errorMessage: string | undefined;
threshold?: ReactNode;
headerMenuList?: MenuItemKeys[];
isWarning: boolean;
@@ -64,7 +70,6 @@ function WidgetHeader({
onClone,
parentHover,
queryResponse,
errorMessage,
threshold,
headerMenuList,
isWarning,
@@ -212,12 +217,8 @@ function WidgetHeader({
});
const renderErrorMessage = useMemo(
() =>
errorMessage
?.split('\n')
// eslint-disable-next-line react/no-array-index-key
.map((item, i) => <p key={i}>{item}</p>),
[errorMessage],
() => <ErrorContent error={queryResponse.error as APIError} />,
[queryResponse.error],
);
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
@@ -278,23 +279,23 @@ function WidgetHeader({
<Spinner style={{ paddingRight: '0.25rem' }} />
)}
{queryResponse.isError && (
<Tooltip
title={renderErrorMessage}
<ErrorPopover
content={renderErrorMessage}
placement={errorTooltipPosition}
className="widget-api-actions"
overlayStyle={{ padding: 0, maxWidth: '600px' }}
overlayInnerStyle={{ padding: 0 }}
autoAdjustOverflow
>
<CircleX size={20} />
</Tooltip>
<CircleX
size={16}
style={{ cursor: 'pointer' }}
color={Color.BG_CHERRY_500}
/>
</ErrorPopover>
)}
{isWarning && (
<Tooltip
title={WARNING_MESSAGE}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<WarningOutlined />
</Tooltip>
{isWarning && queryResponse.data?.warning && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
{globalSearchAvailable && (
<SearchOutlined

View File

@@ -4,17 +4,10 @@ import GraphLayoutContainer from './GridCardLayout';
interface GridGraphProps {
handle: FullScreenHandle;
enableDrillDown?: boolean;
}
function GridGraph(props: GridGraphProps): JSX.Element {
const { handle, enableDrillDown = false } = props;
return (
<GraphLayoutContainer handle={handle} enableDrillDown={enableDrillDown} />
);
const { handle } = props;
return <GraphLayoutContainer handle={handle} />;
}
export default GridGraph;
GridGraph.defaultProps = {
enableDrillDown: false,
};

View File

@@ -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 { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useCallback } from 'react';
import { useMutation } from 'react-query';
@@ -32,7 +32,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
GlobalReducer
>((state) => state.globalTime);
const queryRangeMutation = useMutation(getQueryRangeFormat);
const queryRangeMutation = useMutation(getSubstituteVars);
const getUpdatedQuery = useCallback(
async ({
@@ -40,7 +40,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
selectedDashboard,
}: UseUpdatedQueryOptions): Promise<Query> => {
// Prepare query payload with resolved variables
const { queryPayload } = prepareQueryRangePayload({
const { queryPayload } = prepareQueryRangePayloadV5({
query: widgetConfig.query,
graphType: getGraphType(widgetConfig.panelTypes),
selectedTime: widgetConfig.timePreferance,
@@ -53,7 +53,10 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
// Map query data from API response
return mapQueryDataFromApi(queryResult.compositeQuery, widgetConfig?.query);
return mapQueryDataFromApi(
queryResult.data.compositeQuery,
widgetConfig?.query,
);
},
[globalSelectedInterval, queryRangeMutation],
);

View File

@@ -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(

View File

@@ -22,7 +22,6 @@ export type GridTableComponentProps = {
widgetId?: string;
renderColumnCell?: QueryTableProps['renderColumnCell'];
customColTitles?: Record<string, string>;
enableDrillDown?: boolean;
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnType } from 'antd/es/table';
import { ColumnsType, ColumnType } from 'antd/es/table';
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
@@ -9,12 +9,6 @@ import { isEmpty, isNaN } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
// Custom column type that extends ColumnType to include isValueColumn
export interface CustomDataColumnType<T> extends ColumnType<T> {
isValueColumn?: boolean;
queryName?: string;
}
// Helper function to evaluate the condition based on the operator
function evaluateCondition(
operator: string | undefined,
@@ -186,9 +180,9 @@ export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
renderColumnCell?: QueryTableProps['renderColumnCell'],
): { columns: CustomDataColumnType<RowData>[]; dataSource: RowData[] } {
const columns: CustomDataColumnType<RowData>[] =
data.columns?.reduce<CustomDataColumnType<RowData>[]>((acc, item) => {
): { columns: ColumnsType<RowData>; dataSource: RowData[] } {
const columns: ColumnsType<RowData> =
data.columns?.reduce<ColumnsType<RowData>>((acc, item) => {
// is the column is the value column then we need to check for the available legend
const legend = item.isValueColumn
? getQueryLegend(currentQuery, item.queryName)
@@ -199,13 +193,11 @@ export function createColumnsAndDataSource(
(query) => query.queryName === item.queryName,
)?.aggregations?.length || 0;
const column: CustomDataColumnType<RowData> = {
const column: ColumnType<RowData> = {
dataIndex: item.id || item.name,
// if no legend present then rely on the column name value
title: !isNewAggregation && !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
isValueColumn: item.isValueColumn,
queryName: item.queryName,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
};

View File

@@ -1,7 +1,11 @@
import { orange } from '@ant-design/colors';
import { SettingOutlined } from '@ant-design/icons';
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 { TitleWrapper } from './BodyTitleRenderer.styles';
@@ -29,7 +33,9 @@ function BodyTitleRenderer({
getDataTypes(value),
),
`${value}`,
isFilterIn ? OPERATORS.HAS : OPERATORS.NHAS,
isFilterIn
? QUERY_BUILDER_FUNCTIONS.HAS
: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
true,
parentIsArray ? getDataTypes([value]) : getDataTypes(value),
);

View File

@@ -73,6 +73,8 @@ export default function TableRow({
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
if (!column.key) return null;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
@@ -97,6 +99,7 @@ export default function TableRow({
fontSize={fontSize}
columnKey={column.key as string}
onClick={handleShowLogDetails}
className={column.key as string}
>
{cloneElement(children, props)}
</TableCellStyled>

View File

@@ -136,7 +136,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
key={column.key}
fontSize={tableViewProps?.fontSize}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
{...(isDragColumn && { className: `dragHandler ${column.key}` })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}

View File

@@ -1,3 +1,4 @@
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -8,5 +9,6 @@ export type LogsExplorerListProps = {
logs: ILog[];
onEndReached: (index: number) => void;
isError: boolean;
error?: Error | APIError;
isFilterApplied: boolean;
};

View File

@@ -141,6 +141,7 @@ describe('LogsExplorerList - empty states', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,
@@ -205,6 +206,7 @@ describe('LogsExplorerList - empty states', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,

View File

@@ -2,6 +2,7 @@ import './LogsExplorerList.style.scss';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
@@ -12,7 +13,6 @@ import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
@@ -21,6 +21,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import APIError from 'types/api/error';
// interfaces
import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -45,9 +46,11 @@ function LogsExplorerList({
logs,
onEndReached,
isError,
error,
isFilterApplied,
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { activeLogId } = useCopyLogLink();
const {
@@ -255,7 +258,9 @@ function LogsExplorerList({
/>
)}
{isError && !isLoading && !isFetching && <LogsError />}
{isError && !isLoading && !isFetching && error && (
<ErrorInPlace error={error as APIError} />
)}
{!isLoading && !isError && logs.length > 0 && (
<>

View File

@@ -1,4 +1,5 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { isEmpty } from 'lodash-es';
import { IField } from 'types/api/logs/fields';
import {
IBuilderQuery,
@@ -8,11 +9,13 @@ import {
export const convertKeysToColumnFields = (
keys: TelemetryFieldKey[],
): IField[] =>
keys.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
keys
.filter((item) => !isEmpty(item.name))
.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
/**
* Determines if a query represents a trace-to-logs navigation
* by checking for the presence of a trace_id filter.

View File

@@ -1,7 +1,9 @@
import APIError from 'types/api/error';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export type LogsExplorerTableProps = {
data: QueryDataV3[];
isLoading: boolean;
isError: boolean;
error?: Error | APIError;
};

View File

@@ -1,11 +1,12 @@
import './LogsExplorerTable.styles.scss';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { initialQueriesMap } from 'constants/queryBuilder';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { QueryTable } from 'container/QueryTable';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo } from 'react';
import APIError from 'types/api/error';
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
@@ -13,6 +14,7 @@ function LogsExplorerTable({
data,
isLoading,
isError,
error,
}: LogsExplorerTableProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@@ -20,8 +22,8 @@ function LogsExplorerTable({
return <LogsLoading />;
}
if (isError) {
return <LogsError />;
if (isError && error) {
return <ErrorInPlace error={error as APIError} />;
}
return (

View File

@@ -51,8 +51,10 @@ import { ArrowUp10, Minus, Sliders } from 'lucide-react';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
memo,
MutableRefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
@@ -62,7 +64,9 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -87,6 +91,7 @@ function LogsExplorerViewsContainer({
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
setWarning,
}: {
selectedView: ExplorerViews;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
@@ -94,6 +99,7 @@ function LogsExplorerViewsContainer({
listQueryKeyRef: MutableRefObject<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chartQueryKeyRef: MutableRefObject<any>;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const dispatch = useDispatch();
@@ -269,6 +275,7 @@ function LogsExplorerViewsContainer({
isFetching,
isError,
isSuccess,
error,
} = useGetExplorerQueryRange(
requestData,
panelType,
@@ -372,6 +379,13 @@ function LogsExplorerViewsContainer({
[activeLogId, orderBy, listQuery, selectedView],
);
useEffect(() => {
if (data?.payload) {
setWarning(data?.warning);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload, data?.warning]);
const handleEndReached = useCallback(() => {
if (!listQuery) return;
@@ -771,6 +785,7 @@ function LogsExplorerViewsContainer({
logs={logs}
onEndReached={handleEndReached}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
/>
)}
@@ -780,8 +795,10 @@ function LogsExplorerViewsContainer({
isLoading={isLoading || isFetching}
data={data}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
dataSource={DataSource.LOGS}
setWarning={setWarning}
/>
)}
@@ -794,6 +811,7 @@ function LogsExplorerViewsContainer({
}
isLoading={isLoading || isFetching}
isError={isError}
error={error as APIError}
/>
)}
</div>

View File

@@ -82,6 +82,48 @@ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
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', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
@@ -131,6 +173,7 @@ const renderer = (): RenderResult =>
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</VirtuosoMockContext.Provider>,
@@ -172,21 +215,6 @@ describe('LogsExplorerViews -', () => {
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
});
it('check error state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: false,
isFetching: false,
isError: true,
});
const { queryByText } = renderer();
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
});
it('should add activeLogId filter when present in URL', async () => {
// Mock useCopyLogLink to return an activeLogId
(useCopyLogLink as jest.Mock).mockReturnValue({
@@ -206,6 +234,7 @@ describe('LogsExplorerViews -', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,

View File

@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react';
import { Switch } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
@@ -12,9 +13,11 @@ import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Warning } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -118,6 +121,8 @@ function Explorer(): JSX.Element {
[],
);
const [warning, setWarning] = useState<Warning | undefined>(undefined);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">
@@ -131,6 +136,7 @@ function Explorer(): JSX.Element {
/>
</div>
<div className="explore-header-right-actions">
{!isEmpty(warning) && <WarningPopover warningData={warning} />}
<DateTimeSelector showAutoRefresh />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
@@ -167,7 +173,10 @@ function Explorer(): JSX.Element {
</Button.Group> */}
<div className="explore-content">
{selectedTab === ExplorerTabs.TIME_SERIES && (
<TimeSeries showOneChartPerQuery={showOneChartPerQuery} />
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
/>
)}
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* {selectedTab === ExplorerTabs.RELATED_METRICS && (

View File

@@ -8,7 +8,6 @@ import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
@@ -22,7 +21,10 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { TimeSeriesProps } from './types';
import { splitQueryIntoOneChartPerQuery } from './utils';
function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
function TimeSeries({
showOneChartPerQuery,
setWarning,
}: TimeSeriesProps): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
@@ -61,8 +63,6 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const { showErrorModal } = useErrorModal();
const queries = useQueries(
queryPayloads.map((payload, index) => ({
queryKey: [
@@ -104,9 +104,6 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
return failureCount < 3;
},
onError: (error: APIError): void => {
showErrorModal(error);
},
})),
);
@@ -150,6 +147,8 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
data={datapoint}
yAxisUnit={yAxisUnit}
dataSource={DataSource.METRICS}
error={queries[index].error as APIError}
setWarning={setWarning}
/>
</div>
))}

View File

@@ -1,6 +1,7 @@
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export enum ExplorerTabs {
@@ -10,6 +11,7 @@ export enum ExplorerTabs {
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
}
export interface RelatedMetricsProps {

View File

@@ -1,5 +1,6 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
import { initialQueriesMap } from 'constants/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -110,6 +111,15 @@ export function getMetricDetailsQuery(
isJSON: false,
dataType: DataTypes.String,
},
aggregations: [
{
metricName,
timeAggregation: timeAggregation as TimeAggregation,
spaceAggregation: spaceAggregation as SpaceAggregation,
reduceTo: 'avg',
temporality: '',
},
],
aggregateOperator,
timeAggregation,
spaceAggregation,

View File

@@ -1,9 +1,11 @@
/* eslint-disable no-nested-ternary */
import './Summary.styles.scss';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap } from 'constants/queryBuilder';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
@@ -12,11 +14,13 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import InspectModal from '../Inspect';
import MetricDetails from '../MetricDetails';
import { MetricsLoading } from '../MetricsLoading/MetricsLoading';
import {
IS_INSPECT_MODAL_OPEN_KEY,
IS_METRIC_DETAILS_OPEN_KEY,
@@ -267,30 +271,64 @@ function Summary(): JSX.Element {
});
};
const isMetricsListDataEmpty = useMemo(
() =>
formattedMetricsData.length === 0 && !isMetricsLoading && !isMetricsFetching,
[formattedMetricsData, isMetricsLoading, isMetricsFetching],
);
const isMetricsTreeMapDataEmpty = useMemo(
() =>
!treeMapData?.payload?.data[heatmapView]?.length &&
!isTreeMapLoading &&
!isTreeMapFetching,
[
treeMapData?.payload?.data,
heatmapView,
isTreeMapLoading,
isTreeMapFetching,
],
);
console.log({
isMetricsListDataEmpty,
isMetricsTreeMapDataEmpty,
treeMapData,
sec: treeMapData?.payload?.data[heatmapView],
});
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
<MetricsSearch query={searchQuery} onChange={handleFilterChange} />
<MetricsTreemap
data={treeMapData?.payload}
isLoading={isTreeMapLoading || isTreeMapFetching}
isError={isProportionViewError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={handleSetHeatmapView}
/>
<MetricsTable
isLoading={isMetricsLoading || isMetricsFetching}
isError={isListViewError}
data={formattedMetricsData}
pageSize={pageSize}
currentPage={currentPage}
onPaginationChange={onPaginationChange}
setOrderBy={handleSetOrderBy}
totalCount={metricsData?.payload?.data?.total || 0}
openMetricDetails={openMetricDetails}
queryFilters={queryFilters}
/>
{isMetricsLoading || isTreeMapLoading ? (
<MetricsLoading />
) : isMetricsListDataEmpty && isMetricsTreeMapDataEmpty ? (
<NoLogs dataSource={DataSource.METRICS} />
) : (
<>
<MetricsTreemap
data={treeMapData?.payload}
isLoading={isTreeMapLoading || isTreeMapFetching}
isError={isProportionViewError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={handleSetHeatmapView}
/>
<MetricsTable
isLoading={isMetricsLoading || isMetricsFetching}
isError={isListViewError}
data={formattedMetricsData}
pageSize={pageSize}
currentPage={currentPage}
onPaginationChange={onPaginationChange}
setOrderBy={handleSetOrderBy}
totalCount={metricsData?.payload?.data?.total || 0}
openMetricDetails={openMetricDetails}
queryFilters={queryFilters}
/>
</>
)}
</div>
{isMetricDetailsOpen && (
<MetricDetails

View File

@@ -11,7 +11,7 @@ function GridGraphs(props: GridGraphsProps): JSX.Element {
const { handle } = props;
return (
<GridComponentSliderContainer>
<GridGraphLayout handle={handle} enableDrillDown />
<GridGraphLayout handle={handle} />
</GridComponentSliderContainer>
);
}

View File

@@ -239,10 +239,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip
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"
/>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={queryResponse.isFetching}
type="primary"

View File

@@ -10,6 +10,12 @@
align-items: center;
justify-content: space-between;
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.plot-tag {
display: inline-flex;
padding: 4px 4px 4px 6px;

View File

@@ -1,7 +1,9 @@
import { Card, Typography } from 'antd';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { WidgetGraphContainerProps } from 'container/NewWidget/types';
import APIError from 'types/api/error';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { NotFoundContainer } from './styles';
@@ -14,7 +16,6 @@ function WidgetGraphContainer({
setRequestData,
selectedWidget,
isLoadingPanelData,
enableDrillDown = false,
}: WidgetGraphContainerProps): JSX.Element {
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
@@ -37,7 +38,7 @@ function WidgetGraphContainer({
if (queryResponse?.error) {
return (
<NotFoundContainer>
<Typography>{queryResponse.error.message}</Typography>
<ErrorInPlace error={queryResponse.error as APIError} />
</NotFoundContainer>
);
}
@@ -85,7 +86,6 @@ function WidgetGraphContainer({
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedGraph={selectedGraph}
enableDrillDown={enableDrillDown}
/>
);
}

View File

@@ -36,7 +36,6 @@ function WidgetGraph({
queryResponse,
setRequestData,
selectedGraph,
enableDrillDown = false,
}: WidgetGraphProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const lineChartRef = useRef<ToggleGraphProps>();
@@ -189,7 +188,6 @@ function WidgetGraph({
onClickHandler={graphClickHandler}
graphVisibility={graphVisibility}
setGraphVisibility={setGraphVisibility}
enableDrillDown={enableDrillDown}
/>
</div>
);
@@ -203,11 +201,6 @@ interface WidgetGraphProps {
>;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
selectedGraph: PANEL_TYPES;
enableDrillDown?: boolean;
}
export default WidgetGraph;
WidgetGraph.defaultProps = {
enableDrillDown: false,
};

View File

@@ -1,11 +1,14 @@
import './WidgetGraph.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { Card } from 'container/GridCardLayout/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { memo } from 'react';
import { Warning } from 'types/api';
import { WidgetGraphContainerProps } from '../../types';
import PlotTag from './PlotTag';
@@ -18,7 +21,6 @@ function WidgetGraph({
setRequestData,
selectedWidget,
isLoadingPanelData,
enableDrillDown = false,
}: WidgetGraphContainerProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
@@ -35,7 +37,12 @@ function WidgetGraph({
return (
<Container $panelType={selectedGraph} className="widget-graph">
<div className="header">
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
<div className="header-left">
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
{!isEmpty(queryResponse.data?.warning) && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
{queryResponse.error && (
@@ -50,7 +57,6 @@ function WidgetGraph({
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedWidget={selectedWidget}
enableDrillDown={enableDrillDown}
/>
</Container>
);

View File

@@ -27,7 +27,6 @@ function LeftContainer({
setRequestData,
isLoadingPanelData,
setQueryResponse,
enableDrillDown = false,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
// const { selectedDashboard } = useDashboard();
@@ -65,7 +64,6 @@ function LeftContainer({
setRequestData={setRequestData}
selectedWidget={selectedWidget}
isLoadingPanelData={isLoadingPanelData}
enableDrillDown={enableDrillDown}
/>
<QueryContainer className="query-section-left-container">
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />

View File

@@ -36,11 +36,6 @@
}
}
.right-header {
display: flex;
gap: 16px;
}
.save-btn {
display: flex;
height: 32px;

View File

@@ -3,7 +3,8 @@ import './ColumnUnitSelector.styles.scss';
import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
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 YAxisUnitSelector from '../YAxisUnitSelector';
@@ -31,12 +32,49 @@ export function ColumnUnitSelector(
[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 (
<section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text>
{aggregationQueries.map(({ value, label }) => (
<YAxisUnitSelector
defaultValue={columnUnits[value]}
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}

View File

@@ -4,6 +4,7 @@ import { Button, Collapse, ColorPicker, Tooltip, Typography } from 'antd';
import { themeColors } from 'constants/theme';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { Palette } from 'lucide-react';
@@ -69,7 +70,11 @@ function LegendColors({
const legendLabels = useMemo(() => {
if (queryResponse?.data?.payload?.data?.result) {
return queryResponse.data.payload.data.result.map((item: any) =>
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
getLegend(
item,
currentQuery,
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
),
);
}

View File

@@ -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 && (

View File

@@ -16,11 +16,13 @@ const findCategoryByName = (
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
function YAxisUnitSelector({
defaultValue,
value,
onSelect,
fieldLabel,
handleClear,
}: {
defaultValue: string;
value: string;
onSelect: OnSelectType;
fieldLabel: string;
handleClear?: () => void;
@@ -40,6 +42,7 @@ function YAxisUnitSelector({
options={options}
allowClear
defaultValue={findCategoryById(defaultValue)?.name}
value={findCategoryById(value)?.name || ''}
onClear={handleClear}
onSelect={onSelectHandler}
filterOption={(inputValue, option): boolean => {

View File

@@ -339,6 +339,7 @@ function RightContainer({
<YAxisUnitSelector
defaultValue={yAxisUnit}
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE

View File

@@ -21,7 +21,6 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
@@ -73,10 +72,7 @@ import {
placeWidgetBetweenRows,
} from './utils';
function NewWidget({
selectedGraph,
enableDrillDown = false,
}: NewWidgetProps): JSX.Element {
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
@@ -694,26 +690,6 @@ function NewWidget({
}
}, [selectedLogFields, selectedTracesFields, currentQuery, selectedGraph]);
const showSwitchToViewModeButton =
enableDrillDown && !isNewDashboard && !!query.get('widgetId');
const handleSwitchToViewMode = useCallback(() => {
if (!query.get('widgetId')) return;
const widgetId = query.get('widgetId') || '';
const queryParams = {
[QueryParams.expandedWidgetId]: widgetId,
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(currentQuery),
),
};
const updatedSearch = createQueryParams(queryParams);
safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
search: updatedSearch,
});
}, [query, safeNavigate, dashboardId, currentQuery]);
return (
<Container>
<div className="edit-header">
@@ -730,42 +706,31 @@ function NewWidget({
</Typography.Text>
</Flex>
</div>
<div className="right-header">
{showSwitchToViewModeButton && (
<Button
data-testid="switch-to-view-mode"
disabled={isSaveDisabled || !currentQuery}
onClick={handleSwitchToViewMode}
>
Switch to View Mode
</Button>
)}
{isSaveDisabled && (
<Button
type="primary"
data-testid="new-widget-save"
loading={updateDashboardMutation.isLoading}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
className="save-btn"
>
Save Changes
</Button>
)}
{!isSaveDisabled && (
<Button
type="primary"
data-testid="new-widget-save"
loading={updateDashboardMutation.isLoading}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
icon={<Check size={14} />}
className="save-btn"
>
Save Changes
</Button>
)}
</div>
{isSaveDisabled && (
<Button
type="primary"
data-testid="new-widget-save"
loading={updateDashboardMutation.isLoading}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
className="save-btn"
>
Save Changes
</Button>
)}
{!isSaveDisabled && (
<Button
type="primary"
data-testid="new-widget-save"
loading={updateDashboardMutation.isLoading}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
icon={<Check size={14} />}
className="save-btn"
>
Save Changes
</Button>
)}
</div>
<PanelContainer>
@@ -784,7 +749,6 @@ function NewWidget({
setRequestData={setRequestData}
isLoadingPanelData={isLoadingPanelData}
setQueryResponse={setQueryResponse}
enableDrillDown={enableDrillDown}
/>
)}
</OverlayScrollbar>

View File

@@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { SuccessResponse, Warning } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
@@ -12,7 +12,6 @@ export interface NewWidgetProps {
selectedGraph: PANEL_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
fillSpans: Widgets['fillSpans'];
enableDrillDown?: boolean;
}
export interface WidgetGraphProps {
@@ -33,17 +32,17 @@ export interface WidgetGraphProps {
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
>
>;
enableDrillDown?: boolean;
}
export type WidgetGraphContainerProps = {
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
SuccessResponse<MetricRangePayloadProps, unknown> & {
warning?: Warning;
},
Error
>;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
selectedGraph: PANEL_TYPES;
selectedWidget: Widgets;
isLoadingPanelData: boolean;
enableDrillDown?: boolean;
};

View File

@@ -27,14 +27,22 @@ export default function NoLogs({
logEvent('Traces Explorer: Navigate to onboarding', {});
} else if (dataSource === DataSource.LOGS) {
logEvent('Logs Explorer: Navigate to onboarding', {});
} else if (dataSource === DataSource.METRICS) {
logEvent('Metrics Explorer: Navigate to onboarding', {});
}
history.push(
dataSource === 'traces'
? ROUTES.GET_STARTED_APPLICATION_MONITORING
: ROUTES.GET_STARTED_LOGS_MANAGEMENT,
);
let link;
if (dataSource === DataSource.TRACES) {
link = ROUTES.GET_STARTED_APPLICATION_MONITORING;
} else if (dataSource === DataSource.METRICS) {
link = ROUTES.GET_STARTED_WITH_CLOUD;
} else {
link = ROUTES.GET_STARTED_LOGS_MANAGEMENT;
}
history.push(link);
} else if (dataSource === 'traces') {
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
} else if (dataSource === DataSource.METRICS) {
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
} else {
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
}

View File

@@ -6,6 +6,7 @@ import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKey
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { has } from 'lodash-es';
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -452,7 +453,9 @@ const useOptionsMenu = ({
() => ({
addColumn: {
isFetching: isSearchedAttributesFetchingV5,
value: preferences?.columns || defaultOptionsQuery.selectColumns,
value:
preferences?.columns.filter((item) => has(item, 'name')) ||
defaultOptionsQuery.selectColumns.filter((item) => has(item, 'name')),
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
onBlur: handleBlur,

View File

@@ -2,15 +2,12 @@ import { ToggleGraphProps } from 'components/Graph/types';
import Uplot from 'components/Uplot';
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
import _noop from 'lodash-es/noop';
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { buildHistogramData } from './histogram';
import { PanelWrapperProps } from './panelWrapper.types';
@@ -23,58 +20,11 @@ function HistogramPanelWrapper({
isFullViewMode,
onToggleModelHandler,
onClickHandler,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
const {
coordinates,
popoverPosition,
clickedData,
onClose,
onClick,
subMenu,
setSubMenu,
} = useCoordinates();
const { menuItemsConfig } = useGraphContextMenu({
widgetId: widget.id || '',
query: widget.query,
graphData: clickedData,
onClose,
coordinates,
subMenu,
setSubMenu,
});
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
const [
,
,
,
,
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
,
focusedSeries,
] = args;
const data = getUplotClickData({
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
focusedSeries,
});
if (data && data?.record?.queryName) {
onClick(data.coord, { ...data.record, label: data.label });
}
},
[onClick],
);
const histogramData = buildHistogramData(
queryResponse.data?.payload.data.result,
@@ -123,9 +73,7 @@ function HistogramPanelWrapper({
setGraphsVisibilityStates: setGraphVisibility,
graphsVisibilityStates: graphVisibility,
mergeAllQueries: widget.mergeAllActiveQueries,
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
onClickHandler: onClickHandler || _noop,
}),
[
containerDimensions,
@@ -137,8 +85,6 @@ function HistogramPanelWrapper({
widget.id,
widget.mergeAllActiveQueries,
widget.panelTypes,
clickHandlerWithContextMenu,
enableDrillDown,
onClickHandler,
],
);
@@ -146,13 +92,6 @@ function HistogramPanelWrapper({
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={histogramOptions} data={histogramData} ref={lineChartRef} />
<ContextMenu
coordinates={coordinates}
popoverPosition={popoverPosition}
title={menuItemsConfig.header as string}
items={menuItemsConfig.items}
onClose={onClose}
/>
{isFullViewMode && setGraphVisibility && !widget.mergeAllActiveQueries && (
<GraphManager
data={histogramData}

View File

@@ -21,7 +21,6 @@ function PanelWrapper({
onOpenTraceBtnClick,
customSeries,
customOnRowClick,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@@ -50,7 +49,6 @@ function PanelWrapper({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customOnRowClick={customOnRowClick}
customSeries={customSeries}
enableDrillDown={enableDrillDown}
/>
);
}

View File

@@ -6,13 +6,10 @@ import { Pie } from '@visx/shape';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { themeColors } from 'constants/theme';
import { getPieChartClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isNaN } from 'lodash-es';
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
import { useRef, useState } from 'react';
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
@@ -22,7 +19,6 @@ import { lightenColor, tooltipStyles } from './utils';
function PiePanelWrapper({
queryResponse,
widget,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const [active, setActive] = useState<{
label: string;
@@ -52,7 +48,6 @@ function PiePanelWrapper({
label: string;
value: string;
color: string;
record: any;
}[] = [].concat(
...(panelData
.map((d) => {
@@ -60,7 +55,6 @@ function PiePanelWrapper({
return {
label,
value: d?.values?.[0]?.[1],
record: d,
color:
widget?.customLegendColors?.[label] ||
generateColor(
@@ -148,26 +142,6 @@ function PiePanelWrapper({
return active.color === color ? color : lightenedColor;
};
const {
coordinates,
popoverPosition,
clickedData,
onClose,
onClick,
subMenu,
setSubMenu,
} = useCoordinates();
const { menuItemsConfig } = useGraphContextMenu({
widgetId: widget.id || '',
query: widget.query,
graphData: clickedData,
onClose,
coordinates,
subMenu,
setSubMenu,
});
return (
<div className="piechart-wrapper">
{!pieChartData.length && <div className="piechart-no-data">No data</div>}
@@ -191,7 +165,7 @@ function PiePanelWrapper({
height={size}
>
{
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, sonarjs/cognitive-complexity
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
(pie) =>
pie.arcs.map((arc) => {
const { label } = arc.data;
@@ -252,17 +226,6 @@ function PiePanelWrapper({
hideTooltip();
setActive(null);
}}
onClick={(e): void => {
if (enableDrillDown) {
const data = getPieChartClickData(arc);
if (data && data?.queryName) {
onClick(
{ x: e.clientX, y: e.clientY },
{ ...data, label: data.label },
);
}
}
}}
>
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
@@ -321,13 +284,6 @@ function PiePanelWrapper({
})
}
</Pie>
<ContextMenu
coordinates={coordinates}
popoverPosition={popoverPosition}
title={menuItemsConfig.header as string}
items={menuItemsConfig.items}
onClose={onClose}
/>
{/* Add total value in the center */}
<text

View File

@@ -12,7 +12,6 @@ function TablePanelWrapper({
openTracesButton,
onOpenTraceBtnClick,
customOnRowClick,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
@@ -32,7 +31,6 @@ function TablePanelWrapper({
widgetId={widget.id}
renderColumnCell={widget.renderColumnCell}
customColTitles={widget.customColTitles}
enableDrillDown={enableDrillDown}
// eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG}
/>

View File

@@ -6,8 +6,6 @@ import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -15,16 +13,14 @@ import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import uPlot from 'uplot';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { PanelWrapperProps } from './panelWrapper.types';
import { getTimeRangeFromUplotAxis } from './utils';
function UplotPanelWrapper({
queryResponse,
@@ -38,7 +34,6 @@ function UplotPanelWrapper({
selectedGraph,
customTooltipElement,
customSeries,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
@@ -70,25 +65,6 @@ function UplotPanelWrapper({
const containerDimensions = useResizeObserver(graphRef);
const {
coordinates,
popoverPosition,
clickedData,
onClose,
onClick,
subMenu,
setSubMenu,
} = useCoordinates();
const { menuItemsConfig } = useGraphContextMenu({
widgetId: widget.id || '',
query: widget.query,
graphData: clickedData,
onClose,
coordinates,
subMenu,
setSubMenu,
});
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
@@ -138,42 +114,6 @@ function UplotPanelWrapper({
const { timezone } = useTimezone();
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
const [
xValue,
,
,
,
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
axesData,
focusedSeries,
] = args;
const data = getUplotClickData({
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
focusedSeries,
});
console.log('onClickData: ', data);
// Compute time range if needed and if axes data is available
let timeRange;
if (axesData) {
const { xAxis } = axesData;
timeRange = getTimeRangeFromUplotAxis(xAxis, xValue);
}
if (data && data?.record?.queryName) {
onClick(data.coord, { ...data.record, label: data.label, timeRange });
}
},
[onClick],
);
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -183,9 +123,7 @@ function UplotPanelWrapper({
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
onClickHandler: onClickHandler || _noop,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
@@ -214,7 +152,7 @@ function UplotPanelWrapper({
containerDimensions,
isDarkMode,
onDragSelect,
clickHandlerWithContextMenu,
onClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
@@ -225,8 +163,6 @@ function UplotPanelWrapper({
customTooltipElement,
timezone.value,
customSeries,
enableDrillDown,
onClickHandler,
widget,
],
);
@@ -234,13 +170,6 @@ function UplotPanelWrapper({
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={options} data={chartData} ref={lineChartRef} />
<ContextMenu
coordinates={coordinates}
popoverPosition={popoverPosition}
title={menuItemsConfig.header as string}
items={menuItemsConfig.items}
onClose={onClose}
/>
{widget?.stackedBarChart && isFullViewMode && (
<Alert
message="Selecting multiple legends is currently not supported in case of stacked bar charts"

View File

@@ -266,34 +266,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
demo-app
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
demo-app
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
4.35 s
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
4.35 s
</div>
</div>
</td>
@@ -304,34 +292,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
customer
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
customer
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
</div>
</div>
</td>
@@ -342,34 +318,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
mysql
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
mysql
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
</div>
</div>
</td>
@@ -380,34 +344,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
frontend
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
frontend
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
287 ms
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
287 ms
</div>
</div>
</td>
@@ -418,34 +370,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
driver
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
driver
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
230 ms
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
230 ms
</div>
</div>
</td>
@@ -456,34 +396,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
route
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
route
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
66.4 ms
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
66.4 ms
</div>
</div>
</td>
@@ -494,34 +422,22 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
redis
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
redis
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
31.3 ms
</div>
<div>
<div
class="line-clamped-wrapper__text"
>
31.3 ms
</div>
</div>
</td>

View File

@@ -30,7 +30,6 @@ export type PanelWrapperProps = {
onOpenTraceBtnClick?: (record: RowData) => void;
customOnRowClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
enableDrillDown?: boolean;
};
export type TooltipData = {

View File

@@ -71,21 +71,3 @@ export const lightenColor = (color: string, opacity: number): string => {
// Create a new RGBA color string with the specified opacity
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};
export const getTimeRangeFromUplotAxis = (
axis: any,
xValue: number,
): { startTime: number; endTime: number } => {
// Use splits if available, otherwise fallback to 10 minutes (600000 milliseconds)
let gap =
(axis as any)._splits && (axis as any)._splits.length > 1
? (axis as any)._splits[1] - (axis as any)._splits[0]
: 600000; // 10 minutes in milliseconds
gap = Math.max(gap, 600000); // Minimum gap of 10 minutes in milliseconds
const startTime = xValue - gap;
const endTime = xValue + gap;
return { startTime, endTime };
};

View File

@@ -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;

View File

@@ -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 => {

View File

@@ -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);

View File

@@ -4,7 +4,8 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
query: IBuilderQuery;
onChange: (value: BaseAutocompleteData) => void;
onChange: (value: BaseAutocompleteData, isEditMode?: boolean) => void;
defaultValue?: string;
onSelect?: (value: BaseAutocompleteData) => void;
index?: number;
};

View File

@@ -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 {
@@ -24,7 +24,6 @@ import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { ExtendedSelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer';
import { transformToUpperCase } from 'utils/transformToUpperCase';
import { removePrefix } from '../GroupByFilter/utils';
import { selectStyle } from '../QueryBuilderSearch/config';
@@ -38,10 +37,10 @@ export const AggregatorFilter = memo(function AggregatorFilter({
onChange,
defaultValue,
onSelect,
index,
}: AgregatorFilterProps): JSX.Element {
const queryClient = useQueryClient();
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
const [searchText, setSearchText] = useState<string>('');
// this function is only relevant for metrics and now operators are part of aggregations
const queryAggregation = useMemo(
@@ -49,6 +48,10 @@ export const AggregatorFilter = memo(function AggregatorFilter({
[query.aggregations],
);
const [searchText, setSearchText] = useState<string>(
(query.aggregations?.[0] as MetricAggregation)?.metricName || '',
);
const debouncedSearchText = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, value] = getAutocompleteValueAndType(searchText);
@@ -57,12 +60,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,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, true);
}
}
}, [
query.dataSource,
queryAggregation?.metricName,
query.aggregateAttribute?.type,
aggregateAttributeData,
onChange,
index,
query,
]);
const handleSearchText = useCallback((text: string): void => {
setSearchText(text);
}, []);
const placeholder: string =
query.dataSource === DataSource.METRICS
? `${transformToUpperCase(query.dataSource)} name`
? `Search metric name`
: 'Aggregate attribute';
const getAttributesData = useCallback(
@@ -124,12 +164,14 @@ export const AggregatorFilter = memo(function AggregatorFilter({
debouncedValue,
queryAggregation.timeAggregation,
query.dataSource,
index,
])?.payload?.attributeKeys || [],
[
debouncedValue,
queryAggregation.timeAggregation,
query.dataSource,
queryClient,
index,
],
);
@@ -140,6 +182,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
searchText,
queryAggregation.timeAggregation,
query.dataSource,
index,
],
async () =>
getAggregateAttribute({
@@ -155,6 +198,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
query.dataSource,
queryClient,
searchText,
index,
]);
const handleChangeCustomValue = useCallback(
@@ -170,11 +214,16 @@ export const AggregatorFilter = memo(function AggregatorFilter({
);
const handleBlur = useCallback(async () => {
if (searchText) {
if (searchText && searchText !== queryAggregation.metricName) {
const aggregateAttributes = await getResponseAttributes();
handleChangeCustomValue(searchText, aggregateAttributes);
}
}, [getResponseAttributes, handleChangeCustomValue, searchText]);
}, [
getResponseAttributes,
handleChangeCustomValue,
searchText,
queryAggregation?.metricName,
]);
const handleChange = useCallback(
(

View File

@@ -195,6 +195,7 @@ export const GroupByFilter = memo(function GroupByFilter({
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
data-testid="group-by"
placeholder={localValues.length === 0 ? 'Everything (no breakdown)' : ''}
/>
);
});

View File

@@ -1,113 +0,0 @@
import './Breakoutoptions.styles.scss';
import { Input, Skeleton } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { useGetAggregateKeys } from 'hooks/infraMonitoring/useGetAggregateKeys';
import useDebounce from 'hooks/useDebounce';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { useCallback, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { BreakoutOptionsProps } from './contextConfig';
function OptionsSkeleton(): JSX.Element {
return (
<div className="breakout-options-skeleton">
{Array.from({ length: 5 }).map((_, index) => (
<Skeleton.Input
active
size="small"
// eslint-disable-next-line react/no-array-index-key
key={index}
/>
))}
</div>
);
}
function BreakoutOptions({
queryData,
onColumnClick,
}: BreakoutOptionsProps): JSX.Element {
const { groupBy = [] } = queryData;
const [searchText, setSearchText] = useState<string>('');
const debouncedSearchText = useDebounce(searchText, 400);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.trim().toLowerCase();
setSearchText(value);
},
[],
);
// TODO: change the api call to get the keys
const { isFetching, data } = useGetAggregateKeys(
{
aggregateAttribute: queryData.aggregateAttribute?.key || '',
dataSource: queryData.dataSource,
aggregateOperator: queryData?.aggregateOperator || '',
searchText: debouncedSearchText,
},
{
queryKey: [
queryData?.aggregateAttribute?.key,
queryData.dataSource,
queryData.aggregateOperator,
debouncedSearchText,
],
enabled: !!queryData,
},
);
const breakoutOptions = useMemo(() => {
const groupByKeys = groupBy.map((item: BaseAutocompleteData) => item.key);
return data?.payload?.attributeKeys?.filter(
(item: BaseAutocompleteData) => !groupByKeys.includes(item.key),
);
}, [data, groupBy]);
console.log('>> queryData', queryData);
console.log('>> groupBy', groupBy);
console.log('>> breakoutOptions', breakoutOptions);
return (
<div>
<section className="search" style={{ padding: '8px 0' }}>
<Input
type="text"
value={searchText}
placeholder="Search breakout options..."
onChange={handleInputChange}
/>
</section>
<div style={{ height: '200px' }}>
<OverlayScrollbar
options={{
overflow: {
x: 'hidden',
},
}}
>
{/* eslint-disable-next-line react/jsx-no-useless-fragment */}
<>
{isFetching ? (
<OptionsSkeleton />
) : (
breakoutOptions?.map((item: BaseAutocompleteData) => (
<ContextMenu.Item
key={item.key}
onClick={(): void => onColumnClick(item)}
>
{item.key}
</ContextMenu.Item>
))
)}
</>
</OverlayScrollbar>
</div>
</div>
);
}
export default BreakoutOptions;

View File

@@ -1,7 +0,0 @@
.breakout-options-skeleton {
.ant-skeleton-input {
width: 100% !important;
height: 20px !important;
margin: 8px 5px;
}
}

View File

@@ -1,150 +0,0 @@
import { QUERY_BUILDER_OPERATORS_BY_TYPES } from 'constants/queryBuilder';
import ContextMenu, { ClickedData } from 'periscope/components/ContextMenu';
import { ReactNode } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import BreakoutOptions from './BreakoutOptions';
import {
getAggregateColumnHeader,
getBaseMeta,
getQueryData,
} from './drilldownUtils';
import { AGGREGATE_OPTIONS, SUPPORTED_OPERATORS } from './menuOptions';
import { getBreakoutQuery } from './tableDrilldownUtils';
import { AggregateData } from './useAggregateDrilldown';
export type ContextMenuItem = ReactNode;
export enum ConfigType {
GROUP = 'group',
AGGREGATE = 'aggregate',
}
export interface ContextMenuConfigParams {
configType: ConfigType;
query: Query;
clickedData: ClickedData;
panelType?: string;
onColumnClick: (key: string, query?: Query) => void;
subMenu?: string;
}
export interface GroupContextMenuConfig {
header?: string;
items?: ContextMenuItem;
}
export interface AggregateContextMenuConfig {
header?: string | ReactNode;
items?: ContextMenuItem;
}
export interface BreakoutOptionsProps {
queryData: IBuilderQuery;
onColumnClick: (groupBy: BaseAutocompleteData) => void;
}
export function getGroupContextMenuConfig({
query,
clickedData,
panelType,
onColumnClick,
}: Omit<ContextMenuConfigParams, 'configType'>): GroupContextMenuConfig {
const filterKey = clickedData?.column?.dataIndex;
const header = `Filter by ${filterKey}`;
const filterDataType =
getBaseMeta(query, filterKey as string)?.dataType || 'string';
const operators =
QUERY_BUILDER_OPERATORS_BY_TYPES[
filterDataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
];
const filterOperators = operators.filter(
(operator) => SUPPORTED_OPERATORS[operator],
);
if (panelType === 'table' && clickedData?.column) {
return {
header,
items: filterOperators.map((operator) => (
<ContextMenu.Item
key={operator}
icon={SUPPORTED_OPERATORS[operator].icon}
onClick={(): void => onColumnClick(SUPPORTED_OPERATORS[operator].value)}
>
{SUPPORTED_OPERATORS[operator].label}
</ContextMenu.Item>
)),
};
}
return {};
}
export function getAggregateContextMenuConfig({
subMenu,
query,
onColumnClick,
aggregateData,
}: {
subMenu?: string;
query: Query;
onColumnClick: (key: string, query?: Query) => void;
aggregateData: AggregateData | null;
}): AggregateContextMenuConfig {
console.log('getAggregateContextMenuConfig', { query, aggregateData });
if (subMenu === 'breakout') {
const queryData = getQueryData(query, aggregateData?.queryName || '');
return {
header: 'Breakout by',
items: (
<BreakoutOptions
queryData={queryData}
onColumnClick={(groupBy: BaseAutocompleteData): void => {
// Use aggregateData.filters
const filtersToAdd = aggregateData?.filters || [];
const breakoutQuery = getBreakoutQuery(
query,
aggregateData,
groupBy,
filtersToAdd,
);
onColumnClick('breakout', breakoutQuery);
}}
/>
),
};
}
// Use aggregateData.queryName
const queryName = aggregateData?.queryName;
const { dataSource, aggregations } = getAggregateColumnHeader(
query,
queryName as string,
);
console.log('dataSource', dataSource);
console.log('aggregations', aggregations);
return {
header: (
<div>
<div style={{ textTransform: 'capitalize' }}>{dataSource}</div>
<div>{aggregations}</div>
</div>
),
items: AGGREGATE_OPTIONS.map(({ key, label, icon }) => (
<ContextMenu.Item
key={key}
icon={icon}
onClick={(): void => onColumnClick(key)}
>
{label}
</ContextMenu.Item>
)),
};
}

Some files were not shown because too many files have changed in this diff Show More