Compare commits

..

1 Commits

Author SHA1 Message Date
nikhilmantri0902
e8e0f9a9d3 fix: added promql changes 2025-08-21 20:50:19 +05:30
459 changed files with 5864 additions and 18524 deletions

View File

@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.129.2
image: signoz/signoz-schema-migrator:v0.129.0
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.129.2
image: signoz/signoz-schema-migrator:v0.129.0
container_name: schema-migrator-async
command:
- async

4
.github/CODEOWNERS vendored
View File

@@ -42,7 +42,3 @@
/pkg/telemetrymetadata/ @srikanthccv
/pkg/telemetrymetrics/ @srikanthccv
/pkg/telemetrytraces/ @srikanthccv
# AuthN / AuthZ Owners
/pkg/authz/ @vikrantgupta25 @grandwizard28

View File

@@ -62,7 +62,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
GO_NAME: signoz-community
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build

View File

@@ -93,7 +93,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -92,7 +92,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -18,7 +18,7 @@ jobs:
with:
PRIMUS_REF: main
GO_TEST_CONTEXT: ./...
GO_VERSION: 1.24
GO_VERSION: 1.23
fmt:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -27,7 +27,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
lint:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -36,7 +36,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
deps:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -45,7 +45,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.23
build:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
@@ -57,7 +57,7 @@ jobs:
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: qemu-install
uses: docker/setup-qemu-action@v3
- name: aarch64-install

View File

@@ -58,7 +58,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -122,7 +122,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
# copy the caches from build
- name: get-sha

View File

@@ -72,7 +72,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -135,7 +135,7 @@ jobs:
- name: setup-go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
# copy the caches from build
- name: get-sha

2
.gitignore vendored
View File

@@ -86,8 +86,6 @@ queries.active
.devenv/**/tmp/**
.qodo
.dev
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@@ -2,11 +2,10 @@ FROM node:18-bullseye AS build
WORKDIR /opt/
COPY ./frontend/ ./
ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 yarn install
RUN CI=1 yarn build
FROM golang:1.24-bullseye
FROM golang:1.23-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.93.0
image: signoz/signoz:v0.92.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -207,7 +207,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.129.2
image: signoz/signoz-otel-collector:v0.129.0
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -231,7 +231,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.129.2
image: signoz/signoz-schema-migrator:v0.129.0
deploy:
restart_policy:
condition: on-failure

View File

@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.93.0
image: signoz/signoz:v0.92.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -148,7 +148,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.129.2
image: signoz/signoz-otel-collector:v0.129.0
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -174,7 +174,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.129.2
image: signoz/signoz-schema-migrator:v0.129.0
deploy:
restart_policy:
condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.93.0}
image: signoz/signoz:${VERSION:-v0.92.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -211,7 +211,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -237,7 +237,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-sync
command:
- sync
@@ -248,7 +248,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.93.0}
image: signoz/signoz:${VERSION:-v0.92.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -165,7 +165,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-sync
command:
- sync
@@ -177,7 +177,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-async
command:
- async

View File

@@ -13,11 +13,11 @@ import (
"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"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
@@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
))
}
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
password, err := types.NewFactorPassword(uuid.NewString())
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
return newUser, nil
return integrationUser, nil
}
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (

View File

@@ -1,7 +1,7 @@
package model
import (
"errors"
"fmt"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
)
@@ -57,7 +57,7 @@ func Unauthorized(err error) *ApiError {
func BadRequestStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorBadData,
Err: errors.New(s),
Err: fmt.Errorf(s),
}
}
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
func InternalErrorStr(s string) *ApiError {
return &ApiError{
Typ: basemodel.ErrorInternal,
Err: errors.New(s),
Err: fmt.Errorf(s),
}
}

View File

@@ -1,5 +1,4 @@
node_modules
build
*.typegen.ts
i18-generate-hash.js
src/parser/TraceOperatorParser/**
i18-generate-hash.js

View File

@@ -10,6 +10,4 @@ public/
**/*.json
# Ignore all files in parser folder:
src/parser/**
src/TraceOperator/parser/**
src/parser/**

View File

@@ -16,7 +16,6 @@ const config: Config.InitialOptions = {
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
@@ -26,7 +25,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -43,19 +43,11 @@
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
@@ -100,7 +92,6 @@
"i18next-http-backend": "^1.3.2",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"kbar": "0.1.0-beta.48",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",

View File

@@ -4,7 +4,6 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import AppLoading from 'components/AppLoading/AppLoading';
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
@@ -26,7 +25,6 @@ import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
@@ -370,42 +368,39 @@ function App(): JSX.Element {
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<KBarCommandPaletteProvider>
<UserpilotRouteTracker />
<KBarCommandPalette />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</KBarCommandPaletteProvider>
<UserpilotRouteTracker />
<NotificationProvider>
<ErrorModalProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>

View File

@@ -1,25 +0,0 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/settings/getRetention';
// Only works for logs
const getRetentionV2 = async (): Promise<
SuccessResponseV2<PayloadProps<'logs'>>
> => {
try {
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
`/settings/ttl`,
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getRetentionV2;

View File

@@ -1,14 +1,14 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/settings/setRetention';
const setRetention = async (
props: Props,
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadPropsV2>(
const response = await axios.post<PayloadProps>(
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
@@ -17,11 +17,13 @@ const setRetention = async (
);
return {
httpStatusCode: response.status,
data: response.data,
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,32 +0,0 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
const setRetentionV2 = async ({
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
try {
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default setRetentionV2;

View File

@@ -1,284 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,
QueryBuilderFormula,
QueryRangeRequestV5,
QueryRangeResponseV5,
RequestType,
ScalarData,
TelemetryFieldKey,
TimeSeries,
TimeSeriesData,
TimeSeriesValue,
} from 'types/api/v5/queryRange';
import { convertV5ResponseToLegacy } from './convertV5Response';
describe('convertV5ResponseToLegacy', () => {
function makeBaseSuccess<T>(
payload: T,
params: QueryRangeRequestV5,
): SuccessResponse<T, QueryRangeRequestV5> {
return {
statusCode: 200,
message: 'success',
payload,
error: null,
params,
};
}
function makeBaseParams(
requestType: RequestType,
queries: QueryRangeRequestV5['compositeQuery']['queries'],
): QueryRangeRequestV5 {
return {
schemaVersion: 'v1',
start: 1,
end: 2,
requestType,
compositeQuery: { queries },
variables: {},
formatOptions: { formatTableResultForUI: false, fillGaps: false },
};
}
it('converts time_series response into legacy series structure', () => {
const timeSeries: TimeSeriesData = {
queryName: 'A',
aggregations: [
{
index: 0,
alias: '__result_0',
meta: {},
series: [
({
labels: [
{
key: ({ name: 'service.name' } as unknown) as TelemetryFieldKey,
value: 'adservice',
},
],
values: [
({ timestamp: 1000, value: 10 } as unknown) as TimeSeriesValue,
({ timestamp: 2000, value: 12 } as unknown) as TimeSeriesValue,
],
} as unknown) as TimeSeries,
],
},
],
};
const v5Data: QueryRangeResponseV5 = {
type: 'time_series',
data: { results: [timeSeries] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('time_series', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [{ expression: 'count()' }],
},
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}' };
const result = convertV5ResponseToLegacy(input, legendMap, false);
expect(result.payload.data.resultType).toBe('time_series');
expect(result.payload.data.result).toHaveLength(1);
const q = result.payload.data.result[0];
expect(q.queryName).toBe('A');
expect(q.legend).toBe('{{service.name}}');
expect(q.series?.[0]).toEqual(
expect.objectContaining({
labels: { 'service.name': 'adservice' },
values: [
{ timestamp: 1000, value: '10' },
{ timestamp: 2000, value: '12' },
],
metaData: expect.objectContaining({
alias: '__result_0',
index: 0,
queryName: 'A',
}),
}),
);
});
it('converts scalar to legacy table (formatForWeb=false) with names/ids resolved from aggregations', () => {
const scalar: ScalarData = {
columns: [
// group column
({
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as unknown) as ScalarData['columns'][number],
// aggregation 0
({
name: '__result_0',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
// aggregation 1
({
name: '__result_1',
queryName: 'A',
aggregationIndex: 1,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
// formula F1
({
name: '__result',
queryName: 'F1',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown) as ScalarData['columns'][number],
],
data: [['adservice', 606, 1.452, 151.5]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('scalar', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [
{ expression: 'count()' },
{ expression: 'avg(app.ads.count)', alias: 'avg' },
],
},
},
{
type: 'builder_formula',
spec: ({
name: 'F1',
expression: 'A * 0.25',
} as unknown) as QueryBuilderFormula,
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}', F1: '' };
const result = convertV5ResponseToLegacy(input, legendMap, false);
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{ name: 'count()', queryName: 'A', isValueColumn: true, id: 'A.count()' },
{
name: 'avg',
queryName: 'A',
isValueColumn: true,
id: 'A.avg(app.ads.count)',
},
{ name: 'F1', queryName: 'F1', isValueColumn: true, id: 'F1' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
'A.count()': 606,
'A.avg(app.ads.count)': 1.452,
F1: 151.5,
},
});
});
it('converts scalar with formatForWeb=true to UI-friendly table', () => {
const scalar: ScalarData = {
columns: [
{
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as any,
{
name: '__result_0',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as any,
],
data: [['adservice', 580]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
};
const params = makeBaseParams('scalar', [
{
type: 'builder_query',
spec: {
name: 'A',
signal: 'traces',
stepInterval: 60,
disabled: false,
aggregations: [{ expression: 'count()' }],
},
},
]);
const input: SuccessResponse<
MetricRangePayloadV5,
QueryRangeRequestV5
> = makeBaseSuccess({ data: v5Data }, params);
const legendMap = { A: '{{service.name}}' };
const result = convertV5ResponseToLegacy(input, legendMap, true);
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
// Single aggregation: name resolves to legend, id resolves to queryName
{ name: '{{service.name}}', queryName: 'A', isValueColumn: true, id: 'A' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
data: {
'service.name': 'adservice',
A: 580,
},
});
});
});

View File

@@ -1,637 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string, simple-import-sort/imports, @typescript-eslint/indent, no-mixed-spaces-and-tabs */
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
ClickHouseQuery,
LogAggregation,
LogBuilderQuery,
MetricBuilderQuery,
PromQuery,
QueryBuilderFormula as V5QueryBuilderFormula,
QueryEnvelope,
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
jest.mock('lib/getStartEndRangeTime', () => ({
__esModule: true,
default: jest.fn(() => ({ start: '100', end: '200' })),
}));
describe('prepareQueryRangePayloadV5', () => {
const start = 1_710_000_000; // seconds
const end = 1_710_000_600; // seconds
const baseBuilderQuery = (
overrides?: Partial<IBuilderQuery>,
): IBuilderQuery => ({
queryName: 'A',
dataSource: DataSource.METRICS,
aggregations: [
{
metricName: 'cpu_usage',
temporality: '',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: 'avg',
},
],
timeAggregation: 'sum',
spaceAggregation: 'avg',
temporality: '',
functions: [
{
name: 'timeShift',
args: [{ value: '5m' }],
},
],
filter: { expression: '' },
filters: { items: [], op: 'AND' },
groupBy: [],
expression: 'A',
disabled: false,
having: [],
limit: null,
stepInterval: 600,
orderBy: [],
reduceTo: 'avg',
legend: 'Legend A',
...overrides,
});
const baseFormula = (
overrides?: Partial<IBuilderFormula>,
): IBuilderFormula => ({
expression: 'A + 1',
disabled: false,
queryName: 'F1',
legend: 'Formula Legend',
limit: undefined,
having: [],
stepInterval: undefined,
orderBy: [],
...overrides,
});
it('builds payload for builder queries with formulas and variables', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q1',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [baseFormula()],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
start,
end,
variables: { svc: 'api', count: 5, flag: true },
fillGaps: true,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A', F1: 'Formula Legend' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: expect.arrayContaining([
expect.objectContaining({
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'metrics',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: [
expect.objectContaining({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: undefined,
}),
],
}),
}),
expect.objectContaining({
type: 'builder_formula',
spec: expect.objectContaining({
name: 'F1',
expression: 'A + 1',
legend: 'Formula Legend',
}),
}),
]),
}),
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: true,
}),
start: start * 1000,
end: end * 1000,
variables: expect.objectContaining({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
}),
}),
}),
);
// Legend map combines builder and formulas
expect(result.legendMap).toEqual({ A: 'Legend A', F1: 'Formula Legend' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.schemaVersion).toBe('v1');
expect(payload.start).toBe(start * 1000);
expect(payload.end).toBe(end * 1000);
expect(payload.requestType).toBe('time_series');
expect(payload.formatOptions?.formatTableResultForUI).toBe(false);
expect(payload.formatOptions?.fillGaps).toBe(true);
// Variables mapped as { key: { value } }
expect(payload.variables).toEqual({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
});
// Queries include one builder_query and one builder_formula
expect(payload.compositeQuery.queries).toHaveLength(2);
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const builderSpec = builderQuery.spec as MetricBuilderQuery;
expect(builderSpec.name).toBe('A');
expect(builderSpec.signal).toBe('metrics');
expect(builderSpec.aggregations?.[0]).toMatchObject({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
});
// reduceTo should not be present for non-scalar panels
expect(builderSpec.aggregations?.[0].reduceTo).toBeUndefined();
// functions should be preserved/normalized
expect(builderSpec.functions?.[0]?.name).toBe('timeShift');
const formulaQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_formula',
) as QueryEnvelope;
const formulaSpec = formulaQuery.spec as V5QueryBuilderFormula;
expect(formulaSpec.name).toBe('F1');
expect(formulaSpec.expression).toBe('A + 1');
expect(formulaSpec.legend).toBe('Formula Legend');
});
it('builds payload for PromQL queries and respects originalGraphType for formatting', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.PROM,
id: 'q2',
unit: undefined,
promql: [
{
name: 'A',
query: 'up',
disabled: false,
legend: 'LP',
},
],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
originalGraphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'LP' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'promql',
spec: expect.objectContaining({
name: 'A',
query: 'up',
legend: 'LP',
stats: false,
}),
},
],
}),
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
expect(result.legendMap).toEqual({ A: 'LP' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('time_series');
expect(payload.formatOptions?.formatTableResultForUI).toBe(true);
expect(payload.compositeQuery.queries).toHaveLength(1);
const prom = payload.compositeQuery.queries[0];
expect(prom.type).toBe('promql');
const promSpec = prom.spec as PromQuery;
expect(promSpec.name).toBe('A');
expect(promSpec.query).toBe('up');
expect(promSpec.legend).toBe('LP');
expect(promSpec.stats).toBe(false);
});
it('builds payload for ClickHouse queries and maps requestType from panel', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.CLICKHOUSE,
id: 'q3',
unit: undefined,
promql: [],
clickhouse_sql: [
{
name: 'Q',
query: 'SELECT 1',
disabled: false,
legend: 'LC',
},
],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { Q: 'LC' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'clickhouse_sql',
spec: expect.objectContaining({
name: 'Q',
query: 'SELECT 1',
legend: 'LC',
}),
},
],
}),
requestType: 'scalar',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
expect(result.legendMap).toEqual({ Q: 'LC' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('scalar');
expect(payload.compositeQuery.queries).toHaveLength(1);
const ch = payload.compositeQuery.queries[0];
expect(ch.type).toBe('clickhouse_sql');
const chSpec = ch.spec as ClickHouseQuery;
expect(chSpec.name).toBe('Q');
expect(chSpec.query).toBe('SELECT 1');
expect(chSpec.legend).toBe('LC');
});
it('uses getStartEndRangeTime when start/end are not provided', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q4',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: {},
queryPayload: expect.objectContaining({
compositeQuery: { queries: [] },
requestType: 'time_series',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: false,
}),
start: 100 * 1000,
end: 200 * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.start).toBe(100 * 1000);
expect(payload.end).toBe(200 * 1000);
});
it('includes reduceTo for metrics in scalar panels (TABLE)', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q5',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'metrics',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: [
expect.objectContaining({
metricName: 'cpu_usage',
timeAggregation: 'sum',
spaceAggregation: 'avg',
reduceTo: 'avg',
temporality: undefined,
}),
],
}),
},
],
}),
requestType: 'scalar',
formatOptions: expect.objectContaining({
formatTableResultForUI: true,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const builderSpec = builderQuery.spec as MetricBuilderQuery;
expect(builderSpec.aggregations?.[0].reduceTo).toBe('avg');
});
it('omits aggregations for raw request type (LIST panel)', () => {
const logAgg: LogAggregation[] = [{ expression: 'count()' }];
const logsQuery = baseBuilderQuery({
dataSource: DataSource.LOGS,
aggregations: logAgg,
} as Partial<IBuilderQuery>);
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'q6',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [logsQuery],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'logs',
stepInterval: 600,
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
aggregations: undefined,
}),
},
],
}),
requestType: 'raw',
formatOptions: expect.objectContaining({
formatTableResultForUI: false,
fillGaps: false,
}),
start: start * 1000,
end: end * 1000,
variables: {},
}),
}),
);
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('raw');
const builderQuery = payload.compositeQuery.queries.find(
(q) => q.type === 'builder_query',
) as QueryEnvelope;
// For RAW request type, aggregations should be omitted
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.aggregations).toBeUndefined();
});
it('maps groupBy, order, having, aggregations and filter for logs builder query', () => {
const getStartEndRangeTime = jest.requireMock('lib/getStartEndRangeTime')
.default as jest.Mock;
getStartEndRangeTime.mockReturnValueOnce({
start: '1754623641',
end: '1754645241',
});
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.QUERY_BUILDER,
id: 'e643e387-1996-4449-97b6-9ef4498a0573',
unit: undefined,
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: '',
dataType: DataTypes.EMPTY,
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
filter: { expression: "service.name = 'adservice'" },
aggregations: [
{ expression: 'count() as cnt avg(code.lineno) ' } as LogAggregation,
],
functions: [],
filters: {
items: [
{
id: '14c790ec-54d1-42f0-a889-3b4f0fb79852',
op: '=',
key: { id: 'service.name', key: 'service.name', type: '' },
value: 'adservice',
},
],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 80,
having: { expression: 'count() > 0' },
limit: 600,
orderBy: [{ columnName: 'service.name', order: 'desc' }],
groupBy: [
{
key: 'service.name',
type: '',
},
],
legend: '{{service.name}}',
reduceTo: 'avg',
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: 'custom' as never,
variables: {},
};
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect.objectContaining({
legendMap: { A: '{{service.name}}' },
queryPayload: expect.objectContaining({
schemaVersion: 'v1',
start: 1754623641000,
end: 1754645241000,
requestType: 'time_series',
compositeQuery: expect.objectContaining({
queries: [
{
type: 'builder_query',
spec: expect.objectContaining({
name: 'A',
signal: 'logs',
stepInterval: 80,
disabled: false,
filter: { expression: "service.name = 'adservice'" },
groupBy: [
{
name: 'service.name',
fieldDataType: '',
fieldContext: '',
},
],
limit: 600,
order: [
{
key: { name: 'service.name' },
direction: 'desc',
},
],
legend: '{{service.name}}',
having: { expression: 'count() > 0' },
aggregations: [
{ expression: 'count()', alias: 'cnt' },
{ expression: 'avg(code.lineno)' },
],
}),
},
],
}),
formatOptions: { formatTableResultForUI: false, fillGaps: false },
variables: {},
}),
}),
);
});
});

