Compare commits

..

117 Commits

Author SHA1 Message Date
Aditya Singh
f8b16e1034 feat: minor refactor 2025-08-05 22:45:15 +05:30
Aditya Singh
749dff2200 feat: minor refactor 2025-08-05 20:03:33 +05:30
Aditya Singh
de05394859 feat: fix header color 2025-08-05 17:56:41 +05:30
Aditya Singh
a6a9bf5bad Merge branch 'feat/drilldowns' of github.com:SigNoz/signoz into feat/drilldowns 2025-08-05 17:37:50 +05:30
Aditya Singh
e767c229aa Merge branch 'main' of github.com:SigNoz/signoz into feat/drilldowns 2025-08-05 17:37:31 +05:30
Aditya Singh
b9cf516201 feat: aggregation header val 2025-08-05 17:30:34 +05:30
Aditya Singh
f87e80a0f5 Merge branch 'main' into feat/drilldowns 2025-08-05 13:41:29 +05:30
Aditya Singh
f114d0249d feat: revert qbv5 2025-08-05 13:32:35 +05:30
Aditya Singh
b4fbd7c673 feat: snapshot update 2025-08-05 12:01:30 +05:30
Aditya Singh
e25d625c4b feat: minor refactor 2025-08-05 11:39:22 +05:30
Aditya Singh
9ca0cc90b0 Merge branch 'main' of github.com:SigNoz/signoz into feat/drilldowns 2025-08-04 19:58:31 +05:30
Aditya Singh
90758dbd32 feat: context menu hook refactor 2025-08-01 15:11:51 +05:30
Aditya Singh
9b559d6251 feat: context menu - increase width and add overlay 2025-07-19 15:16:47 +05:30
Aditya Singh
bdfb712395 feat: add search to breakout and other refactor 2025-07-19 14:39:55 +05:30
Aditya Singh
0d2a4b397a feat: lint fix 2025-07-17 02:03:59 +05:30
Aditya Singh
2c9a51c2ac feat: update click plugin in uplot 2025-07-17 01:43:24 +05:30
Aditya Singh
fb43f12a76 feat: refactor code 2025-07-17 01:05:37 +05:30
Aditya Singh
60e0e84237 feat: drilldown prop drilldowned 2025-07-16 20:29:29 +05:30
Aditya Singh
54d46a1d03 feat: minor refactor 2025-07-16 20:05:26 +05:30
Aditya Singh
73a7246a11 feat: remove unwanted code 2025-07-16 19:47:23 +05:30
Aditya Singh
163d59bf71 feat: add time range to timeseries, bar charts 2025-07-16 17:04:40 +05:30
Aditya Singh
fb672eda11 feat: add drilldown options in uplot 2025-07-16 02:46:42 +05:30
Aditya Singh
43a432b22b feat: add drilldown options in pie chart 2025-07-16 02:20:21 +05:30
Aditya Singh
8107946cb1 feat: added click data utils for uplot and pie charts 2025-07-16 02:16:39 +05:30
Aditya Singh
38ee4aae30 feat: add graph context hook 2025-07-16 02:09:27 +05:30
Aditya Singh
001d9ed9fb feat: fix style 2025-07-16 02:08:44 +05:30
Aditya Singh
e1abae91a3 feat: aggreagate drilldown refactor to use for tables and other panels alike 2025-07-15 20:49:31 +05:30
Aditya Singh
a9ac3b7e15 feat: aggreagate drilldown refactor to use for tables and other panels alike 2025-07-15 19:29:26 +05:30
Aditya Singh
4a98c54e78 feat: minor refactor 2025-07-14 18:06:00 +05:30
Aditya Singh
9ed4a09caf feat: update coordinates fn signature 2025-07-14 16:14:09 +05:30
Aditya Singh
132a31852f fix: remove number data type conversion 2025-07-14 15:08:13 +05:30
Aditya Singh
5686697b6c feat: fix aggreagate context header 2025-07-09 02:43:35 +05:30
Aditya Singh
5f4fc12031 feat: fix datatype 2025-07-09 02:09:24 +05:30
Aditya Singh
fe2c42de90 feat: hide drilldown for non-builder queries 2025-07-09 01:43:25 +05:30
Aditya Singh
d8f2cf1c0e feat: fix metrics view 2025-07-09 01:11:12 +05:30
Aditya Singh
a7e8f31561 feat: style fix 2025-07-08 21:29:58 +05:30
Aditya Singh
d9d6e7b4f1 feat: show reset query 2025-07-08 19:37:43 +05:30
Aditya Singh
f8f1a26a43 feat: style fix 2025-07-08 18:57:19 +05:30
Aditya Singh
79dfd6f17f feat: breakout drilldown option added 2025-07-08 15:31:13 +05:30
Aditya Singh
f386662e00 feat: aggregate col drilldown added 2025-07-03 18:51:36 +05:30
Aditya Singh
b2de302262 feat: context menu config refactor 2025-07-03 14:29:43 +05:30
Aditya Singh
6f63076b8e feat: context menu style fix 2025-07-03 01:24:34 +05:30
Aditya Singh
8007f954e5 feat: use context menu item for filters 2025-07-03 00:54:58 +05:30
Aditya Singh
b39b24c46f feat: context menu style update 2025-07-03 00:53:54 +05:30
Aditya Singh
70472c587d feat: filter drilldown added 2025-07-02 16:02:13 +05:30
Aditya Singh
06e89b7199 feat: added context menu 2025-06-27 11:26:00 +05:30
Aditya Singh
d60ac0d0e1 fix: fix composite query delete on close 2025-06-27 11:25:24 +05:30
Aditya Singh
1e4c213df4 feat: view mode enhancements 2025-06-27 11:24:47 +05:30
SagarRajput-7
9bf112cfcf Merge branch 'main' into feat/query-builder-v2 2025-06-25 16:19:10 +05:30
SagarRajput-7
a611b8f429 feat: new query builder misc fixes (#8359)
* feat: qb fixes

* feat: fixed handlerunquery props

* feat: fixes logs list order by

* feat: fix logs order by issue

* feat: safety check and order by correction

* feat: updated version in new create dashboards

* feat: added new formatOptions for table and fixed the pie chart plotting

* feat: keyboard shortcut overriding issue and pie ch correction in dashboard views

* feat: fixed dashboard data state management across datasource * paneltypes

* feat: fixed explorer pages data management issues

* feat: integrated new backend payload/request diff, to the UI types

* feat: fixed the collapse behaviour of QB - queries

* feat: fix order by and default aggregation to count()
2025-06-25 16:18:15 +05:30
SagarRajput-7
872230169c feat: resolved conflicts 2025-06-25 05:26:52 +05:30
SagarRajput-7
4a28954074 Query builder misc - fixes (#8295)
* feat: trace and logs explorer fixes

* fix: ui fixes

* fix: handle multi arg aggregation

* feat: explorer pages fixes

* feat: added fixes for order by for datasource

* feat: metric order by issue

* feat: support for paneltype selectedview tab switch

* feat: qb v2 compatiblity with url's composite query

* feat: conversion fixes

* feat: where clause and aggregation fix

---------

Co-authored-by: Yunus M <myounis.ar@live.com>
2025-06-25 05:17:57 +05:30
Yunus M
0df2d9e6da feat: fetch more keys is complete list not already fetched 2025-06-25 05:16:45 +05:30
SagarRajput-7
67f412477c feat: query_range migration from v3/v4 -> v5 (#8192)
* feat: query_range migration from v3/v4 -> v5

* feat: cleanup files

* feat: cleanup code

* feat: metric payload improvements

* feat: metric payload improvements

* feat: data retention and qb v2 for dashboard cleanup

* feat: corrected datasource change daata updatation in qb v2

* feat: fix value panel plotting with new query v5

* feat: alert migration

* feat: fixed aggregation css

* feat: explorer pages migration

* feat: trace and logs explorer fixes
2025-06-25 05:16:45 +05:30
Yunus M
43dc060950 fix: responsiveness issues 2025-06-25 05:16:45 +05:30
Yunus M
a21ae43a1f feat: where clause key updates 2025-06-25 05:16:45 +05:30
Yunus M
331a8b386f feat: update styles for light mode 2025-06-25 05:16:45 +05:30
Yunus M
ca6c7afa5c feat: show errors 2025-06-25 05:16:45 +05:30
Yunus M
dc8e5d6df9 feat: update context and show suggestions on select 2025-06-25 05:16:45 +05:30
Yunus M
c68f352aeb feat: add a space after selecting a value from suggestion 2025-06-25 05:16:45 +05:30
Yunus M
7863877a49 feat: improve suggestion ux in query search 2025-06-25 05:16:45 +05:30
Yunus M
76384c2430 feat: ui improvements 2025-06-25 05:16:45 +05:30
Yunus M
4e06d7757b feat: handle close on blur 2025-06-25 05:16:45 +05:30
Yunus M
5c06429ebe feat: query search component clean up 2025-06-25 05:16:45 +05:30
Yunus M
aefc7940a7 feat: handle having option autocomplete ux 2025-06-25 05:16:45 +05:30
Yunus M
0deae0c73b feat: disable clicking on placeholder items in suggestions 2025-06-25 05:16:45 +05:30
Yunus M
a4c16e5847 feat: improve having suggestions 2025-06-25 05:16:45 +05:30
Yunus M
efb741cf35 feat: handle add ons 2025-06-25 05:16:45 +05:30
Yunus M
153f64067c feat: handle list panel type options 2025-06-25 05:16:45 +05:30
Yunus M
c83ae1a485 feat: pass index to query addons 2025-06-25 05:16:45 +05:30
Yunus M
bfd74fb906 feat: update qb elements based on panel type 2025-06-25 05:16:45 +05:30
Yunus M
5d56f05fab feat: hide extra qb elements 2025-06-25 05:16:45 +05:30
Yunus M
57ca53c74c feat: use qb-v2 in explorers and alerts 2025-06-25 05:16:42 +05:30
Yunus M
bde078472b feat: update explorer views 2025-06-25 05:16:09 +05:30
Yunus M
6deb75ff46 feat: update logs, metrics and traces qb 2025-06-25 05:10:59 +05:30
Yunus M
424fd0362d feat: query builder layout updates 2025-06-25 05:10:59 +05:30
Yunus M
1bc51102f6 fix: minor fixes 2025-06-25 05:10:58 +05:30
Yunus M
c1b70c05f1 feat: create separate containers for traces, logs and metrics qbs 2025-06-25 05:10:58 +05:30
Yunus M
8fce0ab1af feat: metrics qb 2025-06-25 05:10:57 +05:30
Yunus M
df1923a7c6 fix: update dropdown css 2025-06-25 05:10:05 +05:30
Yunus M
1e37ae2fd0 feat: remove () from suggestions 2025-06-25 05:10:05 +05:30
Yunus M
7b3ea5cc45 feat: handle parenthesis and conjunction operators 2025-06-25 05:10:05 +05:30
Yunus M
167ddc6c56 feat: support multiple having key value pairs 2025-06-25 05:10:05 +05:30
Yunus M
dbc1e1fc45 feat: move state to context 2025-06-25 05:10:05 +05:30
Yunus M
01e798f3c1 feat: handle having options creation 2025-06-25 05:10:05 +05:30
Yunus M
d9010fb3fc feat: hide already used variables 2025-06-25 05:10:05 +05:30
Yunus M
06363f2e5b fix: show operator suggestions only on manual trigger or valid key 2025-06-25 05:10:05 +05:30
Yunus M
f1853a6bca fix: handle autocomplete 2025-06-25 05:10:05 +05:30
Yunus M
97e9f5dc8d fix: update styles 2025-06-25 05:10:05 +05:30
Yunus M
3b959bd2f6 fix: update css 2025-06-25 05:10:05 +05:30
Yunus M
9662e43418 feat: handle multie select functions 2025-06-25 05:10:05 +05:30
Yunus M
736bb2ebfb feat: handle field suggestions for aggregate operators 2025-06-25 05:10:05 +05:30
Yunus M
879700ea7a feat: support aggregation function with values 2025-06-25 05:10:05 +05:30
Yunus M
438ffe45f2 feat: add groupBy, having, order by, limit and legend format 2025-06-25 05:10:05 +05:30
Yunus M
723b6b6b79 feat: handle multie select values better 2025-06-25 05:10:05 +05:30
Yunus M
d2df098bb3 feat: improve suggestions 2025-06-25 05:10:05 +05:30
Yunus M
196ae10f00 feat: console log context based on cursor position 2025-06-25 05:10:05 +05:30
Yunus M
00eba89e20 fix: handle . notation keywords better 2025-06-25 05:10:05 +05:30
Yunus M
1739a9e27b feat: remove card container above where clause 2025-06-25 05:10:05 +05:30
Yunus M
cfdf714ffa feat: use new qb in logs explorer 2025-06-25 05:10:04 +05:30
Yunus M
49e78b6998 feat: handle parenthesis 2025-06-25 05:10:04 +05:30
Yunus M
762c658c10 feat: handle value selection 2025-06-25 05:10:04 +05:30
Yunus M
48e7e33dea feat: styling updates 2025-06-25 05:10:04 +05:30
Yunus M
dc4996c127 feat: handle string and number values correctly 2025-06-25 05:10:04 +05:30
Yunus M
d95f7b976c feat: handle async value fetching 2025-06-25 05:10:04 +05:30
Yunus M
9a47883064 feat: update the context with additonal properties 2025-06-25 05:10:04 +05:30
Yunus M
39a90fd33c feat: styling updates 2025-06-25 05:10:04 +05:30
Yunus M
722c3482d2 feat: update theme and syntax highlighting 2025-06-25 05:10:04 +05:30
Yunus M
60e84e6681 feat: handle context switch 2025-06-25 05:10:04 +05:30
Yunus M
8d1fa84e6a feat: handle multiple spaces 2025-06-25 05:10:04 +05:30
Yunus M
6c22197bf4 feat: integrate the apis 2025-06-25 05:10:04 +05:30
Yunus M
f6c426d0cc feat: update context logic and return auto-suggestions based on context 2025-06-25 05:10:04 +05:30
Yunus M
e21757b2bd feat: add apis and hooks 2025-06-25 05:10:04 +05:30
Yunus M
a87fbabbe7 feat: update context to recognise conjunction operator 2025-06-25 05:10:04 +05:30
Yunus M
b2847cb05b feat: add codemirror 2025-06-25 05:10:00 +05:30
Yunus M
0b575b41a1 feat: add types, base components 2025-06-25 05:08:52 +05:30
Yunus M
0a3fd7a7dc feat: add antlr4, parser files and grammar 2025-06-25 05:08:52 +05:30
213 changed files with 4789 additions and 6506 deletions

View File

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

View File

@@ -1,34 +0,0 @@
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, Warning } from 'types/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadV3,
QueryRangePayload,
@@ -13,9 +13,7 @@ export const getMetricsQueryRange = async (
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<
(SuccessResponse<MetricRangePayloadV3> & { warning?: Warning }) | ErrorResponse
> => {
): Promise<SuccessResponse<MetricRangePayloadV3> | 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, Warning } from 'types/api';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
DistributionData,
@@ -28,18 +28,14 @@ function getColName(
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
const isSingleAggregation = aggregationsCount === 1;
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;
// Single aggregation: Priority is alias > legend > expression
if (isSingleAggregation) {
return alias || legend || expression;
}
return legend || col.queryName;
// Multiple aggregations: Each follows single rules BUT never shows legend
// Priority: alias > expression (legend is ignored for multiple aggregations)
return alias || expression;
}
function getColId(
@@ -52,14 +48,7 @@ function getColId(
const aggregation =
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
const expression = aggregation?.expression || '';
const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0;
const isMultipleAggregations = aggregationsCount > 1;
if (isMultipleAggregations && expression) {
return `${col.queryName}.${expression}`;
}
return col.queryName;
return `${col.queryName}.${expression}`;
}
/**
@@ -352,7 +341,7 @@ export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> & { warning?: Warning } {
): SuccessResponse<MetricRangePayloadV3> {
const { payload, params } = v5Response;
const v5Data = payload?.data;
@@ -378,18 +367,14 @@ 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,
};
}
@@ -405,7 +390,6 @@ export function convertV5ResponseToLegacy(
...v5Response,
payload: {
data: convertedData,
warning: v5Response.payload?.data?.warning || undefined,
},
};

View File

@@ -5,7 +5,10 @@ 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 } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@@ -27,7 +30,6 @@ 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;
@@ -121,21 +123,17 @@ function createBaseSpec(
functions: isEmpty(queryData.functions)
? undefined
: queryData.functions.map(
(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,
})),
};
},
(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,
})),
}),
),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined

View File

@@ -19,7 +19,6 @@ export interface NavigateToExplorerProps {
endTime?: number;
sameTab?: boolean;
shouldResolveQuery?: boolean;
widgetQuery?: Query;
}
export function useNavigateToExplorer(): (
@@ -31,34 +30,27 @@ export function useNavigateToExplorer(): (
);
const prepareQuery = useCallback(
(
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: [],
},
};
},
(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: [],
},
}),
[currentQuery],
);
@@ -75,7 +67,6 @@ export function useNavigateToExplorer(): (
endTime,
sameTab,
shouldResolveQuery,
widgetQuery,
} = props;
const urlParams = new URLSearchParams();
if (startTime && endTime) {
@@ -86,7 +77,7 @@ export function useNavigateToExplorer(): (
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
}
let preparedQuery = prepareQuery(filters, dataSource, widgetQuery);
let preparedQuery = prepareQuery(filters, dataSource);
if (shouldResolveQuery) {
await getUpdatedQuery({

View File

@@ -1,33 +0,0 @@
.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

@@ -1,59 +0,0 @@
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

@@ -1,79 +0,0 @@
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

@@ -1,33 +0,0 @@
/* 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,6 +169,7 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

@@ -1,11 +1,4 @@
import {
createContext,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
// Types for the context state
export type AggregationOption = { func: string; arg: string };
@@ -13,12 +6,8 @@ export type AggregationOption = { func: string; arg: string };
interface QueryBuilderV2ContextType {
searchText: string;
setSearchText: (text: string) => void;
aggregationOptionsMap: Record<string, AggregationOption[]>;
setAggregationOptions: (
queryName: string,
options: AggregationOption[],
) => void;
getAggregationOptions: (queryName: string) => AggregationOption[];
aggregationOptions: AggregationOption[];
setAggregationOptions: (options: AggregationOption[]) => void;
aggregationInterval: string;
setAggregationInterval: (interval: string) => void;
queryAddValues: any; // Replace 'any' with a more specific type if available
@@ -35,50 +24,26 @@ export function QueryBuilderV2Provider({
children: ReactNode;
}): JSX.Element {
const [searchText, setSearchText] = useState('');
const [aggregationOptionsMap, setAggregationOptionsMap] = useState<
Record<string, AggregationOption[]>
>({});
const [aggregationOptions, setAggregationOptions] = useState<
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,
aggregationOptionsMap,
aggregationOptions,
setAggregationOptions,
getAggregationOptions,
aggregationInterval,
setAggregationInterval,
queryAddValues,
setQueryAddValues,
}),
[
searchText,
aggregationOptionsMap,
aggregationInterval,
queryAddValues,
getAggregationOptions,
setAggregationOptions,
],
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
)}
>
{children}

View File

@@ -7,6 +7,7 @@ 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';
@@ -49,17 +50,17 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
);
useEffect(() => {
setAggregationOptions(query.queryName, [
setAggregationOptions([
{
func: queryAggregation.spaceAggregation || 'count',
arg: queryAggregation.metricName || '',
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
queryAggregation.spaceAggregation,
queryAggregation.metricName,
query.queryName,
setAggregationOptions,
query,
]);
const handleChangeGroupByKeys = useCallback(
@@ -99,22 +100,12 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-time-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<Tooltip
title={
<a
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn more about temporal aggregation
</a>
}
>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE WITHIN TIME SERIES{' '}
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={queryAggregation.timeAggregation || ''}
@@ -127,30 +118,9 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<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-label">
every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
@@ -168,22 +138,12 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<Tooltip
title={
<a
href="https://signoz.io/docs/metrics-management/types-and-aggregation/#aggregation"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn more about spatial aggregation
</a>
}
>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE ACROSS TIME SERIES
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content-item-value">
<SpaceAggregationOptions
panelType={panelType}
@@ -248,30 +208,9 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<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-label">
every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel

View File

@@ -22,11 +22,7 @@ export const MetricsSelect = memo(function MetricsSelect({
return (
<div className="metrics-select-container">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
index={index}
/>
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
</div>
);
});

View File

@@ -95,8 +95,7 @@ function HavingFilter({
queryData: IBuilderQuery;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const { getAggregationOptions } = useQueryBuilderV2Context();
const aggregationOptions = getAggregationOptions(queryData.queryName);
const { aggregationOptions } = useQueryBuilderV2Context();
const having = queryData?.having as Having;
const [input, setInput] = useState(having?.expression || '');

View File

@@ -111,13 +111,17 @@
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: calc(100% + 6px) !important;
top: 38px !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%,
@@ -125,9 +129,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;
color: var(--bg-vanilla-100) !important;
ul {
width: 100% !important;
@@ -163,6 +165,7 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -327,18 +330,16 @@
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,7 +1,6 @@
/* eslint-disable react/require-default-props */
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import { Button, Radio, RadioChangeEvent } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -10,7 +9,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, ExternalLink, ScrollText } from 'lucide-react';
import { BarChart2, ChevronUp, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
@@ -22,8 +21,6 @@ interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
description?: string;
docLink?: string;
}
const ADD_ONS_KEYS = {
@@ -39,45 +36,26 @@ 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',
},
];
@@ -85,58 +63,8 @@ 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,
@@ -284,21 +212,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<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="label">Group By</div>
<div className="input">
<GroupByFilter
disabled={
@@ -320,21 +234,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<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="label">Having</div>
<div className="input">
<HavingFilter
onClose={(): void => {
@@ -366,21 +266,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<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="label">Order By</div>
<div className="input">
<OrderByFilter
entityVersion={version}
@@ -404,21 +290,7 @@ function QueryAddOns({
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<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="label">Reduce to</div>
<div className="input">
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
@@ -458,32 +330,20 @@ function QueryAddOns({
value={selectedViews}
>
{addOns.map((addOn) => (
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
description={addOn.description}
docLink={addOn.docLink}
/>
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
placement="top"
mouseEnterDelay={0.5}
value={addOn}
>
<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>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
))}
</Radio.Group>
</div>

View File

@@ -63,14 +63,17 @@
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;
right: 0px !important;
width: 100% !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%,
@@ -78,7 +81,6 @@
) !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 {
@@ -267,17 +269,19 @@
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-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600 !important;
font-weight: 600;
}
}
}

View File

@@ -1,6 +1,5 @@
import './QueryAggregation.styles.scss';
import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
@@ -54,31 +53,7 @@ function QueryAggregationOptions({
{showAggregationInterval && (
<div className="query-aggregation-interval">
<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-label">every</div>
<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, Tooltip } from 'antd';
import { Button, Popover } 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 { Info, TriangleAlert } from 'lucide-react';
import { 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(queryData.queryName, pairs);
setAggregationOptions(pairs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, maxAggregations, validFunctions]);
@@ -639,50 +639,6 @@ 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,7 +12,22 @@ 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</div>}>
<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>
}
>
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
@@ -28,7 +43,7 @@ export default function QueryFooter({
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
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: absolute !important;
top: calc(100% + 6px) !important;
position: relative !important;
top: 0px !important;
left: 0px !important;
right: 0px !important;
border-radius: 4px;
border: 0px;
@@ -91,8 +91,6 @@
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;
@@ -573,9 +571,9 @@
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300);
border: 0px;
backdrop-filter: blur(20px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
@@ -585,7 +583,7 @@
&:hover,
&[aria-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
font-weight: 600;
}
}
}

View File

@@ -16,13 +16,12 @@ 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, Tooltip } from 'antd';
import { Button, Card, Collapse, Popover, Tag } 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,
@@ -31,7 +30,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
import { Info, TriangleAlert } from 'lucide-react';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
IDetailedError,
@@ -41,11 +40,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';
@@ -89,7 +88,10 @@ function QuerySearch({
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [valueSuggestions, setValueSuggestions] = useState<any[]>([
{ label: 'error', type: 'value' },
{ label: 'frontend', type: 'value' },
]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
@@ -112,27 +114,9 @@ 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(() => {
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]);
setQuery(queryData.filter?.expression || '');
}, [queryData.filter?.expression]);
const [keySuggestions, setKeySuggestions] = useState<
QueryKeyDataSuggestionsProps[] | null
@@ -143,6 +127,7 @@ function QuerySearch({
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [isCompleteKeysList, setIsCompleteKeysList] = useState(false);
const [
isFetchingCompleteValuesList,
setIsFetchingCompleteValuesList,
@@ -153,7 +138,6 @@ 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);
@@ -186,76 +170,36 @@ function QuerySearch({
500,
);
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
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,
});
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const fetchKeySuggestions = useCallback(
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key
) {
setKeySuggestions([]);
return;
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);
});
}
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],
);
setKeySuggestions(Array.from(merged.values()));
setIsCompleteKeysList(complete);
}
};
useEffect(() => {
setKeySuggestions([]);
debouncedFetchKeySuggestions();
fetchKeySuggestions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource, debouncedMetricName]);
@@ -366,11 +310,6 @@ function QuerySearch({
},
]);
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
toggleSuggestions(10);
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
try {
@@ -443,9 +382,13 @@ function QuerySearch({
]);
}
// Force reopen the completion if editor is available and focused
// Force reopen the completion if editor is available
if (editorRef.current) {
toggleSuggestions(10);
setTimeout(() => {
if (isMountedRef.current && editorRef.current) {
startCompletion(editorRef.current);
}
}, 10);
}
}
} catch (error) {
@@ -465,8 +408,7 @@ function QuerySearch({
setIsFetchingCompleteValuesList(false);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[activeKey, dataSource, isFocused],
[activeKey, dataSource, isLoadingSuggestions],
);
const debouncedFetchValueSuggestions = useMemo(
@@ -526,13 +468,14 @@ 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 => {
@@ -540,27 +483,24 @@ function QuerySearch({
setIsFocused(false);
};
useEffect(
() => (): void => {
useEffect(() => {
if (query) {
handleQueryValidation(query);
}
return (): 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);
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
// Update lastExternalQuery to prevent external validation trigger
setLastExternalQuery(newQuery);
handleQueryChange(newQuery);
};
// Helper function to render a badge for the current context mode
@@ -803,14 +743,16 @@ function QuerySearch({
}
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase().trim() ?? '';
const searchText = word?.text.toLowerCase() ?? '';
options = (keySuggestions || []).filter((option) =>
option.label.toLowerCase().includes(searchText),
);
if (options.length === 0 && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
if (!isCompleteKeysList && options.length === 0) {
setTimeout(() => {
fetchKeySuggestions(searchText);
}, 300);
}
// If we have previous pairs, we can prioritize keys that haven't been used yet
@@ -885,32 +827,12 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.STRING
].includes(op.label),
)
.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,
};
});
.map((op) => ({
...op,
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
? 100
: 0,
}));
} else if (keyType === QUERY_BUILDER_KEY_TYPES.BOOLEAN) {
// Prioritize boolean operators
options = options
@@ -919,24 +841,10 @@ function QuerySearch({
QUERY_BUILDER_KEY_TYPES.BOOLEAN
].includes(op.label),
)
.map((op) => {
if (op.label === OPERATORS['=']) {
return {
...op,
boost: 200,
};
}
if (op.label === OPERATORS['!=']) {
return {
...op,
boost: 100,
};
}
return {
...op,
boost: 0,
};
});
.map((op) => ({
...op,
boost: ['=', '!='].includes(op.label) ? 100 : 0,
}));
}
}
}
@@ -1126,15 +1034,26 @@ function QuerySearch({
// Effect to handle focus state and trigger suggestions
useEffect(() => {
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
if (editorRef.current) {
if (!isFocused) {
closeCompletion(editorRef.current);
} else {
startCompletion(editorRef.current);
}
}
}, [isFocused]);
useEffect(() => {
if (!queryContext) return;
// Trigger suggestions based on context
if (editorRef.current) {
toggleSuggestions(10);
// Small delay to ensure the context is fully updated
setTimeout(() => {
if (editorRef.current) {
startCompletion(editorRef.current);
}
}, 50);
}
// Handle value suggestions for value context
@@ -1147,28 +1066,7 @@ function QuerySearch({
fetchValueSuggestions({ key });
}
}
}, [
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>
);
}, [queryContext, activeKey, isLoadingSuggestions, fetchValueSuggestions]);
return (
<div className="code-mirror-where-clause">
@@ -1211,31 +1109,6 @@ 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}
@@ -1294,7 +1167,7 @@ function QuerySearch({
]),
),
]}
placeholder="Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., status = 'error' AND service = 'frontend')"
basicSetup={{
lineNumbers: false,
}}

