Compare commits

..

10 Commits

Author SHA1 Message Date
Abhi kumar
ca30fad405 Merge branch 'main' into fix/issue-9080 2025-10-30 12:18:48 +05:30
Abhi kumar
2bd9ee3647 Merge branch 'main' into fix/issue-9080 2025-10-29 15:19:09 +05:30
Abhi kumar
d6050cda81 Merge branch 'main' into fix/issue-9080 2025-10-28 12:07:54 +05:30
Abhi kumar
f6c98cad6c Merge branch 'main' into fix/issue-9080 2025-10-27 19:39:20 +05:30
Srikanth Chekuri
ed2d4c84f0 Merge branch 'main' into fix/issue-9080 2025-10-23 16:44:25 +05:30
Abhi kumar
32ae61af6d Merge branch 'main' into fix/issue-9080 2025-10-16 15:55:54 +05:30
Abhi Kumar
fafa13fb52 Merge branch 'main' of https://github.com/SigNoz/signoz into fix/issue-9080 2025-10-14 16:21:52 +05:30
Abhi Kumar
5b9269769d chore: fixed ts issues 2025-10-07 11:49:27 +05:30
Abhi Kumar
af2f36adfa chore: removed non required code 2025-10-07 11:48:19 +05:30
Abhi Kumar
471d15945c fix: added fix for cursor jump issue 2025-10-07 11:41:28 +05:30
105 changed files with 499 additions and 5242 deletions

2
.github/CODEOWNERS vendored
View File

@@ -2,7 +2,7 @@
# Owners are automatically requested for review for PRs that changes code
# that they own.
/frontend/ @YounixM @aks07
/frontend/ @SigNoz/frontend @YounixM
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv

View File

@@ -107,6 +107,7 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'

View File

@@ -106,6 +106,7 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'

View File

@@ -17,7 +17,6 @@ jobs:
- bootstrap
- passwordauthn
- callbackauthn
- cloudintegrations
- querier
- ttl
sqlstore-provider:

View File

@@ -1,63 +1,43 @@
version: "2"
linters:
default: none
default: standard
enable:
- bodyclose
- depguard
- errcheck
- forbidigo
- govet
- iface
- ineffassign
- misspell
- nilnil
- sloglint
- depguard
- iface
- unparam
- unused
settings:
depguard:
rules:
noerrors:
deny:
- pkg: errors
desc: Do not use errors package. Use github.com/SigNoz/signoz/pkg/errors instead.
nozap:
deny:
- pkg: go.uber.org/zap
desc: Do not use zap logger. Use slog instead.
forbidigo:
forbid:
- pattern: fmt.Errorf
- pattern: ^(fmt\.Print.*|print|println)$
iface:
enable:
- identical
sloglint:
no-mixed-args: true
kv-only: true
no-global: all
context: all
static-msg: true
key-naming-case: snake
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- pkg/query-service
- ee/query-service
- scripts/
- tmp/
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- forbidigo
linters-settings:
sloglint:
no-mixed-args: true
kv-only: true
no-global: all
context: all
static-msg: true
msg-style: lowercased
key-naming-case: snake
depguard:
rules:
nozap:
deny:
- pkg: "go.uber.org/zap"
desc: "Do not use zap logger. Use slog instead."
noerrors:
deny:
- pkg: "errors"
desc: "Do not use errors package. Use github.com/SigNoz/signoz/pkg/errors instead."
iface:
enable:
- identical
forbidigo:
forbid:
- fmt.Errorf
- ^(fmt\.Print.*|print|println)$
issues:
exclude-dirs:
- "pkg/query-service"
- "ee/query-service"
- "scripts/"

View File

@@ -31,6 +31,7 @@ builds:
- -X github.com/SigNoz/signoz/pkg/version.branch={{ .Branch }}
- -X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
mod_timestamp: "{{ .CommitTimestamp }}"

View File

@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -76,7 +77,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
ingestionUrl, signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
ingestionUrl, signozApiUrl, apiErr := getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't deduce ingestion url and signoz api url",
@@ -185,37 +186,48 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
return cloudIntegrationUser, nil
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
string, string, *basemodel.ApiError,
) {
// TODO: remove this struct from here
url := fmt.Sprintf(
"%s%s",
strings.TrimSuffix(constants.ZeusURL, "/"),
"/v2/deployments/me",
)
type deploymentResponse struct {
Name string `json:"name"`
ClusterInfo struct {
Region struct {
DNS string `json:"dns"`
} `json:"region"`
} `json:"cluster"`
Status string `json:"status"`
Error string `json:"error"`
Data struct {
Name string `json:"name"`
ClusterInfo struct {
Region struct {
DNS string `json:"dns"`
} `json:"region"`
} `json:"cluster"`
} `json:"data"`
}
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
if err != nil {
resp, apiErr := requestAndParseResponse[deploymentResponse](
ctx, url, map[string]string{"X-Signoz-Cloud-Api-Key": licenseKey}, nil,
)
if apiErr != nil {
return "", "", basemodel.WrapApiError(
apiErr, "couldn't query for deployment info",
)
}
if resp.Status != "success" {
return "", "", basemodel.InternalError(fmt.Errorf(
"couldn't query for deployment info: error: %w", err,
"couldn't query for deployment info: status: %s, error: %s",
resp.Status, resp.Error,
))
}
resp := new(deploymentResponse)
err = json.Unmarshal(respBytes, resp)
if err != nil {
return "", "", basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal deployment info response: error: %w", err,
))
}
regionDns := resp.ClusterInfo.Region.DNS
deploymentName := resp.Name
regionDns := resp.Data.ClusterInfo.Region.DNS
deploymentName := resp.Data.Name
if len(regionDns) < 1 || len(deploymentName) < 1 {
// Fail early if actual response structure and expectation here ever diverge

View File

@@ -10,6 +10,9 @@ var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
// this is set via build time variable
var ZeusURL = "https://api.signoz.cloud"
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)
if len(v) == 0 {

View File

@@ -69,7 +69,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"axios": "1.8.2",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",

View File

@@ -1,28 +0,0 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
GetSpanPercentilesProps,
GetSpanPercentilesResponseDataProps,
} from 'types/api/trace/getSpanPercentiles';
const getSpanPercentiles = async (
props: GetSpanPercentilesProps,
): Promise<SuccessResponseV2<GetSpanPercentilesResponseDataProps>> => {
try {
const response = await ApiBaseInstance.post('/span_percentile', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
throw error;
}
};
export default getSpanPercentiles;

View File

@@ -1,371 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { getYAxisFormattedValue, PrecisionOptionsEnum } from '../yAxisConfig';
const testFullPrecisionGetYAxisFormattedValue = (
value: string,
format: string,
): string => getYAxisFormattedValue(value, format, PrecisionOptionsEnum.FULL);
describe('getYAxisFormattedValue - none (full precision legacy assertions)', () => {
test('large integers and decimals', () => {
expect(testFullPrecisionGetYAxisFormattedValue('250034', 'none')).toBe(
'250034',
);
expect(
testFullPrecisionGetYAxisFormattedValue('250034897.12345', 'none'),
).toBe('250034897.12345');
expect(
testFullPrecisionGetYAxisFormattedValue('250034897.02354', 'none'),
).toBe('250034897.02354');
expect(testFullPrecisionGetYAxisFormattedValue('9999999.9999', 'none')).toBe(
'9999999.9999',
);
});
test('preserves leading zeros after decimal until first non-zero', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1.0000234', 'none')).toBe(
'1.0000234',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.00003', 'none')).toBe(
'0.00003',
);
});
test('trims to three significant decimals and removes trailing zeros', () => {
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250034', 'none'),
).toBe('0.000000250034');
expect(testFullPrecisionGetYAxisFormattedValue('0.00000025', 'none')).toBe(
'0.00000025',
);
// Big precision, limiting the javascript precision (~16 digits)
expect(
testFullPrecisionGetYAxisFormattedValue('1.0000000000000001', 'none'),
).toBe('1');
expect(
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'none'),
).toBe('1.005555555595958');
expect(testFullPrecisionGetYAxisFormattedValue('0.000000001', 'none')).toBe(
'0.000000001',
);
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250000', 'none'),
).toBe('0.00000025');
});
test('whole numbers normalize', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'none')).toBe('1000');
expect(testFullPrecisionGetYAxisFormattedValue('99.5458', 'none')).toBe(
'99.5458',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.234567', 'none')).toBe(
'1.234567',
);
expect(testFullPrecisionGetYAxisFormattedValue('99.998', 'none')).toBe(
'99.998',
);
});
test('strip redundant decimal zeros', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000.000', 'none')).toBe(
'1000',
);
expect(testFullPrecisionGetYAxisFormattedValue('99.500', 'none')).toBe(
'99.5',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.000', 'none')).toBe('1');
});
test('edge values', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('-0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'none')).toBe('∞');
expect(testFullPrecisionGetYAxisFormattedValue('-Infinity', 'none')).toBe(
'-∞',
);
expect(testFullPrecisionGetYAxisFormattedValue('invalid', 'none')).toBe(
'NaN',
);
expect(testFullPrecisionGetYAxisFormattedValue('', 'none')).toBe('NaN');
expect(testFullPrecisionGetYAxisFormattedValue('abc123', 'none')).toBe('NaN');
});
test('small decimals keep precision as-is', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.0001', 'none')).toBe(
'0.0001',
);
expect(testFullPrecisionGetYAxisFormattedValue('-0.0001', 'none')).toBe(
'-0.0001',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.000000001', 'none')).toBe(
'0.000000001',
);
});
test('simple decimals preserved', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.1', 'none')).toBe('0.1');
expect(testFullPrecisionGetYAxisFormattedValue('0.2', 'none')).toBe('0.2');
expect(testFullPrecisionGetYAxisFormattedValue('0.3', 'none')).toBe('0.3');
expect(testFullPrecisionGetYAxisFormattedValue('1.0000000001', 'none')).toBe(
'1.0000000001',
);
});
});
describe('getYAxisFormattedValue - units (full precision legacy assertions)', () => {
test('ms', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'ms')).toBe('1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('500', 'ms')).toBe('500 ms');
expect(testFullPrecisionGetYAxisFormattedValue('60000', 'ms')).toBe('1 min');
expect(testFullPrecisionGetYAxisFormattedValue('295.429', 'ms')).toBe(
'295.429 ms',
);
expect(testFullPrecisionGetYAxisFormattedValue('4353.81', 'ms')).toBe(
'4.35381 s',
);
});
test('s', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 's')).toBe('1.5 mins');
expect(testFullPrecisionGetYAxisFormattedValue('30', 's')).toBe('30 s');
expect(testFullPrecisionGetYAxisFormattedValue('3600', 's')).toBe('1 hour');
});
test('m', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 'm')).toBe('1.5 hours');
expect(testFullPrecisionGetYAxisFormattedValue('30', 'm')).toBe('30 min');
expect(testFullPrecisionGetYAxisFormattedValue('1440', 'm')).toBe('1 day');
});
test('bytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'bytes')).toBe(
'1 KiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('512', 'bytes')).toBe('512 B');
expect(testFullPrecisionGetYAxisFormattedValue('1536', 'bytes')).toBe(
'1.5 KiB',
);
});
test('mbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'mbytes')).toBe(
'1 GiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('512', 'mbytes')).toBe(
'512 MiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('1536', 'mbytes')).toBe(
'1.5 GiB',
);
});
test('kbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'kbytes')).toBe(
'1 MiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('512', 'kbytes')).toBe(
'512 KiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('1536', 'kbytes')).toBe(
'1.5 MiB',
);
});
test('short', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'short')).toBe('1 K');
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'short')).toBe(
'1.5 K',
);
expect(testFullPrecisionGetYAxisFormattedValue('999', 'short')).toBe('999');
expect(testFullPrecisionGetYAxisFormattedValue('1000000', 'short')).toBe(
'1 Mil',
);
expect(testFullPrecisionGetYAxisFormattedValue('1555600', 'short')).toBe(
'1.5556 Mil',
);
expect(testFullPrecisionGetYAxisFormattedValue('999999', 'short')).toBe(
'999.999 K',
);
expect(testFullPrecisionGetYAxisFormattedValue('1000000000', 'short')).toBe(
'1 Bil',
);
expect(testFullPrecisionGetYAxisFormattedValue('1500000000', 'short')).toBe(
'1.5 Bil',
);
expect(testFullPrecisionGetYAxisFormattedValue('999999999', 'short')).toBe(
'999.999999 Mil',
);
});
test('percent', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.15', 'percent')).toBe(
'0.15%',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.1234', 'percent')).toBe(
'0.1234%',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.123499', 'percent')).toBe(
'0.123499%',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.5', 'percent')).toBe(
'1.5%',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.0001', 'percent')).toBe(
'0.0001%',
);
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000001', 'percent'),
).toBe('1e-9%');
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250034', 'percent'),
).toBe('0.000000250034%');
expect(testFullPrecisionGetYAxisFormattedValue('0.00000025', 'percent')).toBe(
'0.00000025%',
);
// Big precision, limiting the javascript precision (~16 digits)
expect(
testFullPrecisionGetYAxisFormattedValue('1.0000000000000001', 'percent'),
).toBe('1%');
expect(
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
).toBe('1.005555555595958%');
});
test('ratio', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.5', 'ratio')).toBe(
'0.5 ratio',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.25', 'ratio')).toBe(
'1.25 ratio',
);
expect(testFullPrecisionGetYAxisFormattedValue('2.0', 'ratio')).toBe(
'2 ratio',
);
});
test('temperature units', () => {
expect(testFullPrecisionGetYAxisFormattedValue('25', 'celsius')).toBe(
'25 °C',
);
expect(testFullPrecisionGetYAxisFormattedValue('0', 'celsius')).toBe('0 °C');
expect(testFullPrecisionGetYAxisFormattedValue('-10', 'celsius')).toBe(
'-10 °C',
);
expect(testFullPrecisionGetYAxisFormattedValue('77', 'fahrenheit')).toBe(
'77 °F',
);
expect(testFullPrecisionGetYAxisFormattedValue('32', 'fahrenheit')).toBe(
'32 °F',
);
expect(testFullPrecisionGetYAxisFormattedValue('14', 'fahrenheit')).toBe(
'14 °F',
);
});
test('ms edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'ms')).toBe('0 ms');
expect(testFullPrecisionGetYAxisFormattedValue('-1500', 'ms')).toBe('-1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'ms')).toBe('∞');
});
test('bytes edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'bytes')).toBe('0 B');
expect(testFullPrecisionGetYAxisFormattedValue('-1024', 'bytes')).toBe(
'-1 KiB',
);
});
});
describe('getYAxisFormattedValue - precision option tests', () => {
test('precision 0 drops decimal part', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 0)).toBe('1');
expect(getYAxisFormattedValue('0.9999', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('12345.6789', 'none', 0)).toBe('12345');
expect(getYAxisFormattedValue('0.0000123456', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('1000.000', 'none', 0)).toBe('1000');
expect(getYAxisFormattedValue('0.000000250034', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 0)).toBe('1');
// with unit
expect(getYAxisFormattedValue('4353.81', 'ms', 0)).toBe('4 s');
});
test('precision 1,2,3,4 decimals', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 1)).toBe('1.2');
expect(getYAxisFormattedValue('1.2345', 'none', 2)).toBe('1.23');
expect(getYAxisFormattedValue('1.2345', 'none', 3)).toBe('1.234');
expect(getYAxisFormattedValue('1.2345', 'none', 4)).toBe('1.2345');
expect(getYAxisFormattedValue('0.0000123456', 'none', 1)).toBe('0.00001');
expect(getYAxisFormattedValue('0.0000123456', 'none', 2)).toBe('0.000012');
expect(getYAxisFormattedValue('0.0000123456', 'none', 3)).toBe('0.0000123');
expect(getYAxisFormattedValue('0.0000123456', 'none', 4)).toBe('0.00001234');
expect(getYAxisFormattedValue('1000.000', 'none', 1)).toBe('1000');
expect(getYAxisFormattedValue('1000.000', 'none', 2)).toBe('1000');
expect(getYAxisFormattedValue('1000.000', 'none', 3)).toBe('1000');
expect(getYAxisFormattedValue('1000.000', 'none', 4)).toBe('1000');
expect(getYAxisFormattedValue('0.000000250034', 'none', 1)).toBe('0.0000002');
expect(getYAxisFormattedValue('0.000000250034', 'none', 2)).toBe(
'0.00000025',
); // leading zeros + 2 significant => same trimmed
expect(getYAxisFormattedValue('0.000000250034', 'none', 3)).toBe(
'0.00000025',
);
expect(getYAxisFormattedValue('0.000000250304', 'none', 4)).toBe(
'0.0000002503',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 1)).toBe(
'1.005',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 2)).toBe(
'1.0055',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 3)).toBe(
'1.00555',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 4)).toBe(
'1.005555',
);
// with unit
expect(getYAxisFormattedValue('4353.81', 'ms', 1)).toBe('4.4 s');
expect(getYAxisFormattedValue('4353.81', 'ms', 2)).toBe('4.35 s');
expect(getYAxisFormattedValue('4353.81', 'ms', 3)).toBe('4.354 s');
expect(getYAxisFormattedValue('4353.81', 'ms', 4)).toBe('4.3538 s');
// Percentages
expect(getYAxisFormattedValue('0.123456', 'percent', 2)).toBe('0.12%');
expect(getYAxisFormattedValue('0.123456', 'percent', 4)).toBe('0.1235%'); // approximation
});
test('precision full uses up to DEFAULT_SIGNIFICANT_DIGITS significant digits', () => {
expect(
getYAxisFormattedValue(
'0.00002625429914148441',
'none',
PrecisionOptionsEnum.FULL,
),
).toBe('0.000026254299141');
expect(
getYAxisFormattedValue(
'0.000026254299141484417',
's',
PrecisionOptionsEnum.FULL,
),
).toBe('26254299141484417000000 µs');
expect(
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
).toBe('4.35381 s');
expect(getYAxisFormattedValue('500', 'ms', PrecisionOptionsEnum.FULL)).toBe(
'500 ms',
);
});
});

View File