View File

@@ -1,15 +1,11 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@@ -70,46 +66,9 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
return 'metrics';
}
function isDeprecatedField(fieldName: string): boolean {
const deprecatedIntrinsicFields = [
'traceID',
'spanID',
'parentSpanID',
'spanKind',
'durationNano',
'statusCode',
'statusMessage',
'statusCodeString',
];
const deprecatedCalculatedFields = [
'responseStatusCode',
'externalHttpUrl',
'httpUrl',
'externalHttpMethod',
'httpMethod',
'httpHost',
'dbName',
'dbOperation',
'hasError',
'isRemote',
'serviceName',
'httpRoute',
'msgSystem',
'msgOperation',
'dbSystem',
'rpcSystem',
'rpcService',
'rpcMethod',
'peerService',
];
return (
deprecatedIntrinsicFields.includes(fieldName) ||
deprecatedCalculatedFields.includes(fieldName)
);
}
/**
* Creates base spec for builder queries
*/
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
@@ -121,7 +80,7 @@ function createBaseSpec(
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || null,
stepInterval: queryData?.stepInterval || undefined,
disabled: queryData.disabled,
filter: queryData?.filter?.expression ? queryData.filter : undefined,
groupBy:
@@ -129,8 +88,8 @@ function createBaseSpec(
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType || '',
fieldContext: item?.type || '',
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
@@ -181,33 +140,19 @@ function createBaseSpec(
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => {
const fieldName = column.name ?? column.key;
const isDeprecated = isDeprecatedField(fieldName);
const fieldObj: TelemetryFieldKey = {
name: fieldName,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
signal: column?.signal ?? undefined,
};
// Only add fieldContext if the field is NOT deprecated
if (!isDeprecated && fieldName !== 'name') {
fieldObj.fieldContext =
column?.fieldContext ?? (column?.type as FieldContext);
}
return fieldObj;
},
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
availableAlias?: string,
): { expression: string; alias?: string }[] {
const result: { expression: string; alias?: string }[] = [];
// Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'"
@@ -216,7 +161,7 @@ export function parseAggregations(
let match = regex.exec(expression);
while (match !== null) {
const expr = match[1];
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
let alias = match[2];
if (alias) {
// Remove quotes if present
alias = alias.replace(/^['"]|['"]$/g, '');
@@ -267,14 +212,9 @@ export function createAggregation(
}
if (queryData.aggregations?.length > 0) {
return queryData.aggregations.flatMap(
(agg: { expression: string; alias?: string }) => {
const parsedAggregations = parseAggregations(agg.expression, agg?.alias);
return isEmpty(parsedAggregations)
? [{ expression: 'count()' }]
: parsedAggregations;
},
);
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
? [{ expression: 'count()' }]
: parseAggregations(queryData.aggregations?.[0].expression);
}
return [{ expression: 'count()' }];
@@ -336,101 +276,6 @@ export function convertBuilderQueriesToV5(
);
}
function createTraceOperatorBaseSpec(
queryData: IBuilderTraceOperator,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || undefined,
groupBy:
queryData.groupBy?.length > 0
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? queryData.limit || queryData.pageSize || undefined
: queryData.limit || undefined,
offset:
requestType === 'raw' || requestType === 'trace'
? queryData.offset
: undefined,
order:
queryData.orderBy?.length > 0
? queryData.orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
export function convertTraceOperatorToV5(
traceOperator: Record<string, IBuilderTraceOperator>,
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(traceOperator).map(
([queryName, traceOperatorData]): QueryEnvelope => {
const baseSpec = createTraceOperatorBaseSpec(
traceOperatorData,
requestType,
panelType,
);
// Skip aggregation for raw request type
const aggregations =
requestType === 'raw'
? undefined
: createAggregation(traceOperatorData, panelType);
const spec: QueryEnvelope['spec'] = {
name: queryName,
...baseSpec,
expression: traceOperatorData.expression || '',
aggregations: aggregations as TraceAggregation[],
};
return {
type: 'builder_trace_operator' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
@@ -512,28 +357,14 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const { queryData: data, queryFormulas } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const filteredTraceOperator =
queryTraceOperator && queryTraceOperator.length > 0
? queryTraceOperator.filter((traceOperator) =>
Boolean(traceOperator.expression.trim()),
)
: [];
const currentTraceOperator = mapQueryDataToApi(
filteredTraceOperator,
'queryName',
tableParams,
);
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
...currentTraceOperator.newLegendMap,
};
// Convert builder queries
@@ -566,14 +397,8 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// Combine all query types
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
// Combine both types
queries = [...builderQueries, ...formulaQueries];
break;
}
case EQueryType.PROM: {

View File

@@ -1,16 +1,14 @@
import { render, screen } from '@testing-library/react';
import getLocal from '../../../api/browser/localstorage/get';
import AppLoading from '../AppLoading';
jest.mock('../../../api/browser/localstorage/get', () => ({
// Mock the localStorage API
const mockGet = jest.fn();
jest.mock('api/browser/localstorage/get', () => ({
__esModule: true,
default: jest.fn(),
default: mockGet,
}));
// Access the mocked function
const mockGet = (getLocal as unknown) as jest.Mock;
describe('AppLoading', () => {
const SIGNOZ_TEXT = 'SigNoz';
const TAGLINE_TEXT =

View File

@@ -20,15 +20,13 @@
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-component-container {
.widget-graph-container {
&.bar-panel-container {
height: calc(100% - 110px);
}
.widget-graph-container {
&.bar {
height: calc(100% - 110px);
}
&.graph-panel-container {
height: calc(100% - 80px);
}
&.graph {
height: calc(100% - 80px);
}
}
}
@@ -84,11 +82,9 @@
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-component-container {
.widget-graph-container {
&.bar-panel-container {
height: calc(100% - 110px);
}
.widget-graph-container {
&.bar {
height: calc(100% - 110px);
}
}
}

View File

@@ -32,6 +32,8 @@ export const celeryAllStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
@@ -48,6 +50,8 @@ export const celeryAllStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -84,6 +88,7 @@ export const celeryRetryStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
@@ -98,6 +103,8 @@ export const celeryRetryStateWidgetData = (
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -112,6 +119,8 @@ export const celeryRetryStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -144,6 +153,8 @@ export const celeryFailedStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
@@ -158,6 +169,8 @@ export const celeryFailedStateWidgetData = (
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -172,6 +185,8 @@ export const celeryFailedStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -204,6 +219,8 @@ export const celerySuccessStateWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
@@ -218,6 +235,8 @@ export const celerySuccessStateWidgetData = (
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -232,6 +251,8 @@ export const celerySuccessStateWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -263,6 +284,7 @@ export const celeryTasksByWorkerWidgetData = (
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
@@ -279,6 +301,8 @@ export const celeryTasksByWorkerWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -314,6 +338,8 @@ export const celeryErrorByWorkerWidgetData = (
aggregateAttribute: {
dataType: 'string',
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -327,6 +353,8 @@ export const celeryErrorByWorkerWidgetData = (
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
@@ -345,6 +373,8 @@ export const celeryErrorByWorkerWidgetData = (
groupBy: [
{
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
id: 'celery.hostname--string--tag--false',
@@ -360,6 +390,8 @@ export const celeryErrorByWorkerWidgetData = (
aggregateAttribute: {
dataType: 'string',
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -379,6 +411,8 @@ export const celeryErrorByWorkerWidgetData = (
groupBy: [
{
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
id: 'celery.hostname--string--tag--false',
@@ -411,6 +445,8 @@ export const celeryLatencyByWorkerWidgetData = (
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -427,6 +463,8 @@ export const celeryLatencyByWorkerWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
@@ -460,6 +498,8 @@ export const celeryActiveTasksWidgetData = (
dataType: DataTypes.Float64,
id:
'flower_worker_number_of_currently_executing_tasks--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'flower_worker_number_of_currently_executing_tasks',
type: 'Gauge',
},
@@ -476,6 +516,8 @@ export const celeryActiveTasksWidgetData = (
{
dataType: DataTypes.String,
id: 'worker--string--tag--false',
isColumn: false,
isJSON: false,
key: 'worker',
type: 'tag',
},
@@ -509,6 +551,8 @@ export const celeryTaskLatencyWidgetData = (
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -525,6 +569,8 @@ export const celeryTaskLatencyWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -560,6 +606,8 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -576,6 +624,8 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -610,6 +660,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -624,6 +676,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -638,6 +692,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -673,6 +729,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -687,6 +745,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -701,6 +761,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -734,6 +796,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -748,6 +812,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -762,6 +828,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -801,6 +869,8 @@ export const celeryTimeSeriesTablesWidgetData = (
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
@@ -815,6 +885,8 @@ export const celeryTimeSeriesTablesWidgetData = (
key: {
dataType: DataTypes.String,
id: `${entity}--string--tag--false`,
isColumn: false,
isJSON: false,
key: `${entity}`,
type: 'tag',
},
@@ -829,6 +901,8 @@ export const celeryTimeSeriesTablesWidgetData = (
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
@@ -859,6 +933,8 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -896,6 +972,8 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -910,6 +988,8 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -945,6 +1025,8 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
@@ -959,6 +1041,8 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
@@ -994,6 +1078,7 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
key: 'span_id',
type: '',
},
@@ -1008,6 +1093,8 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},

View File

@@ -39,6 +39,8 @@ export function getFiltersFromQueryParams(
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
@@ -98,7 +100,8 @@ export const createFiltersFromData = (
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
@@ -116,6 +119,8 @@ export const createFiltersFromData = (
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',

View File

@@ -241,6 +241,8 @@ function ClientSideQBSearch(
key: 'body',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,

View File

@@ -1,16 +1,6 @@
.custom-time-picker {
display: flex;
flex-direction: column;
.timeSelection-input {
&:hover {
border-color: #1d212d !important;
}
}
.time-input-suffix {
display: flex;
}
}
.time-options-container {
@@ -145,7 +135,6 @@
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
@@ -174,52 +163,6 @@
cursor: pointer;
}
.time-input-prefix {
.live-dot-icon {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--bg-forest-500);
animation: ripple 1s infinite;
margin-right: 4px;
margin-left: 4px;
}
}
@keyframes ripple {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
.time-input-suffix-icon-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
height: 20px;
width: 20px;
&:hover {
background: rgba(171, 189, 255, 0.08);
}
}
.lightMode {
.date-time-popover__footer {
border-color: var(--bg-vanilla-400);
@@ -237,26 +180,8 @@
}
}
}
.custom-time-picker {
.timeSelection-input {
&:hover {
border-color: var(--bg-vanilla-300) !important;
}
}
}
.timezone-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
}
.time-input-suffix-icon-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
&:hover {
background: rgb(179 179 179 / 20%);
}
}
}

View File

@@ -5,12 +5,13 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { isValidTimeFormat } from 'lib/getMinMax';
@@ -27,10 +28,7 @@ import {
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
@@ -59,9 +57,11 @@ interface CustomTimePickerProps {
customDateTimeVisible?: boolean;
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
showLiveLogs?: boolean;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
handleGoLive?: () => void;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
function CustomTimePicker({
@@ -78,19 +78,14 @@ function CustomTimePicker({
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
onGoLive,
onExitLiveLogs,
showLiveLogs,
handleGoLive,
onTimeChange,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
setSelectedTimePlaceholderValue,
] = useState('Select / Enter Time Range');
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [inputValue, setInputValue] = useState('');
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
@@ -169,13 +164,9 @@ function CustomTimePicker({
};
useEffect(() => {
if (showLiveLogs) {
setSelectedTimePlaceholderValue('Live');
} else {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}
}, [selectedTime, selectedValue, showLiveLogs]);
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]);
const hide = (): void => {
setOpen(false);
@@ -265,11 +256,6 @@ function CustomTimePicker({
};
const handleSelect = (label: string, value: string): void => {
if (label === 'Custom') {
setCustomDTPickerVisible?.(true);
return;
}
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
@@ -332,118 +318,84 @@ function CustomTimePicker({
);
};
const getTooltipTitle = (): string => {
if (selectedTime === 'custom' && inputValue === '' && !open) {
return `${dayjs(minTime / 1000_000)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(
maxTime / 1000_000,
)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`;
}
return '';
};
const getInputPrefix = (): JSX.Element => {
if (showLiveLogs) {
return (
<div className="time-input-prefix">
<div className="live-dot-icon" />
</div>
);
}
return (
<div className="time-input-prefix">
{inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} className="cursor-pointer" />
</Tooltip>
)}
</div>
);
};
return (
<div className="custom-time-picker">
<Tooltip title={getTooltipTitle()} placement="top">
<Popover
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
onGoLive={defaultTo(onGoLive, noop)}
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/>
<Popover
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
onTimeChange={onTimeChange}
/>
) : (
content
)
}
arrow={false}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={
inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
content
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} />
</Tooltip>
)
}
arrow={false}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
status={inputValue && inputStatus === 'error' ? 'error' : ''}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onClick={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
data-1p-ignore
prefix={getInputPrefix()}
suffix={
<div className="time-input-suffix">
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
className="cursor-pointer time-input-suffix-icon-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('datetime');
}}
/>
</div>
}
/>
</Popover>
</Tooltip>
suffix={
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>
{inputStatus === 'error' && inputErrorMessage && (
<Typography.Title level={5} className="valid-format-error">
{inputErrorMessage}
@@ -460,8 +412,7 @@ CustomTimePicker.defaultProps = {
customDateTimeVisible: false,
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
onGoLive: noop,
handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
onExitLiveLogs: noop,
showLiveLogs: false,
onTimeChange: undefined,
};

View File

@@ -4,30 +4,21 @@ import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
LexicalContext,
Option,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
@@ -40,21 +31,16 @@ interface CustomTimePickerPopoverContentProps {
lexicalContext?: LexicalContext,
) => void;
onSelectHandler: (label: string, value: string) => void;
onGoLive: () => void;
handleGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onExitLiveLogs: () => void;
}
interface RecentlyUsedDateTimeRange {
label: string;
value: number;
timestamp: number;
from: string;
to: string;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -65,68 +51,22 @@ function CustomTimePickerPopoverContent({
setCustomDTPickerVisible,
onCustomDateHandler,
onSelectHandler,
onGoLive,
handleGoLive,
selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
onExitLiveLogs,
onTimeChange,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const url = new URLSearchParams(window.location.search);
let panelTypeFromURL = url.get(QueryParams.panelTypes);
try {
panelTypeFromURL = JSON.parse(panelTypeFromURL as string);
} catch {
// fallback → leave as-is
}
const isLogsListView =
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
RecentlyUsedDateTimeRange[]
>([]);
const handleExitLiveLogs = useCallback((): void => {
if (isLogsExplorerPage) {
onExitLiveLogs();
}
}, [isLogsExplorerPage, onExitLiveLogs]);
useEffect(() => {
if (!customDateTimeVisible) {
const customTimeRanges = getCustomTimeRanges();
const formattedCustomTimeRanges: RecentlyUsedDateTimeRange[] = customTimeRanges.map(
(range) => ({
label: `${dayjs(range.from)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(range.to)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`,
from: range.from,
to: range.to,
value: range.timestamp,
timestamp: range.timestamp,
}),
);
setRecentlyUsedTimeRanges(formattedCustomTimeRanges);
}
}, [customDateTimeVisible, timezone.value]);
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
@@ -136,7 +76,6 @@ function CustomTimePickerPopoverContent({
className="time-btns"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
>
@@ -170,87 +109,53 @@ function CustomTimePickerPopoverContent({
);
}
const handleGoLive = (): void => {
onGoLive();
setIsOpen(false);
};
return (
<>
<div className="date-time-popover">
{!customDateTimeVisible && (
<div className="date-time-options">
{isLogsExplorerPage && isLogsListView && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
handleExitLiveLogs();
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
)}
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
customDateTimeVisible ? 'date-picker' : 'relative-times',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{customDateTimeVisible ? (
<DatePickerV2
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
onTimeChange={onTimeChange}
/>
) : (
<div className="time-selector-container">
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
<div className="recently-used-container">
<div className="time-heading">RECENTLY USED</div>
<div className="recently-used-range">
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
<div
className="recently-used-range-item"
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleExitLiveLogs();
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
setIsOpen(false);
}
}}
key={range.value}
onClick={(): void => {
handleExitLiveLogs();
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
setIsOpen(false);
}}
>
{range.label}
</div>
))}
</div>
</div>
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
@@ -284,4 +189,8 @@ function CustomTimePickerPopoverContent({
);
}
CustomTimePickerPopoverContent.defaultProps = {
onTimeChange: undefined,
};
export default CustomTimePickerPopoverContent;

View File

@@ -1,114 +0,0 @@
.date-picker-v2-container {
display: flex;
flex-direction: row;
}
.custom-date-time-picker-v2 {
padding: 12px;
.periscope-calendar {
border-radius: 4px;
border: none !important;
background: none !important;
padding: 8px 0 !important;
}
.periscope-calendar-day {
background: none !important;
&.periscope-calendar-today {
&.text-accent-foreground {
color: var(--bg-vanilla-100) !important;
}
}
button {
&:hover {
background-color: var(--bg-robin-500) !important;
color: var(--bg-vanilla-100) !important;
}
}
}
.custom-time-selector {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
justify-content: space-between;
.time-input {
border-radius: 4px;
border: none !important;
background: none !important;
padding: 8px 4px !important;
color: var(--bg-vanilla-100) !important;
&::-webkit-calendar-picker-indicator {
display: none !important;
-webkit-appearance: none;
appearance: none;
}
&:focus {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
&:focus-visible {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
}
}
.custom-date-time-picker-footer {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
justify-content: flex-end;
margin-top: 16px;
.next-btn {
width: 80px;
}
.clear-btn {
width: 80px;
}
}
}
.invalid-date-range-tooltip {
.ant-tooltip-inner {
color: var(--bg-sakura-500) !important;
}
}
.lightMode {
.custom-date-time-picker-v2 {
.periscope-calendar-day {
&.periscope-calendar-today {
&.text-accent-foreground {
color: var(--bg-ink-500) !important;
}
}
button {
&:hover {
background-color: var(--bg-robin-500) !important;
color: var(--bg-ink-500) !important;
}
}
}
.custom-time-selector {
.time-input {
color: var(--bg-ink-500) !important;
}
}
}
}

View File

@@ -1,311 +0,0 @@
import './DatePickerV2.styles.scss';
import { Calendar } from '@signozhq/calendar';
import { Input } from '@signozhq/input';
import { Button, Tooltip } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { CornerUpLeft, MoveRight } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { addCustomTimeRange } from 'utils/customTimeRangeUtils';
function DatePickerV2({
onSetCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
}: {
onSetCustomDTPickerVisible: (visible: boolean) => void;
setIsOpen: (isOpen: boolean) => void;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext,
) => void;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const timeInputRef = useRef<HTMLInputElement>(null);
const { timezone } = useTimezone();
const [selectedDateTimeFor, setSelectedDateTimeFor] = useState<'to' | 'from'>(
'from',
);
const [selectedFromDateTime, setSelectedFromDateTime] = useState<Dayjs | null>(
dayjs(minTime / 1000_000).tz(timezone.value),
);
const [selectedToDateTime, setSelectedToDateTime] = useState<Dayjs | null>(
dayjs(maxTime / 1000_000).tz(timezone.value),
);
const handleNext = (): void => {
if (selectedDateTimeFor === 'to') {
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
setIsOpen(false);
onSetCustomDTPickerVisible(false);
setSelectedDateTimeFor('from');
} else {
setSelectedDateTimeFor('to');
}
};
const handleDateChange = (date: Date | undefined): void => {
if (!date) {
return;
}
if (selectedDateTimeFor === 'from') {
const prevFromDateTime = selectedFromDateTime;
const newDate = dayjs(date);
const updatedFromDateTime = prevFromDateTime
? prevFromDateTime
.year(newDate.year())
.month(newDate.month())
.date(newDate.date())
: dayjs(date).tz(timezone.value);
setSelectedFromDateTime(updatedFromDateTime);
} else {
// eslint-disable-next-line sonarjs/no-identical-functions
setSelectedToDateTime((prev) => {
const newDate = dayjs(date);
// Update only the date part, keeping time from existing state
return prev
? prev.year(newDate.year()).month(newDate.month()).date(newDate.date())
: dayjs(date).tz(timezone.value);
});
}
// focus the time input
timeInputRef?.current?.focus();
};
const handleTimeChange = (time: string): void => {
// time should have format HH:mm:ss
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) {
return;
}
if (selectedDateTimeFor === 'from') {
setSelectedFromDateTime((prev) => {
if (prev) {
return prev
.set('hour', parseInt(time.split(':')[0], 10))
.set('minute', parseInt(time.split(':')[1], 10))
.set('second', parseInt(time.split(':')[2], 10));
}
return prev;
});
}
if (selectedDateTimeFor === 'to') {
// eslint-disable-next-line sonarjs/no-identical-functions
setSelectedToDateTime((prev) => {
if (prev) {
return prev
.set('hour', parseInt(time.split(':')[0], 10))
.set('minute', parseInt(time.split(':')[1], 10))
.set('second', parseInt(time.split(':')[2], 10));
}
return prev;
});
}
};
const getDefaultMonth = (): Date => {
let defaultDate = null;
if (selectedDateTimeFor === 'from') {
defaultDate = selectedFromDateTime?.toDate();
} else if (selectedDateTimeFor === 'to') {
defaultDate = selectedToDateTime?.toDate();
}
return defaultDate ?? new Date();
};
const isValidRange = (): boolean => {
if (selectedDateTimeFor === 'to') {
return selectedToDateTime?.isAfter(selectedFromDateTime) ?? false;
}
return true;
};
const handleBack = (): void => {
setSelectedDateTimeFor('from');
};
const handleHideCustomDTPicker = (): void => {
onSetCustomDTPickerVisible(false);
};
const handleSelectDateTimeFor = (selectedDateTimeFor: 'to' | 'from'): void => {
setSelectedDateTimeFor(selectedDateTimeFor);
};
return (
<div className="date-picker-v2-container">
<div className="date-time-custom-options-container">
<div
className="back-btn"
onClick={handleHideCustomDTPicker}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleHideCustomDTPicker();
}
}}
>
<CornerUpLeft size={16} />
<span>Back</span>
</div>
<div className="date-time-custom-options">
<div
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleSelectDateTimeFor('from');
}
}}
className={cx(
'date-time-custom-option-from',
selectedDateTimeFor === 'from' && 'active',
)}
onClick={(): void => {
handleSelectDateTimeFor('from');
}}
>
<div className="date-time-custom-option-from-title">FROM</div>
<div className="date-time-custom-option-from-value">
{selectedFromDateTime?.format('YYYY-MM-DD HH:mm:ss')}
</div>
</div>
<div
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleSelectDateTimeFor('to');
}
}}
className={cx(
'date-time-custom-option-to',
selectedDateTimeFor === 'to' && 'active',
)}
onClick={(): void => {
handleSelectDateTimeFor('to');
}}
>
<div className="date-time-custom-option-to-title">TO</div>
<div className="date-time-custom-option-to-value">
{selectedToDateTime?.format('YYYY-MM-DD HH:mm:ss')}
</div>
</div>
</div>
</div>
<div className="custom-date-time-picker-v2">
<Calendar
mode="single"
required
selected={
selectedDateTimeFor === 'from'
? selectedFromDateTime?.toDate()
: selectedToDateTime?.toDate()
}
key={selectedDateTimeFor + selectedDateTimeFor}
onSelect={handleDateChange}
defaultMonth={getDefaultMonth()}
disabled={(current): boolean => {
if (selectedDateTimeFor === 'to') {
// disable dates after today and before selectedFromDateTime
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs()) || false;
}
if (selectedDateTimeFor === 'from') {
// disable dates after selectedToDateTime
return dayjs(current).isAfter(dayjs()) || false;
}
return false;
}}
className="rounded-md border"
navLayout="after"
/>
<div className="custom-time-selector">
<label className="text-xs font-normal block" htmlFor="time-picker">
Timestamp
</label>
<MoveRight size={16} />
<div className="time-input-container">
<Input
type="time"
ref={timeInputRef}
className="time-input"
value={
selectedDateTimeFor === 'from'
? selectedFromDateTime?.format('HH:mm:ss')
: selectedToDateTime?.format('HH:mm:ss')
}
onChange={(e): void => handleTimeChange(e.target.value)}
step="1"
/>
</div>
</div>
<div className="custom-date-time-picker-footer">
{selectedDateTimeFor === 'to' && (
<Button
className="periscope-btn secondary clear-btn"
type="default"
onClick={handleBack}
>
Back
</Button>
)}
<Tooltip
title={
!isValidRange() ? 'Invalid range: TO date should be after FROM date' : ''
}
overlayClassName="invalid-date-range-tooltip"
>
<Button
className="periscope-btn primary next-btn"
type="primary"
onClick={handleNext}
disabled={!isValidRange()}
>
{selectedDateTimeFor === 'from' ? 'Next' : 'Apply'}
</Button>
</Tooltip>
</div>
</div>
</div>
);
}
export default DatePickerV2;

View File

@@ -55,31 +55,37 @@ export const selectedColumns: BaseAutocompleteData[] = [
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
];
@@ -102,7 +108,9 @@ export const getHostTracesQueryPayload = (
id: '------false',
dataType: DataTypes.EMPTY,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
@@ -125,7 +133,6 @@ export const getHostTracesQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,
@@ -147,6 +154,8 @@ export const getHostTracesQueryPayload = (
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
@@ -154,6 +163,8 @@ export const getHostTracesQueryPayload = (
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
@@ -161,6 +172,8 @@ export const getHostTracesQueryPayload = (
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
@@ -168,6 +181,8 @@ export const getHostTracesQueryPayload = (
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
@@ -175,6 +190,8 @@ export const getHostTracesQueryPayload = (
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},

View File

@@ -119,6 +119,8 @@ function HostMetricsDetails({
key: 'host.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'host.name--string--resource--false',
},
op: '=',

View File

@@ -26,7 +26,9 @@ export const getHostLogsQueryPayload = (
id: '------false',
dataType: DataTypes.String,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
@@ -51,7 +53,6 @@ export const getHostLogsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,

View File

@@ -1,50 +0,0 @@
import { Badge } from '@signozhq/badge';
type BadgeColor =
| 'vanilla'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua';
interface HttpStatusBadgeProps {
statusCode: string | number;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
if (statusCode >= 200 && statusCode < 300) {
return 'forest'; // Success - green
}
if (statusCode >= 300 && statusCode < 400) {
return 'robin'; // Redirect - blue
}
if (statusCode >= 400 && statusCode < 500) {
return 'amber'; // Client error - amber
}
if (statusCode >= 500) {
return 'cherry'; // Server error - red
}
if (statusCode >= 100 && statusCode < 200) {
return 'vanilla'; // Informational - neutral
}
return 'robin'; // Default fallback
}
function HttpStatusBadge({
statusCode,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
if (!numericStatusCode || numericStatusCode <= 0) {
return null;
}
const color = getStatusCodeColor(numericStatusCode);
return <Badge color={color}>{statusCode}</Badge>;
}
export default HttpStatusBadge;

View File

@@ -17,7 +17,7 @@ function InputWithLabel({
closeIcon,
}: {
label: string;
initialValue?: string | number | null;
initialValue?: string | number;
placeholder: string;
type?: string;
onClose?: () => void;

View File

@@ -1,152 +0,0 @@
.kbar-command-palette__positioner {
position: fixed;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
z-index: 50;
}
.kbar-command-palette__animator {
width: 100%;
max-width: 600px;
}
.kbar-command-palette__card {
background: var(--bg-ink-500);
color: var(--text-vanilla-100);
border-radius: 3px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex;
flex-direction: column;
}
.kbar-command-palette__search {
padding: 12px 16px;
font-size: 13px;
border: none;
border-bottom: 1px solid var(--border-ink-200);
color: var(--text-vanilla-100);
outline: none;
background-color: var(--bg-ink-500);
}
.kbar-command-palette__section {
padding: 8px 16px 4px;
font-size: 12px;
font-weight: 600;
color: var(--text-robin-500);
font-family: 'Inter', sans-serif;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.kbar-command-palette__item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s ease;
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: var(--bg-ink-400);
}
.kbar-command-palette__icon {
flex-shrink: 0;
width: 18px;
height: 18px;
color: #444;
}
.kbar-command-palette__shortcut {
margin-left: auto;
display: flex;
gap: 4px;
}
.kbar-command-palette__key {
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
background: var(--bg-ink-300);
color: var(--text-vanilla-300);
text-transform: uppercase;
font-family: 'Space Mono', monospace;
}
.kbar-command-palette__results-container {
div {
&::-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);
}
}
}
.lightMode {
.kbar-command-palette__positioner {
background: rgba(0, 0, 0, 0.5);
}
.kbar-command-palette__card {
background: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.kbar-command-palette__search {
border-bottom: 1px solid #e5e5e5;
color: var(--text-ink-500);
background-color: var(--bg-vanilla-100);
}
.kbar-command-palette__item {
color: var(--text-ink-500);
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: #f5f5f5;
}
.kbar-command-palette__icon {
color: #444;
}
.kbar-command-palette__key {
background: #eee;
color: #555;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -1,69 +0,0 @@
import './KBarCommandPalette.scss';
import {
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarResults,
KBarSearch,
useMatches,
} from 'kbar';
function Results(): JSX.Element {
const { results } = useMatches();
const renderResults = ({
item,
active,
}: {
item: any;
active: boolean;
}): JSX.Element =>
typeof item === 'string' ? (
<div className="kbar-command-palette__section">{item}</div>
) : (
<div
className={`kbar-command-palette__item ${
active ? 'kbar-command-palette__item--active' : ''
}`}
>
{item.icon}
<span>{item.name}</span>
{item.shortcut?.length ? (
<span className="kbar-command-palette__shortcut">
{item.shortcut.map((sc: string) => (
<kbd key={sc} className="kbar-command-palette__key">
{sc}
</kbd>
))}
</span>
) : null}
</div>
);
return (
<div className="kbar-command-palette__results-container">
<KBarResults items={results} onRender={renderResults} />
</div>
);
}
function KBarCommandPalette(): JSX.Element {
return (
<KBarPortal>
<KBarPositioner className="kbar-command-palette__positioner">
<KBarAnimator className="kbar-command-palette__animator">
<div className="kbar-command-palette__card">
<KBarSearch
className="kbar-command-palette__search"
placeholder="Search or type a command..."
/>
<Results />
</div>
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
}
export default KBarCommandPalette;

View File

@@ -10,7 +10,11 @@ import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -23,7 +23,6 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@@ -40,7 +39,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
@@ -95,8 +94,6 @@ function LogDetailInner({
const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
@@ -149,34 +146,6 @@ function LogDetailInner({
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
setContextQuery((prev) => {
if (!prev) return prev;
return {
...prev,
builder: {
...prev.builder,
queryData: prev.builder.queryData.map((query, idx) =>
idx === queryIndex
? {
...query,
filter: {
...query.filter,
expression: value,
},
}
: query,
),
},
};
});
},
[],
);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
@@ -336,19 +305,11 @@ function LogDetailInner({
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">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
onChange={(): void => {}}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}

View File

@@ -17,7 +17,7 @@ function AddToQueryHOC({
}: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
event.stopPropagation();
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], dataType);
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
};
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
@@ -41,6 +41,7 @@ export interface AddToQueryHOCProps {
fieldKey: string,
fieldValue: string,
operator: string,
isJSON?: boolean,
dataType?: DataTypes,
) => void;
fontSize: FontSize;

View File

@@ -56,8 +56,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
@@ -85,10 +83,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
@@ -106,8 +101,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
@@ -142,8 +135,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({

View File

@@ -22,10 +22,6 @@
flex: 1;
position: relative;
.qb-trace-view-selector-container {
padding: 12px 8px 8px 8px;
}
}
.qb-content-section {
@@ -183,7 +179,7 @@
flex-direction: column;
gap: 8px;
margin-left: 26px;
margin-left: 32px;
padding-bottom: 16px;
padding-left: 8px;
@@ -199,8 +195,8 @@
}
.formula-container {
padding: 8px;
margin-left: 74px;
margin-left: 82px;
padding: 4px 0px;
.ant-col {
&::before {
@@ -295,13 +291,6 @@
);
}
}
.qb-trace-operator-button-container {
&-text {
display: flex;
align-items: center;
gap: 8px;
}
}
}
}
@@ -342,12 +331,6 @@
);
left: 15px;
}
&.has-trace-operator {
&::before {
height: 0px;
}
}
}
.formula-name {
@@ -364,7 +347,7 @@
&::before {
content: '';
height: 128px;
height: 65px;
content: '';
position: absolute;
left: 0;

View File

@@ -5,13 +5,11 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
@@ -20,7 +18,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@@ -28,7 +25,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@@ -58,11 +54,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
const isMultiQueryAllowed = useMemo(
() => !isListViewPanel || showTraceOperator,
[showTraceOperator, isListViewPanel],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
@@ -106,60 +97,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
listViewTracesFilterConfigs,
]);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [currentQuery.builder.queryTraceOperator]);
const hasAtLeastOneTraceQuery = useMemo(
() =>
currentQuery.builder.queryData.some(
(query) => query.dataSource === DataSource.TRACES,
),
[currentQuery.builder.queryData],
);
const hasTraceOperator = useMemo(
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const shouldShowFooter = useMemo(
() =>
(!showOnlyWhereClause && !isListViewPanel) ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
);
const showQueryList = useMemo(
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
);
const showFormula = useMemo(() => {
if (currentDataSource === DataSource.TRACES) {
return !isListViewPanel;
}
return true;
}, [isListViewPanel, currentDataSource]);
const showAddTraceOperator = useMemo(
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{!isMultiQueryAllowed ? (
{isListViewPanel && (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@@ -167,16 +109,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
) : (
)}
{!isListViewPanel &&
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
@@ -186,17 +127,13 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))
)}
))}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
@@ -221,25 +158,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{shouldShowFooter && (
{!showOnlyWhereClause && !isListViewPanel && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showAddTraceOperator}
/>
)}
{hasTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{showQueryList && (
{!showOnlyWhereClause && !isListViewPanel && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

@@ -160,7 +160,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? null}
initialValue={query?.stepInterval ?? undefined}
/>
</div>
</div>
@@ -283,7 +283,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? null}
initialValue={query?.stepInterval ?? undefined}
className="histogram-every-input"
/>
</div>

View File

@@ -1,11 +1,7 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;

View File

@@ -144,7 +144,6 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
@@ -152,7 +151,6 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@@ -162,7 +160,6 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();

View File

@@ -4,10 +4,7 @@ import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@@ -23,7 +20,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery | IBuilderTraceOperator;
queryData: IBuilderQuery;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
@@ -84,7 +81,9 @@ function QueryAggregationOptions({
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue={queryData?.stepInterval ? queryData?.stepInterval : null}
initialValue={
queryData?.stepInterval ? queryData?.stepInterval : undefined
}
className="query-aggregation-interval-input"
label="Seconds"
placeholder="Auto"

View File

@@ -154,23 +154,15 @@ function QueryAggregationSelect({
const isDarkMode = useIsDarkMode();
const { setAggregationOptions } = useQueryBuilderV2Context();
const formatAggregations = useCallback(
(aggregations: any[] | undefined): string =>
aggregations
?.map(({ expression, alias }: any) =>
alias ? `${expression} as ${alias}` : expression,
)
.join(' ') || '',
[],
);
const [input, setInput] = useState(
formatAggregations(queryData?.aggregations),
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
useEffect(() => {
setInput(formatAggregations(queryData?.aggregations));
}, [queryData?.aggregations, formatAggregations]);
setInput(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
}, [queryData?.aggregations]);
const [cursorPos, setCursorPos] = useState(0);
const [functionArgPairs, setFunctionArgPairs] = useState<

View File

@@ -1,20 +1,12 @@
/* eslint-disable react/require-default-props */
import { Button, Tooltip, Typography } from 'antd';
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
import BetaTag from 'periscope/components/BetaTag/BetaTag';
import { Plus, Sigma } from 'lucide-react';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
showAddFormula = true,
showAddTraceOperator = false,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
addTraceOperator?: () => void;
showAddTraceOperator: boolean;
showAddFormula?: boolean;
}): JSX.Element {
return (
<div className="qb-footer">
@@ -30,65 +22,32 @@ export default function QueryFooter({
</Tooltip>
</div>
{showAddFormula && (
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</Tooltip>
</div>
)}
{showAddTraceOperator && (
<div className="qb-trace-operator-button-container">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add Trace Matching
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-trace-operator-button periscope-btn secondary"
icon={<DraftingCompass size={16} />}
onClick={(): void => addTraceOperator?.()}
>
<div className="qb-trace-operator-button-container-text">
Add Trace Matching
<BetaTag />
</div>
</Button>
</Tooltip>
</div>
)}
Add Formula
</Button>
</Tooltip>
</div>
</div>
</div>
);

View File

@@ -7,7 +7,6 @@
'Helvetica Neue', sans-serif;
.query-where-clause-editor-container {
position: relative;
display: flex;
flex-direction: row;

View File

@@ -1292,7 +1292,7 @@ function QuerySearch({
if (onRun && typeof onRun === 'function') {
onRun(query);
} else {
handleRunQuery();
handleRunQuery(true, true);
}
return true;
},

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
@@ -27,12 +26,9 @@ export const QueryV2 = memo(function QueryV2({
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
hasTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@@ -79,15 +75,6 @@ export const QueryV2 = memo(function QueryV2({
dataSource,
]);
const showInlineQuerySearch = useMemo(() => {
if (!showTraceOperator) {
return false;
}
return (
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
);
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
@@ -121,12 +108,11 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{(!showOnlyWhereClause || showTraceOperator) && (
{!showOnlyWhereClause && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
hasTraceOperator={hasTraceOperator}
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
@@ -136,7 +122,6 @@ export const QueryV2 = memo(function QueryV2({
false
}
isCollapsed={isCollapsed}
showTraceOperator={showTraceOperator}
entityType="query"
entityData={query}
onToggleVisibility={handleToggleDisableQuery}
@@ -154,28 +139,7 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!isCollapsed && showInlineQuerySearch && (
<div className="qb-search-filter-container" style={{ flex: 1 }}>
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
{isMultiQueryAllowed && (
{!isListViewPanel && (
<Dropdown
className="query-actions-dropdown"
menu={{
@@ -217,31 +181,28 @@ export const QueryV2 = memo(function QueryV2({
</div>
)}
{!showInlineQuerySearch && (
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
)}
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
@@ -264,17 +225,16 @@ export const QueryV2 = memo(function QueryV2({
/>
)}
{!showOnlyWhereClause &&
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>

View File

@@ -1,185 +0,0 @@
.qb-trace-operator {
padding: 8px;
display: flex;
gap: 8px;
&.non-list-view {
padding-left: 40px;
position: relative;
&::before {
content: '';
position: absolute;
top: 24px;
left: 12px;
height: 88px;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
&-span-source-label {
display: flex;
align-items: center;
gap: 8px;
height: 24px;
&-query {
font-size: 14px;
font-weight: 400;
color: var(--bg-vanilla-100);
}
&-query-name {
width: 18px;
height: 18px;
display: grid;
place-content: center;
padding: 2px;
border-radius: 2px;
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
color: var(--Sakura-400, #f56c87);
font-size: 12px;
}
}
&-arrow {
position: relative;
&::before {
content: '';
position: absolute;
top: 16px;
transform: translateY(-50%);
left: -26px;
height: 1px;
width: 20px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
&::after {
content: '';
position: absolute;
top: 16px;
left: -10px;
transform: translateY(-50%);
height: 4px;
width: 4px;
border-radius: 50%;
background-color: var(--bg-slate-400);
}
}
&-input {
width: 100%;
}
&-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
&-aggregation-container {
display: flex;
flex-direction: column;
gap: 8px;
}
&-add-ons-container {
width: 100%;
display: flex;
flex-direction: row;
gap: 16px;
}
&-label-with-input {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.qb-trace-operator-editor-container {
flex: 1;
}
&.arrow-left {
&::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
height: 1px;
width: 16px;
background-color: var(--bg-slate-400);
}
}
.label {
color: var(--bg-vanilla-400);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-right: 1px solid var(--bg-slate-400);
}
}
}
.lightMode {
.qb-trace-operator {
&-arrow {
&::before {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
background-color: var(--bg-vanilla-300);
}
}
&.non-list-view {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
&-label-with-input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.label {
color: var(--bg-ink-500) !important;
border-right: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -1,119 +0,0 @@
/* eslint-disable react/require-default-props */
/* eslint-disable sonarjs/no-duplicate-string */
import './TraceOperator.styles.scss';
import { Button, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Trash2 } from 'lucide-react';
import { useCallback } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAddOns from '../QueryAddOns/QueryAddOns';
import QueryAggregation from '../QueryAggregation/QueryAggregation';
import TraceOperatorEditor from './TraceOperatorEditor';
export default function TraceOperator({
traceOperator,
isListViewPanel = false,
}: {
traceOperator: IBuilderTraceOperator;
isListViewPanel?: boolean;
}): JSX.Element {
const { panelType, removeTraceOperator } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: traceOperator,
entityVersion: '',
isForTraceOperator: true,
});
const handleTraceOperatorChange = useCallback(
(traceOperatorExpression: string) => {
handleChangeQueryData('expression', traceOperatorExpression);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
handleChangeQueryData('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
return (
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
<div className="qb-trace-operator-container">
<div
className={cx(
'qb-trace-operator-label-with-input',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
<div className="qb-trace-operator-editor-container">
<TraceOperatorEditor
value={traceOperator?.expression || ''}
traceOperator={traceOperator}
onChange={handleTraceOperatorChange}
/>
</div>
</div>
{!isListViewPanel && (
<div className="qb-trace-operator-aggregation-container">
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
<QueryAggregation
dataSource={DataSource.TRACES}
key={`query-search-${traceOperator.queryName}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={traceOperator}
/>
</div>
<div
className={cx(
'qb-trace-operator-add-ons-container',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<QueryAddOns
index={0}
query={traceOperator}
version="v3"
isForTraceOperator
isListViewPanel={false}
showReduceTo={false}
panelType={panelType}
/>
</div>
</div>
)}
</div>
<Tooltip title="Remove Trace Operator" placement="topLeft">
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
<Trash2 size={14} />
</Button>
</Tooltip>
</div>
);
}

View File

@@ -1,491 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import '../QuerySearch/QuerySearch.styles.scss';
import { CheckCircleFilled } from '@ant-design/icons';
import {
autocompletion,
closeCompletion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
import { Button, Popover } from 'antd';
import cx from 'classnames';
import {
TRACE_OPERATOR_OPERATORS,
TRACE_OPERATOR_OPERATORS_LABELS,
TRACE_OPERATOR_OPERATORS_WITH_PRIORITY,
} from 'constants/antlrQueryConstants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { validateTraceOperatorQuery } from 'utils/queryValidationUtils';
import { getTraceOperatorContextAtCursor } from './utils/traceOperatorContextUtils';
import { getInvolvedQueriesInTraceOperator } from './utils/utils';
// Custom extension to stop events
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
interface TraceOperatorEditorProps {
value: string;
traceOperator: IBuilderTraceOperator;
onChange: (value: string) => void;
placeholder?: string;
onRun?: (query: string) => void;
}
function TraceOperatorEditor({
value,
onChange,
traceOperator,
placeholder = 'Enter your trace operator query',
onRun,
}: TraceOperatorEditorProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [isFocused, setIsFocused] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const editorRef = useRef<EditorView | null>(null);
const [validation, setValidation] = useState<IValidationResult>({
isValid: false,
message: '',
errors: [],
});
// Track if the query was changed externally (from props) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery, handleRunQuery } = useQueryBuilder();
const queryOptions = useMemo(
() =>
currentQuery.builder.queryData
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
.map((query) => ({
label: query.queryName,
type: 'atom',
apply: query.queryName,
})),
[currentQuery.builder.queryData],
);
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const handleQueryValidation = (newQuery: string): void => {
try {
const validationResponse = validateTraceOperatorQuery(newQuery);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process trace operator',
errors: [error as IDetailedError],
});
}
};
// Detect external value changes and mark for validation
useEffect(() => {
const newValue = value || '';
if (newValue !== lastExternalValue) {
setIsExternalQueryChange(true);
setLastExternalValue(newValue);
}
}, [value, lastExternalValue]);
// Validate when the value changes externally (including on mount)
useEffect(() => {
if (isExternalQueryChange && value) {
handleQueryValidation(value);
setIsExternalQueryChange(false);
}
}, [isExternalQueryChange, value]);
// Enhanced autosuggestion function with context awareness
function autoSuggestions(context: CompletionContext): CompletionResult | null {
// This matches words before the cursor position
// eslint-disable-next-line no-useless-escape
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
if (word?.from === word?.to && !context.explicit) return null;
// Get the trace operator context at the cursor position
const queryContext = getTraceOperatorContextAtCursor(value, cursorPos.ch);
// Define autocomplete options based on the context
let options: {
label: string;
type: string;
info?: string;
apply:
| string
| ((view: EditorView, completion: any, from: number, to: number) => void);
detail?: string;
boost?: number;
}[] = [];
// Helper function to add space after selection
const addSpaceAfterSelection = (
view: EditorView,
completion: any,
from: number,
to: number,
shouldAddSpace = true,
): void => {
view.dispatch({
changes: {
from,
to,
insert: shouldAddSpace ? `${completion.apply} ` : `${completion.apply}`,
},
selection: {
anchor:
from +
(shouldAddSpace ? completion.apply.length + 1 : completion.apply.length),
},
});
// Do not reopen here; onUpdate will handle reopening via toggleSuggestions
};
// Helper function to add space after selection to options
const addSpaceToOptions = (opts: typeof options): typeof options =>
opts.map((option) => {
const originalApply = option.apply || option.label;
return {
...option,
apply: (
view: EditorView,
completion: any,
from: number,
to: number,
): void => {
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
},
};
});
if (queryContext.isInAtom) {
// Suggest atoms (identifiers) for trace operators
const involvedQueries = getInvolvedQueriesInTraceOperator([traceOperator]);
options = queryOptions.map((option) => ({
...option,
boost: !involvedQueries.includes(option.apply as string) ? 100 : -99,
}));
// Filter options based on what user is typing
const searchText = word?.text.toLowerCase().trim() ?? '';
options = options.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
// Add space after selection for atoms
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: optionsWithSpace,
};
}
if (queryContext.isInOperator) {
// Suggest operators for trace operators
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
options = operators.map((operator) => ({
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
: operator,
type: 'operator',
apply: operator,
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
}));
// Add space after selection for operators
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: optionsWithSpace,
};
}
if (queryContext.isInParenthesis) {
// Different suggestions based on the context within parenthesis
const curChar = value.charAt(cursorPos.ch - 1) || '';
if (curChar === '(') {
// Right after opening parenthesis, suggest atoms or nested expressions
options = [
{ label: '(', type: 'parenthesis', apply: '(' },
...queryOptions,
];
// Add space after selection for opening parenthesis context
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
options: optionsWithSpace,
};
}
if (curChar === ')') {
// After closing parenthesis, suggest operators
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
options = operators.map((operator) => ({
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
: operator,
type: 'operator',
apply: operator,
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
}));
// Add space after selection for closing parenthesis context
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
options: optionsWithSpace,
};
}
}
// Default: suggest atoms if no specific context
options = [
...queryOptions,
{
label: '(',
type: 'parenthesis',
apply: '(',
},
];
// Filter options based on what user is typing
const searchText = word?.text.toLowerCase().trim() ?? '';
options = options.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
// Add space after selection
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? context.pos,
options: optionsWithSpace,
};
}
const handleUpdate = useCallback(
(viewUpdate: { view: EditorView }): void => {
if (!editorRef.current) {
editorRef.current = viewUpdate.view;
}
const selection = viewUpdate.view.state.selection.main;
const pos = selection.head;
const lineInfo = viewUpdate.view.state.doc.lineAt(pos);
const newPos = {
line: lineInfo.number,
ch: pos - lineInfo.from,
};
if (newPos.line !== cursorPos.line || newPos.ch !== cursorPos.ch) {
setCursorPos(newPos);
// Trigger suggestions on context update
toggleSuggestions(10);
}
},
[cursorPos, toggleSuggestions],
);
const handleChange = (newValue: string): void => {
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
setLastExternalValue(newValue);
onChange(newValue);
};
const handleBlur = (): void => {
handleQueryValidation(value);
setIsFocused(false);
};
// Effect to handle focus state and trigger suggestions on focus
useEffect(() => {
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
return (
<div className="code-mirror-where-clause">
<div className="query-where-clause-editor-container">
<CodeMirror
value={value}
theme={isDarkMode ? copilot : githubLight}
onChange={handleChange}
onUpdate={handleUpdate}
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,
})}
extensions={[
autocompletion({
override: [autoSuggestions],
defaultKeymap: true,
closeOnBlur: true,
activateOnTyping: true,
maxRenderedOptions: 50,
}),
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
Prec.highest(
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
{
key: 'Enter',
preventDefault: true,
// Prevent default behavior of Enter to add new line
// and instead run a custom action
run: (): boolean => true,
},
{
key: 'Mod-Enter',
preventDefault: true,
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(value);
} else {
handleRunQuery();
}
return true;
},
},
{
key: 'Shift-Enter',
preventDefault: true,
// Prevent default behavior of Shift-Enter to add new line
run: (): boolean => true,
},
]),
),
]}
placeholder={placeholder}
basicSetup={{
lineNumbers: false,
}}
onFocus={(): void => {
setIsFocused(true);
}}
onBlur={handleBlur}
/>
{value && validation.isValid === false && !isFocused && (
<div
className={cx('query-status-container', {
hasErrors: validation.errors.length > 0,
})}
>
<Popover
placement="bottomRight"
showArrow={false}
content={
<div className="query-status-content">
<div className="query-status-content-header">
<div className="query-validation">
<div className="query-validation-errors">
{validation.errors.map((error) => (
<div key={error.message} className="query-validation-error">
<div className="query-validation-error">
{error.line}:{error.column} - {error.message}
</div>
</div>
))}
</div>
</div>
</div>
</div>
}
overlayClassName="query-status-popover"
>
{validation.isValid ? (
<Button
type="text"
icon={<CheckCircleFilled />}
className="periscope-btn ghost"
/>
) : (
<Button
type="text"
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
className="periscope-btn ghost"
/>
)}
</Popover>
</div>
)}
</div>
</div>
);
}
TraceOperatorEditor.defaultProps = {
onRun: undefined,
placeholder: 'Enter your trace operator query',
};
export default TraceOperatorEditor;

View File

@@ -1,425 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/cognitive-complexity */
import { Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import {
createTraceOperatorContext,
extractTraceExpressionPairs,
getTraceOperatorContextAtCursor,
} from '../utils/traceOperatorContextUtils';
describe('traceOperatorContextUtils', () => {
describe('createTraceOperatorContext', () => {
it('should create a context object with all required properties', () => {
const mockToken = {
type: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
} as Token;
const context = createTraceOperatorContext(
mockToken,
true,
false,
false,
false,
'atom',
'operator',
[],
null,
);
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
currentToken: 'test',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
atomToken: 'atom',
operatorToken: 'operator',
expressionPairs: [],
currentPair: null,
});
});
it('should create a context object with default values', () => {
const mockToken = {
type: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
} as Token;
const context = createTraceOperatorContext(
mockToken,
false,
true,
false,
false,
);
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
currentToken: 'test',
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
atomToken: undefined,
operatorToken: undefined,
expressionPairs: [],
currentPair: undefined,
});
});
});
describe('extractTraceExpressionPairs', () => {
it('should extract simple expression pair', () => {
const query = 'A => B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].position.leftStart).toBe(0);
expect(result[0].position.leftEnd).toBe(0);
expect(result[0].operator).toBe('=>');
expect(result[0].position.operatorStart).toBe(2);
expect(result[0].position.operatorEnd).toBe(3);
expect(result[0].rightAtom).toBe('B');
expect(result[0].position.rightStart).toBe(5);
expect(result[0].position.rightEnd).toBe(5);
expect(result[0].isComplete).toBe(true);
});
it('should extract multiple expression pairs', () => {
const query = 'A => B && C => D';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(2);
// First pair: A => B
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
// Second pair: C => D
expect(result[1].leftAtom).toBe('C');
expect(result[1].operator).toBe('=>');
expect(result[1].rightAtom).toBe('D');
});
it('should handle NOT operator', () => {
const query = 'NOT A => B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
});
it('should handle parentheses', () => {
const query = '(A => B) && (C => D)';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(2);
expect(result[0].leftAtom).toBe('A');
expect(result[0].rightAtom).toBe('B');
expect(result[1].leftAtom).toBe('C');
expect(result[1].rightAtom).toBe('D');
});
it('should handle incomplete expressions', () => {
const query = 'A =>';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBeUndefined();
expect(result[0].isComplete).toBe(true);
});
it('should handle complex nested expressions', () => {
const query = 'A => B && (C => D || E => F)';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(3);
expect(result[0].leftAtom).toBe('A');
expect(result[0].rightAtom).toBe('B');
expect(result[1].leftAtom).toBe('C');
expect(result[1].rightAtom).toBe('D');
expect(result[2].leftAtom).toBe('E');
expect(result[2].rightAtom).toBe('F');
});
it('should handle whitespace variations', () => {
const query = 'A=>B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
});
it('should handle error cases gracefully', () => {
const query = 'invalid syntax @#$%';
const result = extractTraceExpressionPairs(query);
// Should return an array (even if empty or with partial results)
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThanOrEqual(0);
});
});
describe('getTraceOperatorContextAtCursor', () => {
beforeEach(() => {
// Reset console.error mock
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return default context for empty query', () => {
const result = getTraceOperatorContextAtCursor('', 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should return default context for null query', () => {
const result = getTraceOperatorContextAtCursor(null as any, 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should return default context for undefined query', () => {
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should identify atom context', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'A'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should identify operator context', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 2); // cursor at '='
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(2);
expect(result.stop).toBe(2);
});
it('should identify parenthesis context', () => {
const query = '(A => B)';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at '('
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should handle cursor at space', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at space
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
});
it('should handle cursor at end of query', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at end
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(5);
expect(result.stop).toBe(5);
});
it('should handle complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 8); // cursor at '&'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(7);
expect(result.stop).toBe(8);
});
it('should identify operator position in complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 10); // cursor at 'C'
expect(result.atomToken).toBe('C');
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(10);
expect(result.stop).toBe(10);
});
it('should identify atom position in complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 13); // cursor at '>'
expect(result.atomToken).toBe('C');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(12);
expect(result.stop).toBe(13);
});
it('should handle transition points', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 4); // cursor at 'B'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(4);
expect(result.stop).toBe(4);
});
it('should handle whitespace in complex queries', () => {
const query = 'A=>B && C=>D';
const result = getTraceOperatorContextAtCursor(query, 6); // cursor at '&'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(5);
expect(result.stop).toBe(6);
});
it('should handle NOT operator context', () => {
const query = 'NOT A => B';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'N'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
});
it('should handle parentheses context', () => {
const query = '(A => B)';
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at 'A'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should handle expression pairs context', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at 'A' in "&&"
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
});
it('should handle various cursor positions', () => {
const query = 'A => B';
// Test cursor at each position
for (let i = 0; i < query.length; i++) {
const result = getTraceOperatorContextAtCursor(query, i);
expect(result).toBeDefined();
expect(typeof result.start).toBe('number');
expect(typeof result.stop).toBe('number');
}
});
});
});

View File

@@ -1,46 +0,0 @@
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';
const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
(({ expression } as unknown) as IBuilderTraceOperator);
describe('getInvolvedQueriesInTraceOperator', () => {
it('returns empty array for empty input', () => {
const result = getInvolvedQueriesInTraceOperator([]);
expect(result).toEqual([]);
});
it('extracts identifiers from expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => B'),
]);
expect(result).toEqual(['A', 'B']);
});
it('extracts identifiers from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => (NOT B || C)'),
]);
expect(result).toEqual(['A', 'B', 'C']);
});
it('filters out querynames from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator(
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
),
]);
expect(result).toEqual([
'A1',
'B2',
'C3',
'D4',
'E5',
'F6',
'G7',
'H8',
'I9',
]);
});
});

View File

@@ -1,562 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable no-continue */
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import { IToken } from 'types/antlrQueryTypes';
// Trace Operator Context Interface
export interface ITraceOperatorContext {
tokenType: number;
text: string;
start: number;
stop: number;
currentToken: string;
isInAtom: boolean;
isInOperator: boolean;
isInParenthesis: boolean;
isInExpression: boolean;
atomToken?: string;
operatorToken?: string;
expressionPairs: ITraceExpressionPair[];
currentPair?: ITraceExpressionPair | null;
}
// Trace Expression Pair Interface
export interface ITraceExpressionPair {
leftAtom: string;
operator: string;
rightAtom?: string;
rightExpression?: string;
position: {
leftStart: number;
leftEnd: number;
operatorStart: number;
operatorEnd: number;
rightStart?: number;
rightEnd?: number;
};
isComplete: boolean;
}
// Helper functions to determine token types
function isAtomToken(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.IDENTIFIER;
}
function isOperatorToken(tokenType: number): boolean {
return [
TraceOperatorGrammarLexer.T__2, // '=>'
TraceOperatorGrammarLexer.T__3, // '&&'
TraceOperatorGrammarLexer.T__4, // '||'
TraceOperatorGrammarLexer.T__5, // 'NOT'
TraceOperatorGrammarLexer.T__6, // '->'
].includes(tokenType);
}
function isParenthesisToken(tokenType: number): boolean {
return (
tokenType === TraceOperatorGrammarLexer.T__0 ||
tokenType === TraceOperatorGrammarLexer.T__1
);
}
function isOpeningParenthesis(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.T__0;
}
function isClosingParenthesis(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.T__1;
}
// Function to create a context object
export function createTraceOperatorContext(
token: Token,
isInAtom: boolean,
isInOperator: boolean,
isInParenthesis: boolean,
isInExpression: boolean,
atomToken?: string,
operatorToken?: string,
expressionPairs?: ITraceExpressionPair[],
currentPair?: ITraceExpressionPair | null,
): ITraceOperatorContext {
return {
tokenType: token.type,
text: token.text || '',
start: token.start,
stop: token.stop,
currentToken: token.text || '',
isInAtom,
isInOperator,
isInParenthesis,
isInExpression,
atomToken,
operatorToken,
expressionPairs: expressionPairs || [],
currentPair,
};
}
// Helper to determine token context
function determineTraceTokenContext(
token: IToken,
): {
isInAtom: boolean;
isInOperator: boolean;
isInParenthesis: boolean;
isInExpression: boolean;
} {
const tokenType = token.type;
return {
isInAtom: isAtomToken(tokenType),
isInOperator: isOperatorToken(tokenType),
isInParenthesis: isParenthesisToken(tokenType),
isInExpression: false, // Will be determined by broader context
};
}
/**
* Extracts all expression pairs from a trace operator query string
* This parses the query according to the TraceOperatorGrammar.g4 grammar
*
* @param query The trace operator query string to parse
* @returns An array of ITraceExpressionPair objects representing the expression pairs
*/
export function extractTraceExpressionPairs(
query: string,
): ITraceExpressionPair[] {
try {
const input = query || '';
const chars = CharStreams.fromString(input);
const lexer = new TraceOperatorGrammarLexer(chars);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill();
const allTokens = tokenStream.tokens as IToken[];
const expressionPairs: ITraceExpressionPair[] = [];
let currentPair: Partial<ITraceExpressionPair> | null = null;
let i = 0;
while (i < allTokens.length) {
const token = allTokens[i];
i++;
// Skip EOF and whitespace tokens
if (token.type === TraceOperatorGrammarLexer.EOF || token.channel !== 0) {
continue;
}
// If token is an IDENTIFIER (atom), start or continue a pair
if (isAtomToken(token.type)) {
// If we don't have a current pair, start one
if (!currentPair) {
currentPair = {
leftAtom: token.text,
position: {
leftStart: token.start,
leftEnd: token.stop,
operatorStart: 0,
operatorEnd: 0,
},
};
}
// If we have a current pair but no operator yet, this is still the left atom
else if (!currentPair.operator && currentPair.position) {
currentPair.leftAtom = token.text;
currentPair.position.leftStart = token.start;
currentPair.position.leftEnd = token.stop;
}
// If we have an operator, this is the right atom
else if (
currentPair.operator &&
!currentPair.rightAtom &&
currentPair.position
) {
currentPair.rightAtom = token.text;
currentPair.position.rightStart = token.start;
currentPair.position.rightEnd = token.stop;
currentPair.isComplete = true;
// Add the completed pair to the result
expressionPairs.push(currentPair as ITraceExpressionPair);
currentPair = null;
}
}
// If token is an operator and we have a left atom
else if (
isOperatorToken(token.type) &&
currentPair &&
currentPair.leftAtom &&
currentPair.position
) {
currentPair.operator = token.text;
currentPair.position.operatorStart = token.start;
currentPair.position.operatorEnd = token.stop;
// If this is a NOT operator, it might be followed by another operator
if (token.type === TraceOperatorGrammarLexer.T__5 && i < allTokens.length) {
// Look ahead for the next operator
const nextToken = allTokens[i];
if (isOperatorToken(nextToken.type) && nextToken.channel === 0) {
currentPair.operator = `${token.text} ${nextToken.text}`;
currentPair.position.operatorEnd = nextToken.stop;
i++; // Skip the next token since we've consumed it
}
}
}
// If token is an opening parenthesis after an operator, this is a right expression
else if (
isOpeningParenthesis(token.type) &&
currentPair &&
currentPair.operator &&
!currentPair.rightAtom &&
currentPair.position
) {
// Find the matching closing parenthesis
let parenCount = 1;
let j = i;
let rightExpression = '';
const rightStart = token.start;
let rightEnd = token.stop;
while (j < allTokens.length && parenCount > 0) {
const parenToken = allTokens[j];
if (parenToken.channel === 0) {
if (isOpeningParenthesis(parenToken.type)) {
parenCount++;
} else if (isClosingParenthesis(parenToken.type)) {
parenCount--;
if (parenCount === 0) {
rightEnd = parenToken.stop;
break;
}
}
}
rightExpression += parenToken.text;
j++;
}
if (parenCount === 0) {
currentPair.rightExpression = rightExpression;
currentPair.position.rightStart = rightStart;
currentPair.position.rightEnd = rightEnd;
currentPair.isComplete = true;
// Add the completed pair to the result
expressionPairs.push(currentPair as ITraceExpressionPair);
currentPair = null;
// Skip to the end of the expression
i = j;
}
}
}
// Add any remaining incomplete pair
if (currentPair && currentPair.leftAtom && currentPair.position) {
expressionPairs.push({
...currentPair,
isComplete: !!(currentPair.leftAtom && currentPair.operator),
} as ITraceExpressionPair);
}
return expressionPairs;
} catch (error) {
console.error('Error in extractTraceExpressionPairs:', error);
return [];
}
}
/**
* Gets the current expression pair at the cursor position
*
* @param expressionPairs An array of ITraceExpressionPair objects
* @param query The full query string
* @param cursorIndex The position of the cursor in the query
* @returns The expression pair at the cursor position, or null if not found
*/
export function getCurrentTraceExpressionPair(
expressionPairs: ITraceExpressionPair[],
cursorIndex: number,
): ITraceExpressionPair | null {
try {
if (expressionPairs.length === 0) {
return null;
}
// Find the rightmost pair whose end position is before or at the cursor
let bestMatch: ITraceExpressionPair | null = null;
// eslint-disable-next-line no-restricted-syntax
for (const pair of expressionPairs) {
const { position } = pair;
const pairEnd =
position.rightEnd || position.operatorEnd || position.leftEnd;
const pairStart = position.leftStart;
// If this pair ends at or before the cursor, and it's further right than our previous best match
if (
pairStart <= cursorIndex &&
cursorIndex <= pairEnd + 1 &&
(!bestMatch ||
pairEnd >
(bestMatch.position.rightEnd ||
bestMatch.position.operatorEnd ||
bestMatch.position.leftEnd))
) {
bestMatch = pair;
}
}
return bestMatch;
} catch (error) {
console.error('Error in getCurrentTraceExpressionPair:', error);
return null;
}
}
/**
* Gets the current trace operator context at the cursor position
* This is useful for determining what kind of suggestions to show
*
* @param query The trace operator query string
* @param cursorIndex The position of the cursor in the query
* @returns The trace operator context at the cursor position
*/
export function getTraceOperatorContextAtCursor(
query: string,
cursorIndex: number,
): ITraceOperatorContext {
try {
// Guard against infinite recursion
const stackTrace = new Error().stack || '';
const callCount = (stackTrace.match(/getTraceOperatorContextAtCursor/g) || [])
.length;
if (callCount > 3) {
console.warn(
'Potential infinite recursion detected in getTraceOperatorContextAtCursor',
);
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
};
}
// Create input stream and lexer
const input = query || '';
const chars = CharStreams.fromString(input);
const lexer = new TraceOperatorGrammarLexer(chars);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill();
const allTokens = tokenStream.tokens as IToken[];
// Get expression pairs information
const expressionPairs = extractTraceExpressionPairs(query);
const currentPair = getCurrentTraceExpressionPair(
expressionPairs,
cursorIndex,
);
// Find the token at or just before the cursor
let lastTokenBeforeCursor: IToken | null = null;
for (let i = 0; i < allTokens.length; i++) {
const token = allTokens[i];
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) {
lastTokenBeforeCursor = token;
}
if (token.start > cursorIndex) {
break;
}
}
// Find exact token at cursor
let exactToken: IToken | null = null;
for (let i = 0; i < allTokens.length; i++) {
const token = allTokens[i];
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
exactToken = token;
break;
}
}
// If we don't have any tokens, return default context
if (!lastTokenBeforeCursor && !exactToken) {
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true, // Default to atom context when input is empty
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair: null,
};
}
// Check if cursor is at a space after a token (transition point)
const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' ';
const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' ';
const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' ';
const isTransitionPoint =
(isAtSpace && isAfterToken) ||
(cursorIndex === query.length && isAfterToken);
// If we're at a transition point after a token, progress the context
if (
lastTokenBeforeCursor &&
(isAtSpace || isAfterSpace || isTransitionPoint)
) {
const lastTokenContext = determineTraceTokenContext(lastTokenBeforeCursor);
// Apply context progression: atom → operator → atom/expression → operator → atom
if (lastTokenContext.isInAtom) {
// After atom + space, move to operator context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
atomToken: lastTokenBeforeCursor.text,
expressionPairs,
currentPair,
};
}
if (lastTokenContext.isInOperator) {
// After operator + space, move to atom/expression context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: true, // Expecting an atom or expression after operator
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
operatorToken: lastTokenBeforeCursor.text,
atomToken: currentPair?.leftAtom,
expressionPairs,
currentPair,
};
}
if (
lastTokenContext.isInParenthesis &&
isClosingParenthesis(lastTokenBeforeCursor.type)
) {
// After closing parenthesis, move to operator context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair,
};
}
}
// If cursor is at the end of a token, return the current token context
if (exactToken && cursorIndex === exactToken.stop + 1) {
const tokenContext = determineTraceTokenContext(exactToken);
return {
tokenType: exactToken.type,
text: exactToken.text,
start: exactToken.start,
stop: exactToken.stop,
currentToken: exactToken.text,
...tokenContext,
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
operatorToken: tokenContext.isInOperator
? exactToken.text
: currentPair?.operator,
expressionPairs,
currentPair,
};
}
// Regular token-based context detection
if (exactToken?.channel === 0) {
const tokenContext = determineTraceTokenContext(exactToken);
return {
tokenType: exactToken.type,
text: exactToken.text,
start: exactToken.start,
stop: exactToken.stop,
currentToken: exactToken.text,
...tokenContext,
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
operatorToken: tokenContext.isInOperator
? exactToken.text
: currentPair?.operator,
expressionPairs,
currentPair,
};
}
// Default fallback to atom context
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair,
};
} catch (error) {
console.error('Error in getTraceOperatorContextAtCursor:', error);
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
};
}
}

View File

@@ -1,22 +0,0 @@
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
export const getInvolvedQueriesInTraceOperator = (
traceOperators: IBuilderTraceOperator[],
): string[] => {
if (
!traceOperators ||
traceOperators.length === 0 ||
traceOperators.length > 1
)
return [];
const currentTraceOperator = traceOperators[0];
// Match any word starting with letter or underscore
const tokens =
currentTraceOperator.expression.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || [];
// Filter out operator keywords
const operators = new Set(['NOT']);
return tokens.filter((t) => !operators.has(t));
};

View File

@@ -1,25 +1,9 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable import/no-unresolved */
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
} from '../utils';
import { convertFiltersToExpression } from '../utils';
describe('convertFiltersToExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
@@ -549,426 +533,4 @@ describe('convertFiltersToExpression', () => {
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should return filters with new expression when no existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'test-service',
},
],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe("service.name = 'test-service'");
});
it('should handle empty filters', () => {
const filters = {
items: [],
op: 'AND',
};
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filter.expression).toBe('');
});
it('should handle existing query with matching filters', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS['='],
value: 'updated-service',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe("service.name = 'updated-service'");
// Ensure parser can parse the existing query
expect(extractQueryPairs(existingQuery)).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'service.name',
operator: '=',
value: "'old-service'",
}),
]),
);
});
it('should handle IN operator with existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name IN ['old-service']";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters).toBeDefined();
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2']",
);
});
it('should handle IN operator conversion from equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: OPERATORS.IN,
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name IN ['service1', 'service2'] ",
);
});
it('should handle NOT IN operator conversion from not equals', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'service.name', key: 'service.name', type: 'string' },
op: negateOperator(OPERATORS.IN),
value: ['service1', 'service2'],
},
],
op: 'AND',
};
const existingQuery = "service.name != 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe(
"service.name NOT IN ['service1', 'service2'] ",
);
});
it('should add new filters when they do not exist in existing query', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'new.key', key: 'new.key', type: 'string' },
op: OPERATORS['='],
value: 'new-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2); // Original + new filter
expect(result.filter.expression).toBe(
"service.name = 'old-service' new.key = 'new-value'",
);
});
it('should handle simple value replacement', () => {
const filters = {
items: [
{
id: '1',
key: { id: 'status', key: 'status', type: 'string' },
op: OPERATORS['='],
value: 'error',
},
],
op: 'AND',
};
const existingQuery = "status = 'success'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(1);
expect(result.filter.expression).toBe("status = 'error'");
});
it('should handle filters with no key gracefully', () => {
const filters = {
items: [
{
id: '1',
key: undefined,
op: OPERATORS['='],
value: 'test-value',
},
],
op: 'AND',
};
const existingQuery = "service.name = 'old-service'";
const result = convertFiltersToExpressionWithExistingQuery(
filters,
existingQuery,
);
expect(result.filters.items).toHaveLength(2);
expect(result.filter.expression).toBe("service.name = 'old-service'");
});
});
describe('convertAggregationToExpression', () => {
const mockAttribute: BaseAutocompleteData = {
id: 'test-id',
key: 'test_metric',
type: 'string',
dataType: DataTypes.String,
};
it('should return undefined when no aggregateOperator is provided', () => {
const result = convertAggregationToExpression({
aggregateOperator: '',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toBeUndefined();
});
it('should convert metrics aggregation with required temporality field', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
timeAggregation: 'avg',
spaceAggregation: 'max',
alias: 'test_alias',
reduceTo: 'sum',
temporality: 'delta',
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'avg',
spaceAggregation: 'max',
reduceTo: 'sum',
temporality: 'delta',
},
]);
});
it('should handle noop operators by converting to count', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
timeAggregation: 'noop',
spaceAggregation: 'noop',
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'count',
spaceAggregation: 'count',
},
]);
});
it('should handle missing attribute key gracefully', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: { ...mockAttribute, key: '' },
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
{
metricName: '',
timeAggregation: 'sum',
spaceAggregation: 'sum',
},
]);
});
it('should convert traces aggregation to expression format', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'count',
aggregateAttribute: mockAttribute,
dataSource: DataSource.TRACES,
alias: 'trace_alias',
});
expect(result).toEqual([
{
expression: 'count(test_metric)',
alias: 'trace_alias',
},
]);
});
it('should convert logs aggregation to expression format', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'avg',
aggregateAttribute: mockAttribute,
dataSource: DataSource.LOGS,
alias: 'log_alias',
});
expect(result).toEqual([
{
expression: 'avg(test_metric)',
alias: 'log_alias',
},
]);
});
it('should handle aggregation without attribute key for traces/logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'count',
aggregateAttribute: { ...mockAttribute, key: '' },
dataSource: DataSource.TRACES,
});
expect(result).toEqual([
{
expression: 'count()',
},
]);
});
it('should handle missing alias for traces/logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.LOGS,
});
expect(result).toEqual([
{
expression: 'sum(test_metric)',
},
]);
});
it('should use aggregateOperator as fallback for time and space aggregation', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'max',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'max',
spaceAggregation: 'max',
},
]);
});
it('should handle undefined aggregateAttribute parameter with metrics', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'sum',
aggregateAttribute: mockAttribute,
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
{
metricName: 'test_metric',
timeAggregation: 'sum',
spaceAggregation: 'sum',
reduceTo: undefined,
temporality: undefined,
},
]);
});
it('should handle undefined aggregateAttribute parameter with traces', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: (undefined as unknown) as BaseAutocompleteData,
dataSource: DataSource.TRACES,
});
expect(result).toEqual([
{
expression: 'count()',
},
]);
});
it('should handle undefined aggregateAttribute parameter with logs', () => {
const result = convertAggregationToExpression({
aggregateOperator: 'noop',
aggregateAttribute: (undefined as unknown) as BaseAutocompleteData,
dataSource: DataSource.LOGS,
});
expect(result).toEqual([
{
expression: 'count()',
},
]);
});
});

View File

@@ -6,7 +6,7 @@ import {
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -22,7 +22,7 @@ import {
TraceAggregation,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
@@ -163,19 +163,6 @@ export const convertExpressionToFilters = (
return filters;
};
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
const queryPairs = extractQueryPairs(query);
const queryPairsMap: Map<string, IQueryPair> = new Map();
queryPairs.forEach((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
queryPairsMap.set(key, pair);
});
return queryPairsMap;
};
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
@@ -208,12 +195,24 @@ export const convertFiltersToExpressionWithExistingQuery = (
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
// Map extracted query pairs to key-specific pair information for faster access
let queryPairsMap = getQueryPairsMap(existingQuery.trim());
if (queryPairs.length > 0) {
queryPairsMap = new Map(
queryPairs.map((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
return [key, pair];
}),
);
}
filters?.items?.forEach((filter) => {
const { key, op, value } = filter;
@@ -242,37 +241,10 @@ export const convertFiltersToExpressionWithExistingQuery = (
existingPair.position?.valueEnd
) {
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
// Check if existing values match current filter values (for array-based operators)
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
// Clean quotes from string values for comparison
const cleanValues = (values: any[]): any[] =>
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
const cleanExistingValues = cleanValues(existingPair.valueList);
const cleanFilterValues = cleanValues(filter.value);
// Compare arrays (order-independent) - if identical, keep existing value
const isSameValues =
cleanExistingValues.length === cleanFilterValues.length &&
isEqual(sortBy(cleanExistingValues), sortBy(cleanFilterValues));
if (isSameValues) {
// Values are identical, preserve existing formatting
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
existingPair.value +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
return;
}
}
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
queryPairsMap = getQueryPairsMap(modifiedQuery);
return;
}
@@ -298,7 +270,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notInPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery.trim());
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
@@ -315,7 +286,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
equalsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
@@ -332,7 +302,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
@@ -354,7 +323,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
@@ -367,23 +335,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
if (
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
const existingPair = queryPairsMap.get(
`${filter.key?.key}-${filter.op}`.trim().toLowerCase(),
);
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
const formattedValue = formatValueForExpression(value, op);
// replace the value with the new value
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
queryPairsMap = getQueryPairsMap(modifiedQuery);
}
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
}
@@ -580,25 +531,14 @@ export const convertHavingToExpression = (
* @returns New aggregation format based on data source
*
*/
export const convertAggregationToExpression = ({
aggregateOperator,
aggregateAttribute,
dataSource,
timeAggregation,
spaceAggregation,
alias,
reduceTo,
temporality,
}: {
aggregateOperator: string;
aggregateAttribute: BaseAutocompleteData;
dataSource: DataSource;
timeAggregation?: string;
spaceAggregation?: string;
alias?: string;
reduceTo?: ReduceOperators;
temporality?: string;
}): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
export const convertAggregationToExpression = (
aggregateOperator: string,
aggregateAttribute: BaseAutocompleteData,
dataSource: DataSource,
timeAggregation?: string,
spaceAggregation?: string,
alias?: string,
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
// Skip if no operator or attribute key
if (!aggregateOperator) {
return undefined;
@@ -616,9 +556,7 @@ export const convertAggregationToExpression = ({
if (dataSource === DataSource.METRICS) {
return [
{
metricName: aggregateAttribute?.key || '',
reduceTo,
temporality,
metricName: aggregateAttribute.key,
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
} as MetricAggregation,
@@ -626,9 +564,7 @@ export const convertAggregationToExpression = ({
}
// For traces and logs, use expression format
const expression = aggregateAttribute?.key
? `${normalizedOperator}(${aggregateAttribute?.key})`
: `${normalizedOperator}()`;
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
if (dataSource === DataSource.TRACES) {
return [

View File

@@ -133,6 +133,8 @@ function OtherFilters({
{
key: filter.key,
dataType: filter.dataType,
isColumn: filter.isColumn,
isJSON: filter.isJSON,
type: filter.type,
},
]);

View File

@@ -10,6 +10,8 @@ export const QuickFiltersConfig = [
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
@@ -20,6 +22,8 @@ export const QuickFiltersConfig = [
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},

View File

@@ -53,6 +53,8 @@ export const getFilterConfig = (
key: att.key,
dataType: att.dataType,
type: att.type,
isColumn: att.isColumn,
isJSON: att.isJSON,
},
defaultOpen: index < 2,
} as IQuickFiltersConfig),

View File

@@ -17,19 +17,6 @@
font-weight: var(--font-weight-normal);
}
.view-title-container {
display: flex;
align-items: center;
gap: 6px;
justify-content: center;
.icon-container {
display: flex;
align-items: center;
justify-content: center;
}
}
.tab {
border: 1px solid var(--bg-slate-400);
&:hover {

View File

@@ -6,7 +6,6 @@ import { RadioChangeEvent } from 'antd/es/radio';
interface Option {
value: string;
label: string;
icon?: React.ReactNode;
}
interface SignozRadioGroupProps {
@@ -38,10 +37,7 @@ function SignozRadioGroup({
value={option.value}
className={value === option.value ? 'selected_view tab' : 'tab'}
>
<div className="view-title-container">
{option.icon && <div className="icon-container">{option.icon}</div>}
{option.label}
</div>
{option.label}
</Radio.Button>
))}
</Radio.Group>

View File

@@ -17,27 +17,6 @@ export const OPERATORS = {
'<': '<',
};
export const TRACE_OPERATOR_OPERATORS = {
AND: '&&',
OR: '||',
NOT: 'NOT',
DIRECT_DESCENDENT: '=>',
INDIRECT_DESCENDENT: '->',
};
export const TRACE_OPERATOR_OPERATORS_WITH_PRIORITY = {
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 1,
[TRACE_OPERATOR_OPERATORS.AND]: 2,
[TRACE_OPERATOR_OPERATORS.OR]: 3,
[TRACE_OPERATOR_OPERATORS.NOT]: 4,
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 5,
};
export const TRACE_OPERATOR_OPERATORS_LABELS = {
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 'Direct Descendant',
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 'Indirect Descendant',
};
export const QUERY_BUILDER_FUNCTIONS = {
HAS: 'has',
HASANY: 'hasAny',

View File

@@ -39,8 +39,6 @@ export const DATE_TIME_FORMATS = {
MONTH_DATETIME_SECONDS: 'MMM DD YYYY HH:mm:ss',
MONTH_DATETIME_FULL: 'MMMM DD, YYYY HH:mm',
MONTH_DATETIME_FULL_SECONDS: 'MMM DD, YYYY, HH:mm:ss',
DD_MMM_YYYY_HH_MM: 'DD MMM YYYY, HH:mm',
DD_MMM_YYYY_HH_MM_SS: 'DD MMM YYYY, HH:mm:ss',
// Ordinal formats (1st, 2nd, 3rd, etc)
ORDINAL_DATE: 'Do MMM YYYY',

View File

@@ -32,7 +32,4 @@ export enum LOCALSTORAGE {
BANNER_DISMISSED = 'BANNER_DISMISSED',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
FUNNEL_STEPS = 'FUNNEL_STEPS',
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
}

View File

@@ -12,7 +12,6 @@ import {
HavingForm,
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@@ -51,15 +50,13 @@ import {
export const MAX_FORMULAS = 20;
export const MAX_QUERIES = 26;
export const TRACE_OPERATOR_QUERY_NAME = 'Trace Operator';
export const idDivider = '--';
export const selectValueDivider = '__';
export const baseAutoCompleteIdKeysOrder: (keyof Omit<
BaseAutocompleteData,
'id' | 'isIndexed'
>)[] = ['key', 'dataType', 'type'];
'id' | 'isJSON' | 'isIndexed'
>)[] = ['key', 'dataType', 'type', 'isColumn'];
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
resource: 'resource',
@@ -153,12 +150,14 @@ export const initialHavingValues: HavingForm = {
export const initialAutocompleteData: BaseAutocompleteData = {
id: createIdFromObjectFields(
{ dataType: null, key: '', type: null },
{ dataType: null, key: '', isColumn: null, type: null },
baseAutoCompleteIdKeysOrder,
),
dataType: DataTypes.EMPTY,
key: '',
isColumn: false,
type: '',
isJSON: false,
};
export const initialFilters: TagFilter = {
@@ -190,7 +189,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
sourceNames: alphabet,
}),
disabled: false,
stepInterval: null,
stepInterval: undefined,
having: [],
limit: null,
orderBy: [],
@@ -238,7 +237,7 @@ export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
sourceNames: alphabet,
}),
disabled: false,
stepInterval: null,
stepInterval: undefined,
having: [],
limit: null,
orderBy: [],
@@ -266,11 +265,6 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
legend: '',
};
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
...initialQueryBuilderFormTracesValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
};
export const initialQueryPromQLData: IPromQLQuery = {
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
query: '',
@@ -288,7 +282,6 @@ export const initialClickHouseData: IClickHouseQuery = {
export const initialQueryBuilderData: QueryBuilderData = {
queryData: [initialQueryBuilderFormValues],
queryFormulas: [],
queryTraceOperator: [],
};
export const initialSingleQueryMap: Record<

View File

@@ -1,5 +1,3 @@
import { TRACE_OPERATOR_QUERY_NAME } from './queryBuilder';
export const FORMULA_REGEXP = /F\d+/;
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
@@ -7,5 +5,3 @@ export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
export const TYPE_ADDON_REGEXP = /_(.+)/;
export const SPLIT_FIRST_UNDERSCORE = /(?<!^)_/;
export const TRACE_OPERATOR_REGEXP = new RegExp(TRACE_OPERATOR_QUERY_NAME);

View File

@@ -7,7 +7,6 @@ export const GlobalShortcuts = {
NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
ToggleSidebar: 'b+shift',
NavigateToHome: 'h+shift',
};
export const GlobalShortcutsName = {
@@ -19,17 +18,15 @@ export const GlobalShortcutsName = {
NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
ToggleSidebar: 'shift+b',
NavigateToHome: 'shift+h',
};
export const GlobalShortcutsDescription = {
NavigateToHome: 'Navigate to Home',
NavigateToServices: 'Navigate to Services page',
NavigateToTraces: 'Navigate to Traces Explorer',
NavigateToLogs: 'Navigate to Logs Explorer',
NavigateToDashboards: 'Navigate to Dashboards List',
NavigateToAlerts: 'Navigate to Alerts List',
NavigateToExceptions: 'Navigate to Exceptions List',
NavigateToMessagingQueues: 'Navigate to Messaging Queues',
NavigateToTraces: 'Navigate to Traces page',
NavigateToLogs: 'Navigate to logs page',
NavigateToDashboards: 'Navigate to dashboards page',
NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
ToggleSidebar: 'Toggle sidebar visibility',
};

View File

@@ -10,9 +10,9 @@ export const LogsExplorerShortcuts = {
export const LogsExplorerShortcutsName = {
StageAndRunQuery: `${
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
} + enter`,
}+enter`,
FocusTheSearchBar: 's',
ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'} + /`,
ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`,
};
export const LogsExplorerShortcutsDescription = {

View File

@@ -2,5 +2,4 @@ export const USER_PREFERENCES = {
SIDENAV_PINNED: 'sidenav_pinned',
NAV_SHORTCUTS: 'nav_shortcuts',
LAST_SEEN_CHANGELOG_VERSION: 'last_seen_changelog_version',
SPAN_DETAILS_PINNED_ATTRIBUTES: 'span_details_pinned_attributes',
};

View File

@@ -48,10 +48,10 @@
line-height: 36px;
}
}
&__alert-history-graph {
&__graph {
margin-top: 80px;
.alert-history-graph {
.graph {
width: 100%;
height: 72px;
}

View File

@@ -135,8 +135,8 @@ function StatsCard({
/>
</div>
<div className="stats-card__alert-history-graph">
<div className="alert-history-graph">
<div className="stats-card__graph">
<div className="graph">
{!isEmpty && timeSeries.length > 1 && (
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
)}

View File

@@ -56,6 +56,8 @@ describe('API Monitoring Utils', () => {
const groupBy = [
{
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'http.method',
type: '',
@@ -70,6 +72,8 @@ describe('API Monitoring Utils', () => {
id: 'test-filter',
key: {
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
key: 'test-key',
type: '',
},
@@ -140,6 +144,8 @@ describe('API Monitoring Utils', () => {
[
{
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
key: 'custom.field',
type: '',
},
@@ -174,18 +180,24 @@ describe('API Monitoring Utils', () => {
id: 'group-by-1',
key: 'http.method',
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
type: '',
},
{
id: 'group-by-2',
key: 'http.status_code',
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
type: '',
},
{
id: 'group-by-3',
key: 'service.name',
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
type: 'tag',
},
];
@@ -246,6 +258,8 @@ describe('API Monitoring Utils', () => {
id: 'group-by-1',
key: 'http.method',
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
type: '',
},
];
@@ -267,6 +281,7 @@ describe('API Monitoring Utils', () => {
expect(knownField).toBeDefined();
if (knownField && knownField.key) {
expect(knownField.key.dataType).toBe(DataTypes.String);
expect(knownField.key.isColumn).toBe(true);
}
// Should include the unknown field
@@ -415,6 +430,8 @@ describe('API Monitoring Utils', () => {
id: 'test-filter',
key: {
dataType: DataTypes.String,
isColumn: true,
isJSON: false,
key: 'test-key',
type: '',
},
@@ -526,6 +543,8 @@ describe('API Monitoring Utils', () => {
id: 'test-filter',
key: {
dataType: 'string',
isColumn: true,
isJSON: false,
key: 'test.key',
type: '',
},
@@ -1382,6 +1401,8 @@ describe('API Monitoring Utils', () => {
id: 'custom-filter',
key: {
dataType: 'string',
isColumn: true,
isJSON: false,
key: 'custom.key',
type: '',
},

View File

@@ -32,6 +32,8 @@ import { SPAN_ATTRIBUTES } from './constants';
const httpUrlKey = {
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: SPAN_ATTRIBUTES.URL_PATH,
type: 'tag',
};

View File

@@ -58,6 +58,8 @@ function TopErrors({
id: '92b8a1c1',
key: {
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: SPAN_ATTRIBUTES.URL_PATH,
type: 'tag',
},

View File

@@ -99,6 +99,8 @@ function DomainList(): JSX.Element {
key: 'kind_string',
dataType: 'string',
type: '',
isColumn: true,
isJSON: false,
},
op: '=',
value: 'Client',

View File

@@ -123,6 +123,8 @@ describe('AllEndPoints', () => {
{
key: 'http.status_code',
dataType: 'string',
isColumn: true,
isJSON: false,
type: '',
},
],

View File

@@ -76,6 +76,8 @@ jest.mock(
key: 'test.key',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
op: '=',
value: 'test-value',
@@ -333,6 +335,8 @@ describe('EndPointDetails Component', () => {
key: 'service.name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
op: '=',
value: 'test-service',

View File

@@ -172,6 +172,8 @@ describe('StatusCodeBarCharts', () => {
key: {
dataType: 'string',
id: 'response_status_code--string--tag--false',
isColumn: false,
isJSON: false,
key: 'response_status_code',
type: 'tag',
},
@@ -183,6 +185,8 @@ describe('StatusCodeBarCharts', () => {
key: {
dataType: 'string',
id: 'response_status_code--string--tag--false',
isColumn: false,
isJSON: false,
key: 'response_status_code',
type: 'tag',
},

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Toaster } from '@signozhq/sonner';
import { Flex } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@@ -669,18 +668,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { updateUserPreferenceInContext } = useAppContext();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const sideNavPinnedPreference = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
@@ -710,6 +697,18 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
? sideNavPinnedPreference
: getSidebarStateFromLocalStorage();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { updateUserPreferenceInContext } = useAppContext();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const handleToggleSidebar = useCallback((): void => {
const newState = !isSideNavPinned;
@@ -853,8 +852,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}
<Toaster />
</Layout>
);
}

View File

@@ -1,6 +1,6 @@
.explorer-options-container {
position: fixed;
bottom: 0px;
bottom: 8px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
@@ -74,7 +74,6 @@
display: flex;
gap: 16px;
z-index: 1;
.ant-select-selector {
padding: 0 !important;
}

View File

@@ -27,7 +27,7 @@
}
.explorer-show-btn {
border-radius: 6px 6px 0px 0px;
border-radius: 10px 10px 0px 0px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.4);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);

View File

@@ -34,7 +34,7 @@ function ExplorerOrderBy({ query, onChange }: OrderByFilterProps): JSX.Element {
const keysOptions = createOptions(data?.payload?.attributeKeys || []);
const customOptions = createOptions([
{ key: 'timestamp', type: '', dataType: DataTypes.EMPTY },
{ key: 'timestamp', isColumn: true, type: '', dataType: DataTypes.EMPTY },
]);
const baseOptions = [

View File

@@ -142,6 +142,7 @@ function ChartPreview({
return false;
}
}, [query]);
const queryResponse = useGetQueryRange(
{
query: query || initialQueriesMap.metrics,

View File

@@ -54,7 +54,6 @@ function QuerySection({
queryVariant: 'static',
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
showFunctions={
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||

View File

@@ -5,7 +5,6 @@ import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@@ -150,17 +149,10 @@ function FormAlertRules({
]);
const queryOptions = useMemo(() => {
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
currentQuery.builder.queryTraceOperator,
);
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
[EQueryType.QUERY_BUILDER]: () => [
...(getSelectedQueryOptions(currentQuery.builder.queryData)?.filter(
(option) =>
!involvedQueriesInTraceOperator.includes(option.value as string),
) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryTraceOperator) || []),
],
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
[EQueryType.CLICKHOUSE]: () =>
@@ -872,7 +864,7 @@ function FormAlertRules({
queryCategory={currentQuery.queryType}
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={(): void => handleRunQuery()}
runQuery={(): void => handleRunQuery(true, true)}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -5,7 +5,6 @@ import getStep from 'lib/getStep';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
} from 'types/api/queryBuilder/queryBuilderData';
@@ -54,11 +53,7 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
export const getSelectedQueryOptions = (
queries: Array<
| IBuilderQuery
| IBuilderTraceOperator
| IBuilderFormula
| IClickHouseQuery
| IPromQLQuery
IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery
>,
): SelectProps['options'] =>
queries

View File

@@ -2,30 +2,22 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import TextToolTip from 'components/TextToolTip';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { StatusCodes } from 'http-status-codes';
import find from 'lodash-es/find';
import { useAppContext } from 'providers/App/App';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useInterval } from 'react-use';
import {
ErrorResponse,
ErrorResponseV2,
SuccessResponse,
SuccessResponseV2,
} from 'types/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IDiskType,
PayloadProps as GetDisksPayload,
} from 'types/api/disks/getDisks';
import APIError from 'types/api/error';
import { TTTLType } from 'types/api/settings/common';
import {
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
@@ -135,7 +127,7 @@ function GeneralSettings({
useEffect(() => {
if (logsCurrentTTLValues) {
setLogsTotalRetentionPeriod(logsCurrentTTLValues.default_ttl_days * 24);
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
setLogsS3RetentionPeriod(
logsCurrentTTLValues.logs_move_ttl_duration_hrs
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
@@ -344,40 +336,20 @@ function GeneralSettings({
}
try {
onPostApiLoadingHandler(type);
const setTTLResponse = await setRetentionApi({
type,
totalDuration: `${apiCallTotalRetention || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${apiCallS3Retention || -1}h`,
});
let hasSetTTLFailed = false;
try {
if (type === 'logs') {
await setRetentionApiV2({
type,
defaultTTLDays: apiCallTotalRetention ? apiCallTotalRetention / 24 : -1, // convert Hours to days
coldStorageVolume: '',
coldStorageDuration: 0,
ttlConditions: [],
});
} else {
await setRetentionApi({
type,
totalDuration: `${apiCallTotalRetention || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${apiCallS3Retention || -1}h`,
});
}
} catch (error) {
if (setTTLResponse.statusCode === 409) {
hasSetTTLFailed = true;
if ((error as APIError).getHttpStatusCode() === StatusCodes.CONFLICT) {
notifications.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
});
} else {
notifications.error({
message: 'Error',
description: (error as APIError).getErrorMessage(),
placement: 'topRight',
});
}
notifications.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
});
}
if (type === 'metrics') {
@@ -404,14 +376,11 @@ function GeneralSettings({
logsTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setLogsCurrentTTLValues((prev) => ({
...prev,
setLogsCurrentTTLValues({
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
default_ttl_days: logsTotalRetentionPeriod
? logsTotalRetentionPeriod / 24 // convert Hours to days
: -1,
}));
status: '',
});
}
} catch (error) {
notifications.error({
@@ -430,7 +399,6 @@ function GeneralSettings({
const renderConfig = [
{
name: 'Metrics',
type: 'metrics',
retentionFields: [
{
name: t('total_retention_period'),
@@ -472,7 +440,6 @@ function GeneralSettings({
},
{
name: 'Traces',
type: 'traces',
retentionFields: [
{
name: t('total_retention_period'),
@@ -512,7 +479,6 @@ function GeneralSettings({
},
{
name: 'Logs',
type: 'logs',
retentionFields: [
{
name: t('total_retention_period'),
@@ -571,7 +537,6 @@ function GeneralSettings({
/>
{category.retentionFields.map((retentionField) => (
<Retention
type={category.type as TTTLType}
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
@@ -660,7 +625,7 @@ interface GeneralSettingsProps {
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
>['refetch'];
logsTtlValuesRefetch: UseQueryResult<
ErrorResponseV2 | SuccessResponseV2<GetRetentionPeriodLogsPayload>
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
>['refetch'];
}

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