View File

@@ -21,7 +21,6 @@ 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';
/**
@@ -87,10 +86,6 @@ export const convertFiltersToExpression = (
return '';
}
if (isFunctionOperator(op)) {
return `${op}(${key.key}, ${value})`;
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
@@ -544,16 +539,43 @@ 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 {
if (isMultipleAggregations && aggregation.expression) {
return `${queryName}.${aggregation.expression}`;
}
return queryName;
return `${queryName}.${aggregation.expression}`;
}
// function to give you label value for query name taking multiaggregation into account
@@ -577,7 +599,7 @@ export function getQueryLabelWithAggregation(
const isMultipleAggregations = aggregations.length > 1;
aggregations.forEach((agg: any, index: number) => {
const columnId = getColId(queryName, agg, isMultipleAggregations);
const columnId = getColId(queryName, agg);
// 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 { getColumnWidth, RowData } from 'lib/query/createTableColumnsFromQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { debounce, set } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -110,14 +110,11 @@ 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) {
const width = getColumnWidth(dataIndex, columnWidths);
if (width) {
return {
...col,
width, // Apply stored width
};
}
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
return {
...col,
width: columnWidths[dataIndex], // Apply stored width
};
}
return col;
});

View File

@@ -1,208 +0,0 @@
.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

@@ -1,142 +0,0 @@
/* 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,12 +15,6 @@ 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
@@ -82,15 +76,3 @@ 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,6 +46,7 @@ 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_V5 } from 'constants/app';
import { ENTITY_VERSION_V4 } from 'constants/app';
import {
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
@@ -28,7 +28,7 @@ const defaultAnnotations = {
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@@ -62,7 +62,7 @@ export const alertDefaults: AlertDef = {
export const anamolyAlertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
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_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@@ -139,7 +139,7 @@ export const logAlertDefaults: AlertDef = {
export const traceAlertDefaults: AlertDef = {
alertType: AlertTypes.TRACES_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@@ -171,7 +171,7 @@ export const traceAlertDefaults: AlertDef = {
export const exceptionAlertDefaults: AlertDef = {
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
version: ENTITY_VERSION_V5,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {

View File

@@ -1,6 +1,6 @@
import { Form, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ENTITY_VERSION_V4 } 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_V5,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
break;
default:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V5,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
});
}

View File

@@ -2,12 +2,6 @@
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,14 +1,12 @@
import './ChartPreview.styles.scss';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { InfoCircleOutlined } from '@ant-design/icons';
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';
@@ -28,7 +26,6 @@ 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';
@@ -37,10 +34,7 @@ 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';
@@ -50,7 +44,7 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import { ChartContainer } from './styles';
import { ChartContainer, FailedMessageContainer } from './styles';
import { getThresholdLabel } from './utils';
export interface ChartPreviewProps {
@@ -86,7 +80,6 @@ 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<
@@ -178,19 +171,6 @@ 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,
@@ -277,10 +257,6 @@ function ChartPreview({
timezone: timezone.value,
currentQuery,
query: query || currentQuery,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition: LegendPosition.BOTTOM,
}),
[
yAxisUnit,
@@ -298,7 +274,6 @@ function ChartPreview({
timezone.value,
currentQuery,
query,
graphVisibility,
],
);
@@ -314,23 +289,20 @@ 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>
<div className="chart-preview-header">
{headline}
{isWarning && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>
{headline}
<div className="threshold-alert-uplot-chart-container">
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<ErrorInPlace error={queryResponse.error as APIError} />
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartDataAvailable && !isAnomalyDetectionAlert && (

View File

@@ -37,8 +37,11 @@ import {
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import {
IBuilderQuery,
Query,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -179,17 +182,12 @@ function FormAlertRules({
setDetectionMethod(value);
};
const updateFunctions = (data: IBuilderQuery): QueryFunction[] => {
const anomalyFunction: QueryFunction = {
name: 'anomaly' as any,
args: [
{
name: 'z_score_threshold',
value: alertDef.condition.target || 3,
},
],
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
const anomalyFunction = {
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
};
const functions = data.functions || [];
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
@@ -240,18 +238,8 @@ function FormAlertRules({
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(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);
}
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
}
};

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './WidgetFullView.styles.scss';
import {
@@ -8,18 +9,23 @@ 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';
@@ -52,6 +58,7 @@ function FullView({
onClickHandler,
customOnDragSelect,
setCurrentGraphRef,
enableDrillDown = false,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedTime } = useSelector<
@@ -63,6 +70,7 @@ function FullView({
const location = useLocation();
const fullViewRef = useRef<HTMLDivElement>(null);
const { handleRunQuery } = useQueryBuilder();
useEffect(() => {
setCurrentGraphRef(fullViewRef);
@@ -114,6 +122,13 @@ function FullView({
};
});
const { dashboardEditView, handleResetQuery, showResetQuery } = useDrilldown({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
});
useEffect(() => {
setRequestData((prev) => ({
...prev,
@@ -204,71 +219,115 @@ function FullView({
return (
<div className="full-view-container">
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
<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>
)}
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
/>
<Button
style={{
marginLeft: '4px',
}}
onClick={(): void => {
response.refetch();
}}
type="primary"
icon={<SyncOutlined />}
/>
</TimeContainer>
)}
</div>
{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
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 || '');
<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%',
}}
/>
)}
<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>
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>
</div>
);
}

View File

@@ -18,6 +18,7 @@ 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
@@ -25,6 +26,10 @@ export const TimeContainer = styled.div<Props>`
margin-bottom: 1rem;
`
: css``}
.time-container {
display: flex;
}
`;
export const GraphContainer = styled.div<GraphContainerProps>`

View File

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

View File

@@ -0,0 +1,84 @@
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,7 +30,6 @@ 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';
@@ -46,6 +45,7 @@ import { getLocalStorageGraphVisibilityState, handleGraphClick } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
version,
threshold,
headerMenuList,
@@ -62,6 +62,7 @@ function WidgetGraphComponent({
customErrorMessage,
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@@ -184,19 +185,9 @@ 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 = {
[QueryParams.graphType]: clonedWidget?.panelTypes,
[QueryParams.widgetId]: uuid,
...(clonedWidget?.query && {
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(clonedWidget.query),
),
}),
graphType: widget?.panelTypes,
widgetId: uuid,
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
},
@@ -236,6 +227,7 @@ 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 {
@@ -364,6 +356,7 @@ function WidgetGraphComponent({
onClickHandler={onClickHandler ?? graphClickHandler}
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
enableDrillDown={enableDrillDown}
/>
</Modal>
@@ -376,6 +369,7 @@ function WidgetGraphComponent({
onDelete={handleOnDelete}
onClone={onCloneHandler}
queryResponse={queryResponse}
errorMessage={errorMessage}
threshold={threshold}
headerMenuList={headerMenuList}
isWarning={isWarning}
@@ -414,6 +408,7 @@ function WidgetGraphComponent({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customOnRowClick={customOnRowClick}
enableDrillDown={enableDrillDown}
/>
</div>
)}
@@ -426,6 +421,7 @@ WidgetGraphComponent.defaultProps = {
setLayout: undefined,
onClickHandler: undefined,
customTimeRangeWindowForCoRelation: undefined,
enableDrillDown: false,
};
export default WidgetGraphComponent;

View File

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

View File

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

View File

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

View File

@@ -53,11 +53,12 @@ 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 } = props;
const { handle, enableDrillDown = false } = props;
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
@@ -584,6 +585,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
version={ENTITY_VERSION_V5}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}
enableDrillDown={enableDrillDown}
/>
</Card>
</CardContainer>
@@ -670,3 +672,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
}
export default GraphLayout;
GraphLayout.defaultProps = {
enableDrillDown: false,
};

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
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(getSubstituteVars);
const queryRangeMutation = useMutation(getQueryRangeFormat);
const getUpdatedQuery = useCallback(
async ({
@@ -40,7 +40,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
selectedDashboard,
}: UseUpdatedQueryOptions): Promise<Query> => {
// Prepare query payload with resolved variables
const { queryPayload } = prepareQueryRangePayloadV5({
const { queryPayload } = prepareQueryRangePayload({
query: widgetConfig.query,
graphType: getGraphType(widgetConfig.panelTypes),
selectedTime: widgetConfig.timePreferance,
@@ -53,10 +53,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
// Map query data from API response
return mapQueryDataFromApi(
queryResult.data.compositeQuery,
widgetConfig?.query,
);
return mapQueryDataFromApi(queryResult.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 { getColumnUnit, RowData } from 'lib/query/createTableColumnsFromQuery';
import { 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,11 +84,10 @@ function GridTableComponent({
(val): RowData => {
const newValue = { ...val };
Object.keys(val).forEach((k) => {
const unit = getColumnUnit(k, columnUnits);
if (unit) {
if (columnUnits[k]) {
// 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]), unit);
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
} else if (val[k] === null) {
newValue[k] = 'n/a';
}
@@ -122,8 +121,7 @@ function GridTableComponent({
render: (text: string, ...rest: any): ReactNode => {
let textForThreshold = text;
const dataIndex = (e as ColumnType<RowData>)?.dataIndex || e.title;
const unit = getColumnUnit(dataIndex as string, columnUnits || {});
if (unit) {
if (columnUnits && columnUnits?.[dataIndex as string]) {
textForThreshold = rest[0][`${dataIndex}_without_unit`];
}
const isNumber = !Number.isNaN(Number(textForThreshold));
@@ -133,7 +131,7 @@ function GridTableComponent({
thresholds,
dataIndex as string,
Number(textForThreshold),
unit,
columnUnits?.[dataIndex as string],
);
const idx = thresholds.findIndex(

View File

@@ -22,6 +22,7 @@ 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 { ColumnsType, ColumnType } from 'antd/es/table';
import { 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,6 +9,12 @@ 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,
@@ -180,9 +186,9 @@ export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
renderColumnCell?: QueryTableProps['renderColumnCell'],
): { columns: ColumnsType<RowData>; dataSource: RowData[] } {
const columns: ColumnsType<RowData> =
data.columns?.reduce<ColumnsType<RowData>>((acc, item) => {
): { columns: CustomDataColumnType<RowData>[]; dataSource: RowData[] } {
const columns: CustomDataColumnType<RowData>[] =
data.columns?.reduce<CustomDataColumnType<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)
@@ -193,11 +199,13 @@ export function createColumnsAndDataSource(
(query) => query.queryName === item.queryName,
)?.aggregations?.length || 0;
const column: ColumnType<RowData> = {
const column: CustomDataColumnType<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,11 +1,7 @@
import { orange } from '@ant-design/colors';
import { SettingOutlined } from '@ant-design/icons';
import { Dropdown, MenuProps } from 'antd';
import {
negateOperator,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { OPERATORS } from 'constants/queryBuilder';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { TitleWrapper } from './BodyTitleRenderer.styles';
@@ -33,9 +29,7 @@ function BodyTitleRenderer({
getDataTypes(value),
),
`${value}`,
isFilterIn
? QUERY_BUILDER_FUNCTIONS.HAS
: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
isFilterIn ? OPERATORS.HAS : OPERATORS.NHAS,
true,
parentIsArray ? getDataTypes([value]) : getDataTypes(value),
);

View File

@@ -73,8 +73,6 @@ 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,
@@ -99,7 +97,6 @@ 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 ${column.key}` })}
{...(isDragColumn && { className: 'dragHandler' })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ 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
@@ -13,6 +12,7 @@ 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,7 +21,6 @@ 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';
@@ -46,11 +45,9 @@ function LogsExplorerList({
logs,
onEndReached,
isError,
error,
isFilterApplied,
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { activeLogId } = useCopyLogLink();
const {
@@ -258,9 +255,7 @@ function LogsExplorerList({
/>
)}
{isError && !isLoading && !isFetching && error && (
<ErrorInPlace error={error as APIError} />
)}
{isError && !isLoading && !isFetching && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<>

View File

@@ -1,5 +1,4 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { isEmpty } from 'lodash-es';
import { IField } from 'types/api/logs/fields';
import {
IBuilderQuery,
@@ -9,13 +8,11 @@ import {
export const convertKeysToColumnFields = (
keys: TelemetryFieldKey[],
): IField[] =>
keys
.filter((item) => !isEmpty(item.name))
.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
keys.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,9 +1,7 @@
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,12 +1,11 @@
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';
@@ -14,7 +13,6 @@ function LogsExplorerTable({
data,
isLoading,
isError,
error,
}: LogsExplorerTableProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@@ -22,8 +20,8 @@ function LogsExplorerTable({
return <LogsLoading />;
}
if (isError && error) {
return <ErrorInPlace error={error as APIError} />;
if (isError) {
return <LogsError />;
}
return (

View File

@@ -51,10 +51,8 @@ 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,
@@ -64,9 +62,7 @@ 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 {
@@ -91,7 +87,6 @@ function LogsExplorerViewsContainer({
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
setWarning,
}: {
selectedView: ExplorerViews;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
@@ -99,7 +94,6 @@ 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();
@@ -275,7 +269,6 @@ function LogsExplorerViewsContainer({
isFetching,
isError,
isSuccess,
error,
} = useGetExplorerQueryRange(
requestData,
panelType,
@@ -379,13 +372,6 @@ 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;
@@ -785,7 +771,6 @@ function LogsExplorerViewsContainer({
logs={logs}
onEndReached={handleEndReached}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
/>
)}
@@ -795,10 +780,8 @@ function LogsExplorerViewsContainer({
isLoading={isLoading || isFetching}
data={data}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
dataSource={DataSource.LOGS}
setWarning={setWarning}
/>
)}
@@ -811,7 +794,6 @@ function LogsExplorerViewsContainer({
}
isLoading={isLoading || isFetching}
isError={isError}
error={error as APIError}
/>
)}
</div>

View File

@@ -82,48 +82,6 @@ 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(),
@@ -173,7 +131,6 @@ const renderer = (): RenderResult =>
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</VirtuosoMockContext.Provider>,
@@ -215,6 +172,21 @@ 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({
@@ -234,7 +206,6 @@ describe('LogsExplorerViews -', () => {
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
setWarning={(): void => {}}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,

View File

@@ -4,7 +4,6 @@ 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';
@@ -13,11 +12,9 @@ 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';
@@ -121,8 +118,6 @@ function Explorer(): JSX.Element {
[],
);
const [warning, setWarning] = useState<Warning | undefined>(undefined);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">
@@ -136,7 +131,6 @@ function Explorer(): JSX.Element {
/>
</div>
<div className="explore-header-right-actions">
{!isEmpty(warning) && <WarningPopover warningData={warning} />}
<DateTimeSelector showAutoRefresh />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
@@ -173,10 +167,7 @@ function Explorer(): JSX.Element {
</Button.Group> */}
<div className="explore-content">
{selectedTab === ExplorerTabs.TIME_SERIES && (
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
/>
<TimeSeries showOneChartPerQuery={showOneChartPerQuery} />
)}
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* {selectedTab === ExplorerTabs.RELATED_METRICS && (

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
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';
@@ -111,15 +110,6 @@ 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,11 +1,9 @@
/* 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';
@@ -14,13 +12,11 @@ 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,
@@ -271,64 +267,30 @@ 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} />
{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}
/>
</>
)}
<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} />
<GridGraphLayout handle={handle} enableDrillDown />
</GridComponentSliderContainer>
);
}

View File

@@ -239,7 +239,10 @@ 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" />
<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"
/>
<Button
loading={queryResponse.isFetching}
type="primary"

View File

@@ -10,12 +10,6 @@
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,9 +1,7 @@
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';
@@ -16,6 +14,7 @@ function WidgetGraphContainer({
setRequestData,
selectedWidget,
isLoadingPanelData,
enableDrillDown = false,
}: WidgetGraphContainerProps): JSX.Element {
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
@@ -38,7 +37,7 @@ function WidgetGraphContainer({
if (queryResponse?.error) {
return (
<NotFoundContainer>
<ErrorInPlace error={queryResponse.error as APIError} />
<Typography>{queryResponse.error.message}</Typography>
</NotFoundContainer>
);
}
@@ -86,6 +85,7 @@ function WidgetGraphContainer({
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedGraph={selectedGraph}
enableDrillDown={enableDrillDown}
/>
);
}

View File

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

View File

@@ -1,14 +1,11 @@
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';
@@ -21,6 +18,7 @@ function WidgetGraph({
setRequestData,
selectedWidget,
isLoadingPanelData,
enableDrillDown = false,
}: WidgetGraphContainerProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
@@ -37,12 +35,7 @@ function WidgetGraph({
return (
<Container $panelType={selectedGraph} className="widget-graph">
<div className="header">
<div className="header-left">
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
{!isEmpty(queryResponse.data?.warning) && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
{queryResponse.error && (
@@ -57,6 +50,7 @@ function WidgetGraph({
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedWidget={selectedWidget}
enableDrillDown={enableDrillDown}
/>
</Container>
);

View File

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

View File

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

View File

@@ -3,8 +3,7 @@ import './ColumnUnitSelector.styles.scss';
import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { isEmpty } from 'lodash-es';
import { Dispatch, SetStateAction, useCallback, useEffect } from 'react';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { ColumnUnit } from 'types/api/dashboard/getAll';
import YAxisUnitSelector from '../YAxisUnitSelector';
@@ -32,49 +31,12 @@ 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,7 +4,6 @@ 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';
@@ -70,11 +69,7 @@ function LegendColors({
const legendLabels = useMemo(() => {
if (queryResponse?.data?.payload?.data?.result) {
return queryResponse.data.payload.data.result.map((item: any) =>
getLegend(
item,
currentQuery,
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
),
getLabelName(item.metric || {}, item.queryName || '', item.legend || ''),
);
}

View File

@@ -5,7 +5,6 @@ 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';
@@ -198,11 +197,7 @@ function Threshold({
const isInvalidUnitComparison = useMemo(
() =>
unit !== 'none' &&
convertUnit(
value,
unit,
getColumnUnit(tableSelectedOption, columnUnits || {}),
) === null,
convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null,
[unit, value, columnUnits, tableSelectedOption],
);
@@ -317,9 +312,7 @@ function Threshold({
{isEditMode ? (
<Select
defaultValue={unit}
options={unitOptions(
getColumnUnit(tableSelectedOption, columnUnits || {}) || '',
)}
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
onChange={handleUnitChange}
showSearch
className="unit-selection"
@@ -358,7 +351,7 @@ function Threshold({
{isInvalidUnitComparison && (
<Typography.Text className="invalid-unit">
Threshold unit ({unit}) is not valid in comparison with the column unit (
{getColumnUnit(tableSelectedOption, columnUnits || {}) || 'none'})
{columnUnits?.[tableSelectedOption] || 'none'})
</Typography.Text>
)}
{isEditMode && (

View File

@@ -16,13 +16,11 @@ 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;
@@ -42,7 +40,6 @@ function YAxisUnitSelector({
options={options}
allowClear
defaultValue={findCategoryById(defaultValue)?.name}
value={findCategoryById(value)?.name || ''}
onClear={handleClear}
onSelect={onSelectHandler}
filterOption={(inputValue, option): boolean => {

View File

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

View File

@@ -21,6 +21,7 @@ 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';
@@ -72,7 +73,10 @@ import {
placeWidgetBetweenRows,
} from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
function NewWidget({
selectedGraph,
enableDrillDown = false,
}: NewWidgetProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
@@ -690,6 +694,26 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}
}, [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">
@@ -706,31 +730,42 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
</Typography.Text>
</Flex>
</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 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>
</div>
<PanelContainer>
@@ -749,6 +784,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
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, Warning } from 'types/api';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
@@ -12,6 +12,7 @@ export interface NewWidgetProps {
selectedGraph: PANEL_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
fillSpans: Widgets['fillSpans'];
enableDrillDown?: boolean;
}
export interface WidgetGraphProps {
@@ -32,17 +33,17 @@ export interface WidgetGraphProps {
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
>
>;
enableDrillDown?: boolean;
}
export type WidgetGraphContainerProps = {
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown> & {
warning?: Warning;
},
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
selectedGraph: PANEL_TYPES;
selectedWidget: Widgets;
isLoadingPanelData: boolean;
enableDrillDown?: boolean;
};

View File

@@ -27,22 +27,14 @@ 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', {});
}
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);
history.push(
dataSource === 'traces'
? ROUTES.GET_STARTED_APPLICATION_MONITORING
: ROUTES.GET_STARTED_LOGS_MANAGEMENT,
);
} 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,7 +6,6 @@ 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';
@@ -453,9 +452,7 @@ const useOptionsMenu = ({
() => ({
addColumn: {
isFetching: isSearchedAttributesFetchingV5,
value:
preferences?.columns.filter((item) => has(item, 'name')) ||
defaultOptionsQuery.selectColumns.filter((item) => has(item, 'name')),
value: preferences?.columns || defaultOptionsQuery.selectColumns,
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
onBlur: handleBlur,

View File

@@ -2,12 +2,15 @@ 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 { useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { buildHistogramData } from './histogram';
import { PanelWrapperProps } from './panelWrapper.types';
@@ -20,11 +23,58 @@ 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,
@@ -73,7 +123,9 @@ function HistogramPanelWrapper({
setGraphsVisibilityStates: setGraphVisibility,
graphsVisibilityStates: graphVisibility,
mergeAllQueries: widget.mergeAllActiveQueries,
onClickHandler: onClickHandler || _noop,
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
}),
[
containerDimensions,
@@ -85,6 +137,8 @@ function HistogramPanelWrapper({
widget.id,
widget.mergeAllActiveQueries,
widget.panelTypes,
clickHandlerWithContextMenu,
enableDrillDown,
onClickHandler,
],
);
@@ -92,6 +146,13 @@ 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,6 +21,7 @@ function PanelWrapper({
onOpenTraceBtnClick,
customSeries,
customOnRowClick,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@@ -49,6 +50,7 @@ function PanelWrapper({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customOnRowClick={customOnRowClick}
customSeries={customSeries}
enableDrillDown={enableDrillDown}
/>
);
}

View File

@@ -6,10 +6,13 @@ 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';
@@ -19,6 +22,7 @@ import { lightenColor, tooltipStyles } from './utils';
function PiePanelWrapper({
queryResponse,
widget,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const [active, setActive] = useState<{
label: string;
@@ -48,6 +52,7 @@ function PiePanelWrapper({
label: string;
value: string;
color: string;
record: any;
}[] = [].concat(
...(panelData
.map((d) => {
@@ -55,6 +60,7 @@ function PiePanelWrapper({
return {
label,
value: d?.values?.[0]?.[1],
record: d,
color:
widget?.customLegendColors?.[label] ||
generateColor(
@@ -142,6 +148,26 @@ 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>}
@@ -165,7 +191,7 @@ function PiePanelWrapper({
height={size}
>
{
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, sonarjs/cognitive-complexity
(pie) =>
pie.arcs.map((arc) => {
const { label } = arc.data;
@@ -226,6 +252,17 @@ 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)} />
@@ -284,6 +321,13 @@ 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,6 +12,7 @@ function TablePanelWrapper({
openTracesButton,
onOpenTraceBtnClick,
customOnRowClick,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
@@ -31,6 +32,7 @@ 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,6 +6,8 @@ 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';
@@ -13,14 +15,16 @@ 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 { useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, 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,
@@ -34,6 +38,7 @@ function UplotPanelWrapper({
selectedGraph,
customTooltipElement,
customSeries,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
@@ -65,6 +70,25 @@ 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,
@@ -114,6 +138,42 @@ 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({
@@ -123,7 +183,9 @@ function UplotPanelWrapper({
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler: onClickHandler || _noop,
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
@@ -152,7 +214,7 @@ function UplotPanelWrapper({
containerDimensions,
isDarkMode,
onDragSelect,
onClickHandler,
clickHandlerWithContextMenu,
minTimeScale,
maxTimeScale,
graphVisibility,
@@ -163,6 +225,8 @@ function UplotPanelWrapper({
customTooltipElement,
timezone.value,
customSeries,
enableDrillDown,
onClickHandler,
widget,
],
);
@@ -170,6 +234,13 @@ 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,22 +266,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
demo-app
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
demo-app
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
4.35 s
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
4.35 s
</div>
</div>
</div>
</td>
@@ -292,22 +304,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
customer
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
customer
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
</div>
</div>
</div>
</td>
@@ -318,22 +342,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
mysql
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
mysql
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
431 ms
</div>
</div>
</div>
</td>
@@ -344,22 +380,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
frontend
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
frontend
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
287 ms
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
287 ms
</div>
</div>
</div>
</td>
@@ -370,22 +418,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
driver
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
driver
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
230 ms
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
230 ms
</div>
</div>
</div>
</td>
@@ -396,22 +456,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
route
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
route
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
66.4 ms
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
66.4 ms
</div>
</div>
</div>
</td>
@@ -422,22 +494,34 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
redis
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
redis
</div>
</div>
</div>
</td>
<td
class="ant-table-cell"
>
<div>
<div
class="line-clamped-wrapper__text"
>
31.3 ms
<div
class=""
role="button"
tabindex="0"
>
<div>
<div
class="line-clamped-wrapper__text"
>
31.3 ms
</div>
</div>
</div>
</td>

View File

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

View File

@@ -71,3 +71,21 @@ 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,8 +16,10 @@ import {
Trash2,
} from 'lucide-react';
import { useLocation } from 'react-router-dom';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { DataSourceDropdown } from '..';
@@ -34,7 +36,7 @@ interface QBEntityOptionsProps {
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
onToggleVisibility: () => void;
onCollapseEntity: () => void;
onQueryFunctionsUpdates?: (functions: QueryFunction[]) => void;
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
showDeleteButton?: boolean;
showCloneOption?: boolean;
isListViewPanel?: boolean;

View File

@@ -9,14 +9,15 @@ import {
import { useIsDarkMode } from 'hooks/useDarkMode';
import { debounce, isNil } from 'lodash-es';
import { X } from 'lucide-react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
interface FunctionProps {
query: IBuilderQuery;
funcData: QueryFunction;
funcData: QueryFunctionProps;
index: any;
handleUpdateFunctionArgs: any;
handleUpdateFunctionName: any;
@@ -32,19 +33,17 @@ export default function Function({
handleDeleteFunction,
}: FunctionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
// Normalize function name to handle backend response case sensitivity
const normalizedFunctionName = normalizeFunctionName(funcData.name);
const { showInput, disabled } = queryFunctionsTypesConfig[
normalizedFunctionName
];
const { showInput, disabled } = queryFunctionsTypesConfig[funcData.name];
let functionValue;
const hasValue = !isNil(funcData.args?.[0]?.value);
const hasValue = !isNil(
funcData.args && funcData.args.length > 0 && funcData.args[0],
);
if (hasValue) {
// eslint-disable-next-line prefer-destructuring
functionValue = funcData.args?.[0]?.value;
functionValue = funcData.args[0];
}
const debouncedhandleUpdateFunctionArgs = debounce(
@@ -58,10 +57,9 @@ export default function Function({
? logsQueryFunctionOptions
: metricQueryFunctionOptions;
const disableRemoveFunction =
normalizedFunctionName === QueryFunctionsTypes.ANOMALY;
const disableRemoveFunction = funcData.name === QueryFunctionsTypes.ANOMALY;
if (normalizedFunctionName === QueryFunctionsTypes.ANOMALY) {
if (funcData.name === QueryFunctionsTypes.ANOMALY) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
@@ -70,7 +68,7 @@ export default function Function({
<Flex className="query-function">
<Select
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
value={normalizedFunctionName}
value={funcData.name}
disabled={disabled}
style={{ minWidth: '100px' }}
onChange={(value): void => {

View File

@@ -6,28 +6,29 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { cloneDeep, pullAt } from 'lodash-es';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
import { normalizeFunctionName } from 'utils/functionNameNormalizer';
import Function from './Function';
import { toFloat64 } from './utils';
const defaultMetricFunctionStruct: QueryFunction = {
name: QueryFunctionsTypes.CUTOFF_MIN as any,
const defaultMetricFunctionStruct: QueryFunctionProps = {
name: QueryFunctionsTypes.CUTOFF_MIN,
args: [],
};
const defaultLogFunctionStruct: QueryFunction = {
name: QueryFunctionsTypes.TIME_SHIFT as any,
const defaultLogFunctionStruct: QueryFunctionProps = {
name: QueryFunctionsTypes.TIME_SHIFT,
args: [],
};
interface QueryFunctionsProps {
query: IBuilderQuery;
queryFunctions: QueryFunction[];
onChange: (functions: QueryFunction[]) => void;
queryFunctions: QueryFunctionProps[];
onChange: (functions: QueryFunctionProps[]) => void;
maxFunctions: number;
}
@@ -86,11 +87,8 @@ export default function QueryFunctions({
onChange,
maxFunctions = 3,
}: QueryFunctionsProps): JSX.Element {
const [functions, setFunctions] = useState<QueryFunction[]>(
queryFunctions.map((func) => ({
...func,
name: normalizeFunctionName(func.name) as any,
})),
const [functions, setFunctions] = useState<QueryFunctionProps[]>(
queryFunctions,
);
const isDarkMode = useIsDarkMode();
@@ -129,7 +127,7 @@ export default function QueryFunctions({
};
const handleDeleteFunction = (
queryFunction: QueryFunction,
queryFunction: QueryFunctionProps,
index: number,
): void => {
const clonedFunctions = cloneDeep(functions);
@@ -140,23 +138,21 @@ export default function QueryFunctions({
};
const handleUpdateFunctionName = (
func: QueryFunction,
func: QueryFunctionProps,
index: number,
value: string,
): void => {
const updateFunctions = cloneDeep(functions);
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
// Normalize function name from backend response to match frontend expectations
const normalizedValue = normalizeFunctionName(value);
updateFunctions[index].name = normalizedValue as any;
updateFunctions[index].name = value;
setFunctions(updateFunctions);
onChange(updateFunctions);
}
};
const handleUpdateFunctionArgs = (
func: QueryFunction,
func: QueryFunctionProps,
index: number,
value: string,
): void => {
@@ -164,12 +160,11 @@ export default function QueryFunctions({
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
updateFunctions[index].args = [
{
value:
updateFunctions[index].name === QueryFunctionsTypes.TIME_SHIFT
? toFloat64(value)
: value,
},
// 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,
];
setFunctions(updateFunctions);
onChange(updateFunctions);

View File

@@ -4,8 +4,7 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
query: IBuilderQuery;
onChange: (value: BaseAutocompleteData, isEditMode?: boolean) => void;
onChange: (value: BaseAutocompleteData) => 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, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { SuccessResponse } from 'types/api';
import {
@@ -24,6 +24,7 @@ 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';
@@ -37,10 +38,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(
@@ -48,10 +49,6 @@ 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);
@@ -60,13 +57,12 @@ export const AggregatorFilter = memo(function AggregatorFilter({
}, [searchText]);
const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY);
const { isFetching, data: aggregateAttributeData } = useQuery(
const { isFetching } = useQuery(
[
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
debouncedValue,
queryAggregation.timeAggregation,
query.dataSource,
index,
],
async () =>
getAggregateAttribute({
@@ -112,49 +108,13 @@ 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
? `Search metric name`
? `${transformToUpperCase(query.dataSource)} name`
: 'Aggregate attribute';
const getAttributesData = useCallback(
@@ -164,14 +124,12 @@ export const AggregatorFilter = memo(function AggregatorFilter({
debouncedValue,
queryAggregation.timeAggregation,
query.dataSource,
index,
])?.payload?.attributeKeys || [],
[
debouncedValue,
queryAggregation.timeAggregation,
query.dataSource,
queryClient,
index,
],
);
@@ -182,7 +140,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
searchText,
queryAggregation.timeAggregation,
query.dataSource,
index,
],
async () =>
getAggregateAttribute({
@@ -198,7 +155,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
query.dataSource,
queryClient,
searchText,
index,
]);
const handleChangeCustomValue = useCallback(
@@ -214,16 +170,11 @@ export const AggregatorFilter = memo(function AggregatorFilter({
);
const handleBlur = useCallback(async () => {
if (searchText && searchText !== queryAggregation.metricName) {
if (searchText) {
const aggregateAttributes = await getResponseAttributes();
handleChangeCustomValue(searchText, aggregateAttributes);
}
}, [
getResponseAttributes,
handleChangeCustomValue,
searchText,
queryAggregation?.metricName,
]);
}, [getResponseAttributes, handleChangeCustomValue, searchText]);
const handleChange = useCallback(
(

View File

@@ -195,7 +195,6 @@ 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

@@ -0,0 +1,113 @@
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

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

View File

@@ -0,0 +1,150 @@
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