@@ -1,158 +1,58 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { formattedValueToString, getValueFormat } from '@grafana/data';
import * as Sentry from '@sentry/react';
import { isNaN } from 'lodash-es';
const DEFAULT_SIGNIFICANT_DIGITS = 15;
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
const MAX_DECIMALS = 15;
export enum PrecisionOptionsEnum {
ZERO = 0,
ONE = 1,
TWO = 2,
THREE = 3,
FOUR = 4,
FULL = 'full',
}
export type PrecisionOption = 0 | 1 | 2 | 3 | 4 | PrecisionOptionsEnum.FULL;
/**
* Formats a number for display, preserving leading zeros after the decimal point
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
* It avoids scientific notation and removes unnecessary trailing zeros.
*
* @example
* formatDecimalWithLeadingZeros(1.2345); // "1.2345"
* formatDecimalWithLeadingZeros(0.0012345); // "0.0012345"
* formatDecimalWithLeadingZeros(5.0); // "5"
*
* @param value The number to format.
* @returns The formatted string.
*/
const formatDecimalWithLeadingZeros = (
value: number,
precision: PrecisionOption,
): string => {
if (value === 0) {
return '0';
}
// Use toLocaleString to get a full decimal representation without scientific notation.
const numStr = value.toLocaleString('en-US', {
useGrouping: false,
maximumFractionDigits: 20,
});
const [integerPart, decimalPart = ''] = numStr.split('.');
// If there's no decimal part, the integer part is the result.
if (!decimalPart) {
return integerPart;
}
// Find the index of the first non-zero digit in the decimal part.
const firstNonZeroIndex = decimalPart.search(/[^0]/);
// If the decimal part consists only of zeros, return just the integer part.
if (firstNonZeroIndex === -1) {
return integerPart;
}
// Determine the number of decimals to keep: leading zeros + up to N significant digits.
const significantDigits =
precision === PrecisionOptionsEnum.FULL
? DEFAULT_SIGNIFICANT_DIGITS
: precision;
const decimalsToKeep = firstNonZeroIndex + (significantDigits || 0);
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
const finalDecimalsToKeep = Math.min(decimalsToKeep, MAX_DECIMALS);
const trimmedDecimalPart = decimalPart.substring(0, finalDecimalsToKeep);
// If precision is 0, we drop the decimal part entirely.
if (precision === 0) {
return integerPart;
}
// Remove any trailing zeros from the result to keep it clean.
const finalDecimalPart = trimmedDecimalPart.replace(/0+$/, '');
// Return the integer part, or the integer and decimal parts combined.
return finalDecimalPart ? `${integerPart}.${finalDecimalPart}` : integerPart;
};
/**
* Formats a Y-axis value based on a given format string.
*
* @param value The string value from the axis.
* @param format The format identifier (e.g. 'none', 'ms', 'bytes', 'short').
* @returns A formatted string ready for display.
*/
export const getYAxisFormattedValue = (
value: string,
format: string,
precision: PrecisionOption = 2, // default precision requested
): string => {
const numValue = parseFloat(value);
// Handle non-numeric or special values first.
if (isNaN(numValue)) return 'NaN';
if (numValue === Infinity) return '∞';
if (numValue === -Infinity) return '-∞';
const decimalPlaces = value.split('.')[1]?.length || undefined;
// Use custom formatter for the 'none' format honoring precision
if (format === 'none') {
return formatDecimalWithLeadingZeros(numValue, precision);
}
// For all other standard formats, delegate to grafana/data's built-in formatter.
const computeDecimals = (): number | undefined => {
if (precision === PrecisionOptionsEnum.FULL) {
return decimalPlaces && decimalPlaces >= DEFAULT_SIGNIFICANT_DIGITS
? decimalPlaces
: DEFAULT_SIGNIFICANT_DIGITS;
}
return precision;
};
const fallbackFormat = (): string => {
if (precision === PrecisionOptionsEnum.FULL) return numValue.toString();
if (precision === 0) return Math.round(numValue).toString();
return precision !== undefined
? numValue
.toFixed(precision)
.replace(/(\.[0-9]*[1-9])0+$/, '$1') // trimming zeros
.replace(/\.$/, '')
: numValue.toString();
};
let decimalPrecision: number | undefined;
const parsedValue = getValueFormat(format)(
parseFloat(value),
undefined,
undefined,
undefined,
);
try {
const formatter = getValueFormat(format);
const formattedValue = formatter(numValue, computeDecimals(), undefined);
if (formattedValue.text && formattedValue.text.includes('.')) {
formattedValue.text = formatDecimalWithLeadingZeros(
parseFloat(formattedValue.text),
precision,
);
const decimalSplitted = parsedValue.text.split('.');
if (decimalSplitted.length === 1) {
decimalPrecision = 0;
} else {
const decimalDigits = decimalSplitted[1].split('');
decimalPrecision = decimalDigits.length;
let nonZeroCtr = 0;
for (let idx = 0; idx < decimalDigits.length; idx += 1) {
if (decimalDigits[idx] !== '0') {
nonZeroCtr += 1;
if (nonZeroCtr >= 2) {
decimalPrecision = idx + 1;
}
} else if (nonZeroCtr) {
decimalPrecision = idx;
break;
}
}
}
return formattedValueToString(formattedValue);
return formattedValueToString(
getValueFormat(format)(
parseFloat(value),
decimalPrecision,
undefined,
undefined,
),
);
} catch (error) {
Sentry.captureEvent({
message: `Error applying formatter: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
level: 'error',
});
return fallbackFormat();
console.error(error);
}
return `${parseFloat(value)}`;
};
export const getToolTipValue = (
value: string | number,
format?: string,
precision?: PrecisionOption,
): string =>
getYAxisFormattedValue(value?.toString(), format || 'none', precision);
export const getToolTipValue = (value: string, format?: string): string => {
try {
return formattedValueToString(
getValueFormat(format)(parseFloat(value), undefined, undefined, undefined),
);
} catch (error) {
console.error(error);
}
return `${value}`;
};

View File

@@ -60,14 +60,6 @@ function Metrics({
setElement,
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const queryPayloads = useMemo(
() =>
getHostQueryPayload(
@@ -155,13 +147,6 @@ function Metrics({
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
),
[

View File

@@ -132,9 +132,9 @@
justify-content: center;
}
.log-detail-drawer__actions {
.json-action-btn {
display: flex;
gap: 4px;
gap: 8px;
}
}

View File

@@ -319,35 +319,31 @@ function LogDetailInner({
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
{selectedView === VIEW_TYPES.JSON && (
<div className="json-action-btn">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
onClick={handleJSONCopy}
/>
</Tooltip>
</div>
</div>
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
)}
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={onLogCopy}
/>
</Tooltip>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
@@ -387,8 +383,7 @@ function LogDetailInner({
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
logLineTimestamp={log.timestamp.toString()}
/>
)}
</Drawer>

View File

@@ -398,7 +398,7 @@
}
.qb-search-container {
.metrics-container {
.metrics-select-container {
margin-bottom: 12px;
}
}

View File

@@ -22,8 +22,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
showOnlyWhereClause = false,
showTraceOperator = false,
version,
onSignalSourceChange,
signalSourceChangeEnabled = false,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
@@ -177,8 +175,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
/>
) : (
currentQuery.builder.queryData.map((query, index) => (
@@ -197,9 +193,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={query.source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
signalSource={config?.signalSource || ''}
/>
))
)}

View File

@@ -1,14 +1,5 @@
.metrics-source-select-container {
.metrics-select-container {
margin-bottom: 8px;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
width: 100%;
.source-selector {
width: 120px;
}
.ant-select-selector {
width: 100%;
@@ -51,7 +42,7 @@
}
.lightMode {
.metrics-source-select-container {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100);

View File

@@ -1,39 +1,21 @@
import './MetricsSelect.styles.scss';
import { Select } from 'antd';
import {
initialQueriesMap,
initialQueryMeterWithType,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const SOURCE_OPTIONS: SelectOption<string, string>[] = [
{ value: 'metrics', label: 'Metrics' },
{ value: 'meter', label: 'Meter' },
];
export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
signalSource,
onSignalSourceChange,
signalSourceChangeEnabled = false,
}: {
query: IBuilderQuery;
index: number;
version: string;
signalSource: 'meter' | '';
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
}): JSX.Element {
const [attributeKeys, setAttributeKeys] = useState<BaseAutocompleteData[]>([]);
@@ -49,67 +31,8 @@ export const MetricsSelect = memo(function MetricsSelect({
},
[handleChangeAggregatorAttribute, attributeKeys],
);
const { updateAllQueriesOperators, handleSetQueryData } = useQueryBuilder();
const source = useMemo(
() => (signalSource === 'meter' ? 'meter' : 'metrics'),
[signalSource],
);
const defaultMeterQuery = useMemo(
() =>
updateAllQueriesOperators(
initialQueryMeterWithType,
PANEL_TYPES.BAR,
DataSource.METRICS,
'meter' as 'meter' | '',
),
[updateAllQueriesOperators],
);
const defaultMetricsQuery = useMemo(
() =>
updateAllQueriesOperators(
initialQueriesMap.metrics,
PANEL_TYPES.BAR,
DataSource.METRICS,
'',
),
[updateAllQueriesOperators],
);
const handleSignalSourceChange = (value: string): void => {
onSignalSourceChange(value);
handleSetQueryData(
index,
value === 'meter'
? {
...defaultMeterQuery.builder.queryData[0],
source: 'meter',
queryName: query.queryName,
}
: {
...defaultMetricsQuery.builder.queryData[0],
source: '',
queryName: query.queryName,
},
);
};
return (
<div className="metrics-source-select-container">
{signalSourceChangeEnabled && (
<Select
className="source-selector"
placeholder="Source"
options={SOURCE_OPTIONS}
value={source}
defaultValue="metrics"
onChange={handleSignalSourceChange}
/>
)}
<div className="metrics-select-container">
<AggregatorFilter
onChange={handleAggregatorAttributeChange}
query={query}

View File

@@ -89,7 +89,7 @@ function QuerySearch({
hardcodedAttributeKeys,
}: {
placeholder?: string;
onChange: (value: string) => void;
onChange: (value: string, syncExpression?: boolean) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
signalSource?: string;
@@ -97,7 +97,7 @@ function QuerySearch({
onRun?: (query: string) => void;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
const [query, setQuery] = useState<string>('');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
@@ -108,6 +108,10 @@ function QuerySearch({
errors: [],
});
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [hasInteractedWithQB, setHasInteractedWithQB] = useState(false);
const handleQueryValidation = (newQuery: string): void => {
try {
const validationResponse = validateQuery(newQuery);
@@ -127,13 +131,28 @@ function QuerySearch({
useEffect(() => {
const newQuery = queryData.filter?.expression || '';
// Only mark as external change if the query actually changed from external source
// Only update query from external source when editor is not focused
// When focused, just update the lastExternalQuery to track changes
if (newQuery !== lastExternalQuery) {
setQuery(newQuery);
setIsExternalQueryChange(true);
setLastExternalQuery(newQuery);
}
}, [queryData.filter?.expression, lastExternalQuery]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryData.filter?.expression]);
useEffect(() => {
// Update the query when the editor is blurred and the query has changed
// Only call onChange if the editor has been focused before (not on initial mount)
if (
!isFocused &&
hasInteractedWithQB &&
query !== queryData.filter?.expression
) {
onChange(query, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocused]);
// Validate query when it changes externally (from queryData)
useEffect(() => {
@@ -149,9 +168,6 @@ function QuerySearch({
const [showExamples] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [
isFetchingCompleteValuesList,
setIsFetchingCompleteValuesList,
@@ -1352,8 +1368,13 @@ function QuerySearch({
}}
onFocus={(): void => {
setIsFocused(true);
setHasInteractedWithQB(true);
}}
onBlur={handleBlur}
onCreateEditor={(view: EditorView): EditorView => {
editorRef.current = view;
return view;
}}
/>
{query && validation.isValid === false && !isFocused && (

View File

@@ -33,14 +33,12 @@ export const QueryV2 = memo(function QueryV2({
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
onSignalSourceChange,
signalSourceChangeEnabled = false,
}: QueryProps & {
ref: React.RefObject<HTMLDivElement>;
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
}): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const {
cloneQuery,
panelType,
handleSetCurrentFilterExpression,
} = useQueryBuilder();
const showFunctions = query?.functions?.length > 0;
const { dataSource } = query;
@@ -102,12 +100,16 @@ export const QueryV2 = memo(function QueryV2({
);
const handleSearchChange = useCallback(
(value: string) => {
(handleChangeQueryData as HandleChangeQueryDataV5)('filter', {
expression: value,
});
(value: string, syncExpression = false) => {
handleSetCurrentFilterExpression(value, query.queryName);
if (syncExpression) {
(handleChangeQueryData as HandleChangeQueryDataV5)('filter', {
expression: value,
});
}
},
[handleChangeQueryData],
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleSetCurrentFilterExpression, handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
@@ -213,14 +215,12 @@ export const QueryV2 = memo(function QueryV2({
<div className="qb-elements-container">
<div className="qb-search-container">
{dataSource === DataSource.METRICS && (
<div className="metrics-container">
<div className="metrics-select-container">
<MetricsSelect
query={query}
index={index}
version={ENTITY_VERSION_V5}
signalSource={signalSource as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange}
signalSourceChangeEnabled={signalSourceChangeEnabled}
/>
</div>
)}
@@ -266,7 +266,7 @@ export const QueryV2 = memo(function QueryV2({
panelType={panelType}
query={query}
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}-${signalSource}`}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
signalSource={signalSource as 'meter' | ''}
/>

View File

@@ -24,7 +24,6 @@ export const DATE_TIME_FORMATS = {
TIME_SECONDS: 'HH:mm:ss',
TIME_UTC: 'HH:mm:ss (UTC Z)',
TIME_UTC_MS: 'HH:mm:ss.SSS (UTC Z)',
TIME_SPAN_PERCENTILE: 'HH:mm:ss MMM DD',
// Short date formats
DATE_SHORT: 'MM/DD',

View File

@@ -90,7 +90,4 @@ export const REACT_QUERY_KEY = {
// Routing Policies Query Keys
GET_ROUTING_POLICIES: 'GET_ROUTING_POLICIES',
// Span Percentiles Query Keys
GET_SPAN_PERCENTILES: 'GET_SPAN_PERCENTILES',
} as const;

View File

@@ -3,5 +3,4 @@ export const USER_PREFERENCES = {
NAV_SHORTCUTS: 'nav_shortcuts',
LAST_SEEN_CHANGELOG_VERSION: 'last_seen_changelog_version',
SPAN_DETAILS_PINNED_ATTRIBUTES: 'span_details_pinned_attributes',
SPAN_PERCENTILE_RESOURCE_ATTRIBUTES: 'span_percentile_resource_attributes',
};

View File

@@ -69,13 +69,6 @@ function StatusCodeBarCharts({
} = endPointStatusCodeLatencyBarChartsDataQuery;
const { startTime: minTime, endTime: maxTime } = timeRange;
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
@@ -214,13 +207,6 @@ function StatusCodeBarCharts({
onDragSelect,
colorMapping,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
[
minTime,

View File

@@ -11,14 +11,12 @@ import { v4 } from 'uuid';
import { useCreateAlertState } from '../context';
import {
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_INFO_THRESHOLD,
INITIAL_RANDOM_THRESHOLD,
INITIAL_WARNING_THRESHOLD,
THRESHOLD_MATCH_TYPE_OPTIONS,
THRESHOLD_OPERATOR_OPTIONS,
} from '../context/constants';
import { AlertThresholdMatchType } from '../context/types';
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
import ThresholdItem from './ThresholdItem';
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
@@ -40,12 +38,12 @@ function AlertThreshold({
alertState,
thresholdState,
setThresholdState,
setEvaluationWindow,
notificationSettings,
setNotificationSettings,
} = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const queryNames = getQueryNames(currentQuery);
useEffect(() => {
@@ -162,54 +160,6 @@ function AlertThreshold({
}),
);
const handleSetEvaluationDetailsForMeter = (): void => {
setEvaluationWindow({
type: 'SET_INITIAL_STATE_FOR_METER',
});
setThresholdState({
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
});
};
const handleSelectedQueryChange = (value: string): void => {
// loop through currenttQuery and find the query that matches the selected query
const query = currentQuery?.builder?.queryData.find(
(query) => query.queryName === value,
);
const currentSelectedQuery = currentQuery?.builder?.queryData.find(
(query) => query.queryName === thresholdState.selectedQuery,
);
const newSelectedQuerySource = query?.source || '';
const currentSelectedQuerySource = currentSelectedQuery?.source || '';
if (newSelectedQuerySource === currentSelectedQuerySource) {
setThresholdState({
type: 'SET_SELECTED_QUERY',
payload: value,
});
return;
}
if (newSelectedQuerySource === 'meter') {
handleSetEvaluationDetailsForMeter();
} else {
setEvaluationWindow({
type: 'SET_INITIAL_STATE',
payload: INITIAL_EVALUATION_WINDOW_STATE,
});
}
setThresholdState({
type: 'SET_SELECTED_QUERY',
payload: value,
});
};
return (
<div
className={classNames(
@@ -225,7 +175,12 @@ function AlertThreshold({
</Typography.Text>
<Select
value={thresholdState.selectedQuery}
onChange={handleSelectedQueryChange}
onChange={(value): void => {
setThresholdState({
type: 'SET_SELECTED_QUERY',
payload: value,
});
}}
style={{ width: 80 }}
options={queryNames}
data-testid="alert-threshold-query-select"

View File

@@ -10,7 +10,6 @@ import { getEvaluationWindowTypeText, getTimeframeText } from './utils';
function EvaluationSettings(): JSX.Element {
const { evaluationWindow, setEvaluationWindow } = useCreateAlertState();
const [
isEvaluationWindowPopoverOpen,
setIsEvaluationWindowPopoverOpen,

View File

@@ -24,11 +24,7 @@ import {
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
import {
AlertThresholdMatchType,
ICreateAlertContextProps,
ICreateAlertProviderProps,
} from './types';
import { ICreateAlertContextProps, ICreateAlertProviderProps } from './types';
import {
advancedOptionsReducer,
alertCreationReducer,
@@ -71,7 +67,6 @@ export function CreateAlertProvider(
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const thresholdsFromURL = queryParams.get(QueryParams.thresholds);
const [alertType, setAlertType] = useState<AlertTypes>(() => {
if (isEditMode) {
@@ -127,28 +122,7 @@ export function CreateAlertProvider(
setThresholdState({
type: 'RESET',
});
if (thresholdsFromURL) {
try {
const thresholds = JSON.parse(thresholdsFromURL);
setThresholdState({
type: 'SET_THRESHOLDS',
payload: thresholds,
});
} catch (error) {
console.error('Error parsing thresholds from URL:', error);
}
setEvaluationWindow({
type: 'SET_INITIAL_STATE_FOR_METER',
});
setThresholdState({
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
});
}
}, [alertType, thresholdsFromURL]);
}, [alertType]);
useEffect(() => {
if (isEditMode && initialAlertState) {

View File

@@ -237,7 +237,6 @@ export type EvaluationWindowAction =
}
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
| { type: 'SET_INITIAL_STATE'; payload: EvaluationWindowState }
| { type: 'SET_INITIAL_STATE_FOR_METER' }
| { type: 'RESET' };
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';

View File

@@ -1,5 +1,3 @@
import { UTC_TIMEZONE } from 'components/CustomTimePicker/timezoneUtils';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { QueryParams } from 'constants/query';
import {
alertDefaults,
@@ -13,7 +11,6 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { CumulativeWindowTimeframes } from '../EvaluationSettings/types';
import {
INITIAL_ADVANCED_OPTIONS_STATE,
INITIAL_ALERT_STATE,
@@ -213,18 +210,6 @@ export const evaluationWindowReducer = (
return INITIAL_EVALUATION_WINDOW_STATE;
case 'SET_INITIAL_STATE':
return action.payload;
case 'SET_INITIAL_STATE_FOR_METER':
return {
...state,
windowType: 'cumulative',
timeframe: CumulativeWindowTimeframes.CURRENT_DAY,
startingAt: {
time: '00:00:00',
number: '0',
timezone: UTC_TIMEZONE.value,
unit: UniversalYAxisUnit.MINUTES,
},
};
default:
return state;
}

View File

@@ -108,13 +108,6 @@ function ChartPreview({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
@@ -303,13 +296,6 @@ function ChartPreview({
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
[
yAxisUnit,

View File

@@ -36,7 +36,6 @@ function QuerySection({
// init namespace for translations
const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory);
const [signalSource, setSignalSource] = useState<string>('metrics');
const handleQueryCategoryChange = (queryType: string): void => {
setQueryCategory(queryType as EQueryType);
@@ -49,17 +48,12 @@ function QuerySection({
const isDarkMode = useIsDarkMode();
const handleSignalSourceChange = (value: string): void => {
setSignalSource(value);
};
const renderMetricUI = (): JSX.Element => (
<QueryBuilderV2
panelType={panelType}
config={{
queryVariant: 'static',
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
signalSource: signalSource === 'meter' ? 'meter' : '',
}}
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
showFunctions={
@@ -68,8 +62,6 @@ function QuerySection({
alertType === AlertTypes.LOGS_BASED_ALERT
}
version={alertDef.version || 'v3'}
onSignalSourceChange={handleSignalSourceChange}
signalSourceChangeEnabled
/>
);

View File

@@ -54,34 +54,16 @@ function GraphManager({
const labelClickedHandler = useCallback(
(labelIndex: number): void => {
if (labelIndex < 0 || labelIndex >= graphsVisibilityStates.length) return;
const newGraphVisibilityStates = Array<boolean>(data.length).fill(false);
newGraphVisibilityStates[labelIndex] = true;
const newGraphVisibilityStates = [...graphsVisibilityStates];
const isCurrentlyVisible = newGraphVisibilityStates[labelIndex];
const visibleCount = newGraphVisibilityStates.filter(Boolean).length;
if (isCurrentlyVisible && visibleCount === 1) {
newGraphVisibilityStates.fill(true);
} else if (isCurrentlyVisible) {
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[labelIndex] = true;
} else {
newGraphVisibilityStates[labelIndex] = true;
}
// Update all graphs based on new state
newGraphVisibilityStates.forEach((state, index) => {
lineChartRef?.current?.toggleGraph(index, state);
parentChartRef?.current?.toggleGraph(index, state);
});
setGraphsVisibilityStates(newGraphVisibilityStates);
},
[
graphsVisibilityStates,
lineChartRef,
parentChartRef,
setGraphsVisibilityStates,
],
[data.length, lineChartRef, parentChartRef, setGraphsVisibilityStates],
);
const columns = getGraphManagerTableColumns({

View File

@@ -1,6 +1,5 @@
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ColumnType } from 'antd/es/table';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
import { DataSetProps, ExtendedChartDataset } from '../types';
@@ -8,20 +7,6 @@ import { getGraphManagerTableHeaderTitle } from '../utils';
import CustomCheckBox from './CustomCheckBox';
import { getLabel } from './GetLabel';
// Helper function to format numeric values based on yAxisUnit
const formatMetricValue = (
value: number | null | undefined,
yAxisUnit?: string,
): string => {
if (value == null || value === undefined || Number.isNaN(value)) {
return '';
}
if (yAxisUnit) {
return getYAxisFormattedValue(value.toString(), yAxisUnit);
}
return value.toString();
};
export const getGraphManagerTableColumns = ({
tableDataSet,
checkBoxOnChangeHandler,
@@ -60,7 +45,6 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Avg,
key: ColumnsKeyAndDataIndex.Avg,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -70,7 +54,6 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Sum,
key: ColumnsKeyAndDataIndex.Sum,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -80,7 +63,6 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Max,
key: ColumnsKeyAndDataIndex.Max,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -90,11 +72,10 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Min,
key: ColumnsKeyAndDataIndex.Min,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
];
export interface GetGraphManagerTableColumnsProps {
interface GetGraphManagerTableColumnsProps {
tableDataSet: ExtendedChartDataset[];
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
labelClickedHandler: (labelIndex: number) => void;

View File

@@ -324,7 +324,6 @@ function FullView({
panelType={selectedPanelType}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={selectedPanelType === PANEL_TYPES.LIST}
signalSourceChangeEnabled
// filterConfigs={filterConfigs}
// queryComponents={queryComponents}
/>

View File

@@ -1,338 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable react/jsx-props-no-spreading */
import { render, screen, userEvent } from 'tests/test-utils';
import GraphManager from '../GridCard/FullView/GraphManager';
import {
getGraphManagerTableColumns,
GetGraphManagerTableColumnsProps,
} from '../GridCard/FullView/TableRender/GraphManagerColumns';
import { GraphManagerProps } from '../GridCard/FullView/types';
// Props
const props = {
tableDataSet: [
{
label: 'Timestamp',
stroke: 'purple',
index: 0,
show: true,
sum: 52791867900,
avg: 1759728930,
max: 1759729800,
min: 1759728060,
},
{
drawStyle: 'line',
lineInterpolation: 'spline',
show: true,
label: '{service.name=""}',
stroke: '#B33300',
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: '#B33300',
},
index: 1,
sum: 2274.96,
avg: 75.83,
max: 115.76,
min: 55.64,
},
{
drawStyle: 'line',
lineInterpolation: 'spline',
show: true,
label: '{service.name="recommendationservice"}',
stroke: '#BB6BD9',
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: '#BB6BD9',
},
index: 2,
sum: 1770.84,
avg: 59.028,
max: 112.16,
min: 0,
},
{
drawStyle: 'line',
lineInterpolation: 'spline',
show: true,
label: '{service.name="loadgenerator"}',
stroke: '#E9967A',
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: '#E9967A',
},
index: 3,
sum: 1801.25,
avg: 60.041,
max: 94.46,
min: 39.86,
},
],
graphVisibilityState: [true, true, true, true],
yAxisUnit: 'ops',
isGraphDisabled: false,
} as GetGraphManagerTableColumnsProps;
describe('GraphManager', () => {
it('should render the columns', () => {
const columns = getGraphManagerTableColumns({
...props,
});
expect(columns).toStrictEqual([
{
dataIndex: 'index',
key: 'index',
render: expect.any(Function),
title: '',
width: 50,
},
{
dataIndex: 'label',
key: 'label',
render: expect.any(Function),
title: 'Label',
width: 300,
},
{
dataIndex: 'avg',
key: 'avg',
render: expect.any(Function),
title: 'Avg (in ops)',
width: 90,
},
{
dataIndex: 'sum',
key: 'sum',
render: expect.any(Function),
title: 'Sum (in ops)',
width: 90,
},
{
dataIndex: 'max',
key: 'max',
render: expect.any(Function),
title: 'Max (in ops)',
width: 90,
},
{
dataIndex: 'min',
key: 'min',
render: expect.any(Function),
title: 'Min (in ops)',
width: 90,
},
]);
});
it('should render graphmanager with correct formatting using y-axis', () => {
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500], // timestamps
[66.167, 76.833, 83.767], // series 1
[46.6, 52.7, 70.867], // series 2
[45.967, 52.967, 69.933], // series 3
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: jest.fn(),
graphsVisibilityStates: [true, true, true, true],
lineChartRef: { current: { toggleGraph: jest.fn() } },
parentChartRef: { current: { toggleGraph: jest.fn() } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name=""}' },
{ label: '{service.name="recommendationservice"}' },
{ label: '{service.name="loadgenerator"}' },
],
width: 100,
height: 100,
},
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<GraphManager {...testProps} />);
// Assert that column headers include y-axis unit formatting
expect(screen.getByText('Avg (in ops)')).toBeInTheDocument();
expect(screen.getByText('Sum (in ops)')).toBeInTheDocument();
expect(screen.getByText('Max (in ops)')).toBeInTheDocument();
expect(screen.getByText('Min (in ops)')).toBeInTheDocument();
// Assert formatting
expect(screen.getByText('75.6 ops/s')).toBeInTheDocument();
expect(screen.getByText('227 ops/s')).toBeInTheDocument();
expect(screen.getByText('83.8 ops/s')).toBeInTheDocument();
expect(screen.getByText('66.2 ops/s')).toBeInTheDocument();
});
it('should handle checkbox click correctly', async () => {
const mockToggleGraph = jest.fn();
const mockSetGraphsVisibilityStates = jest.fn();
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500],
[66.167, 76.833, 83.767],
[46.6, 52.7, 70.867],
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: mockSetGraphsVisibilityStates,
graphsVisibilityStates: [true, true, true],
lineChartRef: { current: { toggleGraph: mockToggleGraph } },
parentChartRef: { current: { toggleGraph: mockToggleGraph } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name=""}' },
{ label: '{service.name="recommendationservice"}' },
],
width: 100,
height: 100,
},
};
render(<GraphManager {...testProps} />);
// Find the first checkbox input (index 1, since index 0 is timestamp)
const checkbox = screen.getAllByRole('checkbox')[0];
expect(checkbox).toBeInTheDocument();
// Simulate checkbox click
await userEvent.click(checkbox);
// Verify toggleGraph was called on both chart refs
expect(mockToggleGraph).toHaveBeenCalledWith(1, false);
expect(mockToggleGraph).toHaveBeenCalledTimes(2); // lineChartRef and parentChartRef
// Verify state update function was called
expect(mockSetGraphsVisibilityStates).toHaveBeenCalledWith([
true,
false,
true,
]);
});
it('should handle label click correctly for visibility toggle', async () => {
const mockToggleGraph = jest.fn();
const mockSetGraphsVisibilityStates = jest.fn();
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500],
[66.167, 76.833, 83.767],
[46.6, 52.7, 70.867],
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: mockSetGraphsVisibilityStates,
graphsVisibilityStates: [true, true, true],
lineChartRef: { current: { toggleGraph: mockToggleGraph } },
parentChartRef: { current: { toggleGraph: mockToggleGraph } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name="loadgenerator"}' },
{ label: '{service.name="recommendationservice"}' },
],
width: 100,
height: 100,
},
};
render(<GraphManager {...testProps} />);
// Find the first label button (skip Cancel and Save buttons)
const buttons = screen.getAllByRole('button');
const label = buttons.find((button) =>
button.textContent?.includes('{service.name="loadgenerator"}'),
) as HTMLElement;
expect(label).toBeInTheDocument();
// Simulate label click
await userEvent.click(label);
// Verify setGraphsVisibilityStates was called with show-only behavior
expect(mockSetGraphsVisibilityStates).toHaveBeenCalledWith([
false,
true,
false,
]);
// Check if toggleGraph was called for each series
expect(mockToggleGraph).toHaveBeenCalledWith(0, false); // timestamp
expect(mockToggleGraph).toHaveBeenCalledWith(1, true); // selected series
expect(mockToggleGraph).toHaveBeenCalledWith(2, false); // other series
expect(mockToggleGraph).toHaveBeenCalledTimes(6); // 3 series × 2 chart refs
});
it('should handle label click to show all when only one is visible', async () => {
const mockToggleGraph = jest.fn();
const mockSetGraphsVisibilityStates = jest.fn();
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500],
[66.167, 76.833, 83.767],
[46.6, 52.7, 70.867],
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: mockSetGraphsVisibilityStates,
graphsVisibilityStates: [false, true, false], // Only one series visible
lineChartRef: { current: { toggleGraph: mockToggleGraph } },
parentChartRef: { current: { toggleGraph: mockToggleGraph } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name=""}' },
{ label: '{service.name="recommendationservice"}' },
],
width: 100,
height: 100,
},
};
render(<GraphManager {...testProps} />);
// Find the visible label button (skip Cancel and Save buttons)
const buttons = screen.getAllByRole('button');
const label = buttons.find((button) =>
button.textContent?.includes('{service.name=""}'),
) as HTMLElement;
expect(label).toBeInTheDocument();
// Simulate label click (should show all since only this one is visible)
await userEvent.click(label);
// Verify setGraphsVisibilityStates was called with show-all behavior
expect(mockSetGraphsVisibilityStates).toHaveBeenCalledWith([
true,
true,
true,
]);
// Check if toggleGraph was called to show all series
expect(mockToggleGraph).toHaveBeenCalledWith(0, true); // timestamp
expect(mockToggleGraph).toHaveBeenCalledWith(1, true); // current series
expect(mockToggleGraph).toHaveBeenCalledWith(2, true); // other series
expect(mockToggleGraph).toHaveBeenCalledTimes(6); // 3 series × 2 chart refs
});
});

View File

@@ -48,7 +48,6 @@ function GridTableComponent({
widgetId,
panelType,
queryRangeRequest,
decimalPrecision,
...props
}: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
@@ -88,19 +87,10 @@ function GridTableComponent({
const newValue = { ...val };
Object.keys(val).forEach((k) => {
const unit = getColumnUnit(k, columnUnits);
// Apply formatting if:
// 1. Column has a unit defined, OR
// 2. decimalPrecision is specified (format all values)
const shouldFormat = unit || decimalPrecision !== undefined;
if (shouldFormat) {
if (unit) {
// the check below takes care of not adding units for rows that have n/a or null values
if (val[k] !== 'n/a' && val[k] !== null) {
newValue[k] = getYAxisFormattedValue(
String(val[k]),
unit || 'none',
decimalPrecision,
);
newValue[k] = getYAxisFormattedValue(String(val[k]), unit);
} else if (val[k] === null) {
newValue[k] = 'n/a';
}
@@ -113,7 +103,7 @@ function GridTableComponent({
return mutateDataSource;
},
[columnUnits, decimalPrecision],
[columnUnits],
);
const dataSource = useMemo(() => applyColumnUnits(originalDataSource), [

View File

@@ -1,5 +1,4 @@
import { TableProps } from 'antd';
import { PrecisionOption } from 'components/Graph/yAxisConfig';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
import {
@@ -16,7 +15,6 @@ export type GridTableComponentProps = {
query: Query;
thresholds?: ThresholdProps[];
columnUnits?: ColumnUnit;
decimalPrecision?: PrecisionOption;
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
sticky?: TableProps<RowData>['sticky'];
searchTerm?: string;

View File

@@ -99,11 +99,7 @@ function GridValueComponent({
rawValue={value}
value={
yAxisUnit
? getYAxisFormattedValue(
String(value),
yAxisUnit,
widget?.decimalPrecision,
)
? getYAxisFormattedValue(String(value), yAxisUnit)
: value.toString()
}
/>

View File

@@ -115,13 +115,6 @@ function EntityMetrics<T>({
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const { currentQuery } = useQueryBuilder();
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const chartData = useMemo(
() =>
@@ -191,13 +184,6 @@ function EntityMetrics<T>({
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
});
}),
[

View File

@@ -418,11 +418,6 @@
font-size: 12px;
font-weight: 600;
}
.set-alert-btn {
cursor: pointer;
margin-left: 24px;
}
}
}

View File

@@ -19,7 +19,6 @@ import {
TablePaginationConfig,
TableProps as AntDTableProps,
Tag,
Tooltip,
Typography,
} from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
@@ -35,20 +34,15 @@ import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import Tags from 'components/Tags/Tags';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import { initialQueryMeterWithType } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { INITIAL_ALERT_THRESHOLD_STATE } from 'container/CreateAlertV2/context/constants';
import dayjs from 'dayjs';
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { cloneDeep, isNil, isUndefined } from 'lodash-es';
import { isNil, isUndefined } from 'lodash-es';
import {
ArrowUpRight,
BellPlus,
CalendarClock,
Check,
Copy,
@@ -66,7 +60,6 @@ import { useTimezone } from 'providers/Timezone';
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { ErrorResponse } from 'types/api';
import {
@@ -78,7 +71,6 @@ import {
IngestionKeyProps,
PaginationProps,
} from 'types/api/ingestionKeys/types';
import { MeterAggregateOperator } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { getDaysUntilExpiry } from 'utils/timeUtils';
@@ -178,8 +170,6 @@ function MultiIngestionSettings(): JSX.Element {
const { isEnterpriseSelfHostedUser } = useGetTenantLicense();
const history = useHistory();
const [
hasCreateLimitForIngestionKeyError,
setHasCreateLimitForIngestionKeyError,
@@ -704,68 +694,6 @@ function MultiIngestionSettings(): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const handleCreateAlert = (
APIKey: IngestionKeyProps,
signal: LimitProps,
): void => {
let metricName = '';
switch (signal.signal) {
case 'metrics':
metricName = 'signoz.meter.metric.datapoint.count';
break;
case 'traces':
metricName = 'signoz.meter.span.size';
break;
case 'logs':
metricName = 'signoz.meter.log.size';
break;
default:
return;
}
const threshold =
signal.signal === 'metrics'
? signal.config?.day?.count || 0
: signal.config?.day?.size || 0;
const query = {
...initialQueryMeterWithType,
builder: {
...initialQueryMeterWithType.builder,
queryData: [
{
...initialQueryMeterWithType.builder.queryData[0],
aggregations: [
{
...initialQueryMeterWithType.builder.queryData[0].aggregations?.[0],
metricName,
timeAggregation: MeterAggregateOperator.INCREASE,
spaceAggregation: MeterAggregateOperator.SUM,
},
],
filter: {
expression: `signoz.workspace.key.id='${APIKey.id}'`,
},
},
],
},
};
const stringifiedQuery = JSON.stringify(query);
const thresholds = cloneDeep(INITIAL_ALERT_THRESHOLD_STATE.thresholds);
thresholds[0].thresholdValue = threshold;
const URL = `${ROUTES.ALERTS_NEW}?showNewCreateAlertsPage=true&${
QueryParams.compositeQuery
}=${encodeURIComponent(stringifiedQuery)}&${
QueryParams.thresholds
}=${encodeURIComponent(JSON.stringify(thresholds))}`;
history.push(URL);
};
const columns: AntDTableProps<IngestionKeyProps>['columns'] = [
{
title: 'Ingestion Key',
@@ -1255,27 +1183,6 @@ function MultiIngestionSettings(): JSX.Element {
</>
))}
</div>
{((signalCfg.usesSize &&
limit?.config?.day?.size !== undefined) ||
(signalCfg.usesCount &&
limit?.config?.day?.count !== undefined)) && (
<Tooltip
title="Set alert on this limit"
placement="top"
arrow={false}
>
<Button
icon={<BellPlus size={14} color={Color.BG_CHERRY_400} />}
className="set-alert-btn periscope-btn ghost"
type="text"
data-testid={`set-alert-btn-${signalName}`}
onClick={(): void =>
handleCreateAlert(APIKey, limitsDict[signalName])
}
/>
</Tooltip>
)}
</div>
{/* SECOND limit usage/limit */}

View File

@@ -1,60 +1,10 @@
import { QueryParams } from 'constants/query';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { LimitProps } from 'types/api/ingestionKeys/limits/types';
import {
AllIngestionKeyProps,
IngestionKeyProps,
} from 'types/api/ingestionKeys/types';
import { render, screen } from 'tests/test-utils';
import MultiIngestionSettings from '../MultiIngestionSettings';
// Extend the existing types to include limits with proper structure
interface TestIngestionKeyProps extends Omit<IngestionKeyProps, 'limits'> {
limits?: LimitProps[];
}
interface TestAllIngestionKeyProps extends Omit<AllIngestionKeyProps, 'data'> {
data: TestIngestionKeyProps[];
}
// Mock useHistory.push to capture navigation URL used by MultiIngestionSettings
const mockPush = jest.fn() as jest.MockedFunction<(path: string) => void>;
jest.mock('react-router-dom', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useHistory: (): { push: typeof mockPush } => ({ push: mockPush }),
};
});
// Mock deployments data hook to avoid unrelated network calls in this page
jest.mock(
'hooks/CustomDomain/useGetDeploymentsData',
(): Record<string, unknown> => ({
useGetDeploymentsData: (): {
data: undefined;
isLoading: boolean;
isFetching: boolean;
isError: boolean;
} => ({
data: undefined,
isLoading: false,
isFetching: false,
isError: false,
}),
}),
);
const TEST_CREATED_UPDATED = '2024-01-01T00:00:00Z';
const TEST_EXPIRES_AT = '2030-01-01T00:00:00Z';
const TEST_WORKSPACE_ID = 'w1';
const INGESTION_SETTINGS_ROUTE = '/ingestion-settings';
describe('MultiIngestionSettings Page', () => {
beforeEach(() => {
mockPush.mockClear();
render(<MultiIngestionSettings />);
});
afterEach(() => {
@@ -62,10 +12,6 @@ describe('MultiIngestionSettings Page', () => {
});
it('renders MultiIngestionSettings page without crashing', () => {
render(<MultiIngestionSettings />, undefined, {
initialRoute: INGESTION_SETTINGS_ROUTE,
});
expect(screen.getByText('Ingestion Keys')).toBeInTheDocument();
expect(
@@ -81,181 +27,4 @@ describe('MultiIngestionSettings Page', () => {
expect(aboutKeyslink).toHaveClass('learn-more');
expect(aboutKeyslink).toHaveAttribute('rel', 'noreferrer');
});
it('navigates to create alert with metrics count threshold', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// Arrange API response with a metrics daily count limit so the alert button is visible
const response: TestAllIngestionKeyProps = {
status: 'success',
data: [
{
name: 'Key One',
expires_at: TEST_EXPIRES_AT,
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
tags: [],
limits: [
{
id: 'l1',
signal: 'metrics',
config: { day: { count: 1000 } },
},
],
},
],
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
};
server.use(
rest.get('*/workspaces/me/keys*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(response)),
),
);
// Render with initial route to test navigation
render(<MultiIngestionSettings />, undefined, {
initialRoute: INGESTION_SETTINGS_ROUTE,
});
// Wait for ingestion key to load and expand the row to show limits
await screen.findByText('Key One');
const expandButton = screen.getByRole('button', { name: /right Key One/i });
await user.click(expandButton);
// Wait for limits section to render and click metrics alert button by test id
await screen.findByText('LIMITS');
const metricsAlertBtn = (await screen.findByTestId(
'set-alert-btn-metrics',
)) as HTMLButtonElement;
await user.click(metricsAlertBtn);
// Wait for navigation to occur
await waitFor(() => {
expect(mockPush).toHaveBeenCalledTimes(1);
});
// Assert: navigation occurred with correct query parameters
const navigationCall = mockPush.mock.calls[0][0] as string;
// Check URL contains alerts/new route
expect(navigationCall).toContain('/alerts/new');
expect(navigationCall).toContain('showNewCreateAlertsPage=true');
// Parse query parameters
const urlParams = new URLSearchParams(navigationCall.split('?')[1]);
const thresholds = JSON.parse(urlParams.get(QueryParams.thresholds) || '{}');
expect(thresholds).toBeDefined();
expect(thresholds[0].thresholdValue).toBe(1000);
// Verify compositeQuery parameter exists and contains correct data
const compositeQuery = JSON.parse(
urlParams.get(QueryParams.compositeQuery) || '{}',
);
expect(compositeQuery.builder).toBeDefined();
expect(compositeQuery.builder.queryData).toBeDefined();
// Check that the query contains the correct filter expression for the key
const firstQueryData = compositeQuery.builder.queryData[0];
expect(firstQueryData.filter.expression).toContain(
"signoz.workspace.key.id='k1'",
);
// Verify metric name for metrics signal
expect(firstQueryData.aggregations[0].metricName).toBe(
'signoz.meter.metric.datapoint.count',
);
});
it('navigates to create alert for logs with size threshold', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// Arrange API response with a logs daily size limit so the alert button is visible
const response: TestAllIngestionKeyProps = {
status: 'success',
data: [
{
name: 'Key Two',
expires_at: TEST_EXPIRES_AT,
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k2',
created_at: TEST_CREATED_UPDATED,
updated_at: TEST_CREATED_UPDATED,
tags: [],
limits: [
{
id: 'l2',
signal: 'logs',
config: { day: { size: 2048 } },
},
],
},
],
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
};
server.use(
rest.get('*/workspaces/me/keys*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(response)),
),
);
render(<MultiIngestionSettings />, undefined, {
initialRoute: INGESTION_SETTINGS_ROUTE,
});
// Wait for ingestion key to load and expand the row to show limits
await screen.findByText('Key Two');
const expandButton = screen.getByRole('button', { name: /right Key Two/i });
await user.click(expandButton);
// Wait for limits section to render and click logs alert button by test id
await screen.findByText('LIMITS');
const logsAlertBtn = (await screen.findByTestId(
'set-alert-btn-logs',
)) as HTMLButtonElement;
await user.click(logsAlertBtn);
// Wait for navigation to occur
await waitFor(() => {
expect(mockPush).toHaveBeenCalledTimes(1);
});
// Assert: navigation occurred with correct query parameters
const navigationCall = mockPush.mock.calls[0][0] as string;
// Check URL contains alerts/new route
expect(navigationCall).toContain('/alerts/new');
expect(navigationCall).toContain('showNewCreateAlertsPage=true');
// Parse query parameters
const urlParams = new URLSearchParams(navigationCall.split('?')[1]);
// Verify thresholds parameter
const thresholds = JSON.parse(urlParams.get(QueryParams.thresholds) || '{}');
expect(thresholds).toBeDefined();
expect(thresholds[0].thresholdValue).toBe(2048);
// Verify compositeQuery parameter exists and contains correct data
const compositeQuery = JSON.parse(
urlParams.get(QueryParams.compositeQuery) || '{}',
);
expect(compositeQuery.builder).toBeDefined();
expect(compositeQuery.builder.queryData).toBeDefined();
// Check that the query contains the correct filter expression for the key
const firstQueryData = compositeQuery.builder.queryData[0];
expect(firstQueryData.filter.expression).toContain(
"signoz.workspace.key.id='k2'",
);
// Verify metric name for logs signal
expect(firstQueryData.aggregations[0].metricName).toBe(
'signoz.meter.log.size',
);
});
});

View File

@@ -1,11 +1,9 @@
import './InfraMetrics.styles.scss';
import { Empty } from 'antd';
import { Empty, Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import SignozRadioGroup from 'components/SignozRadioGroup/SignozRadioGroup';
import { History, Table } from 'lucide-react';
import { useMemo, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { useState } from 'react';
import { VIEW_TYPES } from './constants';
import NodeMetrics from './NodeMetrics';
@@ -16,8 +14,7 @@ interface MetricsDataProps {
nodeName: string;
hostName: string;
clusterName: string;
timestamp: string;
dataSource: DataSource.LOGS | DataSource.TRACES;
logLineTimestamp: string;
}
function InfraMetrics({
@@ -25,56 +22,22 @@ function InfraMetrics({
nodeName,
hostName,
clusterName,
timestamp,
dataSource = DataSource.LOGS,
logLineTimestamp,
}: MetricsDataProps): JSX.Element {
const [selectedView, setSelectedView] = useState<string>(() =>
podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE,
);
const viewOptions = useMemo(() => {
const options = [
{
label: (
<div className="view-title">
<Table size={14} />
Node
</div>
),
value: VIEW_TYPES.NODE,
},
];
if (podName) {
options.push({
label: (
<div className="view-title">
<History size={14} />
Pod
</div>
),
value: VIEW_TYPES.POD,
});
}
return options;
}, [podName]);
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
if (!podName && !nodeName && !hostName) {
const emptyStateDescription =
dataSource === DataSource.TRACES
? 'No data available. Please select a span containing a pod, node, or host attributes to view metrics.'
: 'No data available. Please select a valid log line containing a pod, node, or host attributes to view metrics.';
return (
<div className="empty-container">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={emptyStateDescription}
description="No data available. Please select a valid log line containing a pod, node, or host attributes to view metrics."
/>
</div>
);
@@ -82,26 +45,46 @@ function InfraMetrics({
return (
<div className="infra-metrics-container">
<SignozRadioGroup
value={selectedView}
onChange={handleModeChange}
<Radio.Group
className="views-tabs"
options={viewOptions}
/>
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={selectedView === VIEW_TYPES.NODE ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.NODE}
>
<div className="view-title">
<Table size={14} />
Node
</div>
</Radio.Button>
{podName && (
<Radio.Button
className={selectedView === VIEW_TYPES.POD ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.POD}
>
<div className="view-title">
<History size={14} />
Pod
</div>
</Radio.Button>
)}
</Radio.Group>
{/* TODO(Rahul): Make a common config driven component for this and other infra metrics components */}
{selectedView === VIEW_TYPES.NODE && (
<NodeMetrics
nodeName={nodeName}
clusterName={clusterName}
hostName={hostName}
timestamp={timestamp}
logLineTimestamp={logLineTimestamp}
/>
)}
{selectedView === VIEW_TYPES.POD && podName && (
<PodMetrics
podName={podName}
clusterName={clusterName}
timestamp={timestamp}
logLineTimestamp={logLineTimestamp}
/>
)}
</div>

View File

@@ -29,15 +29,15 @@ function NodeMetrics({
nodeName,
clusterName,
hostName,
timestamp,
logLineTimestamp,
}: {
nodeName: string;
clusterName: string;
hostName: string;
timestamp: string;
logLineTimestamp: string;
}): JSX.Element {
const { start, end, verticalLineTimestamp } = useMemo(() => {
const logTimestamp = dayjs(timestamp);
const logTimestamp = dayjs(logLineTimestamp);
const now = dayjs();
const startTime = logTimestamp.subtract(3, 'hour');
@@ -50,7 +50,7 @@ function NodeMetrics({
end: endTime.unix(),
verticalLineTimestamp: logTimestamp.unix(),
};
}, [timestamp]);
}, [logLineTimestamp]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -83,13 +83,6 @@ function NodeMetrics({
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
@@ -116,13 +109,6 @@ function NodeMetrics({
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
),
[

View File

@@ -23,14 +23,14 @@ import { getPodQueryPayload, podWidgetInfo } from './constants';
function PodMetrics({
podName,
clusterName,
timestamp,
logLineTimestamp,
}: {
podName: string;
clusterName: string;
timestamp: string;
logLineTimestamp: string;
}): JSX.Element {
const { start, end, verticalLineTimestamp } = useMemo(() => {
const logTimestamp = dayjs(timestamp);
const logTimestamp = dayjs(logLineTimestamp);
const now = dayjs();
const startTime = logTimestamp.subtract(3, 'hour');
@@ -43,15 +43,7 @@ function PodMetrics({
end: endTime.unix(),
verticalLineTimestamp: logTimestamp.unix(),
};
}, [timestamp]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
}, [logLineTimestamp]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -99,13 +91,6 @@ function PodMetrics({
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
),
[

View File

@@ -61,6 +61,8 @@ export const logsQueryRangeSuccessNewFormatResponse = {
};
export const mockQueryBuilderContextValue = {
currentFilterExpression: {},
handleSetCurrentFilterExpression: noop,
isDefaultQuery: (): boolean => false,
currentQuery: {
...initialQueriesMap.logs,

View File

@@ -33,7 +33,6 @@ function Explorer(): JSX.Element {
handleRunQuery,
stagedQuery,
updateAllQueriesOperators,
handleSetQueryData,
currentQuery,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
@@ -51,15 +50,6 @@ function Explorer(): JSX.Element {
[updateAllQueriesOperators],
);
useEffect(() => {
handleSetQueryData(0, {
...initialQueryMeterWithType.builder.queryData[0],
source: 'meter',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(

View File

@@ -290,6 +290,13 @@ function Summary(): JSX.Element {
],
);
console.log({
isMetricsListDataEmpty,
isMetricsTreeMapDataEmpty,
treeMapData,
sec: treeMapData?.payload?.data[heatmapView],
});
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">

View File

@@ -168,7 +168,6 @@ function QuerySection({
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
queryComponents={queryComponents}
signalSourceChangeEnabled
/>
</div>
),

View File

@@ -158,8 +158,7 @@
}
}
.log-scale,
.decimal-precision-selector {
.log-scale {
margin-top: 16px;
display: flex;
justify-content: space-between;

View File

@@ -192,17 +192,3 @@ export const panelTypeVsContextLinks: {
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsDecimalPrecision: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: true,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@@ -12,10 +12,6 @@ import {
Switch,
Typography,
} from 'antd';
import {
PrecisionOption,
PrecisionOptionsEnum,
} from 'components/Graph/yAxisConfig';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import GraphTypes, {
@@ -52,7 +48,6 @@ import {
panelTypeVsColumnUnitPreferences,
panelTypeVsContextLinks,
panelTypeVsCreateAlert,
panelTypeVsDecimalPrecision,
panelTypeVsFillSpan,
panelTypeVsLegendColors,
panelTypeVsLegendPosition,
@@ -100,8 +95,6 @@ function RightContainer({
selectedTime,
yAxisUnit,
setYAxisUnit,
decimalPrecision,
setDecimalPrecision,
setGraphHandler,
thresholds,
combineHistogram,
@@ -167,7 +160,6 @@ function RightContainer({
panelTypeVsColumnUnitPreferences[selectedGraph];
const allowContextLinks =
panelTypeVsContextLinks[selectedGraph] && enableDrillDown;
const allowDecimalPrecision = panelTypeVsDecimalPrecision[selectedGraph];
const { currentQuery } = useQueryBuilder();
@@ -364,30 +356,6 @@ function RightContainer({
}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
@@ -585,8 +553,6 @@ interface RightContainerProps {
setBucketWidth: Dispatch<SetStateAction<number>>;
setBucketCount: Dispatch<SetStateAction<number>>;
setYAxisUnit: Dispatch<SetStateAction<string>>;
decimalPrecision: PrecisionOption;
setDecimalPrecision: Dispatch<SetStateAction<PrecisionOption>>;
setGraphHandler: (type: PANEL_TYPES) => void;
thresholds: ThresholdProps[];
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;

View File

@@ -4,10 +4,6 @@ import './NewWidget.styles.scss';
import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import {
PrecisionOption,
PrecisionOptionsEnum,
} from 'components/Graph/yAxisConfig';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { adjustQueryForV5 } from 'components/QueryBuilderV2/utils';
import { QueryParams } from 'constants/query';
@@ -182,10 +178,6 @@ function NewWidget({
selectedWidget?.yAxisUnit || 'none',
);
const [decimalPrecision, setDecimalPrecision] = useState<PrecisionOption>(
selectedWidget?.decimalPrecision ?? PrecisionOptionsEnum.TWO,
);
const [stackedBarChart, setStackedBarChart] = useState<boolean>(
selectedWidget?.stackedBarChart || false,
);
@@ -265,7 +257,6 @@ function NewWidget({
opacity,
nullZeroValues: selectedNullZeroValue,
yAxisUnit,
decimalPrecision,
thresholds,
softMin,
softMax,
@@ -299,7 +290,6 @@ function NewWidget({
thresholds,
title,
yAxisUnit,
decimalPrecision,
bucketWidth,
bucketCount,
combineHistogram,
@@ -503,8 +493,6 @@ function NewWidget({
title: selectedWidget?.title,
stackedBarChart: selectedWidget?.stackedBarChart || false,
yAxisUnit: selectedWidget?.yAxisUnit,
decimalPrecision:
selectedWidget?.decimalPrecision || PrecisionOptionsEnum.TWO,
panelTypes: graphType,
query: adjustedQueryForV5,
thresholds: selectedWidget?.thresholds,
@@ -534,8 +522,6 @@ function NewWidget({
title: selectedWidget?.title,
stackedBarChart: selectedWidget?.stackedBarChart || false,
yAxisUnit: selectedWidget?.yAxisUnit,
decimalPrecision:
selectedWidget?.decimalPrecision || PrecisionOptionsEnum.TWO,
panelTypes: graphType,
query: adjustedQueryForV5,
thresholds: selectedWidget?.thresholds,
@@ -850,8 +836,6 @@ function NewWidget({
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}

View File

@@ -1,6 +1,5 @@
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/yAxisConfig';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
@@ -555,7 +554,6 @@ export const getDefaultWidgetData = (
softMax: null,
softMin: null,
stackedBarChart: name === PANEL_TYPES.BAR,
decimalPrecision: PrecisionOptionsEnum.TWO, // default decimal precision
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
...field,
type: field.fieldContext ?? '',

View File

@@ -347,7 +347,6 @@ function OnboardingAddDataSource(): JSX.Element {
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SEARCHED}`,
{
searchedDataSource: query,
resultCount: filteredDataSources.length,
},
);
}, 300);

View File

@@ -26,7 +26,6 @@ function HistogramPanelWrapper({
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const legendScrollPositionRef = useRef<number>(0);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
@@ -130,10 +129,6 @@ function HistogramPanelWrapper({
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: number) => {
legendScrollPositionRef.current = position;
},
}),
[
containerDimensions,

View File

@@ -104,7 +104,6 @@ function PiePanelWrapper({
const formattedTotal = getYAxisFormattedValue(
totalValue.toString(),
widget?.yAxisUnit || 'none',
widget?.decimalPrecision,
);
// Extract numeric part and unit separately for styling
@@ -220,7 +219,6 @@ function PiePanelWrapper({
const displayValue = getYAxisFormattedValue(
arc.data.value,
widget?.yAxisUnit || 'none',
widget?.decimalPrecision,
);
// Determine text anchor based on position in the circle

View File

@@ -40,7 +40,6 @@ function TablePanelWrapper({
enableDrillDown={enableDrillDown}
panelType={widget.panelTypes}
queryRangeRequest={queryRangeRequest}
decimalPrecision={widget.decimalPrecision}
// eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG}
/>

View File

@@ -249,7 +249,6 @@ function UplotPanelWrapper({
}) => {
legendScrollPositionRef.current = position;
},
decimalPrecision: widget.decimalPrecision,
}),
[
queryResponse.data?.payload,

View File

@@ -27,7 +27,7 @@ describe('Value panel wrappper tests', () => {
);
// selected y axis unit as miliseconds (ms)
expect(getByText('295.43')).toBeInTheDocument();
expect(getByText('295')).toBeInTheDocument();
expect(getByText('ms')).toBeInTheDocument();
});

View File

@@ -330,7 +330,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
431.25 ms
431 ms
</div>
</div>
</div>
@@ -368,7 +368,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
431.25 ms
431 ms
</div>
</div>
</div>
@@ -406,7 +406,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
287.11 ms
287 ms
</div>
</div>
</div>
@@ -444,7 +444,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
230.02 ms
230 ms
</div>
</div>
</div>
@@ -482,7 +482,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
66.37 ms
66.4 ms
</div>
</div>
</div>

View File

@@ -51,7 +51,7 @@ exports[`Value panel wrappper tests should render tooltip when there are conflic
class="ant-typography value-graph-text css-dev-only-do-not-override-2i2tap"
style="color: Blue; font-size: 16px;"
>
295.43
295
</span>
<span
class="ant-typography value-graph-unit css-dev-only-do-not-override-2i2tap"

View File

@@ -1,118 +0,0 @@
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
import uPlot from 'uplot';
// Mock dependencies
jest.mock('lib/uPlotLib/plugins/tooltipPlugin', () => jest.fn(() => ({})));
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => jest.fn(() => ({})));
const mockApiResponse = {
data: {
result: [
{
metric: { __name__: 'test_metric' },
queryName: 'test_query',
values: [[1640995200, '10'] as [number, string]],
},
],
resultType: 'time_series',
newResult: {
data: {
result: [],
resultType: 'time_series',
},
},
},
};
const mockDimensions = { width: 800, height: 400 };
const mockHistogramData: uPlot.AlignedData = [[1640995200], [10]];
const TEST_HISTOGRAM_ID = 'test-histogram';
describe('Histogram Chart Options Legend Scroll Position', () => {
let originalRequestAnimationFrame: typeof global.requestAnimationFrame;
beforeEach(() => {
jest.clearAllMocks();
originalRequestAnimationFrame = global.requestAnimationFrame;
});
afterEach(() => {
global.requestAnimationFrame = originalRequestAnimationFrame;
});
it('should set up scroll position tracking in histogram chart ready hook', () => {
const mockSetScrollPosition = jest.fn();
const options = getUplotHistogramChartOptions({
id: TEST_HISTOGRAM_ID,
dimensions: mockDimensions,
isDarkMode: false,
apiResponse: mockApiResponse,
histogramData: mockHistogramData,
legendScrollPosition: 0,
setLegendScrollPosition: mockSetScrollPosition,
});
// Create mock chart with legend element
const mockChart = ({
root: document.createElement('div'),
} as unknown) as uPlot;
const legend = document.createElement('div');
legend.className = 'u-legend';
mockChart.root.appendChild(legend);
const addEventListenerSpy = jest.spyOn(legend, 'addEventListener');
// Execute ready hook
if (options.hooks?.ready) {
options.hooks.ready.forEach((hook) => hook?.(mockChart));
}
// Verify that scroll event listener was added and cleanup function was stored
expect(addEventListenerSpy).toHaveBeenCalledWith(
'scroll',
expect.any(Function),
);
expect(
(mockChart as uPlot & { _legendScrollCleanup?: () => void })
._legendScrollCleanup,
).toBeDefined();
});
it('should restore histogram chart scroll position when provided', () => {
const mockScrollPosition = 50;
const mockSetScrollPosition = jest.fn();
const options = getUplotHistogramChartOptions({
id: TEST_HISTOGRAM_ID,
dimensions: mockDimensions,
isDarkMode: false,
apiResponse: mockApiResponse,
histogramData: mockHistogramData,
legendScrollPosition: mockScrollPosition,
setLegendScrollPosition: mockSetScrollPosition,
});
// Create mock chart with legend element
const mockChart = ({
root: document.createElement('div'),
} as unknown) as uPlot;
const legend = document.createElement('div');
legend.className = 'u-legend';
legend.scrollTop = 0;
mockChart.root.appendChild(legend);
// Mock requestAnimationFrame
const mockRequestAnimationFrame = jest.fn((callback) => callback());
global.requestAnimationFrame = mockRequestAnimationFrame;
// Execute ready hook
if (options.hooks?.ready) {
options.hooks.ready.forEach((hook) => hook?.(mockChart));
}
// Verify that requestAnimationFrame was called and scroll position was restored
expect(mockRequestAnimationFrame).toHaveBeenCalledWith(expect.any(Function));
expect(legend.scrollTop).toBe(mockScrollPosition);
});
});

View File

@@ -35,8 +35,6 @@ export type QueryBuilderProps = {
showTraceOperator?: boolean;
version: string;
onChangeTraceView?: (view: TraceView) => void;
onSignalSourceChange?: (value: string) => void;
signalSourceChangeEnabled?: boolean;
};
export enum TraceView {

View File

@@ -52,10 +52,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
(query.aggregations?.[0] as MetricAggregation)?.metricName || '',
);
useEffect(() => {
setSearchText('');
}, [signalSource]);
const debouncedSearchText = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, value] = getAutocompleteValueAndType(searchText);
@@ -71,7 +67,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
queryAggregation.timeAggregation,
query.dataSource,
index,
signalSource,
],
async () =>
getAggregateAttribute({
@@ -105,7 +100,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
setOptionsData(options);
setAttributeKeys?.(data?.payload?.attributeKeys || []);
},
keepPreviousData: false,
},
);
@@ -170,11 +164,8 @@ export const AggregatorFilter = memo(function AggregatorFilter({
queryAggregation.timeAggregation,
query.dataSource,
index,
signalSource,
])?.payload?.attributeKeys || [];
setAttributeKeys?.(attributeKeys);
return attributeKeys;
}, [
debouncedValue,
@@ -182,7 +173,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
query.dataSource,
queryClient,
index,
signalSource,
setAttributeKeys,
]);

View File

@@ -271,7 +271,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
icon: <ChartArea size={16} />,
isNew: false,
isEnabled: true,
isBeta: false,
isBeta: true,
itemKey: 'meter-explorer',
},
{

View File

@@ -55,337 +55,6 @@
flex-direction: column;
gap: 8px;
.span-name-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.loading-spinner-container {
padding: 4px 8px;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
display: inline-flex;
}
.span-percentile-value {
color: var(--text-sakura-400, #f56c87);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
min-width: 48px;
padding: 4px 8px;
border-left: 1px solid var(--bg-slate-400);
cursor: pointer;
}
}
.span-percentiles-container {
display: flex;
flex-direction: column;
position: relative;
fill: linear-gradient(
139deg,
rgba(18, 19, 23, 0.32) 0%,
rgba(18, 19, 23, 0.36) 98.68%
);
stroke-width: 1px;
stroke: var(--bg-slate-500, #161922);
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
backdrop-filter: blur(20px);
border: 1px solid var(--bg-slate-500);
border-radius: 4px;
.span-percentiles-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px 8px 12px;
border-bottom: 1px solid var(--bg-slate-500);
.span-percentiles-header-text {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
}
.span-percentile-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
.span-percentile-content-title {
.span-percentile-value {
color: var(--text-sakura-400, #f56c87);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
}
.span-percentile-value-loader {
display: inline-flex;
align-items: flex-end;
justify-content: flex-end;
margin-right: 4px;
margin-left: 4px;
line-height: 18px;
}
}
.span-percentile-timerange {
width: 100%;
.span-percentile-timerange-select {
width: 100%;
margin-top: 8px;
margin-bottom: 16px;
.ant-select-selector {
border-radius: 50px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-slate-500, #161922);
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.28px;
height: 32px;
}
}
}
.span-percentile-values-table {
.span-percentile-values-table-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
.span-percentile-values-table-header {
color: var(--text-vanilla-400);
text-align: right;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 181.818% */
text-transform: uppercase;
}
}
.span-percentile-values-table-data-rows {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
.span-percentile-values-table-data-rows-skeleton {
display: flex;
flex-direction: column;
gap: 4px;
.ant-skeleton-title {
width: 100% !important;
margin-top: 0px !important;
}
.ant-skeleton-paragraph {
margin-top: 8px;
& > li + li {
margin-top: 10px;
width: 100% !important;
}
}
}
}
.span-percentile-values-table-data-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0px 4px;
.span-percentile-values-table-data-row-key {
flex: 0 0 auto;
color: var(--text-vanilla-100);
text-align: right;
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
}
.span-percentile-values-table-data-row-value {
color: var(--text-vanilla-400);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'ss02' on;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
.dashed-line {
flex: 1;
height: 0; /* line only */
margin: 0 8px;
border-top: 1px dashed var(--bg-slate-300);
/* Use border image to control dash length & spacing */
border-top-width: 1px;
border-top-style: solid; /* temporary solid for image */
border-image: repeating-linear-gradient(
to right,
#1d212d 0,
#1d212d 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
}
.current-span-percentile-row {
border-radius: 2px;
background: rgba(78, 116, 248, 0.2);
.span-percentile-values-table-data-row-key {
color: var(--text-robin-300);
}
.dashed-line {
flex: 1;
height: 0; /* line only */
margin: 0 8px;
border-top: 1px dashed #abbdff;
/* Use border image to control dash length & spacing */
border-top-width: 1px;
border-top-style: solid; /* temporary solid for image */
border-image: repeating-linear-gradient(
to right,
#abbdff 0,
#abbdff 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
.span-percentile-values-table-data-row-value {
color: var(--text-robin-400);
}
}
}
}
.resource-attributes-select-container {
overflow: hidden;
width: calc(100% + 16px);
position: absolute;
top: 32px;
left: -8px;
z-index: 1000;
.resource-attributes-select-container-header {
.resource-attributes-select-container-input {
border-radius: 0px;
border: none !important;
box-shadow: none !important;
height: 36px;
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
border-radius: 4px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 1) 0%,
rgba(18, 19, 23, 1) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select {
width: 100%;
}
.resource-attributes-items {
height: 200px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
.resource-attributes-select-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px 8px 12px;
.resource-attributes-select-item-checkbox {
.ant-checkbox-disabled {
background-color: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
.resource-attributes-select-item-value {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
}
.attribute-key {
color: var(--bg-vanilla-400);
font-family: Inter;
@@ -399,6 +68,7 @@
.value-wrapper {
display: flex;
padding: 2px 8px;
align-items: center;
width: fit-content;
max-width: 100%;
@@ -407,7 +77,6 @@
background: var(--bg-slate-500);
.attribute-value {
padding: 2px 8px;
color: var(--bg-vanilla-400);
font-family: 'Inter';
font-size: 14px;
@@ -531,44 +200,6 @@
}
}
.span-percentile-tooltip {
.ant-tooltip-content {
width: 300px;
max-width: 300px;
}
.span-percentile-tooltip-text {
color: var(--text-vanilla-400);
font-variant-numeric: lining-nums tabular-nums stacked-fractions ordinal
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
letter-spacing: -0.06px;
.span-percentile-tooltip-text-percentile {
color: var(--text-sakura-500);
font-variant-numeric: lining-nums tabular-nums stacked-fractions slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 12px;
}
.span-percentile-tooltip-text-link {
color: var(--text-vanilla-400);
text-align: right;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
}
}
}
.span-details-drawer-docked {
width: 48px;
flex: 0 48px !important;
@@ -577,7 +208,6 @@
justify-content: center;
}
}
.resizable-handle {
box-sizing: border-box;
border: 2px solid transparent;
@@ -604,164 +234,6 @@
.description {
.item {
.span-name-wrapper {
.span-percentile-value {
color: var(--text-sakura-400, #f56c87);
border-left: 1px solid var(--bg-slate-300);
}
}
.span-percentiles-container {
fill: linear-gradient(
139deg,
rgba(18, 19, 23, 0.32) 0%,
rgba(18, 19, 23, 0.36) 98.68%
);
stroke-width: 1px;
stroke: var(--bg-slate-500);
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
backdrop-filter: blur(20px);
border: 1px solid var(--bg-vanilla-300);
border-radius: 4px;
.span-percentiles-header {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.span-percentile-content {
.span-percentile-content-title {
.span-percentile-value {
color: var(--text-sakura-400, #f56c87);
}
}
.span-percentile-timerange {
.span-percentile-timerange-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
color: var(--text-slate-300);
}
}
}
.span-percentile-values-table {
.span-percentile-values-table-header-row {
.span-percentile-values-table-header {
color: var(--text-vanilla-400);
}
}
.span-percentile-values-table-data-row {
.span-percentile-values-table-data-row-key {
color: var(--text-ink-100);
}
.span-percentile-values-table-data-row-value {
color: var(--text-ink-400);
}
.dashed-line {
flex: 1;
height: 0; /* line only */
margin: 0 8px;
border-top: 1px dashed var(--bg-slate-300);
/* Use border image to control dash length & spacing */
border-top-width: 1px;
border-top-style: solid; /* temporary solid for image */
border-image: repeating-linear-gradient(
to right,
var(--bg-slate-300) 0,
var(--bg-slate-300) 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
}
.current-span-percentile-row {
border-radius: 2px;
background: rgba(78, 116, 248, 0.2);
.span-percentile-values-table-data-row-key {
color: var(--text-robin-300, #95acfb);
}
.dashed-line {
border-top: 1px dashed #abbdff;
/* Use border image to control dash length & spacing */
border-top-width: 1px;
border-top-style: solid; /* temporary solid for image */
border-image: repeating-linear-gradient(
to right,
#abbdff 0,
#abbdff 10px,
transparent 10px,
transparent 20px
)
1 stretch;
}
.span-percentile-values-table-data-row-value {
color: var(--text-robin-400);
}
}
}
}
.resource-attributes-select-container {
.resource-attributes-select-container-header {
.resource-attributes-select-container-input {
border: none !important;
box-shadow: none !important;
height: 36px;
border-bottom: 1px solid var(--bg-vanilla-400) !important;
background: var(--bg-vanilla-300);
color: var(--text-ink-400);
}
}
border-radius: 4px;
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(20px);
.resource-attributes-items {
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
.resource-attributes-select-item {
.resource-attributes-select-item-checkbox {
.ant-checkbox-disabled {
background-color: var(--bg-robin-500);
color: var(--text-ink-100);
}
.resource-attributes-select-item-value {
color: var(--text-ink-100);
}
}
}
}
}
.attribute-key {
color: var(--bg-ink-400);
}

View File

@@ -1,53 +1,14 @@
import './SpanDetailsDrawer.styles.scss';
import {
Button,
Checkbox,
Input,
Select,
Skeleton,
Tabs,
TabsProps,
Tooltip,
Typography,
} from 'antd';
import { Button, Tabs, TabsProps, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import getSpanPercentiles from 'api/trace/getSpanPercentiles';
import getUserPreference from 'api/v1/user/preferences/name/get';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import LogsIcon from 'assets/AlertHistory/LogsIcon';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import SignozRadioGroup from 'components/SignozRadioGroup/SignozRadioGroup';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { themeColors } from 'constants/theme';
import { USER_PREFERENCES } from 'constants/userPreferences';
import dayjs from 'dayjs';
import useClickOutside from 'hooks/useClickOutside';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import {
Anvil,
BarChart2,
Bookmark,
Check,
ChevronDown,
Link2,
Loader2,
PanelRight,
PlusIcon,
Search,
} from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useMutation, useQuery } from 'react-query';
import { Anvil, Bookmark, Link2, PanelRight, Search } from 'lucide-react';
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { formatEpochTimestamp } from 'utils/timeUtils';
@@ -56,7 +17,6 @@ import { RelatedSignalsViews } from './constants';
import Events from './Events/Events';
import LinkedSpans from './LinkedSpans/LinkedSpans';
import SpanRelatedSignals from './SpanRelatedSignals/SpanRelatedSignals';
import { hasInfraMetadata } from './utils';
interface ISpanDetailsDrawerProps {
isSpanDetailsDocked: boolean;
@@ -66,45 +26,6 @@ interface ISpanDetailsDrawerProps {
traceEndTime: number;
}
const timerangeOptions = [
{
label: '1 hour',
value: 1,
},
{
label: '2 hours',
value: 2,
},
{
label: '3 hours',
value: 3,
},
{
label: '6 hours',
value: 6,
},
{
label: '12 hours',
value: 12,
},
{
label: '24 hours',
value: 24,
},
];
interface IResourceAttribute {
key: string;
value: string;
isSelected: boolean;
}
const DEFAULT_RESOURCE_ATTRIBUTES = {
serviceName: 'service.name',
name: 'name',
};
// eslint-disable-next-line sonarjs/cognitive-complexity
function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
const {
isSpanDetailsDocked,
@@ -118,60 +39,12 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
const [shouldAutoFocusSearch, setShouldAutoFocusSearch] = useState<boolean>(
false,
);
const [isSpanPercentilesOpen, setIsSpanPercentilesOpen] = useState<boolean>(
false,
);
const [isRelatedSignalsOpen, setIsRelatedSignalsOpen] = useState<boolean>(
false,
);
const [activeDrawerView, setActiveDrawerView] = useState<RelatedSignalsViews>(
RelatedSignalsViews.LOGS,
);
const [selectedTimeRange, setSelectedTimeRange] = useState<number>(1);
const [
resourceAttributesSearchQuery,
setResourceAttributesSearchQuery,
] = useState<string>('');
const [spanPercentileData, setSpanPercentileData] = useState<{
percentile: number;
description: string;
percentiles: Record<string, number>;
} | null>(null);
const [
showResourceAttributesSelector,
setShowResourceAttributesSelector,
] = useState<boolean>(false);
const [selectedResourceAttributes, setSelectedResourceAttributes] = useState<
Record<string, string>
>({});
const [spanResourceAttributes, updateSpanResourceAttributes] = useState<
IResourceAttribute[]
>([] as IResourceAttribute[]);
const [initialWaitCompleted, setInitialWaitCompleted] = useState<boolean>(
false,
);
const [
shouldFetchSpanPercentilesData,
setShouldFetchSpanPercentilesData,
] = useState<boolean>(false);
const [
shouldUpdateUserPreference,
setShouldUpdateUserPreference,
] = useState<boolean>(false);
const handleTimeRangeChange = useCallback((value: number): void => {
setShouldFetchSpanPercentilesData(true);
setSelectedTimeRange(value);
}, []);
const color = generateColor(
selectedSpan?.serviceName || '',
themeColors.traceDetailColors,
@@ -187,35 +60,6 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
setIsRelatedSignalsOpen(false);
}, []);
const relatedSignalsOptions = useMemo(() => {
const baseOptions = [
{
label: (
<div className="view-title">
<LogsIcon width={14} height={14} />
Logs
</div>
),
value: RelatedSignalsViews.LOGS,
},
];
// Only show Infra option if span has infrastructure metadata
if (hasInfraMetadata(selectedSpan)) {
baseOptions.push({
label: (
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
),
value: RelatedSignalsViews.INFRA,
});
}
return baseOptions;
}, [selectedSpan]);
function getItems(span: Span, startTime: number): TabsProps['items'] {
return [
{
@@ -279,265 +123,6 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
];
}
const resourceAttributesSelectorRef = useRef<HTMLDivElement | null>(null);
useClickOutside({
ref: resourceAttributesSelectorRef,
onClickOutside: () => {
if (resourceAttributesSelectorRef.current) {
setShowResourceAttributesSelector(false);
}
},
eventType: 'mousedown',
});
const spanPercentileTooltipText = useMemo(
() => (
<div className="span-percentile-tooltip-text">
<Typography.Text>
This span duration is{' '}
<span className="span-percentile-tooltip-text-percentile">
p{Math.floor(spanPercentileData?.percentile || 0)}
</span>{' '}
out of the distribution for this resource evaluated for {selectedTimeRange}{' '}
hour(s) since the span start time.
</Typography.Text>
<br />
<br />
<Typography.Text className="span-percentile-tooltip-text-link">
Click to learn more
</Typography.Text>
</div>
),
[spanPercentileData?.percentile, selectedTimeRange],
);
const endTime = useMemo(
() => Math.floor(Number(selectedSpan?.timestamp) / 1000) * 1000,
[selectedSpan?.timestamp],
);
const startTime = useMemo(
() =>
dayjs(selectedSpan?.timestamp)
.subtract(Number(selectedTimeRange), 'hour')
.unix() * 1000,
[selectedSpan?.timestamp, selectedTimeRange],
);
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
);
// TODO: Span percentile should be eventually moved to context and not fetched on every span change
const {
data: userSelectedResourceAttributes,
isError: isErrorUserSelectedResourceAttributes,
} = useQuery({
queryFn: () =>
getUserPreference({
name: USER_PREFERENCES.SPAN_PERCENTILE_RESOURCE_ATTRIBUTES,
}),
queryKey: [
'getUserPreferenceByPreferenceName',
USER_PREFERENCES.SPAN_PERCENTILE_RESOURCE_ATTRIBUTES,
selectedSpan?.spanId,
],
enabled: selectedSpan !== null && selectedSpan?.tagMap !== undefined,
});
const {
isLoading: isLoadingSpanPercentilesData,
isFetching: isFetchingSpanPercentilesData,
data,
refetch: refetchSpanPercentilesData,
isError: isErrorSpanPercentilesData,
} = useQuery({
queryFn: () =>
getSpanPercentiles({
start: startTime || 0,
end: endTime || 0,
spanDuration: selectedSpan?.durationNano || 0,
serviceName: selectedSpan?.serviceName || '',
name: selectedSpan?.name || '',
resourceAttributes: selectedResourceAttributes,
}),
queryKey: [
REACT_QUERY_KEY.GET_SPAN_PERCENTILES,
selectedSpan?.spanId,
startTime,
endTime,
],
enabled:
selectedSpan !== null &&
shouldFetchSpanPercentilesData &&
!showResourceAttributesSelector &&
initialWaitCompleted,
onSuccess: (response) => {
if (response.httpStatusCode !== 200) {
return;
}
if (shouldUpdateUserPreference) {
updateUserPreferenceMutation({
name: USER_PREFERENCES.SPAN_PERCENTILE_RESOURCE_ATTRIBUTES,
value: [...Object.keys(selectedResourceAttributes)],
});
setShouldUpdateUserPreference(false);
}
},
});
// Prod Req - Wait for 2 seconds before fetching span percentile data on initial load
useEffect(() => {
setInitialWaitCompleted(false);
const timer = setTimeout(() => {
setInitialWaitCompleted(true);
}, 2000); // 2-second delay
return (): void => clearTimeout(timer); // Cleanup on re-run or unmount
}, [selectedSpan?.spanId]);
useEffect(() => {
if (data?.httpStatusCode !== 200) {
setSpanPercentileData(null);
return;
}
if (data) {
const percentileData = {
percentile: data?.data?.position?.percentile || 0,
description: data?.data?.position?.description || '',
percentiles: data?.data?.percentiles || {},
};
setSpanPercentileData(percentileData);
}
}, [data]);
useEffect(() => {
if (userSelectedResourceAttributes) {
const userSelectedResourceAttributesList = (userSelectedResourceAttributes
?.data?.value as string[]).map((attribute: string) => attribute);
let selectedResourceAttributesMap: Record<string, string> = {};
userSelectedResourceAttributesList.forEach((attribute: string) => {
selectedResourceAttributesMap[attribute] =
selectedSpan?.tagMap?.[attribute] || '';
});
// filter out the attributes that are not in the selectedSpan?.tagMap
selectedResourceAttributesMap = Object.fromEntries(
Object.entries(selectedResourceAttributesMap).filter(
([key]) => selectedSpan?.tagMap?.[key] !== undefined,
),
);
const resourceAttributes = Object.entries(selectedSpan?.tagMap || {}).map(
([key, value]) => ({
key,
value,
isSelected:
key === DEFAULT_RESOURCE_ATTRIBUTES.serviceName ||
key === DEFAULT_RESOURCE_ATTRIBUTES.name ||
(key in selectedResourceAttributesMap &&
selectedResourceAttributesMap[key] !== '' &&
selectedResourceAttributesMap[key] !== undefined),
}),
);
// selected resources should be at the top of the list
const selectedResourceAttributes = resourceAttributes.filter(
(resourceAttribute) => resourceAttribute.isSelected,
);
const unselectedResourceAttributes = resourceAttributes.filter(
(resourceAttribute) => !resourceAttribute.isSelected,
);
const sortedResourceAttributes = [
...selectedResourceAttributes,
...unselectedResourceAttributes,
];
updateSpanResourceAttributes(sortedResourceAttributes);
setSelectedResourceAttributes(
selectedResourceAttributesMap as Record<string, string>,
);
setShouldFetchSpanPercentilesData(true);
}
if (isErrorUserSelectedResourceAttributes) {
const resourceAttributes = Object.entries(selectedSpan?.tagMap || {}).map(
([key, value]) => ({
key,
value,
isSelected:
key === DEFAULT_RESOURCE_ATTRIBUTES.serviceName ||
key === DEFAULT_RESOURCE_ATTRIBUTES.name,
}),
);
updateSpanResourceAttributes(resourceAttributes);
setShouldFetchSpanPercentilesData(true);
}
}, [
userSelectedResourceAttributes,
isErrorUserSelectedResourceAttributes,
selectedSpan?.tagMap,
]);
const handleResourceAttributeChange = useCallback(
(key: string, value: string, isSelected: boolean): void => {
updateSpanResourceAttributes((prev) =>
prev.map((resourceAttribute) =>
resourceAttribute.key === key
? { ...resourceAttribute, isSelected }
: resourceAttribute,
),
);
const newSelectedResourceAttributes = { ...selectedResourceAttributes };
if (isSelected) {
newSelectedResourceAttributes[key] = value;
} else {
delete newSelectedResourceAttributes[key];
}
setSelectedResourceAttributes(newSelectedResourceAttributes);
setShouldFetchSpanPercentilesData(true);
setShouldUpdateUserPreference(true);
},
[selectedResourceAttributes],
);
useEffect(() => {
if (
shouldFetchSpanPercentilesData &&
!showResourceAttributesSelector &&
initialWaitCompleted
) {
refetchSpanPercentilesData();
setShouldFetchSpanPercentilesData(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
shouldFetchSpanPercentilesData,
showResourceAttributesSelector,
initialWaitCompleted,
]);
return (
<>
<section className="header">
@@ -558,226 +143,13 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
<section className="description">
<div className="item">
<Typography.Text className="attribute-key">span name</Typography.Text>
<div className="value-wrapper span-name-wrapper">
<Tooltip title={selectedSpan.name}>
<Tooltip title={selectedSpan.name}>
<div className="value-wrapper">
<Typography.Text className="attribute-value" ellipsis>
{selectedSpan.name}
</Typography.Text>
</Tooltip>
{isLoadingSpanPercentilesData && (
<div className="loading-spinner-container">
<Loader2 size={16} className="animate-spin" />
</div>
)}
{!isLoadingSpanPercentilesData && spanPercentileData && (
<Tooltip
title={isSpanPercentilesOpen ? '' : spanPercentileTooltipText}
placement="bottomRight"
overlayClassName="span-percentile-tooltip"
arrow={false}
>
<Typography.Text
className="span-percentile-value"
onClick={(): void => setIsSpanPercentilesOpen((prev) => !prev)}
>
p{Math.floor(spanPercentileData?.percentile || 0)}
</Typography.Text>
</Tooltip>
)}
</div>
<AnimatePresence initial={false}>
{isSpanPercentilesOpen && !isErrorSpanPercentilesData && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
key="box"
>
<div className="span-percentiles-container">
<div className="span-percentiles-header">
<Typography.Text
className="span-percentiles-header-text"
onClick={(): void => setIsSpanPercentilesOpen((prev) => !prev)}
>
<ChevronDown size={16} /> Span Percentile
</Typography.Text>
{showResourceAttributesSelector ? (
<Check
data-testid="check-icon"
size={16}
className="cursor-pointer span-percentiles-header-icon"
onClick={(): void => setShowResourceAttributesSelector(false)}
/>
) : (
<PlusIcon
data-testid="plus-icon"
size={16}
className="cursor-pointer span-percentiles-header-icon"
onClick={(): void => setShowResourceAttributesSelector(true)}
/>
)}
</div>
{showResourceAttributesSelector && (
<div
className="resource-attributes-select-container"
ref={resourceAttributesSelectorRef}
>
<div className="resource-attributes-select-container-header">
<Input
placeholder="Search resource attributes"
className="resource-attributes-select-container-input"
value={resourceAttributesSearchQuery}
onChange={(e): void =>
setResourceAttributesSearchQuery(e.target.value as string)
}
/>
</div>
<div className="resource-attributes-items">
{spanResourceAttributes
.filter((resourceAttribute) =>
resourceAttribute.key
.toLowerCase()
.includes(resourceAttributesSearchQuery.toLowerCase()),
)
.map((resourceAttribute) => (
<div
className="resource-attributes-select-item"
key={resourceAttribute.key}
>
<div className="resource-attributes-select-item-checkbox">
<Checkbox
checked={resourceAttribute.isSelected}
onChange={(e): void => {
handleResourceAttributeChange(
resourceAttribute.key,
resourceAttribute.value,
e.target.checked,
);
}}
disabled={
resourceAttribute.key === 'service.name' ||
resourceAttribute.key === 'name'
}
>
<div className="resource-attributes-select-item-value">
{resourceAttribute.key}
</div>
</Checkbox>
</div>
</div>
))}
</div>
</div>
)}
<div className="span-percentile-content">
<Typography.Text className="span-percentile-content-title">
This span duration is{' '}
{!isLoadingSpanPercentilesData &&
!isFetchingSpanPercentilesData &&
spanPercentileData ? (
<span className="span-percentile-value">
p{Math.floor(spanPercentileData?.percentile || 0)}
</span>
) : (
<span className="span-percentile-value-loader">
<Loader2 size={12} className="animate-spin" />
</span>
)}{' '}
out of the distribution for this resource evaluated for{' '}
{selectedTimeRange} hour(s) since the span start time.
</Typography.Text>
<div className="span-percentile-timerange">
<Select
labelInValue
placeholder="Select timerange"
className="span-percentile-timerange-select"
value={{
label: `${selectedTimeRange}h : ${dayjs(selectedSpan?.timestamp)
.subtract(selectedTimeRange, 'hour')
.format(DATE_TIME_FORMATS.TIME_SPAN_PERCENTILE)} - ${dayjs(
selectedSpan?.timestamp,
).format(DATE_TIME_FORMATS.TIME_SPAN_PERCENTILE)}`,
value: selectedTimeRange,
}}
onChange={(value): void => {
handleTimeRangeChange(Number(value.value));
}}
options={timerangeOptions}
/>
</div>
<div className="span-percentile-values-table">
<div className="span-percentile-values-table-header-row">
<Typography.Text className="span-percentile-values-table-header">
Percentile
</Typography.Text>
<Typography.Text className="span-percentile-values-table-header">
Duration
</Typography.Text>
</div>
<div className="span-percentile-values-table-data-rows">
{isLoadingSpanPercentilesData || isFetchingSpanPercentilesData ? (
<Skeleton
active
paragraph={{ rows: 3 }}
className="span-percentile-values-table-data-rows-skeleton"
/>
) : (
<>
{Object.entries(spanPercentileData?.percentiles || {}).map(
([percentile, duration]) => (
<div
className="span-percentile-values-table-data-row"
key={percentile}
>
<Typography.Text className="span-percentile-values-table-data-row-key">
{percentile}
</Typography.Text>
<div className="dashed-line" />
<Typography.Text className="span-percentile-values-table-data-row-value">
{getYAxisFormattedValue(`${duration / 1000000}`, 'ms')}
</Typography.Text>
</div>
),
)}
<div className="span-percentile-values-table-data-row current-span-percentile-row">
<Typography.Text className="span-percentile-values-table-data-row-key">
p{Math.floor(spanPercentileData?.percentile || 0)}
</Typography.Text>
<div className="dashed-line" />
<Typography.Text className="span-percentile-values-table-data-row-value">
(this span){' '}
{getYAxisFormattedValue(
`${selectedSpan.durationNano / 1000000}`,
'ms',
)}
</Typography.Text>
</div>
</>
)}
</div>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</Tooltip>
</div>
<div className="item">
<Typography.Text className="attribute-key">span id</Typography.Text>
@@ -854,7 +226,17 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
<div className="related-signals-section">
<SignozRadioGroup
value=""
options={relatedSignalsOptions}
options={[
{
label: (
<div className="view-title">
<LogsIcon width={14} height={14} />
Logs
</div>
),
value: RelatedSignalsViews.LOGS,
},
]}
onChange={handleRelatedSignalsChange}
className="related-signals-radio"
/>

View File

@@ -30,11 +30,6 @@
display: flex;
flex-direction: column;
}
.view-title {
display: flex;
align-items: center;
gap: 8px;
}
.views-tabs-container {
padding: 16px 15px;
@@ -93,10 +88,28 @@
}
}
.infra-metrics-container {
padding-inline: 16px;
.infra-metrics-card {
border: 1px solid var(--bg-slate-400);
.infra-placeholder {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
box-sizing: border-box;
.infra-placeholder-content {
text-align: center;
color: var(--bg-slate-400);
svg {
margin-bottom: 1rem;
color: var(--bg-slate-400);
}
.ant-typography {
font-size: 16px;
color: var(--bg-slate-400);
}
}
}
}

View File

@@ -11,20 +11,17 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import { getEmptyLogsListConfig } from 'container/LogsExplorerList/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { BarChart2, Compass, X } from 'lucide-react';
import { Compass, X } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Span } from 'types/api/trace/getTraceV2';
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
import { RelatedSignalsViews } from '../constants';
import SpanLogs from '../SpanLogs/SpanLogs';
import { useSpanContextLogs } from '../SpanLogs/useSpanContextLogs';
import { hasInfraMetadata } from '../utils';
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
@@ -50,21 +47,6 @@ function SpanRelatedSignals({
);
const isDarkMode = useIsDarkMode();
// Extract infrastructure metadata from span attributes
const infraMetadata = useMemo(() => {
// Only return metadata if span has infrastructure metadata
if (!hasInfraMetadata(selectedSpan)) {
return null;
}
return {
clusterName: selectedSpan.tagMap['k8s.cluster.name'] || '',
podName: selectedSpan.tagMap['k8s.pod.name'] || '',
nodeName: selectedSpan.tagMap['k8s.node.name'] || '',
hostName: selectedSpan.tagMap['host.name'] || '',
spanTimestamp: dayjs(selectedSpan.timestamp).format(),
};
}, [selectedSpan]);
const {
logs,
isLoading,
@@ -86,34 +68,10 @@ function SpanRelatedSignals({
setSelectedView(e.target.value);
}, []);
const tabOptions = useMemo(() => {
const baseOptions = [
{
label: (
<div className="view-title">
<LogsIcon width={14} height={14} />
Logs
</div>
),
value: RelatedSignalsViews.LOGS,
},
];
// Add Infra option if infrastructure metadata is available
if (infraMetadata) {
baseOptions.push({
label: (
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
),
value: RelatedSignalsViews.INFRA,
});
}
return baseOptions;
}, [infraMetadata]);
const handleClose = useCallback((): void => {
setSelectedView(RelatedSignalsViews.LOGS);
onClose();
}, [onClose]);
const handleExplorerPageRedirect = useCallback((): void => {
const startTimeMs = traceStartTime - FIVE_MINUTES_IN_MS;
@@ -187,7 +145,7 @@ function SpanRelatedSignals({
</>
}
placement="right"
onClose={onClose}
onClose={handleClose}
open={isOpen}
style={{
overscrollBehavior: 'contain',
@@ -202,7 +160,35 @@ function SpanRelatedSignals({
<div className="views-tabs-container">
<SignozRadioGroup
value={selectedView}
options={tabOptions}
options={[
{
label: (
<div className="view-title">
<LogsIcon width={14} height={14} />
Logs
</div>
),
value: RelatedSignalsViews.LOGS,
},
// {
// label: (
// <div className="view-title">
// <LogsIcon width={14} height={14} />
// Metrics
// </div>
// ),
// value: RelatedSignalsViews.METRICS,
// },
// {
// label: (
// <div className="view-title">
// <Server size={14} />
// Infra
// </div>
// ),
// value: RelatedSignalsViews.INFRA,
// },
]}
onChange={handleTabChange}
className="related-signals-radio"
/>
@@ -211,7 +197,6 @@ function SpanRelatedSignals({
icon={<Compass size={18} />}
className="open-in-explorer"
onClick={handleExplorerPageRedirect}
data-testid="open-in-explorer-button"
>
Open in Logs Explorer
</Button>
@@ -235,17 +220,6 @@ function SpanRelatedSignals({
emptyStateConfig={!hasTraceIdLogs ? emptyStateConfig : undefined}
/>
)}
{selectedView === RelatedSignalsViews.INFRA && infraMetadata && (
<InfraMetrics
clusterName={infraMetadata.clusterName}
podName={infraMetadata.podName}
nodeName={infraMetadata.nodeName}
hostName={infraMetadata.hostName}
timestamp={infraMetadata.spanTimestamp}
dataSource={DataSource.TRACES}
/>
)}
</div>
)}
</Drawer>

View File

@@ -1,502 +0,0 @@
import ROUTES from 'constants/routes';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { server } from 'mocks-server/server';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import SpanDetailsDrawer from '../SpanDetailsDrawer';
import {
expectedHostOnlyMetadata,
expectedInfraMetadata,
expectedNodeOnlyMetadata,
expectedPodOnlyMetadata,
mockEmptyMetricsResponse,
mockNodeMetricsResponse,
mockPodMetricsResponse,
mockSpanWithHostOnly,
mockSpanWithInfraMetadata,
mockSpanWithNodeOnly,
mockSpanWithoutInfraMetadata,
mockSpanWithPodOnly,
} from './infraMetricsTestData';
// Mock external dependencies
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${ROUTES.TRACE_DETAIL}`,
}),
}));
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: mockSafeNavigate,
}),
}));
const mockUpdateAllQueriesOperators = jest.fn().mockReturnValue({
builder: {
queryData: [
{
dataSource: 'logs',
queryName: 'A',
aggregateOperator: 'noop',
filters: { items: [], op: 'AND' },
expression: 'A',
disabled: false,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
groupBy: [],
limit: null,
having: [],
},
],
queryFormulas: [],
},
queryType: 'builder',
});
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): any => ({
updateAllQueriesOperators: mockUpdateAllQueriesOperators,
currentQuery: {
builder: {
queryData: [
{
dataSource: 'logs',
queryName: 'A',
filters: { items: [], op: 'AND' },
},
],
},
},
}),
}));
const mockWindowOpen = jest.fn();
Object.defineProperty(window, 'open', {
writable: true,
value: mockWindowOpen,
});
// Mock uplot to avoid rendering issues
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// Mock GetMetricQueryRange to track API calls
jest.mock('lib/dashboard/getQueryResults', () => ({
GetMetricQueryRange: jest.fn(),
}));
// Mock generateColor
jest.mock('lib/uPlotLib/utils/generateColor', () => ({
generateColor: jest.fn().mockReturnValue('#1f77b4'),
}));
// Mock OverlayScrollbar
jest.mock(
'components/OverlayScrollbar/OverlayScrollbar',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function ({ children }: any) {
return <div data-testid="overlay-scrollbar">{children}</div>;
},
);
// Mock Virtuoso
jest.mock('react-virtuoso', () => ({
Virtuoso: jest.fn(({ data, itemContent }) => (
<div data-testid="virtuoso">
{data?.map((item: any, index: number) => (
<div key={item.id || index} data-testid={`log-item-${item.id}`}>
{itemContent(index, item)}
</div>
))}
</div>
)),
}));
// Mock InfraMetrics component for focused testing
jest.mock(
'container/LogDetailedView/InfraMetrics/InfraMetrics',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function MockInfraMetrics({
podName,
nodeName,
hostName,
clusterName,
timestamp,
dataSource,
}: any) {
return (
<div data-testid="infra-metrics">
<div data-testid="infra-pod-name">{podName}</div>
<div data-testid="infra-node-name">{nodeName}</div>
<div data-testid="infra-host-name">{hostName}</div>
<div data-testid="infra-cluster-name">{clusterName}</div>
<div data-testid="infra-timestamp">{timestamp}</div>
<div data-testid="infra-data-source">{dataSource}</div>
</div>
);
},
);
// Mock PreferenceContextProvider
jest.mock('providers/preferences/context/PreferenceContextProvider', () => ({
PreferenceContextProvider: ({ children }: any): JSX.Element => (
<div>{children}</div>
),
}));
describe('SpanDetailsDrawer - Infra Metrics', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, sonarjs/no-unused-collection
let apiCallHistory: any[] = [];
beforeEach(() => {
jest.clearAllMocks();
apiCallHistory = [];
mockSafeNavigate.mockClear();
mockWindowOpen.mockClear();
mockUpdateAllQueriesOperators.mockClear();
// Setup API call tracking for infra metrics
(GetMetricQueryRange as jest.Mock).mockImplementation((query) => {
apiCallHistory.push(query);
// Return mock responses for different query types
if (
query?.query?.builder?.queryData?.[0]?.filters?.items?.some(
(item: any) => item.key?.key === 'k8s_pod_name',
)
) {
return Promise.resolve(mockPodMetricsResponse);
}
if (
query?.query?.builder?.queryData?.[0]?.filters?.items?.some(
(item: any) => item.key?.key === 'k8s_node_name',
)
) {
return Promise.resolve(mockNodeMetricsResponse);
}
return Promise.resolve(mockEmptyMetricsResponse);
});
});
afterEach(() => {
server.resetHandlers();
});
// Mock QueryBuilder context value
const mockQueryBuilderContextValue = {
currentQuery: {
builder: {
queryData: [
{
dataSource: 'logs',
queryName: 'A',
filters: { items: [], op: 'AND' },
},
],
},
},
stagedQuery: {
builder: {
queryData: [
{
dataSource: 'logs',
queryName: 'A',
filters: { items: [], op: 'AND' },
},
],
},
},
updateAllQueriesOperators: mockUpdateAllQueriesOperators,
panelType: 'list',
redirectWithQuery: jest.fn(),
handleRunQuery: jest.fn(),
handleStageQuery: jest.fn(),
resetQuery: jest.fn(),
};
const renderSpanDetailsDrawer = (props = {}): void => {
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue as any}>
<SpanDetailsDrawer
isSpanDetailsDocked={false}
setIsSpanDetailsDocked={jest.fn()}
selectedSpan={mockSpanWithInfraMetadata}
traceStartTime={1640995200000} // 2022-01-01 00:00:00
traceEndTime={1640995260000} // 2022-01-01 00:01:00
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</QueryBuilderContext.Provider>,
);
};
it('should detect infra metadata from span attributes', async () => {
renderSpanDetailsDrawer();
// Click on metrics tab
const infraMetricsButton = screen.getByRole('radio', { name: /metrics/i });
expect(infraMetricsButton).toBeInTheDocument();
fireEvent.click(infraMetricsButton);
// Wait for infra metrics to load
await waitFor(() => {
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('infra-metrics')).toBeInTheDocument();
});
// Verify metadata extraction
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('infra-pod-name')).toHaveTextContent(
expectedInfraMetadata.podName,
);
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('infra-node-name')).toHaveTextContent(
expectedInfraMetadata.nodeName,
);
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('infra-host-name')).toHaveTextContent(
expectedInfraMetadata.hostName,
);
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('infra-cluster-name')).toHaveTextContent(
expectedInfraMetadata.clusterName,
);
expect(screen.getByTestId('infra-data-source')).toHaveTextContent(
DataSource.TRACES,
);
});
it('should not show infra tab when span lacks infra metadata', async () => {
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue as any}>
<SpanDetailsDrawer
isSpanDetailsDocked={false}
setIsSpanDetailsDocked={jest.fn()}
selectedSpan={mockSpanWithoutInfraMetadata}
traceStartTime={1640995200000}
traceEndTime={1640995260000}
/>
</QueryBuilderContext.Provider>,
);
// Should NOT show infra tab, only logs tab
expect(
screen.queryByRole('radio', { name: /metrics/i }),
).not.toBeInTheDocument();
expect(screen.getByRole('radio', { name: /logs/i })).toBeInTheDocument();
});
it('should show infra tab when span has infra metadata', async () => {
renderSpanDetailsDrawer();
// Should show both logs and infra tabs
expect(screen.getByRole('radio', { name: /metrics/i })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: /logs/i })).toBeInTheDocument();
});
it('should handle pod-only metadata correctly', async () => {
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue as any}>
<SpanDetailsDrawer
isSpanDetailsDocked={false}
setIsSpanDetailsDocked={jest.fn()}
selectedSpan={mockSpanWithPodOnly}
traceStartTime={1640995200000}
traceEndTime={1640995260000}
/>
</QueryBuilderContext.Provider>,
);
// Click on infra tab
const infraMetricsButton = screen.getByRole('radio', { name: /metrics/i });
fireEvent.click(infraMetricsButton);
await waitFor(() => {
expect(screen.getByTestId('infra-metrics')).toBeInTheDocument();
});
// Verify pod-only metadata
expect(screen.getByTestId('infra-pod-name')).toHaveTextContent(
expectedPodOnlyMetadata.podName,
);
expect(screen.getByTestId('infra-cluster-name')).toHaveTextContent(
expectedPodOnlyMetadata.clusterName,
);
expect(screen.getByTestId('infra-node-name')).toHaveTextContent(
expectedPodOnlyMetadata.nodeName,
);
expect(screen.getByTestId('infra-host-name')).toHaveTextContent(
expectedPodOnlyMetadata.hostName,
);
});
it('should handle node-only metadata correctly', async () => {
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue as any}>
<SpanDetailsDrawer
isSpanDetailsDocked={false}
setIsSpanDetailsDocked={jest.fn()}
selectedSpan={mockSpanWithNodeOnly}
traceStartTime={1640995200000}
traceEndTime={1640995260000}
/>
</QueryBuilderContext.Provider>,
);
// Click on infra tab
const infraMetricsButton = screen.getByRole('radio', { name: /metrics/i });
fireEvent.click(infraMetricsButton);
await waitFor(() => {
expect(screen.getByTestId('infra-metrics')).toBeInTheDocument();
});
// Verify node-only metadata
expect(screen.getByTestId('infra-node-name')).toHaveTextContent(
expectedNodeOnlyMetadata.nodeName,
);
expect(screen.getByTestId('infra-pod-name')).toHaveTextContent(
expectedNodeOnlyMetadata.podName,
);
expect(screen.getByTestId('infra-cluster-name')).toHaveTextContent(
expectedNodeOnlyMetadata.clusterName,
);
expect(screen.getByTestId('infra-host-name')).toHaveTextContent(
expectedNodeOnlyMetadata.hostName,
);
});
it('should handle host-only metadata correctly', async () => {
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue as any}>
<SpanDetailsDrawer
isSpanDetailsDocked={false}
setIsSpanDetailsDocked={jest.fn()}
selectedSpan={mockSpanWithHostOnly}
traceStartTime={1640995200000}
traceEndTime={1640995260000}
/>
</QueryBuilderContext.Provider>,
);
// Click on infra tab
const infraMetricsButton = screen.getByRole('radio', { name: /metrics/i });
fireEvent.click(infraMetricsButton);
await waitFor(() => {
expect(screen.getByTestId('infra-metrics')).toBeInTheDocument();
});
// Verify host-only metadata
expect(screen.getByTestId('infra-host-name')).toHaveTextContent(
expectedHostOnlyMetadata.hostName,
);
expect(screen.getByTestId('infra-pod-name')).toHaveTextContent(
expectedHostOnlyMetadata.podName,
);
expect(screen.getByTestId('infra-node-name')).toHaveTextContent(
expectedHostOnlyMetadata.nodeName,
);
expect(screen.getByTestId('infra-cluster-name')).toHaveTextContent(
expectedHostOnlyMetadata.clusterName,
);
});
it('should switch between logs and infra tabs correctly', async () => {
renderSpanDetailsDrawer();
// Initially should show logs tab content
const logsButton = screen.getByRole('radio', { name: /logs/i });
const infraMetricsButton = screen.getByRole('radio', { name: /metrics/i });
expect(logsButton).toBeInTheDocument();
expect(infraMetricsButton).toBeInTheDocument();
// Ensure logs tab is active and wait for content to load
fireEvent.click(logsButton);
await waitFor(() => {
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
});
// Click on infra tab
fireEvent.click(infraMetricsButton);
await waitFor(() => {
expect(screen.getByTestId('infra-metrics')).toBeInTheDocument();
});
// Should not show logs content anymore
expect(
screen.queryByTestId('open-in-explorer-button'),
).not.toBeInTheDocument();
// Switch back to logs tab
fireEvent.click(logsButton);
// Should not show infra metrics anymore
await waitFor(() => {
expect(screen.queryByTestId('infra-metrics')).not.toBeInTheDocument();
});
// Verify logs content is shown again
await waitFor(() => {
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
});
});
it('should pass correct data source and handle multiple infra identifiers', async () => {
renderSpanDetailsDrawer();
// Should show infra tab when span has any of: clusterName, podName, nodeName, hostName
expect(screen.getByRole('radio', { name: /metrics/i })).toBeInTheDocument();
// Click on infra tab
const infraMetricsButton = screen.getByRole('radio', { name: /metrics/i });
fireEvent.click(infraMetricsButton);
await waitFor(() => {
expect(screen.getByTestId('infra-metrics')).toBeInTheDocument();
});
// Verify TRACES data source is passed
expect(screen.getByTestId('infra-data-source')).toHaveTextContent(
DataSource.TRACES,
);
// All infra identifiers should be passed through
expect(screen.getByTestId('infra-pod-name')).toHaveTextContent(
'test-pod-abc123',
);
expect(screen.getByTestId('infra-node-name')).toHaveTextContent(
'test-node-456',
);
expect(screen.getByTestId('infra-host-name')).toHaveTextContent(
'test-host.example.com',
);
expect(screen.getByTestId('infra-cluster-name')).toHaveTextContent(
'test-cluster',
);
});
});

View File

@@ -1,8 +1,3 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/no-identical-functions */
import getSpanPercentiles from 'api/trace/getSpanPercentiles';
import getUserPreference from 'api/v1/user/preferences/name/get';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
@@ -15,8 +10,6 @@ import {
userEvent,
waitFor,
} from 'tests/test-utils';
import { SuccessResponseV2 } from 'types/api';
import { GetSpanPercentilesResponseDataProps } from 'types/api/trace/getSpanPercentiles';
import SpanDetailsDrawer from '../SpanDetailsDrawer';
import {
@@ -31,17 +24,11 @@ import {
mockSpanLogsResponse,
} from './mockData';
// Get typed mocks
const mockGetSpanPercentiles = jest.mocked(getSpanPercentiles);
const mockGetUserPreference = jest.mocked(getUserPreference);
const mockSafeNavigate = jest.fn();
// Mock external dependencies
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string; search: string } => ({
useLocation: (): { pathname: string } => ({
pathname: `${ROUTES.TRACE_DETAIL}`,
search: 'trace_id=test-trace-id',
}),
}));
@@ -51,8 +38,9 @@ jest.mock('@signozhq/button', () => ({
),
}));
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.MockedFunction<() => void> } => ({
useSafeNavigate: (): any => ({
safeNavigate: mockSafeNavigate,
}),
}));
@@ -80,10 +68,7 @@ const mockUpdateAllQueriesOperators = jest.fn().mockReturnValue({
});
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
updateAllQueriesOperators: jest.MockedFunction<() => any>;
currentQuery: any;
} => ({
useQueryBuilder: (): any => ({
updateAllQueriesOperators: mockUpdateAllQueriesOperators,
currentQuery: {
builder: {
@@ -128,46 +113,26 @@ jest.mock('lib/uPlotLib/utils/generateColor', () => ({
generateColor: jest.fn().mockReturnValue('#1f77b4'),
}));
// Mock getSpanPercentiles API
jest.mock('api/trace/getSpanPercentiles', () => ({
__esModule: true,
default: jest.fn(),
}));
// Mock getUserPreference API
jest.mock('api/v1/user/preferences/name/get', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock(
'components/OverlayScrollbar/OverlayScrollbar',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function ({ children }: { children: React.ReactNode }) {
function ({ children }: any) {
return <div data-testid="overlay-scrollbar">{children}</div>;
},
);
// Mock Virtuoso to avoid complex virtualization
jest.mock('react-virtuoso', () => ({
Virtuoso: jest.fn(
({
data,
itemContent,
}: {
data: any[];
itemContent: (index: number, item: any) => React.ReactNode;
}) => (
<div data-testid="virtuoso">
{data?.map((item: any, index: number) => (
<div key={item.id || index} data-testid={`log-item-${item.id}`}>
{itemContent(index, item)}
</div>
))}
</div>
),
),
Virtuoso: jest.fn(({ data, itemContent }) => (
<div data-testid="virtuoso">
{data?.map((item: any, index: number) => (
<div key={item.id || index} data-testid={`log-item-${item.id}`}>
{itemContent(index, item)}
</div>
))}
</div>
)),
}));
// Mock RawLogView component
@@ -180,12 +145,7 @@ jest.mock(
onLogClick,
isHighlighted,
helpTooltip,
}: {
data: any;
onLogClick: (data: any, event: React.MouseEvent) => void;
isHighlighted: boolean;
helpTooltip: string;
}) {
}: any) {
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
@@ -204,11 +164,9 @@ jest.mock(
// Mock PreferenceContextProvider
jest.mock('providers/preferences/context/PreferenceContextProvider', () => ({
PreferenceContextProvider: ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => <div>{children}</div>,
PreferenceContextProvider: ({ children }: any): JSX.Element => (
<div>{children}</div>
),
}));
// Mock QueryBuilder context value
@@ -259,51 +217,6 @@ const renderSpanDetailsDrawer = (props = {}): void => {
);
};
// Constants for repeated strings
const SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER = 'Search resource attributes';
const P75_TEXT = 'p75';
const SPAN_PERCENTILE_TEXT = 'Span Percentile';
// Mock data for span percentiles
const mockSpanPercentileResponse = {
httpStatusCode: 200 as const,
data: {
percentiles: {
p50: 500000000, // 500ms in nanoseconds
p90: 1000000000, // 1s in nanoseconds
p95: 1500000000, // 1.5s in nanoseconds
p99: 2000000000, // 2s in nanoseconds
},
position: {
percentile: 75.5,
description: 'This span is in the 75th percentile',
},
},
};
const mockUserPreferenceResponse = {
statusCode: 200,
httpStatusCode: 200,
error: null,
message: 'Success',
data: {
name: 'span_percentile_resource_attributes',
description: 'Resource attributes for span percentile calculation',
valueType: 'array',
defaultValue: [],
value: ['service.name', 'name', 'http.method'],
allowedValues: [],
allowedScopes: [],
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
},
};
const mockSpanPercentileErrorResponse = ({
httpStatusCode: 500,
data: null,
} as unknown) as SuccessResponseV2<GetSpanPercentilesResponseDataProps>;
describe('SpanDetailsDrawer', () => {
let apiCallHistory: any = {};
@@ -318,14 +231,12 @@ describe('SpanDetailsDrawer', () => {
mockSafeNavigate.mockClear();
mockWindowOpen.mockClear();
mockUpdateAllQueriesOperators.mockClear();
mockGetSpanPercentiles.mockClear();
mockGetUserPreference.mockClear();
// Setup API call tracking
(GetMetricQueryRange as jest.Mock).mockImplementation((query) => {
// Determine response based on v5 filter expressions
const filterExpression = (query as any)?.query?.builder?.queryData?.[0]
?.filter?.expression;
const filterExpression =
query.query?.builder?.queryData?.[0]?.filter?.expression;
if (!filterExpression) return Promise.resolve(mockEmptyLogsResponse);
@@ -410,17 +321,17 @@ describe('SpanDetailsDrawer', () => {
} = apiCallHistory;
// 1. Span logs query (trace_id + span_id)
expect((spanQuery as any).query.builder.queryData[0].filter.expression).toBe(
expect(spanQuery.query.builder.queryData[0].filter.expression).toBe(
expectedSpanFilterExpression,
);
// 2. Before logs query (trace_id + id < first_span_log_id)
expect(
(beforeQuery as any).query.builder.queryData[0].filter.expression,
).toBe(expectedBeforeFilterExpression);
expect(beforeQuery.query.builder.queryData[0].filter.expression).toBe(
expectedBeforeFilterExpression,
);
// 3. After logs query (trace_id + id > last_span_log_id)
expect((afterQuery as any).query.builder.queryData[0].filter.expression).toBe(
expect(afterQuery.query.builder.queryData[0].filter.expression).toBe(
expectedAfterFilterExpression,
);
@@ -449,19 +360,13 @@ describe('SpanDetailsDrawer', () => {
} = apiCallHistory;
// Verify ordering: span query should use 'desc' (default)
expect((spanQuery as any).query.builder.queryData[0].orderBy[0].order).toBe(
'desc',
);
expect(spanQuery.query.builder.queryData[0].orderBy[0].order).toBe('desc');
// Before query should use 'desc' (default)
expect((beforeQuery as any).query.builder.queryData[0].orderBy[0].order).toBe(
'desc',
);
expect(beforeQuery.query.builder.queryData[0].orderBy[0].order).toBe('desc');
// After query should use 'asc' for chronological order
expect((afterQuery as any).query.builder.queryData[0].orderBy[0].order).toBe(
'asc',
);
expect(afterQuery.query.builder.queryData[0].orderBy[0].order).toBe('asc');
});
it('should navigate to logs explorer with span filters when span log is clicked', async () => {
@@ -622,435 +527,6 @@ describe('SpanDetailsDrawer', () => {
expect(contextLogAfter).toHaveClass('log-context');
expect(contextLogBefore).not.toHaveAttribute('title');
});
// Span Percentile Tests
describe('Span Percentile Functionality', () => {
beforeEach(() => {
// Setup default mocks for percentile tests
mockGetUserPreference.mockResolvedValue(mockUserPreferenceResponse);
mockGetSpanPercentiles.mockResolvedValue(mockSpanPercentileResponse);
});
it('should display span percentile value after successful API call', async () => {
renderSpanDetailsDrawer();
// Wait for the 2-second delay and API call to complete
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should show loading spinner while fetching percentile data', async () => {
// Mock a delayed response
mockGetSpanPercentiles.mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => resolve(mockSpanPercentileResponse), 1000);
}),
);
renderSpanDetailsDrawer();
// Wait for loading spinner to appear (it's visible as a div with class loading-spinner-container)
await waitFor(
() => {
const spinnerContainer = document.querySelector(
'.loading-spinner-container',
);
expect(spinnerContainer).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should expand percentile details when percentile value is clicked', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
// Click on the percentile value to expand details
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.click(percentileValue);
// Verify percentile details are expanded
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
// Look for the text that's actually rendered
expect(screen.getByText(/This span duration is/)).toBeInTheDocument();
expect(
screen.getByText(/out of the distribution for this resource/),
).toBeInTheDocument();
});
});
it('should display percentile table with correct values', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.click(percentileValue);
// Wait for the percentile details to expand
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
});
// Wait for the table to be visible (it might take a moment to render)
await waitFor(
() => {
expect(screen.getByText('Percentile')).toBeInTheDocument();
expect(screen.getByText('Duration')).toBeInTheDocument();
},
{ timeout: 5000 },
);
// Verify percentile values are displayed
expect(screen.getByText('p50')).toBeInTheDocument();
expect(screen.getByText('p90')).toBeInTheDocument();
expect(screen.getByText('p95')).toBeInTheDocument();
expect(screen.getByText('p99')).toBeInTheDocument();
// Verify current span row - use getAllByText since there are multiple p75 elements
expect(screen.getAllByText(P75_TEXT)).toHaveLength(3); // Should appear in value, expanded details, and table
// Verify the table has the current span indicator (there are multiple occurrences)
expect(screen.getAllByText(/this span/i).length).toBeGreaterThan(0);
});
it('should allow time range selection and trigger API call', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load and expand
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.click(percentileValue);
// Wait for percentile details to expand
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
});
// Find the time range selector and verify it exists
const timeRangeSelector = screen.getByRole('combobox');
expect(timeRangeSelector).toBeInTheDocument();
// Verify the default time range is displayed
expect(screen.getByText(/1.*hour/i)).toBeInTheDocument();
// Verify API was called with default parameters
await waitFor(() => {
expect(mockGetSpanPercentiles).toHaveBeenCalledWith(
expect.objectContaining({
start: expect.any(Number),
end: expect.any(Number),
spanDuration: mockSpan.durationNano,
serviceName: mockSpan.serviceName,
name: mockSpan.name,
resourceAttributes: expect.any(Object),
}),
);
});
});
it('should show resource attributes selector when plus icon is clicked', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load and expand
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.click(percentileValue);
// Wait for percentile details to expand
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
});
// Click the plus icon using test ID
const plusIcon = screen.getByTestId('plus-icon');
fireEvent.click(plusIcon);
// Verify resource attributes selector is shown
await waitFor(() => {
expect(
screen.getByPlaceholderText(SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER),
).toBeInTheDocument();
});
});
it('should filter resource attributes based on search query', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load and expand
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.click(percentileValue);
// Wait for percentile details to expand and show resource attributes
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
});
const plusIcon = screen.getByTestId('plus-icon');
fireEvent.click(plusIcon);
await waitFor(() => {
expect(
screen.getByPlaceholderText(SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER),
).toBeInTheDocument();
});
// Type in search query
const searchInput = screen.getByPlaceholderText(
SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER,
);
fireEvent.change(searchInput, { target: { value: 'http' } });
// Verify only matching attributes are shown (use getAllByText for all since they appear in multiple places)
expect(screen.getAllByText('http.method').length).toBeGreaterThan(0);
expect(screen.getAllByText('http.url').length).toBeGreaterThan(0);
expect(screen.getAllByText('http.status_code').length).toBeGreaterThan(0);
});
it('should handle resource attribute selection and trigger API call', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load and expand
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.click(percentileValue);
// Wait for percentile details to expand and show resource attributes
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
});
const plusIcon = screen.getByTestId('plus-icon');
fireEvent.click(plusIcon);
await waitFor(() => {
expect(
screen.getByPlaceholderText(SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER),
).toBeInTheDocument();
});
// Find and click a checkbox for a resource attribute
const httpMethodCheckbox = screen.getByRole('checkbox', {
name: /http\.method/i,
});
fireEvent.click(httpMethodCheckbox);
// Verify API was called with updated resource attributes
await waitFor(() => {
expect(mockGetSpanPercentiles).toHaveBeenCalledWith(
expect.objectContaining({
resourceAttributes: expect.objectContaining({
'http.method': 'GET',
}),
}),
);
});
});
it('should handle API error gracefully', async () => {
// Mock API error
mockGetSpanPercentiles.mockResolvedValue(mockSpanPercentileErrorResponse);
renderSpanDetailsDrawer();
// Wait for the 2-second delay
await waitFor(
() => {
// Verify no percentile value is displayed on error
expect(screen.queryByText(/p\d+/)).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should not display percentile value when API returns non-200 status', async () => {
// Mock API response with non-200 status
mockGetSpanPercentiles.mockResolvedValue(({
httpStatusCode: 500 as const,
data: null,
} as unknown) as Awaited<ReturnType<typeof getSpanPercentiles>>);
renderSpanDetailsDrawer();
// Wait for the 2-second delay
await waitFor(
() => {
// Verify no percentile value is displayed
expect(screen.queryByText(/p\d+/)).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should display tooltip with correct content', async () => {
renderSpanDetailsDrawer();
// Wait for percentile data to load
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
// Hover over the percentile value to show tooltip
const percentileValue = screen.getByText(P75_TEXT);
fireEvent.mouseEnter(percentileValue);
// Verify tooltip content - use more flexible text matching
await waitFor(() => {
expect(screen.getByText(/This span duration is/)).toBeInTheDocument();
expect(screen.getByText(/out of the distribution/)).toBeInTheDocument();
expect(
screen.getByText(/evaluated for 1 hour\(s\) since the span start time/),
).toBeInTheDocument();
expect(screen.getByText('Click to learn more')).toBeInTheDocument();
});
});
it('should handle empty percentile data gracefully', async () => {
// Mock empty percentile response
mockGetSpanPercentiles.mockResolvedValue({
httpStatusCode: 200,
data: {
percentiles: {},
position: {
percentile: 0,
description: '',
},
},
});
renderSpanDetailsDrawer();
// Wait for the 2-second delay
await waitFor(
() => {
// Verify p0 is displayed for empty data
expect(screen.getByText('p0')).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should call API with correct parameters', async () => {
renderSpanDetailsDrawer();
// Wait for API call to be made
await waitFor(
() => {
expect(mockGetSpanPercentiles).toHaveBeenCalled();
},
{ timeout: 3000 },
);
// Verify API was called with correct parameters
expect(mockGetSpanPercentiles).toHaveBeenCalledWith({
start: expect.any(Number),
end: expect.any(Number),
spanDuration: mockSpan.durationNano,
serviceName: mockSpan.serviceName,
name: mockSpan.name,
resourceAttributes: expect.any(Object),
});
});
it('should handle user preference loading', async () => {
renderSpanDetailsDrawer();
// Verify getUserPreference was called
await waitFor(() => {
expect(mockGetUserPreference).toHaveBeenCalledWith({
name: 'span_percentile_resource_attributes',
});
});
});
it('should close resource attributes selector when check icon is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderSpanDetailsDrawer();
// Wait for percentile data to load and expand
await waitFor(
() => {
expect(screen.getByText(P75_TEXT)).toBeInTheDocument();
},
{ timeout: 3000 },
);
const percentileValue = screen.getByText(P75_TEXT);
await user.click(percentileValue);
// Wait for percentile details to expand and show resource attributes
await waitFor(() => {
expect(screen.getByText(SPAN_PERCENTILE_TEXT)).toBeInTheDocument();
});
const plusIcon = screen.getByTestId('plus-icon');
await user.click(plusIcon);
await waitFor(() => {
expect(
screen.getByPlaceholderText(SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER),
).toBeInTheDocument();
});
// Click the check icon to close the selector
const checkIcon = screen.getByTestId('check-icon');
await user.click(checkIcon);
// Verify resource attributes selector is hidden
await waitFor(() => {
expect(
screen.queryByPlaceholderText(SEARCH_RESOURCE_ATTRIBUTES_PLACEHOLDER),
).not.toBeInTheDocument();
});
});
});
});
describe('SpanDetailsDrawer - Search Visibility User Flows', () => {

View File

@@ -1,169 +0,0 @@
import { Span } from 'types/api/trace/getTraceV2';
// Constants
const TEST_TRACE_ID = 'test-trace-id';
const TEST_CLUSTER_NAME = 'test-cluster';
const TEST_POD_NAME = 'test-pod-abc123';
const TEST_NODE_NAME = 'test-node-456';
const TEST_HOST_NAME = 'test-host.example.com';
// Mock span with infrastructure metadata (pod + node + host)
export const mockSpanWithInfraMetadata: Span = {
spanId: 'infra-span-id',
traceId: TEST_TRACE_ID,
// eslint-disable-next-line sonarjs/no-duplicate-string
name: 'api-service',
serviceName: 'api-service',
timestamp: 1640995200000000, // 2022-01-01 00:00:00 in microseconds
durationNano: 2000000000, // 2 seconds in nanoseconds
spanKind: 'server',
statusCodeString: 'STATUS_CODE_OK',
statusMessage: '',
parentSpanId: '',
references: [],
event: [],
tagMap: {
'k8s.cluster.name': TEST_CLUSTER_NAME,
'k8s.pod.name': TEST_POD_NAME,
'k8s.node.name': TEST_NODE_NAME,
'host.name': TEST_HOST_NAME,
'service.name': 'api-service',
'http.method': 'GET',
},
hasError: false,
rootSpanId: '',
kind: 0,
rootName: '',
hasChildren: false,
hasSibling: false,
subTreeNodeCount: 0,
level: 0,
};
// Mock span with only pod metadata
export const mockSpanWithPodOnly: Span = {
...mockSpanWithInfraMetadata,
spanId: 'pod-only-span-id',
tagMap: {
'k8s.cluster.name': TEST_CLUSTER_NAME,
'k8s.pod.name': TEST_POD_NAME,
'service.name': 'api-service',
},
};
// Mock span with only node metadata
export const mockSpanWithNodeOnly: Span = {
...mockSpanWithInfraMetadata,
spanId: 'node-only-span-id',
tagMap: {
'k8s.node.name': TEST_NODE_NAME,
'service.name': 'api-service',
},
};
// Mock span with only host metadata
export const mockSpanWithHostOnly: Span = {
...mockSpanWithInfraMetadata,
spanId: 'host-only-span-id',
tagMap: {
'host.name': TEST_HOST_NAME,
'service.name': 'api-service',
},
};
// Mock span without any infrastructure metadata
export const mockSpanWithoutInfraMetadata: Span = {
...mockSpanWithInfraMetadata,
spanId: 'no-infra-span-id',
tagMap: {
'service.name': 'api-service',
'http.method': 'GET',
'http.status_code': '200',
},
};
// Mock infrastructure metrics API responses
export const mockPodMetricsResponse = {
payload: {
data: {
newResult: {
data: {
result: [
{
metric: { pod_name: TEST_POD_NAME },
values: [
[1640995200, '0.5'], // CPU usage
[1640995260, '0.6'],
],
},
],
},
},
},
},
};
export const mockNodeMetricsResponse = {
payload: {
data: {
newResult: {
data: {
result: [
{
metric: { node_name: TEST_NODE_NAME },
values: [
[1640995200, '2.1'], // Memory usage
[1640995260, '2.3'],
],
},
],
},
},
},
},
};
export const mockEmptyMetricsResponse = {
payload: {
data: {
newResult: {
data: {
result: [],
},
},
},
},
};
// Expected infrastructure metadata extractions
export const expectedInfraMetadata = {
clusterName: TEST_CLUSTER_NAME,
podName: TEST_POD_NAME,
nodeName: TEST_NODE_NAME,
hostName: TEST_HOST_NAME,
};
export const expectedPodOnlyMetadata = {
clusterName: TEST_CLUSTER_NAME,
podName: TEST_POD_NAME,
nodeName: '',
hostName: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
spanTimestamp: '2022-01-01T00:00:00.000Z',
};
export const expectedNodeOnlyMetadata = {
clusterName: '',
podName: '',
nodeName: TEST_NODE_NAME,
hostName: '',
spanTimestamp: '2022-01-01T00:00:00.000Z',
};
export const expectedHostOnlyMetadata = {
clusterName: '',
podName: '',
nodeName: '',
hostName: TEST_HOST_NAME,
spanTimestamp: '2022-01-01T00:00:00.000Z',
};

View File

@@ -1,11 +1,11 @@
export enum RelatedSignalsViews {
LOGS = 'logs',
// METRICS = 'metrics',
INFRA = 'infra',
// INFRA = 'infra',
}
export const RELATED_SIGNALS_VIEW_TYPES = {
LOGS: RelatedSignalsViews.LOGS,
// METRICS: RelatedSignalsViews.METRICS,
INFRA: RelatedSignalsViews.INFRA,
// INFRA: RelatedSignalsViews.INFRA,
};

View File

@@ -1,22 +0,0 @@
import { Span } from 'types/api/trace/getTraceV2';
/**
* Infrastructure metadata keys that indicate infra signals are available
*/
export const INFRA_METADATA_KEYS = [
'k8s.cluster.name',
'k8s.pod.name',
'k8s.node.name',
'host.name',
] as const;
/**
* Checks if a span has any infrastructure metadata attributes
* @param span - The span to check for infrastructure metadata
* @returns true if the span has at least one infrastructure metadata key, false otherwise
*/
export function hasInfraMetadata(span: Span | undefined): boolean {
if (!span?.tagMap) return false;
return INFRA_METADATA_KEYS.some((key) => span.tagMap?.[key]);
}

View File

@@ -81,13 +81,6 @@ function TimeSeriesView({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
@@ -210,13 +203,6 @@ function TimeSeriesView({
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition: LegendPosition.BOTTOM,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
});
return (

View File

@@ -35,21 +35,21 @@ function TraceMetadata(props: ITraceMetadataProps): JSX.Element {
totalSpans,
notFound,
} = props;
const handlePreviousBtnClick = (): void => {
if (window.history.length > 1) {
history.goBack();
} else {
history.push(ROUTES.TRACES_EXPLORER);
}
};
return (
<div className="trace-metadata">
<section className="metadata-info">
<div className="first-row">
<Button className="previous-btn" onClick={handlePreviousBtnClick}>
<ArrowLeft size={14} />
<Button className="previous-btn">
<ArrowLeft
size={14}
onClick={(): void => {
if (window.history.length > 1) {
history.goBack();
} else {
history.push(ROUTES.TRACES_EXPLORER);
}
}}
/>
</Button>
<div className="trace-name">
<DraftingCompass size={14} className="drafting" />

View File

@@ -47,7 +47,6 @@ export interface GetUPlotChartOptions {
panelType?: PANEL_TYPES;
onDragSelect?: (startTime: number, endTime: number) => void;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
onClickHandler?: OnClickPluginOpts['onClick'];
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates?: FullViewProps['setGraphsVisibilityStates'];
@@ -193,7 +192,6 @@ export const getUPlotChartOptions = ({
apiResponse,
onDragSelect,
yAxisUnit,
decimalPrecision,
minTimeScale,
maxTimeScale,
onClickHandler = _noop,
@@ -361,7 +359,6 @@ export const getUPlotChartOptions = ({
colorMapping,
customTooltipElement,
query: query || currentQuery,
decimalPrecision,
}),
onClickPlugin({
onClick: onClickHandler,
@@ -700,27 +697,7 @@ export const getUPlotChartOptions = ({
}
};
requestAnimationFrame(() => {
const currentMarkerElement = thElement.querySelector(
'.u-marker',
) as HTMLElement;
if (currentMarkerElement) {
currentMarkerElement.classList.add('u-marker-clickable');
currentMarkerElement.addEventListener(
'click',
markerClickHandler,
false,
);
currentMarkerElement.addEventListener(
'mousedown',
(e) => {
e.preventDefault();
markerClickHandler(e);
},
false,
);
}
});
currentMarker.addEventListener('click', markerClickHandler);
// Store cleanup function for marker click listener
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {
@@ -730,7 +707,6 @@ export const getUPlotChartOptions = ({
// Text click handler - show only/show all behavior (existing behavior)
if (textElement) {
// Create the click handler function
const textClickHandler = (e: Event): void => {
e.stopPropagation?.(); // Prevent event bubbling
@@ -764,45 +740,7 @@ export const getUPlotChartOptions = ({
}
};
// Use requestAnimationFrame to ensure DOM is fully ready
requestAnimationFrame(() => {
// Re-query the element to ensure we have the current DOM element
const currentTextElement = thElement.querySelector(
'.legend-text',
) as HTMLElement;
if (currentTextElement) {
// Force the element to be clickable
currentTextElement.style.cursor = 'pointer';
currentTextElement.style.pointerEvents = 'auto';
// Add multiple event listeners to ensure we catch the click
currentTextElement.addEventListener(
'click',
textClickHandler,
false,
);
currentTextElement.addEventListener(
'mousedown',
(e) => {
e.preventDefault();
textClickHandler(e);
},
false,
);
// Also add to the parent th element as a fallback
thElement.addEventListener(
'click',
(e) => {
if (e.target === currentTextElement) {
textClickHandler();
}
},
false,
);
}
});
textElement.addEventListener('click', textClickHandler);
// Store cleanup function for text click listener
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {

View File

@@ -17,11 +17,6 @@ import { drawStyles } from './utils/constants';
import { generateColor } from './utils/generateColor';
import getAxes from './utils/getAxes';
// Extended uPlot interface with custom properties
interface ExtendedUPlot extends uPlot {
_legendScrollCleanup?: () => void;
}
type GetUplotHistogramChartOptionsProps = {
id?: string;
apiResponse?: MetricRangePayloadProps;
@@ -35,8 +30,6 @@ type GetUplotHistogramChartOptionsProps = {
setGraphsVisibilityStates?: Dispatch<SetStateAction<boolean[]>>;
mergeAllQueries?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
legendScrollPosition?: number;
setLegendScrollPosition?: (position: number) => void;
};
type GetHistogramSeriesProps = {
@@ -131,8 +124,6 @@ export const getUplotHistogramChartOptions = ({
mergeAllQueries,
onClickHandler = _noop,
panelType,
legendScrollPosition,
setLegendScrollPosition,
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
({
id,
@@ -188,94 +179,33 @@ export const getUplotHistogramChartOptions = ({
(self): void => {
const legend = self.root.querySelector('.u-legend');
if (legend) {
const legendElement = legend as HTMLElement;
// Enhanced legend scroll position preservation
if (setLegendScrollPosition && typeof legendScrollPosition === 'number') {
const handleScroll = (): void => {
setLegendScrollPosition(legendElement.scrollTop);
};
// Add scroll event listener to save position
legendElement.addEventListener('scroll', handleScroll);
// Restore scroll position
requestAnimationFrame(() => {
legendElement.scrollTop = legendScrollPosition;
});
// Store cleanup function
const extSelf = self as ExtendedUPlot;
extSelf._legendScrollCleanup = (): void => {
legendElement.removeEventListener('scroll', handleScroll);
};
}
const seriesEls = legend.querySelectorAll('.u-series');
const seriesArray = Array.from(seriesEls);
seriesArray.forEach((seriesEl, index) => {
// Add click handlers for marker and text separately
const thElement = seriesEl.querySelector('th');
if (thElement) {
const currentMarker = thElement.querySelector('.u-marker');
const textElement =
thElement.querySelector('.legend-text') || thElement;
// Marker click handler - checkbox behavior (toggle individual series)
if (currentMarker) {
currentMarker.addEventListener('click', (e) => {
e.stopPropagation?.(); // Prevent event bubbling to text handler
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
// Toggle the specific series visibility (checkbox behavior)
newGraphVisibilityStates[index + 1] = !newGraphVisibilityStates[
index + 1
];
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
seriesEl.addEventListener('click', () => {
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
if (
newGraphVisibilityStates[index + 1] &&
newGraphVisibilityStates.every((value, i) =>
i === index + 1 ? value : !value,
)
) {
newGraphVisibilityStates.fill(true);
} else {
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[index + 1] = true;
}
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
}
// Text click handler - show only/show all behavior (existing behavior)
textElement.addEventListener('click', (e) => {
e.stopPropagation?.(); // Prevent event bubbling
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
// Show only this series / show all behavior
if (
newGraphVisibilityStates[index + 1] &&
newGraphVisibilityStates.every((value, i) =>
i === index + 1 ? value : !value,
)
) {
// If only this series is visible, show all
newGraphVisibilityStates.fill(true);
} else {
// Otherwise, show only this series
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[index + 1] = true;
}
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
}
});
}
});
});
}
},

View File

@@ -1,4 +1,4 @@
import { getToolTipValue, PrecisionOption } from 'components/Graph/yAxisConfig';
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { themeColors } from 'constants/theme';
import dayjs from 'dayjs';
@@ -44,7 +44,6 @@ const generateTooltipContent = (
idx: number,
isDarkMode: boolean,
yAxisUnit?: string,
decimalPrecision?: PrecisionOption,
series?: uPlot.Options['series'],
isBillingUsageGraphs?: boolean,
isHistogramGraphs?: boolean,
@@ -128,7 +127,7 @@ const generateTooltipContent = (
let tooltipItemLabel = label;
if (Number.isFinite(value)) {
const tooltipValue = getToolTipValue(value, yAxisUnit, decimalPrecision);
const tooltipValue = getToolTipValue(value, yAxisUnit);
const dataIngestedFormated = getToolTipValue(dataIngested);
if (
duplicatedLegendLabels[label] ||
@@ -240,7 +239,6 @@ type ToolTipPluginProps = {
isBillingUsageGraphs?: boolean;
isHistogramGraphs?: boolean;
isMergedSeries?: boolean;
decimalPrecision?: PrecisionOption;
stackBarChart?: boolean;
isDarkMode: boolean;
customTooltipElement?: HTMLDivElement;
@@ -261,7 +259,6 @@ const tooltipPlugin = ({
timezone,
colorMapping,
query,
decimalPrecision,
}: // eslint-disable-next-line sonarjs/cognitive-complexity
ToolTipPluginProps): any => {
let over: HTMLElement;
@@ -323,7 +320,6 @@ ToolTipPluginProps): any => {
idx,
isDarkMode,
yAxisUnit,
decimalPrecision,
u.series,
isBillingUsageGraphs,
isHistogramGraphs,

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { getToolTipValue, PrecisionOption } from 'components/Graph/yAxisConfig';
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { uPlotXAxisValuesFormat } from './constants';
@@ -18,13 +18,11 @@ const getAxes = ({
yAxisUnit,
panelType,
isLogScale,
decimalPrecision,
}: {
isDarkMode: boolean;
yAxisUnit?: string;
panelType?: PANEL_TYPES;
isLogScale?: boolean;
decimalPrecision?: PrecisionOption;
// eslint-disable-next-line sonarjs/cognitive-complexity
}): any => [
{
@@ -63,7 +61,7 @@ const getAxes = ({
if (v === null || v === undefined || Number.isNaN(v)) {
return '';
}
const value = getToolTipValue(v.toString(), yAxisUnit, decimalPrecision);
const value = getToolTipValue(v.toString(), yAxisUnit);
return `${value}`;
}),
gap: 5,

View File

@@ -183,6 +183,8 @@ describe('Logs Explorer Tests', () => {
>
<QueryBuilderContext.Provider
value={{
currentFilterExpression: {},
handleSetCurrentFilterExpression: noop,
isDefaultQuery: (): boolean => false,
currentQuery: {
...initialQueriesMap.metrics,

View File

@@ -65,6 +65,7 @@ import { sanitizeOrderByForExplorer } from 'utils/sanitizeOrderBy';
import { v4 as uuid } from 'uuid';
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
currentFilterExpression: {},
currentQuery: initialQueriesMap.metrics,
supersetQuery: initialQueriesMap.metrics,
lastUsedQuery: null,
@@ -74,6 +75,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
initialDataSource: null,
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
handleSetCurrentFilterExpression: () => {},
handleSetQueryData: () => {},
handleSetTraceOperatorData: () => {},
handleSetFormulaData: () => {},
@@ -133,6 +135,9 @@ export function QueryBuilderProvider({
const [supersetQuery, setSupersetQuery] = useState<QueryState>(queryState);
const [lastUsedQuery, setLastUsedQuery] = useState<number | null>(0);
const [stagedQuery, setStagedQuery] = useState<Query | null>(null);
const [currentFilterExpression, setCurrentFilterExpression] = useState<
Record<string, string | undefined>
>({});
const [queryType, setQueryType] = useState<EQueryType>(queryTypeParam);
@@ -257,6 +262,7 @@ export function QueryBuilderProvider({
setStagedQuery(nextQuery);
setCurrentQuery(newQueryState);
setCurrentFilterExpression({});
setQueryType(type);
},
[prepareQueryBuilderData],
@@ -386,6 +392,16 @@ export function QueryBuilderProvider({
[],
);
const handleSetCurrentFilterExpression = useCallback(
(expression: string, queryName: string) => {
setCurrentFilterExpression((prevState) => ({
...prevState,
[queryName]: expression,
}));
},
[],
);
const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => {
@@ -1027,9 +1043,10 @@ export function QueryBuilderProvider({
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
currentFilterExpression[item.queryName] ??
(item.filter?.expression?.trim() === ''
? ''
: item.filter?.expression ?? '',
: item.filter?.expression ?? ''),
},
filters: {
items: [],
@@ -1053,7 +1070,13 @@ export function QueryBuilderProvider({
},
queryType,
});
}, [currentQuery, location.pathname, queryType, redirectWithQueryBuilderData]);
}, [
currentQuery,
location.pathname,
queryType,
redirectWithQueryBuilderData,
currentFilterExpression,
]);
useEffect(() => {
if (location.pathname !== currentPathnameRef.current) {
@@ -1141,6 +1164,7 @@ export function QueryBuilderProvider({
const contextValues: QueryBuilderContextType = useMemo(
() => ({
currentFilterExpression,
currentQuery: query,
supersetQuery: superQuery,
lastUsedQuery,
@@ -1150,6 +1174,7 @@ export function QueryBuilderProvider({
initialDataSource,
panelType,
isEnabledQuery,
handleSetCurrentFilterExpression,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
@@ -1175,6 +1200,7 @@ export function QueryBuilderProvider({
isStagedQueryUpdated,
}),
[
currentFilterExpression,
query,
superQuery,
lastUsedQuery,
@@ -1182,6 +1208,7 @@ export function QueryBuilderProvider({
initialDataSource,
panelType,
isEnabledQuery,
handleSetCurrentFilterExpression,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,

View File

@@ -74,12 +74,6 @@ body {
.u-marker {
border-radius: 50%;
// Clickable marker styles
&.u-marker-clickable {
cursor: pointer;
pointer-events: auto;
}
}
}

View File

@@ -1,4 +1,3 @@
import { PrecisionOption } from 'components/Graph/yAxisConfig';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
@@ -114,7 +113,6 @@ export interface IBaseWidget {
timePreferance: timePreferenceType;
stepSize?: number;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption; // number of decimals or 'full precision'
stackedBarChart?: boolean;
bucketCount?: number;
bucketWidth?: number;

View File

@@ -1,21 +0,0 @@
export interface GetSpanPercentilesProps {
start: number;
end: number;
spanDuration: number;
serviceName: string;
name: string;
resourceAttributes: Record<string, string>;
}
export interface GetSpanPercentilesResponseDataProps {
percentiles: Record<string, number>;
position: {
percentile: number;
description: string;
};
}
export interface GetSpanPercentilesResponsePayloadProps {
status: string;
data: GetSpanPercentilesResponseDataProps;
}

View File

@@ -227,6 +227,7 @@ export type QueryBuilderData = {
};
export type QueryBuilderContextType = {
currentFilterExpression: Record<string, string | undefined>;
currentQuery: Query;
stagedQuery: Query | null;
lastUsedQuery: number | null;
@@ -241,6 +242,10 @@ export type QueryBuilderContextType = {
index: number,
traceOperatorData: IBuilderTraceOperator,
) => void;
handleSetCurrentFilterExpression: (
expression: string,
queryName: string,
) => void;
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
handleSetQueryItemData: (
index: number,

View File

@@ -6369,13 +6369,13 @@ axe-core@^4.6.2:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz"
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
axios@1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.0.tgz#11248459be05a5ee493485628fa0e4323d0abfc3"
integrity sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==
axios@1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.1.1:
@@ -9677,7 +9677,7 @@ force-graph@1:
kapsule "^1.14"
lodash-es "4"
form-data@4.0.4, form-data@^3.0.0, form-data@^4.0.4:
form-data@4.0.4, form-data@^3.0.0, form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==

View File

@@ -2,6 +2,7 @@ package alertmanagerbatcher
import (
"context"
"io"
"log/slog"
"testing"
@@ -10,7 +11,7 @@ import (
)
func TestBatcherWithOneAlertAndDefaultConfigs(t *testing.T) {
batcher := New(slog.New(slog.DiscardHandler), NewConfig())
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), NewConfig())
_ = batcher.Start(context.Background())
batcher.Add(context.Background(), &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
@@ -24,7 +25,7 @@ func TestBatcherWithOneAlertAndDefaultConfigs(t *testing.T) {
}
func TestBatcherWithBatchSize(t *testing.T) {
batcher := New(slog.New(slog.DiscardHandler), Config{Size: 2, Capacity: 4})
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
_ = batcher.Start(context.Background())
var alerts alertmanagertypes.PostableAlerts
@@ -44,7 +45,7 @@ func TestBatcherWithBatchSize(t *testing.T) {
}
func TestBatcherWithCClosed(t *testing.T) {
batcher := New(slog.New(slog.DiscardHandler), Config{Size: 2, Capacity: 4})
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
_ = batcher.Start(context.Background())
var alerts alertmanagertypes.PostableAlerts

View File

@@ -2,14 +2,14 @@ package alertmanagerserver
import (
"context"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/prometheus/alertmanager/dispatch"
"io"
"log/slog"
"net/http"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/prometheus/alertmanager/dispatch"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/nfroutingstoretest"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
@@ -89,7 +89,7 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
srvCfg := NewConfig()
stateStore := alertmanagertypestest.NewStateStore()
registry := prometheus.NewRegistry()
logger := slog.New(slog.DiscardHandler)
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, orgID)

View File

@@ -3,6 +3,7 @@ package alertmanagerserver
import (
"bytes"
"context"
"io"
"log/slog"
"net"
"net/http"
@@ -25,7 +26,7 @@ import (
func TestServerSetConfigAndStop(t *testing.T) {
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{GroupInterval: 1 * time.Minute, RepeatInterval: 1 * time.Minute, GroupWait: 1 * time.Minute}, "1")
@@ -37,7 +38,7 @@ func TestServerSetConfigAndStop(t *testing.T) {
func TestServerTestReceiverTypeWebhook(t *testing.T) {
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{GroupInterval: 1 * time.Minute, RepeatInterval: 1 * time.Minute, GroupWait: 1 * time.Minute}, "1")
@@ -85,7 +86,7 @@ func TestServerPutAlerts(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
@@ -133,7 +134,7 @@ func TestServerTestAlert(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
@@ -238,7 +239,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")

View File

@@ -2,6 +2,7 @@ package factory
import (
"context"
"io"
"log/slog"
"sync"
"testing"
@@ -32,7 +33,7 @@ func TestRegistryWith2Services(t *testing.T) {
s1 := newTestService(t)
s2 := newTestService(t)
registry, err := NewRegistry(slog.New(slog.DiscardHandler), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
registry, err := NewRegistry(slog.New(slog.NewTextHandler(io.Discard, nil)), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -53,7 +54,7 @@ func TestRegistryWith2ServicesWithoutWait(t *testing.T) {
s1 := newTestService(t)
s2 := newTestService(t)
registry, err := NewRegistry(slog.New(slog.DiscardHandler), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
registry, err := NewRegistry(slog.New(slog.NewTextHandler(io.Discard, nil)), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
require.NoError(t, err)
ctx := context.Background()

View File

@@ -1,6 +1,7 @@
package middleware
import (
"io"
"log/slog"
"net"
"net/http"
@@ -16,7 +17,7 @@ func TestTimeout(t *testing.T) {
writeTimeout := 6 * time.Second
defaultTimeout := 2 * time.Second
maxTimeout := 4 * time.Second
m := NewTimeout(slog.New(slog.DiscardHandler), []string{"/excluded"}, defaultTimeout, maxTimeout)
m := NewTimeout(slog.New(slog.NewTextHandler(io.Discard, nil)), []string{"/excluded"}, defaultTimeout, maxTimeout)
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)

View File

@@ -1,6 +1,7 @@
package instrumentationtest
import (
"io"
"log/slog"
"github.com/SigNoz/signoz/pkg/factory"
@@ -20,7 +21,7 @@ type noopInstrumentation struct {
func New() instrumentation.Instrumentation {
return &noopInstrumentation{
logger: slog.New(slog.DiscardHandler),
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
meterProvider: noopmetric.NewMeterProvider(),
tracerProvider: nooptrace.NewTracerProvider(),
}

View File

@@ -374,12 +374,18 @@ func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opt
return existingUser, nil
}
err = module.CreateUser(ctx, user, opts...)
newUser, err := types.NewUser(user.DisplayName, user.Email, user.Role, user.OrgID)
if err != nil {
return nil, err
}
return user, nil
err = module.CreateUser(ctx, newUser, opts...)
if err != nil {
return nil, err
}
return newUser, nil
}
func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error {

View File

@@ -1860,7 +1860,6 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri
response.DefaultTTLDays = 15
response.TTLConditions = []model.CustomRetentionRule{}
response.Status = constants.StatusFailed
response.ColdStorageTTLDays = -1
return response, nil
}
@@ -1895,7 +1894,6 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri
response.ExpectedLogsTime = ttlResult.ExpectedLogsTime
response.ExpectedLogsMoveTime = ttlResult.ExpectedLogsMoveTime
response.Status = ttlResult.Status
response.ColdStorageTTLDays = -1
if ttlResult.LogsTime > 0 {
response.DefaultTTLDays = ttlResult.LogsTime / 24
}

View File

@@ -2,6 +2,7 @@ package signoz
import (
"context"
"io"
"log/slog"
"testing"
@@ -12,7 +13,7 @@ import (
// This is a test to ensure that all fields of config implement the factory.Config interface and are valid with
// their default values.
func TestValidateConfig(t *testing.T) {
logger := slog.New(slog.DiscardHandler)
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
_, err := NewConfig(context.Background(), logger, configtest.NewResolverConfig(), DeprecatedFlags{})
assert.NoError(t, err)
}

View File

@@ -103,7 +103,7 @@ func (r BasicRuleThresholds) ShouldAlert(series v3.Series, unit string) (Vector,
for _, threshold := range thresholds {
smpl, shouldAlert := threshold.shouldAlert(series, unit)
if shouldAlert {
smpl.Target = *threshold.TargetValue
smpl.Target = threshold.target(unit)
smpl.TargetUnit = threshold.TargetUnit
resultVector = append(resultVector, smpl)
}

View File

@@ -1,294 +0,0 @@
package ruletypes
import (
"testing"
"github.com/stretchr/testify/assert"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)
func TestBasicRuleThresholdShouldAlert_UnitConversion(t *testing.T) {
target := 100.0
tests := []struct {
name string
threshold BasicRuleThreshold
series v3.Series
ruleUnit string
shouldAlert bool
}{
{
name: "milliseconds to seconds conversion - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100ms
TargetUnit: "ms",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.15, Timestamp: 1000}, // 150ms in seconds
},
},
ruleUnit: "s",
shouldAlert: true,
},
{
name: "milliseconds to seconds conversion - should not alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100ms
TargetUnit: "ms",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.05, Timestamp: 1000}, // 50ms in seconds
},
},
ruleUnit: "s",
shouldAlert: false,
},
{
name: "seconds to milliseconds conversion - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100s
TargetUnit: "s",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 150000, Timestamp: 1000}, // 150000ms = 150s
},
},
ruleUnit: "ms",
shouldAlert: true,
},
// Binary byte conversions
{
name: "bytes to kibibytes conversion - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100 bytes
TargetUnit: "bytes",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.15, Timestamp: 1000}, // 0.15KiB ≈ 153.6 bytes
},
},
ruleUnit: "kbytes",
shouldAlert: true,
},
{
name: "kibibytes to mebibytes conversion - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100KiB
TargetUnit: "kbytes",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.15, Timestamp: 1000},
},
},
ruleUnit: "mbytes",
shouldAlert: true,
},
// ValueIsBelow with unit conversion
{
name: "milliseconds to seconds with ValueIsBelow - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100ms
TargetUnit: "ms",
MatchType: AtleastOnce,
CompareOp: ValueIsBelow,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.05, Timestamp: 1000}, // 50ms in seconds
},
},
ruleUnit: "s",
shouldAlert: true,
},
{
name: "milliseconds to seconds with OnAverage - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100ms
TargetUnit: "ms",
MatchType: OnAverage,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.08, Timestamp: 1000}, // 80ms
{Value: 0.12, Timestamp: 2000}, // 120ms
{Value: 0.15, Timestamp: 3000}, // 150ms
},
},
ruleUnit: "s",
shouldAlert: true,
},
{
name: "decimal megabytes to gigabytes with InTotal - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100MB
TargetUnit: "decmbytes",
MatchType: InTotal,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.04, Timestamp: 1000}, // 40MB
{Value: 0.05, Timestamp: 2000}, // 50MB
{Value: 0.03, Timestamp: 3000}, // 30MB
},
},
ruleUnit: "decgbytes",
shouldAlert: true,
},
{
name: "milliseconds to seconds with AllTheTimes - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100ms
TargetUnit: "ms",
MatchType: AllTheTimes,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.11, Timestamp: 1000}, // 110ms
{Value: 0.12, Timestamp: 2000}, // 120ms
{Value: 0.15, Timestamp: 3000}, // 150ms
},
},
ruleUnit: "s",
shouldAlert: true,
},
{
name: "kilobytes to megabytes with Last - should not alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100kB
TargetUnit: "deckbytes",
MatchType: Last,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.15, Timestamp: 1000}, // 150kB
{Value: 0.05, Timestamp: 2000}, // 50kB (last value)
},
},
ruleUnit: "decmbytes",
shouldAlert: false,
},
// Mixed units - bytes/second rate conversions
{
name: "bytes per second to kilobytes per second - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100 bytes/s
TargetUnit: "Bps",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 0.15, Timestamp: 1000},
},
},
ruleUnit: "KBs",
shouldAlert: true,
},
// Same unit (no conversion needed)
{
name: "same unit - no conversion needed - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100ms
TargetUnit: "ms",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 150, Timestamp: 1000}, // 150ms
},
},
ruleUnit: "ms",
shouldAlert: true,
},
// Empty unit (unitless) - no conversion
{
name: "empty unit - no conversion - should alert",
threshold: BasicRuleThreshold{
Name: "test",
TargetValue: &target, // 100 (unitless)
TargetUnit: "",
MatchType: AtleastOnce,
CompareOp: ValueIsAbove,
},
series: v3.Series{
Labels: map[string]string{"service": "test"},
Points: []v3.Point{
{Value: 150, Timestamp: 1000}, // 150 (unitless)
},
},
ruleUnit: "",
shouldAlert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
thresholds := BasicRuleThresholds{tt.threshold}
vector, err := thresholds.ShouldAlert(tt.series, tt.ruleUnit)
assert.NoError(t, err)
alert := len(vector) > 0
assert.Equal(t, tt.shouldAlert, alert)
if tt.shouldAlert && alert {
sample := vector[0]
hasThresholdLabel := false
for _, label := range sample.Metric {
if label.Name == LabelThresholdName && label.Value == "test" {
hasThresholdLabel = true
break
}
}
assert.True(t, hasThresholdLabel)
assert.Equal(t, *tt.threshold.TargetValue, sample.Target)
assert.Equal(t, tt.threshold.TargetUnit, sample.TargetUnit)
}
})
}
}

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