Compare commits
80 Commits
dyn-var-sr
...
demo/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7206bb82fe | ||
|
|
a1ad2b7835 | ||
|
|
a2ab97a347 | ||
|
|
7c1ca7544d | ||
|
|
1b0dcb86b5 | ||
|
|
cb49bc795b | ||
|
|
3f1aeb3077 | ||
|
|
cc2a905e0b | ||
|
|
eba024fc5d | ||
|
|
561ec8fd40 | ||
|
|
aa1dfc6eb1 | ||
|
|
3248012716 | ||
|
|
4ce56ebab4 | ||
|
|
bb80d69819 | ||
|
|
49aaecd02c | ||
|
|
98f4e840cd | ||
|
|
74824e7853 | ||
|
|
b574fee2d4 | ||
|
|
675b66a7b9 | ||
|
|
f55aeb5b5a | ||
|
|
ae3806ce64 | ||
|
|
9c489ebc84 | ||
|
|
f6d432cfce | ||
|
|
6ca6f615b0 | ||
|
|
36e7820edd | ||
|
|
f51cce844b | ||
|
|
b2d3d61b44 | ||
|
|
4e2c7c6309 | ||
|
|
885045d704 | ||
|
|
9dc2e82ce1 | ||
|
|
19e60ee688 | ||
|
|
ea89714cb4 | ||
|
|
4be618bcde | ||
|
|
2bfecce3cb | ||
|
|
eefbcbd1eb | ||
|
|
a3f366ee36 | ||
|
|
cff547c303 | ||
|
|
d6287cba52 | ||
|
|
44b09fbef2 | ||
|
|
081eb64893 | ||
|
|
6338af55dd | ||
|
|
5450b92650 | ||
|
|
a9179321e1 | ||
|
|
90366975d8 | ||
|
|
33f47993d3 | ||
|
|
9170846111 | ||
|
|
54baa9d76d | ||
|
|
0ed6aac74e | ||
|
|
b994fed409 | ||
|
|
a9eb992f67 | ||
|
|
ed95815a6a | ||
|
|
2e2888346f | ||
|
|
525c5ac081 | ||
|
|
66cede4c03 | ||
|
|
33ea94991a | ||
|
|
bae461d1f8 | ||
|
|
9df82cc952 | ||
|
|
d3d927c84d | ||
|
|
36ab1ce8a2 | ||
|
|
7bbf3ffba3 | ||
|
|
6ab5c3cf2e | ||
|
|
c2384e387d | ||
|
|
a00f263bad | ||
|
|
9d648915cc | ||
|
|
e6bd7484fa | ||
|
|
d780c7482e | ||
|
|
ffa8d0267e | ||
|
|
f0505a9c0e | ||
|
|
09e212bd64 | ||
|
|
75f3131e65 | ||
|
|
b1b571ace9 | ||
|
|
876f580f75 | ||
|
|
7999f261ef | ||
|
|
66b8574f74 | ||
|
|
d7b8be11a4 | ||
|
|
aa3935cc31 | ||
|
|
002c755ca5 | ||
|
|
558739b4e7 | ||
|
|
efdfa48ad0 | ||
|
|
693c4451ee |
@@ -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
4
.github/CODEOWNERS
vendored
@@ -42,7 +42,3 @@
|
||||
/pkg/telemetrymetadata/ @srikanthccv
|
||||
/pkg/telemetrymetrics/ @srikanthccv
|
||||
/pkg/telemetrytraces/ @srikanthccv
|
||||
|
||||
# AuthN / AuthZ Owners
|
||||
|
||||
/pkg/authz/ @vikrantgupta25 @grandwizard28
|
||||
|
||||
2
.github/workflows/build-community.yaml
vendored
2
.github/workflows/build-community.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-enterprise.yaml
vendored
2
.github/workflows/build-enterprise.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-staging.yaml
vendored
2
.github/workflows/build-staging.yaml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/goci.yaml
vendored
10
.github/workflows/goci.yaml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/gor-signoz-community.yaml
vendored
4
.github/workflows/gor-signoz-community.yaml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/gor-signoz.yaml
vendored
4
.github/workflows/gor-signoz.yaml
vendored
@@ -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
2
.gitignore
vendored
@@ -86,8 +86,6 @@ queries.active
|
||||
.devenv/**/tmp/**
|
||||
.qodo
|
||||
|
||||
.dev
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -5,7 +5,7 @@ COPY ./frontend/ ./
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/'],
|
||||
|
||||
@@ -43,18 +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/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",
|
||||
@@ -99,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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { ApiBaseInstance } from 'api';
|
||||
|
||||
import { getFieldKeys } from '../getFieldKeys';
|
||||
|
||||
// Mock the API instance
|
||||
jest.mock('api', () => ({
|
||||
ApiBaseInstance: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('getFieldKeys API', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockSuccessResponse = {
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: {
|
||||
'service.name': [],
|
||||
'http.status_code': [],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should call API with correct parameters when no args provided', async () => {
|
||||
// Mock successful API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
|
||||
|
||||
// Call function with no parameters
|
||||
await getFieldKeys();
|
||||
|
||||
// Verify API was called correctly with empty params object
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call API with signal parameter when provided', async () => {
|
||||
// Mock successful API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
|
||||
|
||||
// Call function with signal parameter
|
||||
await getFieldKeys('traces');
|
||||
|
||||
// Verify API was called with signal parameter
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
||||
params: { signal: 'traces' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call API with name parameter when provided', async () => {
|
||||
// Mock successful API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: { service: [] },
|
||||
complete: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function with name parameter
|
||||
await getFieldKeys(undefined, 'service');
|
||||
|
||||
// Verify API was called with name parameter
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
||||
params: { name: 'service' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call API with both signal and name when provided', async () => {
|
||||
// Mock successful API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
keys: { service: [] },
|
||||
complete: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function with both parameters
|
||||
await getFieldKeys('logs', 'service');
|
||||
|
||||
// Verify API was called with both parameters
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
||||
params: { signal: 'logs', name: 'service' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return properly formatted response', async () => {
|
||||
// Mock API to return our response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
|
||||
|
||||
// Call the function
|
||||
const result = await getFieldKeys('traces');
|
||||
|
||||
// Verify the returned structure matches our expected format
|
||||
expect(result).toEqual({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: mockSuccessResponse.data.data,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { ApiBaseInstance } from 'api';
|
||||
|
||||
import { getFieldValues } from '../getFieldValues';
|
||||
|
||||
// Mock the API instance
|
||||
jest.mock('api', () => ({
|
||||
ApiBaseInstance: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('getFieldValues API', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call the API with correct parameters (no options)', async () => {
|
||||
// Mock API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend', 'backend'],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function without parameters
|
||||
await getFieldValues();
|
||||
|
||||
// Verify API was called correctly with empty params
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the API with signal parameter', async () => {
|
||||
// Mock API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend', 'backend'],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function with signal parameter
|
||||
await getFieldValues('traces');
|
||||
|
||||
// Verify API was called with signal parameter
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
||||
params: { signal: 'traces' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the API with name parameter', async () => {
|
||||
// Mock API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend', 'backend'],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function with name parameter
|
||||
await getFieldValues(undefined, 'service.name');
|
||||
|
||||
// Verify API was called with name parameter
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
||||
params: { name: 'service.name' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the API with value parameter', async () => {
|
||||
// Mock API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend'],
|
||||
},
|
||||
complete: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function with value parameter
|
||||
await getFieldValues(undefined, 'service.name', 'front');
|
||||
|
||||
// Verify API was called with value parameter
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
||||
params: { name: 'service.name', value: 'front' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the API with time range parameters', async () => {
|
||||
// Mock API response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend', 'backend'],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call function with time range parameters
|
||||
const startUnixMilli = 1625097600000000; // Note: nanoseconds
|
||||
const endUnixMilli = 1625184000000000;
|
||||
await getFieldValues(
|
||||
'logs',
|
||||
'service.name',
|
||||
undefined,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
);
|
||||
|
||||
// Verify API was called with time range parameters (converted to milliseconds)
|
||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
||||
params: {
|
||||
signal: 'logs',
|
||||
name: 'service.name',
|
||||
startUnixMilli: '1625097600', // Should be converted to seconds (divided by 1000000)
|
||||
endUnixMilli: '1625184000', // Should be converted to seconds (divided by 1000000)
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize the response values', async () => {
|
||||
// Mock API response with multiple value types
|
||||
const mockResponse = {
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend', 'backend'],
|
||||
numberValues: [200, 404],
|
||||
boolValues: [true, false],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// Call the function
|
||||
const result = await getFieldValues('traces', 'mixed.values');
|
||||
|
||||
// Verify the response has normalized values array
|
||||
expect(result.payload?.normalizedValues).toContain('frontend');
|
||||
expect(result.payload?.normalizedValues).toContain('backend');
|
||||
expect(result.payload?.normalizedValues).toContain('200');
|
||||
expect(result.payload?.normalizedValues).toContain('404');
|
||||
expect(result.payload?.normalizedValues).toContain('true');
|
||||
expect(result.payload?.normalizedValues).toContain('false');
|
||||
expect(result.payload?.normalizedValues?.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should return a properly formatted success response', async () => {
|
||||
// Create mock response
|
||||
const mockApiResponse = {
|
||||
data: {
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: ['frontend', 'backend'],
|
||||
},
|
||||
complete: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Mock API to return our response
|
||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
// Call the function
|
||||
const result = await getFieldValues('traces', 'service.name');
|
||||
|
||||
// Verify the returned structure
|
||||
expect(result).toEqual({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: expect.objectContaining({
|
||||
values: expect.any(Object),
|
||||
normalizedValues: expect.any(Array),
|
||||
complete: true,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ApiBaseInstance } from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
|
||||
|
||||
/**
|
||||
* Get field keys for a given signal type
|
||||
* @param signal Type of signal (traces, logs, metrics)
|
||||
* @param name Optional search text
|
||||
*/
|
||||
export const getFieldKeys = async (
|
||||
signal?: 'traces' | 'logs' | 'metrics',
|
||||
name?: string,
|
||||
): Promise<SuccessResponse<FieldKeyResponse> | ErrorResponse> => {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (signal) {
|
||||
params.signal = encodeURIComponent(signal);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
params.name = encodeURIComponent(name);
|
||||
}
|
||||
|
||||
const response = await ApiBaseInstance.get('/fields/keys', { params });
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getFieldKeys;
|
||||
@@ -1,80 +0,0 @@
|
||||
import { ApiBaseInstance } from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
|
||||
|
||||
/**
|
||||
* Get field values for a given signal type and field name
|
||||
* @param signal Type of signal (traces, logs, metrics)
|
||||
* @param name Name of the attribute for which values are being fetched
|
||||
* @param value Optional search text
|
||||
*/
|
||||
export const getFieldValues = async (
|
||||
signal?: 'traces' | 'logs' | 'metrics',
|
||||
name?: string,
|
||||
searchText?: string,
|
||||
startUnixMilli?: number,
|
||||
endUnixMilli?: number,
|
||||
existingQuery?: string,
|
||||
): Promise<SuccessResponse<FieldValueResponse> | ErrorResponse> => {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (signal) {
|
||||
params.signal = encodeURIComponent(signal);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
params.name = encodeURIComponent(name);
|
||||
}
|
||||
|
||||
if (searchText) {
|
||||
params.searchText = encodeURIComponent(searchText);
|
||||
}
|
||||
|
||||
if (startUnixMilli) {
|
||||
params.startUnixMilli = Math.floor(startUnixMilli / 1000000).toString();
|
||||
}
|
||||
|
||||
if (endUnixMilli) {
|
||||
params.endUnixMilli = Math.floor(endUnixMilli / 1000000).toString();
|
||||
}
|
||||
|
||||
if (existingQuery) {
|
||||
params.existingQuery = existingQuery;
|
||||
}
|
||||
|
||||
const response = await ApiBaseInstance.get('/fields/values', { params });
|
||||
|
||||
// Normalize values from different types (stringValues, boolValues, etc.)
|
||||
if (response.data?.data?.values) {
|
||||
const allValues: string[] = [];
|
||||
Object.entries(response.data.data.values).forEach(
|
||||
([key, valueArray]: [string, any]) => {
|
||||
// Skip RelatedValues as they should be kept separate
|
||||
if (key === 'relatedValues') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(valueArray)) {
|
||||
allValues.push(...valueArray.map(String));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add a normalized values array to the response
|
||||
response.data.data.normalizedValues = allValues;
|
||||
|
||||
// Add relatedValues to the response as per FieldValueResponse
|
||||
if (response.data.data.values.relatedValues) {
|
||||
response.data.data.relatedValues = response.data.data.values.relatedValues;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getFieldValues;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,633 +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()],
|
||||
},
|
||||
},
|
||||
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: [] },
|
||||
},
|
||||
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: [] },
|
||||
},
|
||||
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: [] },
|
||||
},
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
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: {},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,10 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
@@ -24,7 +27,6 @@ import {
|
||||
TelemetryFieldKey,
|
||||
TraceAggregation,
|
||||
VariableItem,
|
||||
VariableType,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -67,46 +69,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,
|
||||
@@ -118,7 +83,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:
|
||||
@@ -126,8 +91,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,
|
||||
@@ -178,33 +143,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'"
|
||||
@@ -213,7 +164,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, '');
|
||||
@@ -264,14 +215,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()' }];
|
||||
@@ -333,6 +279,103 @@ 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,
|
||||
);
|
||||
let spec: QueryEnvelope['spec'];
|
||||
|
||||
// Skip aggregation for raw request type
|
||||
const aggregations =
|
||||
requestType === 'raw'
|
||||
? undefined
|
||||
: createAggregation(traceOperatorData, panelType);
|
||||
|
||||
spec = {
|
||||
name: queryName,
|
||||
returnSpansFrom: traceOperatorData.returnSpansFrom || '',
|
||||
...baseSpec,
|
||||
expression: traceOperatorData.expression || '',
|
||||
aggregations: aggregations as TraceAggregation[],
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'builder_trace_operator' as QueryType,
|
||||
spec,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PromQL queries to V5 format
|
||||
*/
|
||||
@@ -407,7 +450,6 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
fillGaps,
|
||||
dynamicVariables,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
@@ -415,14 +457,27 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
|
||||
switch (query.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas } = query.builder;
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = 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',
|
||||
);
|
||||
|
||||
// Combine legend maps
|
||||
legendMap = {
|
||||
...currentQueryData.newLegendMap,
|
||||
...currentFormulas.newLegendMap,
|
||||
...currentTraceOperator.newLegendMap,
|
||||
};
|
||||
|
||||
// Convert builder queries
|
||||
@@ -455,8 +510,36 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const traceOperatorQueries = convertTraceOperatorToV5(
|
||||
currentTraceOperator.data,
|
||||
requestType,
|
||||
graphType,
|
||||
);
|
||||
|
||||
// const traceOperatorQueries = Object.entries(currentTraceOperator.data).map(
|
||||
// ([queryName, traceOperatorData]): QueryEnvelope => ({
|
||||
// type: 'builder_trace_operator' as const,
|
||||
// spec: {
|
||||
// name: queryName,
|
||||
// expression: traceOperatorData.expression || '',
|
||||
// legend: isEmpty(traceOperatorData.legend)
|
||||
// ? undefined
|
||||
// : traceOperatorData.legend,
|
||||
// limit: 10,
|
||||
// order: traceOperatorData.orderBy?.map(
|
||||
// // eslint-disable-next-line sonarjs/no-identical-functions
|
||||
// (order: any): OrderBy => ({
|
||||
// key: {
|
||||
// name: order.columnName,
|
||||
// },
|
||||
// direction: order.order,
|
||||
// }),
|
||||
// ),
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// Combine both types
|
||||
queries = [...builderQueries, ...formulaQueries];
|
||||
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
@@ -499,12 +582,7 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
fillGaps: fillGaps || false,
|
||||
},
|
||||
variables: Object.entries(variables).reduce((acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
value,
|
||||
type: dynamicVariables
|
||||
?.find((v) => v.name === key)
|
||||
?.type?.toLowerCase() as VariableType,
|
||||
};
|
||||
acc[key] = { value };
|
||||
return acc;
|
||||
}, {} as Record<string, VariableItem>),
|
||||
};
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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: '=',
|
||||
|
||||
@@ -241,6 +241,8 @@ function ClientSideQBSearch(
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'body--string----true',
|
||||
},
|
||||
op: OPERATORS.CONTAINS,
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
@@ -146,6 +154,8 @@ export const getHostTracesQueryPayload = (
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
@@ -153,6 +163,8 @@ export const getHostTracesQueryPayload = (
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
@@ -160,6 +172,8 @@ export const getHostTracesQueryPayload = (
|
||||
key: 'durationNano',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'durationNano--float64--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
@@ -167,6 +181,8 @@ export const getHostTracesQueryPayload = (
|
||||
key: 'httpMethod',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'httpMethod--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
@@ -174,6 +190,8 @@ export const getHostTracesQueryPayload = (
|
||||
key: 'responseStatusCode',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
|
||||
@@ -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: '=',
|
||||
|
||||
@@ -26,7 +26,9 @@ export const getHostLogsQueryPayload = (
|
||||
id: '------false',
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
isColumn: false,
|
||||
type: '',
|
||||
isJSON: false,
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useGetDynamicVariables } from 'hooks/dashboard/useGetDynamicVariables';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -72,8 +71,6 @@ function Metrics({
|
||||
[hostName, timeRange.startTime, timeRange.endTime, dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const { dynamicVariables } = useGetDynamicVariables();
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload, index) => ({
|
||||
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
||||
@@ -81,8 +78,7 @@ function Metrics({
|
||||
signal,
|
||||
}: QueryFunctionContext): Promise<
|
||||
SuccessResponse<MetricRangePayloadProps>
|
||||
> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4, dynamicVariables, signal),
|
||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
||||
enabled: !!payload && visibilities[index],
|
||||
keepPreviousData: true,
|
||||
})),
|
||||
|
||||
@@ -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;
|
||||
@@ -17,7 +17,7 @@ function InputWithLabel({
|
||||
closeIcon,
|
||||
}: {
|
||||
label: string;
|
||||
initialValue?: string | number | null;
|
||||
initialValue?: string | number;
|
||||
placeholder: string;
|
||||
type?: string;
|
||||
onClose?: () => void;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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'> &
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>> => ({
|
||||
|
||||
@@ -23,13 +23,11 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
||||
import {
|
||||
filterOptionsBySearch,
|
||||
handleScrollToBottom,
|
||||
prioritizeOrAddOptionForMultiSelect,
|
||||
SPACEKEY,
|
||||
} from './utils';
|
||||
@@ -39,7 +37,7 @@ enum ToggleTagValue {
|
||||
All = 'All',
|
||||
}
|
||||
|
||||
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
|
||||
const ALL_SELECTED_VALUE = '__all__'; // Constant for the special value
|
||||
|
||||
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
placeholder = 'Search...',
|
||||
@@ -64,10 +62,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
allowClear = false,
|
||||
onRetry,
|
||||
maxTagTextLength,
|
||||
onDropdownVisibleChange,
|
||||
showIncompleteDataMessage = false,
|
||||
showLabels = false,
|
||||
enableRegexOption = false,
|
||||
...rest
|
||||
}) => {
|
||||
// ===== State & Refs =====
|
||||
@@ -84,8 +78,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||
const [visibleOptions, setVisibleOptions] = useState<OptionData[]>([]);
|
||||
const isClickInsideDropdownRef = useRef(false);
|
||||
const justOpenedRef = useRef<boolean>(false);
|
||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
|
||||
|
||||
// Convert single string value to array for consistency
|
||||
const selectedValues = useMemo(
|
||||
@@ -132,12 +124,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
return allAvailableValues.every((val) => selectedValues.includes(val));
|
||||
}, [selectedValues, allAvailableValues, enableAllSelection]);
|
||||
|
||||
// Define allOptionShown earlier in the code
|
||||
const allOptionShown = useMemo(
|
||||
() => value === ALL_SELECTED_VALUE || value === 'ALL',
|
||||
[value],
|
||||
);
|
||||
|
||||
// Value passed to the underlying Ant Select component
|
||||
const displayValue = useMemo(
|
||||
() => (isAllSelected ? [ALL_SELECTED_VALUE] : selectedValues),
|
||||
@@ -146,18 +132,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
|
||||
// ===== Internal onChange Handler =====
|
||||
const handleInternalChange = useCallback(
|
||||
(newValue: string | string[], directCaller?: boolean): void => {
|
||||
(newValue: string | string[]): void => {
|
||||
// Ensure newValue is an array
|
||||
const currentNewValue = Array.isArray(newValue) ? newValue : [];
|
||||
|
||||
if (
|
||||
(allOptionShown || isAllSelected) &&
|
||||
!directCaller &&
|
||||
currentNewValue.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onChange) return;
|
||||
|
||||
// Case 1: Cleared (empty array or undefined)
|
||||
@@ -166,7 +144,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: "__ALL__" is selected (means select all actual values)
|
||||
// Case 2: "__all__" is selected (means select all actual values)
|
||||
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
|
||||
const allActualOptions = allAvailableValues.map(
|
||||
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
|
||||
@@ -197,14 +175,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
allOptionShown,
|
||||
isAllSelected,
|
||||
onChange,
|
||||
allAvailableValues,
|
||||
options,
|
||||
enableAllSelection,
|
||||
],
|
||||
[onChange, allAvailableValues, options, enableAllSelection],
|
||||
);
|
||||
|
||||
// ===== Existing Callbacks (potentially needing adjustment later) =====
|
||||
@@ -539,46 +510,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
}
|
||||
|
||||
// Normal single value handling
|
||||
const trimmedValue = value.trim();
|
||||
setSearchText(trimmedValue);
|
||||
setSearchText(value.trim());
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
justOpenedRef.current = true;
|
||||
}
|
||||
|
||||
// Reset active index when search changes if dropdown is open
|
||||
if (isOpen && trimmedValue) {
|
||||
setActiveIndex(-1);
|
||||
// see if the trimmed value matched any option and set that active index
|
||||
const matchedOption = filteredOptions.find(
|
||||
(option) =>
|
||||
option.label.toLowerCase() === trimmedValue.toLowerCase() ||
|
||||
option.value?.toLowerCase() === trimmedValue.toLowerCase(),
|
||||
);
|
||||
if (matchedOption) {
|
||||
setActiveIndex(1);
|
||||
} else {
|
||||
// check if the trimmed value is a regex pattern and set that active index
|
||||
const isRegex =
|
||||
trimmedValue.startsWith('.*') && trimmedValue.endsWith('.*');
|
||||
if (isRegex && enableRegexOption) {
|
||||
setActiveIndex(0);
|
||||
} else {
|
||||
setActiveIndex(enableRegexOption ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onSearch) onSearch(trimmedValue);
|
||||
if (onSearch) onSearch(value.trim());
|
||||
},
|
||||
[
|
||||
onSearch,
|
||||
isOpen,
|
||||
selectedValues,
|
||||
onChange,
|
||||
filteredOptions,
|
||||
enableRegexOption,
|
||||
],
|
||||
[onSearch, isOpen, selectedValues, onChange],
|
||||
);
|
||||
|
||||
// ===== UI & Rendering Functions =====
|
||||
@@ -590,34 +528,28 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
(text: string, searchQuery: string): React.ReactNode => {
|
||||
if (!searchQuery || !highlightSearch) return text;
|
||||
|
||||
try {
|
||||
const parts = text.split(
|
||||
new RegExp(
|
||||
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
|
||||
'gi',
|
||||
),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
// Create a unique key that doesn't rely on array index
|
||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||
const parts = text.split(
|
||||
new RegExp(
|
||||
`(${searchQuery.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')})`,
|
||||
'gi',
|
||||
),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
// Create a unique key that doesn't rely on array index
|
||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||
|
||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||
<span key={uniqueKey} className="highlight-text">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
} catch (error) {
|
||||
// If regex fails, return the original text without highlighting
|
||||
console.error('Error in text highlighting:', error);
|
||||
return text;
|
||||
}
|
||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||
<span key={uniqueKey} className="highlight-text">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[highlightSearch],
|
||||
);
|
||||
@@ -628,10 +560,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
|
||||
if (isAllSelected) {
|
||||
// If all are selected, deselect all
|
||||
handleInternalChange([], true);
|
||||
handleInternalChange([]);
|
||||
} else {
|
||||
// Otherwise, select all
|
||||
handleInternalChange([ALL_SELECTED_VALUE], true);
|
||||
handleInternalChange([ALL_SELECTED_VALUE]);
|
||||
}
|
||||
}, [options, isAllSelected, handleInternalChange]);
|
||||
|
||||
@@ -806,26 +738,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
// Enhanced keyboard navigation with support for maxTagCount
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLElement>): void => {
|
||||
// Simple early return if ALL is selected - block all possible keyboard interactions
|
||||
// that could remove the ALL tag, but still allow dropdown navigation and search
|
||||
if (
|
||||
(allOptionShown || isAllSelected) &&
|
||||
(e.key === 'Backspace' || e.key === 'Delete')
|
||||
) {
|
||||
// Only prevent default if the input is empty or cursor is at start position
|
||||
const activeElement = document.activeElement as HTMLInputElement;
|
||||
const isInputActive = activeElement?.tagName === 'INPUT';
|
||||
const isInputEmpty = isInputActive && !activeElement?.value;
|
||||
const isCursorAtStart =
|
||||
isInputActive && activeElement?.selectionStart === 0;
|
||||
|
||||
if (isInputEmpty || isCursorAtStart) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get flattened list of all selectable options
|
||||
const getFlatOptions = (): OptionData[] => {
|
||||
if (!visibleOptions) return [];
|
||||
@@ -840,13 +752,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
if (hasAll) {
|
||||
flatList.push({
|
||||
label: 'ALL',
|
||||
value: ALL_SELECTED_VALUE, // Special value for the ALL option
|
||||
value: '__all__', // Special value for the ALL option
|
||||
type: 'defined',
|
||||
});
|
||||
}
|
||||
|
||||
// Add Regex to flat list
|
||||
if (!isEmpty(searchText) && enableRegexOption) {
|
||||
if (!isEmpty(searchText)) {
|
||||
// Only add regex wrapper if it doesn't already look like a regex pattern
|
||||
const isAlreadyRegex =
|
||||
searchText.startsWith('.*') && searchText.endsWith('.*');
|
||||
@@ -872,17 +784,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
|
||||
const flatOptions = getFlatOptions();
|
||||
|
||||
// If we just opened the dropdown and have options, set first option as active
|
||||
if (justOpenedRef.current && flatOptions.length > 0) {
|
||||
setActiveIndex(0);
|
||||
justOpenedRef.current = false;
|
||||
}
|
||||
|
||||
// If no option is active but we have options and dropdown is open, activate the first one
|
||||
if (isOpen && activeIndex === -1 && flatOptions.length > 0) {
|
||||
setActiveIndex(0);
|
||||
}
|
||||
|
||||
// Get the active input element to check cursor position
|
||||
const activeElement = document.activeElement as HTMLInputElement;
|
||||
const isInputActive = activeElement?.tagName === 'INPUT';
|
||||
@@ -1228,7 +1129,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
// If there's an active option in the dropdown, prioritize selecting it
|
||||
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
|
||||
const selectedOption = flatOptions[activeIndex];
|
||||
if (selectedOption.value === ALL_SELECTED_VALUE) {
|
||||
if (selectedOption.value === '__all__') {
|
||||
handleSelectAll();
|
||||
} else if (selectedOption.value && onChange) {
|
||||
const newValues = selectedValues.includes(selectedOption.value)
|
||||
@@ -1258,10 +1159,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
setActiveIndex(-1);
|
||||
// Call onDropdownVisibleChange when Escape is pressed to close dropdown
|
||||
if (onDropdownVisibleChange) {
|
||||
onDropdownVisibleChange(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case SPACEKEY:
|
||||
@@ -1271,7 +1168,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
const selectedOption = flatOptions[activeIndex];
|
||||
|
||||
// Check if it's the ALL option
|
||||
if (selectedOption.value === ALL_SELECTED_VALUE) {
|
||||
if (selectedOption.value === '__all__') {
|
||||
handleSelectAll();
|
||||
} else if (selectedOption.value && onChange) {
|
||||
const newValues = selectedValues.includes(selectedOption.value)
|
||||
@@ -1317,7 +1214,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
justOpenedRef.current = true; // Set flag to initialize active option on next render
|
||||
setActiveIndex(0);
|
||||
setActiveChipIndex(-1);
|
||||
break;
|
||||
|
||||
@@ -1363,14 +1260,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
}
|
||||
},
|
||||
[
|
||||
allOptionShown,
|
||||
isAllSelected,
|
||||
isOpen,
|
||||
activeIndex,
|
||||
getVisibleChipIndices,
|
||||
getLastVisibleChipIndex,
|
||||
selectedChips,
|
||||
isSelectionMode,
|
||||
isOpen,
|
||||
activeChipIndex,
|
||||
selectedValues,
|
||||
visibleOptions,
|
||||
@@ -1386,9 +1278,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
startSelection,
|
||||
selectionEnd,
|
||||
extendSelection,
|
||||
onDropdownVisibleChange,
|
||||
activeIndex,
|
||||
handleSelectAll,
|
||||
enableRegexOption,
|
||||
getVisibleChipIndices,
|
||||
getLastVisibleChipIndex,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1413,14 +1306,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
// Add a scroll handler for the dropdown
|
||||
const handleDropdownScroll = useCallback(
|
||||
(e: React.UIEvent<HTMLDivElement>): void => {
|
||||
setIsScrolledToBottom(handleScrollToBottom(e));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Custom dropdown render with sections support
|
||||
const customDropdownRender = useCallback((): React.ReactElement => {
|
||||
// Process options based on current search
|
||||
@@ -1439,7 +1324,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
const customOptions: OptionData[] = [];
|
||||
|
||||
// add regex options first since they appear first in the UI
|
||||
if (!isEmpty(searchText) && enableRegexOption) {
|
||||
if (!isEmpty(searchText)) {
|
||||
// Only add regex wrapper if it doesn't already look like a regex pattern
|
||||
const isAlreadyRegex =
|
||||
searchText.startsWith('.*') && searchText.endsWith('.*');
|
||||
@@ -1462,17 +1347,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// Now add all custom options at the beginning, removing duplicates based on value
|
||||
const allOptions = [...customOptions, ...nonSectionOptions];
|
||||
const seenValues = new Set<string>();
|
||||
const enhancedNonSectionOptions = allOptions.filter((option) => {
|
||||
const value = option.value || '';
|
||||
if (seenValues.has(value)) {
|
||||
return false;
|
||||
}
|
||||
seenValues.add(value);
|
||||
return true;
|
||||
});
|
||||
// Now add all custom options at the beginning
|
||||
const enhancedNonSectionOptions = [...customOptions, ...nonSectionOptions];
|
||||
|
||||
const allOptionValues = getAllAvailableValues(processedOptions);
|
||||
const allOptionsSelected =
|
||||
@@ -1506,7 +1382,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
onMouseDown={handleDropdownMouseDown}
|
||||
onClick={handleDropdownClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onScroll={handleDropdownScroll}
|
||||
onBlur={handleBlur}
|
||||
role="listbox"
|
||||
aria-multiselectable="true"
|
||||
@@ -1564,19 +1439,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
{/* Non-section options when not searching */}
|
||||
{enhancedNonSectionOptions.length > 0 && (
|
||||
<div className="no-section-options">
|
||||
<Virtuoso
|
||||
style={{
|
||||
minHeight: Math.min(300, enhancedNonSectionOptions.length * 40),
|
||||
maxHeight: enhancedNonSectionOptions.length * 40,
|
||||
}}
|
||||
data={enhancedNonSectionOptions}
|
||||
itemContent={(index, item): React.ReactNode =>
|
||||
(mapOptions([item]) as unknown) as React.ReactElement
|
||||
}
|
||||
totalCount={enhancedNonSectionOptions.length}
|
||||
itemSize={(): number => 40}
|
||||
overscan={5}
|
||||
/>
|
||||
{mapOptions(enhancedNonSectionOptions)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1589,40 +1452,23 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
{section.label}
|
||||
</div>
|
||||
<div role="group" aria-label={`${section.label} options`}>
|
||||
<Virtuoso
|
||||
style={{
|
||||
minHeight: Math.min(300, (section.options?.length || 0) * 40),
|
||||
maxHeight: (section.options?.length || 0) * 40,
|
||||
}}
|
||||
data={section.options || []}
|
||||
itemContent={(index, item): React.ReactNode =>
|
||||
(mapOptions([item]) as unknown) as React.ReactElement
|
||||
}
|
||||
totalCount={section.options?.length || 0}
|
||||
itemSize={(): number => 40}
|
||||
overscan={5}
|
||||
/>
|
||||
{section.options && mapOptions(section.options)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div key={section.label} />
|
||||
),
|
||||
) : null,
|
||||
)}
|
||||
|
||||
{/* Navigation help footer */}
|
||||
<div className="navigation-footer" role="note">
|
||||
{!loading &&
|
||||
!errorMessage &&
|
||||
!noDataMessage &&
|
||||
!(showIncompleteDataMessage && isScrolledToBottom) && (
|
||||
<section className="navigate">
|
||||
<ArrowDown size={8} className="icons" />
|
||||
<ArrowUp size={8} className="icons" />
|
||||
<ArrowLeft size={8} className="icons" />
|
||||
<ArrowRight size={8} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
)}
|
||||
{!loading && !errorMessage && !noDataMessage && (
|
||||
<section className="navigate">
|
||||
<ArrowDown size={8} className="icons" />
|
||||
<ArrowUp size={8} className="icons" />
|
||||
<ArrowLeft size={8} className="icons" />
|
||||
<ArrowRight size={8} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
@@ -1636,33 +1482,21 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
<div className="navigation-text">
|
||||
{errorMessage || SOMETHING_WENT_WRONG}
|
||||
</div>
|
||||
{onRetry && (
|
||||
<div className="navigation-icons">
|
||||
<ReloadOutlined
|
||||
twoToneColor={Color.BG_CHERRY_400}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onRetry();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="navigation-icons">
|
||||
<ReloadOutlined
|
||||
twoToneColor={Color.BG_CHERRY_400}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
if (onRetry) onRetry();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showIncompleteDataMessage &&
|
||||
isScrolledToBottom &&
|
||||
!loading &&
|
||||
!errorMessage && (
|
||||
<div className="navigation-text-incomplete">
|
||||
Use search for more options
|
||||
</div>
|
||||
)}
|
||||
|
||||
{noDataMessage &&
|
||||
!loading &&
|
||||
!(showIncompleteDataMessage && isScrolledToBottom) &&
|
||||
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
|
||||
{noDataMessage && !loading && (
|
||||
<div className="navigation-text">{noDataMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1679,7 +1513,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
handleDropdownMouseDown,
|
||||
handleDropdownClick,
|
||||
handleKeyDown,
|
||||
handleDropdownScroll,
|
||||
handleBlur,
|
||||
activeIndex,
|
||||
loading,
|
||||
@@ -1689,32 +1522,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
renderOptionWithIndex,
|
||||
handleSelectAll,
|
||||
onRetry,
|
||||
showIncompleteDataMessage,
|
||||
isScrolledToBottom,
|
||||
enableRegexOption,
|
||||
]);
|
||||
|
||||
// Custom handler for dropdown visibility changes
|
||||
const handleDropdownVisibleChange = useCallback(
|
||||
(visible: boolean): void => {
|
||||
setIsOpen(visible);
|
||||
if (visible) {
|
||||
justOpenedRef.current = true;
|
||||
setActiveIndex(0);
|
||||
setActiveChipIndex(-1);
|
||||
} else {
|
||||
setSearchText('');
|
||||
setActiveIndex(-1);
|
||||
// Don't clear activeChipIndex when dropdown closes to maintain tag focus
|
||||
}
|
||||
// Pass through to the parent component's handler if provided
|
||||
if (onDropdownVisibleChange) {
|
||||
onDropdownVisibleChange(visible);
|
||||
}
|
||||
},
|
||||
[onDropdownVisibleChange],
|
||||
);
|
||||
|
||||
// ===== Side Effects =====
|
||||
|
||||
// Clear search when dropdown closes
|
||||
@@ -1776,16 +1585,55 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
// Custom Tag Render (needs significant updates)
|
||||
const tagRender = useCallback(
|
||||
(props: CustomTagProps): React.ReactElement => {
|
||||
const { label: labelProp, value, closable, onClose } = props;
|
||||
|
||||
const label = showLabels
|
||||
? options.find((option) => option.value === value)?.label || labelProp
|
||||
: labelProp;
|
||||
const { label, value, closable, onClose } = props;
|
||||
|
||||
// If the display value is the special ALL value, render the ALL tag
|
||||
if (allOptionShown) {
|
||||
// Don't render a visible tag - will be shown as placeholder
|
||||
return <div style={{ display: 'none' }} />;
|
||||
if (value === ALL_SELECTED_VALUE && isAllSelected) {
|
||||
const handleAllTagClose = (
|
||||
e: React.MouseEvent | React.KeyboardEvent,
|
||||
): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleInternalChange([]); // Clear selection when ALL tag is closed
|
||||
};
|
||||
|
||||
const handleAllTagKeyDown = (e: React.KeyboardEvent): void => {
|
||||
if (e.key === 'Enter' || e.key === SPACEKEY) {
|
||||
handleAllTagClose(e);
|
||||
}
|
||||
// Prevent Backspace/Delete propagation if needed, handle in main keydown handler
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('ant-select-selection-item', {
|
||||
'ant-select-selection-item-active': activeChipIndex === 0, // Treat ALL tag as index 0 when active
|
||||
'ant-select-selection-item-selected': selectedChips.includes(0),
|
||||
})}
|
||||
style={
|
||||
activeChipIndex === 0 || selectedChips.includes(0)
|
||||
? {
|
||||
borderColor: Color.BG_ROBIN_500,
|
||||
backgroundColor: Color.BG_SLATE_400,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span className="ant-select-selection-item-content">ALL</span>
|
||||
{closable && (
|
||||
<span
|
||||
className="ant-select-selection-item-remove"
|
||||
onClick={handleAllTagClose}
|
||||
onKeyDown={handleAllTagKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Remove ALL tag (deselect all)"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If not isAllSelected, render individual tags using previous logic
|
||||
@@ -1865,69 +1713,52 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
// Fallback for safety, should not be reached
|
||||
return <div />;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isAllSelected, activeChipIndex, selectedChips, selectedValues, maxTagCount],
|
||||
[
|
||||
isAllSelected,
|
||||
handleInternalChange,
|
||||
activeChipIndex,
|
||||
selectedChips,
|
||||
selectedValues,
|
||||
maxTagCount,
|
||||
],
|
||||
);
|
||||
|
||||
// Simple onClear handler to prevent clearing ALL
|
||||
const onClearHandler = useCallback((): void => {
|
||||
// Skip clearing if ALL is selected
|
||||
if (allOptionShown || isAllSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal clear behavior
|
||||
handleInternalChange([], true);
|
||||
if (onClear) onClear();
|
||||
}, [onClear, handleInternalChange, allOptionShown, isAllSelected]);
|
||||
|
||||
// ===== Component Rendering =====
|
||||
return (
|
||||
<div
|
||||
className={cx('custom-multiselect-wrapper', {
|
||||
'all-selected': allOptionShown || isAllSelected,
|
||||
<Select
|
||||
ref={selectRef}
|
||||
className={cx('custom-multiselect', className, {
|
||||
'has-selection': selectedChips.length > 0 && !isAllSelected,
|
||||
'is-all-selected': isAllSelected,
|
||||
})}
|
||||
>
|
||||
{(allOptionShown || isAllSelected) && !searchText && (
|
||||
<div className="all-text">ALL</div>
|
||||
)}
|
||||
<Select
|
||||
ref={selectRef}
|
||||
className={cx('custom-multiselect', className, {
|
||||
'has-selection': selectedChips.length > 0 && !isAllSelected,
|
||||
'is-all-selected': isAllSelected,
|
||||
})}
|
||||
placeholder={placeholder}
|
||||
mode="multiple"
|
||||
showSearch
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
value={displayValue}
|
||||
onChange={(newValue): void => {
|
||||
handleInternalChange(newValue, false);
|
||||
}}
|
||||
onClear={onClearHandler}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
open={isOpen}
|
||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||
popupMatchSelectWidth={dropdownMatchSelectWidth}
|
||||
allowClear={allowClear}
|
||||
getPopupContainer={getPopupContainer ?? popupContainer}
|
||||
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
|
||||
dropdownRender={customDropdownRender}
|
||||
menuItemSelectedIcon={null}
|
||||
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
|
||||
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
|
||||
onKeyDown={handleKeyDown}
|
||||
tagRender={tagRender as any}
|
||||
placement={placement}
|
||||
listHeight={300}
|
||||
searchValue={searchText}
|
||||
maxTagTextLength={maxTagTextLength}
|
||||
maxTagCount={isAllSelected ? undefined : maxTagCount}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
placeholder={placeholder}
|
||||
mode="multiple"
|
||||
showSearch
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
value={displayValue}
|
||||
onChange={handleInternalChange}
|
||||
onClear={(): void => handleInternalChange([])}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
open={isOpen}
|
||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||
popupMatchSelectWidth={dropdownMatchSelectWidth}
|
||||
allowClear={allowClear}
|
||||
getPopupContainer={getPopupContainer ?? popupContainer}
|
||||
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
|
||||
dropdownRender={customDropdownRender}
|
||||
menuItemSelectedIcon={null}
|
||||
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
|
||||
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
|
||||
onKeyDown={handleKeyDown}
|
||||
tagRender={tagRender as any}
|
||||
placement={placement}
|
||||
listHeight={300}
|
||||
searchValue={searchText}
|
||||
maxTagTextLength={maxTagTextLength}
|
||||
maxTagCount={isAllSelected ? 1 : maxTagCount}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { CustomSelectProps, OptionData } from './types';
|
||||
import {
|
||||
filterOptionsBySearch,
|
||||
handleScrollToBottom,
|
||||
prioritizeOrAddOptionForSingleSelect,
|
||||
SPACEKEY,
|
||||
} from './utils';
|
||||
@@ -58,29 +57,17 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
errorMessage,
|
||||
allowClear = false,
|
||||
onRetry,
|
||||
showIncompleteDataMessage = false,
|
||||
...rest
|
||||
}) => {
|
||||
// ===== State & Refs =====
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
|
||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
|
||||
|
||||
// Refs for element access and scroll behavior
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||
// Flag to track if dropdown just opened
|
||||
const justOpenedRef = useRef<boolean>(false);
|
||||
|
||||
// Add a scroll handler for the dropdown
|
||||
const handleDropdownScroll = useCallback(
|
||||
(e: React.UIEvent<HTMLDivElement>): void => {
|
||||
setIsScrolledToBottom(handleScrollToBottom(e));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// ===== Option Filtering & Processing Utilities =====
|
||||
|
||||
@@ -143,33 +130,23 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
(text: string, searchQuery: string): React.ReactNode => {
|
||||
if (!searchQuery || !highlightSearch) return text;
|
||||
|
||||
try {
|
||||
const parts = text.split(
|
||||
new RegExp(
|
||||
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
|
||||
'gi',
|
||||
),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
// Create a deterministic but unique key
|
||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
// Create a deterministic but unique key
|
||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||
|
||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||
<span key={uniqueKey} className="highlight-text">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in text highlighting:', error);
|
||||
return text;
|
||||
}
|
||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||
<span key={uniqueKey} className="highlight-text">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[highlightSearch],
|
||||
);
|
||||
@@ -269,14 +246,9 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
const trimmedValue = value.trim();
|
||||
setSearchText(trimmedValue);
|
||||
|
||||
// Reset active option index when search changes
|
||||
if (isOpen) {
|
||||
setActiveOptionIndex(0);
|
||||
}
|
||||
|
||||
if (onSearch) onSearch(trimmedValue);
|
||||
},
|
||||
[onSearch, isOpen],
|
||||
[onSearch],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -300,23 +272,14 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
const flatList: OptionData[] = [];
|
||||
|
||||
// Process options
|
||||
let processedOptions = isEmpty(value)
|
||||
? filteredOptions
|
||||
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value);
|
||||
|
||||
if (!isEmpty(searchText)) {
|
||||
processedOptions = filterOptionsBySearch(processedOptions, searchText);
|
||||
}
|
||||
|
||||
const { sectionOptions, nonSectionOptions } = splitOptions(
|
||||
processedOptions,
|
||||
isEmpty(value)
|
||||
? filteredOptions
|
||||
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value),
|
||||
);
|
||||
|
||||
// Add custom option if needed
|
||||
if (
|
||||
!isEmpty(searchText) &&
|
||||
!isLabelPresent(processedOptions, searchText)
|
||||
) {
|
||||
if (!isEmpty(searchText) && !isLabelPresent(filteredOptions, searchText)) {
|
||||
flatList.push({
|
||||
label: searchText,
|
||||
value: searchText,
|
||||
@@ -337,52 +300,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
|
||||
const options = getFlatOptions();
|
||||
|
||||
// If we just opened the dropdown and have options, set first option as active
|
||||
if (justOpenedRef.current && options.length > 0) {
|
||||
setActiveOptionIndex(0);
|
||||
justOpenedRef.current = false;
|
||||
}
|
||||
|
||||
// If no option is active but we have options, activate the first one
|
||||
if (activeOptionIndex === -1 && options.length > 0) {
|
||||
setActiveOptionIndex(0);
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (options.length > 0) {
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev < options.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
}
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev < options.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
if (options.length > 0) {
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : options.length - 1,
|
||||
);
|
||||
}
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : options.length - 1,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
// Tab navigation with Shift key support
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (options.length > 0) {
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : options.length - 1,
|
||||
);
|
||||
}
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : options.length - 1,
|
||||
);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
if (options.length > 0) {
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev < options.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
}
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev < options.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -395,7 +339,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
onChange(selectedOption.value, selectedOption);
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
setSearchText('');
|
||||
}
|
||||
} else if (!isEmpty(searchText)) {
|
||||
// Add custom value when no option is focused
|
||||
@@ -408,7 +351,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
onChange(customOption.value, customOption);
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
setSearchText('');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -417,7 +359,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
setSearchText('');
|
||||
break;
|
||||
|
||||
case ' ': // Space key
|
||||
@@ -428,7 +369,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
onChange(selectedOption.value, selectedOption);
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
setSearchText('');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -439,7 +379,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
// Open dropdown when Down or Tab is pressed while closed
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
justOpenedRef.current = true; // Set flag to initialize active option on next render
|
||||
setActiveOptionIndex(0);
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -504,7 +444,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
className="custom-select-dropdown"
|
||||
onClick={handleDropdownClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onScroll={handleDropdownScroll}
|
||||
role="listbox"
|
||||
tabIndex={-1}
|
||||
aria-activedescendant={
|
||||
@@ -515,6 +454,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
<div className="no-section-options">
|
||||
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
|
||||
</div>
|
||||
|
||||
{/* Section options */}
|
||||
{sectionOptions.length > 0 &&
|
||||
sectionOptions.map((section) =>
|
||||
@@ -532,16 +472,13 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
|
||||
{/* Navigation help footer */}
|
||||
<div className="navigation-footer" role="note">
|
||||
{!loading &&
|
||||
!errorMessage &&
|
||||
!noDataMessage &&
|
||||
!(showIncompleteDataMessage && isScrolledToBottom) && (
|
||||
<section className="navigate">
|
||||
<ArrowDown size={8} className="icons" />
|
||||
<ArrowUp size={8} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
)}
|
||||
{!loading && !errorMessage && !noDataMessage && (
|
||||
<section className="navigate">
|
||||
<ArrowDown size={8} className="icons" />
|
||||
<ArrowUp size={8} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
@@ -555,33 +492,21 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
<div className="navigation-text">
|
||||
{errorMessage || SOMETHING_WENT_WRONG}
|
||||
</div>
|
||||
{onRetry && (
|
||||
<div className="navigation-icons">
|
||||
<ReloadOutlined
|
||||
twoToneColor={Color.BG_CHERRY_400}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onRetry();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="navigation-icons">
|
||||
<ReloadOutlined
|
||||
twoToneColor={Color.BG_CHERRY_400}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
if (onRetry) onRetry();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showIncompleteDataMessage &&
|
||||
isScrolledToBottom &&
|
||||
!loading &&
|
||||
!errorMessage && (
|
||||
<div className="navigation-text-incomplete">
|
||||
Use search for more options
|
||||
</div>
|
||||
)}
|
||||
|
||||
{noDataMessage &&
|
||||
!loading &&
|
||||
!(showIncompleteDataMessage && isScrolledToBottom) &&
|
||||
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
|
||||
{noDataMessage && !loading && (
|
||||
<div className="navigation-text">{noDataMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -595,7 +520,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
isLabelPresent,
|
||||
handleDropdownClick,
|
||||
handleKeyDown,
|
||||
handleDropdownScroll,
|
||||
activeOptionIndex,
|
||||
loading,
|
||||
errorMessage,
|
||||
@@ -603,22 +527,8 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
dropdownRender,
|
||||
renderOptionWithIndex,
|
||||
onRetry,
|
||||
showIncompleteDataMessage,
|
||||
isScrolledToBottom,
|
||||
]);
|
||||
|
||||
// Handle dropdown visibility changes
|
||||
const handleDropdownVisibleChange = useCallback((visible: boolean): void => {
|
||||
setIsOpen(visible);
|
||||
if (visible) {
|
||||
justOpenedRef.current = true;
|
||||
setActiveOptionIndex(0);
|
||||
} else {
|
||||
setSearchText('');
|
||||
setActiveOptionIndex(-1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ===== Side Effects =====
|
||||
|
||||
// Clear search text when dropdown closes
|
||||
@@ -672,7 +582,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
onSearch={handleSearch}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
open={isOpen}
|
||||
options={optionsWithHighlight}
|
||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||
|
||||
@@ -35,50 +35,12 @@ $custom-border-color: #2c3044;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.is-all-selected {
|
||||
.ant-select-selection-search-input {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
opacity: 1 !important;
|
||||
color: var(--bg-vanilla-400) !important;
|
||||
font-weight: 500;
|
||||
visibility: visible !important;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
|
||||
.lightMode & {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selection-placeholder {
|
||||
opacity: 0.45 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.all-selected-text {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--bg-vanilla-400);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
.lightMode & {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
background-color: var(--bg-ink-400);
|
||||
border-color: var(--bg-slate-400);
|
||||
cursor: text;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -94,16 +56,6 @@ $custom-border-color: #2c3044;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure adequate space for input area
|
||||
.ant-select-selection-search {
|
||||
min-width: 60px !important;
|
||||
flex: 1 1 auto;
|
||||
.ant-select-selection-search-input {
|
||||
min-width: 60px !important;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-robin-500);
|
||||
@@ -206,7 +158,7 @@ $custom-border-color: #2c3044;
|
||||
// Custom dropdown styles for single select
|
||||
.custom-select-dropdown {
|
||||
padding: 8px 0 0 0;
|
||||
max-height: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
@@ -324,10 +276,6 @@ $custom-border-color: #2c3044;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.navigation-text-incomplete {
|
||||
color: var(--bg-amber-600) !important;
|
||||
}
|
||||
|
||||
.navigation-error {
|
||||
.navigation-text,
|
||||
.navigation-icons {
|
||||
@@ -374,7 +322,7 @@ $custom-border-color: #2c3044;
|
||||
// Custom dropdown styles for multi-select
|
||||
.custom-multiselect-dropdown {
|
||||
padding: 8px 0 0 0;
|
||||
max-height: 350px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
@@ -407,7 +355,6 @@ $custom-border-color: #2c3044;
|
||||
.select-group {
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
|
||||
.group-label {
|
||||
font-weight: 500;
|
||||
@@ -690,7 +637,6 @@ $custom-border-color: #2c3044;
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: #e9e9e9;
|
||||
cursor: text; // Make entire selector clickable for input focus
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
@@ -701,20 +647,6 @@ $custom-border-color: #2c3044;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-search {
|
||||
min-width: 60px !important;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
min-width: 60px !important;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
@@ -724,10 +656,6 @@ $custom-border-color: #2c3044;
|
||||
border: 1px solid #e8e8e8;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
|
||||
font-size: 12px !important;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
|
||||
.ant-select-selection-item-content {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
@@ -908,38 +836,3 @@ $custom-border-color: #2c3044;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-multiselect-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.all-selected {
|
||||
.all-text {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-weight: 500;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
|
||||
.lightMode & {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within .all-text {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
caret-color: auto;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,9 @@ export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
|
||||
highlightSearch?: boolean;
|
||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
popupMatchSelectWidth?: boolean;
|
||||
errorMessage?: string | null;
|
||||
errorMessage?: string;
|
||||
allowClear?: SelectProps['allowClear'];
|
||||
onRetry?: () => void;
|
||||
showIncompleteDataMessage?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomTagProps {
|
||||
@@ -52,14 +51,10 @@ export interface CustomMultiSelectProps
|
||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||
highlightSearch?: boolean;
|
||||
errorMessage?: string | null;
|
||||
errorMessage?: string;
|
||||
popupClassName?: string;
|
||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
maxTagCount?: number;
|
||||
allowClear?: SelectProps['allowClear'];
|
||||
onRetry?: () => void;
|
||||
maxTagTextLength?: number;
|
||||
showIncompleteDataMessage?: boolean;
|
||||
showLabels?: boolean;
|
||||
enableRegexOption?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
||||
|
||||
import { OptionData } from './types';
|
||||
|
||||
export const SPACEKEY = ' ';
|
||||
@@ -100,10 +98,8 @@ export const prioritizeOrAddOptionForMultiSelect = (
|
||||
label: labels?.[value] ?? value, // Use provided label or default to value
|
||||
}));
|
||||
|
||||
const flatOutSelectedOptions = uniqueOptions([...newOptions, ...foundOptions]);
|
||||
|
||||
// Add found & new options to the top
|
||||
return [...flatOutSelectedOptions, ...filteredOptions];
|
||||
return [...newOptions, ...foundOptions, ...filteredOptions];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -137,15 +133,3 @@ export const filterOptionsBySearch = (
|
||||
})
|
||||
.filter(Boolean) as OptionData[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to handle dropdown scroll and detect when scrolled to bottom
|
||||
* Returns true when scrolled to within 20px of the bottom
|
||||
*/
|
||||
export const handleScrollToBottom = (
|
||||
e: React.UIEvent<HTMLDivElement>,
|
||||
): boolean => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
// Consider "scrolled to bottom" when within 20px of the bottom or at the bottom
|
||||
return scrollHeight - scrollTop - clientHeight < 20;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
flex: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
.qb-trace-view-selector-container {
|
||||
padding: 12px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-content-section {
|
||||
@@ -179,7 +183,7 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
margin-left: 32px;
|
||||
margin-left: 26px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
|
||||
@@ -195,8 +199,8 @@
|
||||
}
|
||||
|
||||
.formula-container {
|
||||
margin-left: 82px;
|
||||
padding: 4px 0px;
|
||||
padding: 8px;
|
||||
margin-left: 74px;
|
||||
|
||||
.ant-col {
|
||||
&::before {
|
||||
@@ -331,6 +335,12 @@
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
&.has-trace-operator {
|
||||
&::before {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
@@ -347,7 +357,7 @@
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 65px;
|
||||
height: 128px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -5,11 +5,13 @@ 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,
|
||||
@@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryComponents,
|
||||
isListViewPanel = false,
|
||||
showOnlyWhereClause = false,
|
||||
showTraceOperator = false,
|
||||
version,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
@@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
handleSetConfig,
|
||||
addTraceOperator,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
} = useQueryBuilder();
|
||||
@@ -54,6 +58,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
const isMultiQueryAllowed = useMemo(
|
||||
() =>
|
||||
!showOnlyWhereClause ||
|
||||
!isListViewPanel ||
|
||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||
[showOnlyWhereClause, currentDataSource, showTraceOperator, isListViewPanel],
|
||||
);
|
||||
|
||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
@@ -97,11 +109,45 @@ 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 shouldShowTraceOperator = useMemo(
|
||||
() =>
|
||||
showTraceOperator &&
|
||||
currentDataSource === DataSource.TRACES &&
|
||||
Boolean(traceOperator),
|
||||
[currentDataSource, showTraceOperator, traceOperator],
|
||||
);
|
||||
|
||||
const shouldShowFooter = useMemo(
|
||||
() =>
|
||||
(!showOnlyWhereClause && !isListViewPanel) ||
|
||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
|
||||
);
|
||||
|
||||
const showFormula = useMemo(() => {
|
||||
if (currentDataSource === DataSource.TRACES) {
|
||||
return !isListViewPanel;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isListViewPanel, currentDataSource]);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
{isListViewPanel && (
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
@@ -109,15 +155,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
showTraceOperator={shouldShowTraceOperator}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isListViewPanel &&
|
||||
) : (
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
@@ -127,13 +173,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
isAvailableToDisable={false}
|
||||
showTraceOperator={shouldShowTraceOperator}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
signalSource={config?.signalSource || ''}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
@@ -158,15 +207,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
showAddFormula={showFormula}
|
||||
addNewBuilderQuery={addNewBuilderQuery}
|
||||
addNewFormula={addNewFormula}
|
||||
addTraceOperator={addTraceOperator}
|
||||
showAddTraceOperator={showTraceOperator && !traceOperator}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowTraceOperator && (
|
||||
<TraceOperator
|
||||
isListViewPanel={isListViewPanel}
|
||||
traceOperator={traceOperator as IBuilderTraceOperator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
{isMultiQueryAllowed && (
|
||||
<div className="query-names-section">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<div key={query.queryName} className="query-name">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
.query-add-ons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-ons-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.add-ons-tabs {
|
||||
display: flex;
|
||||
|
||||
@@ -144,6 +144,8 @@ function QueryAddOns({
|
||||
showReduceTo,
|
||||
panelType,
|
||||
index,
|
||||
isForTraceOperator = false,
|
||||
children,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
version: string;
|
||||
@@ -151,6 +153,8 @@ function QueryAddOns({
|
||||
showReduceTo: boolean;
|
||||
panelType: PANEL_TYPES | null;
|
||||
index: number;
|
||||
isForTraceOperator?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||
|
||||
@@ -160,6 +164,7 @@ function QueryAddOns({
|
||||
index,
|
||||
query,
|
||||
entityVersion: '',
|
||||
isForTraceOperator,
|
||||
});
|
||||
|
||||
const { handleSetQueryData } = useQueryBuilder();
|
||||
@@ -486,6 +491,7 @@ function QueryAddOns({
|
||||
</Tooltip>
|
||||
))}
|
||||
</Radio.Group>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,10 @@ import { Tooltip } from 'antd';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||
@@ -20,7 +23,7 @@ function QueryAggregationOptions({
|
||||
panelType?: string;
|
||||
onAggregationIntervalChange: (value: number) => void;
|
||||
onChange?: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
queryData: IBuilderQuery | IBuilderTraceOperator;
|
||||
}): JSX.Element {
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
@@ -81,7 +84,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"
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -4,9 +4,15 @@ 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">
|
||||
@@ -22,32 +28,62 @@ export default function QueryFooter({
|
||||
</Tooltip>
|
||||
</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}
|
||||
{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>
|
||||
}
|
||||
>
|
||||
Add Formula
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={addNewFormula}
|
||||
>
|
||||
Add Formula
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{showAddTraceOperator && (
|
||||
<div className="qb-add-formula">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add Trace Matching
|
||||
<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={() => addTraceOperator?.()}
|
||||
>
|
||||
Add Trace Matching
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useGetDynamicVariables } from 'hooks/dashboard/useGetDynamicVariables';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
@@ -162,7 +161,13 @@ function QuerySearch({
|
||||
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const { dynamicVariables } = useGetDynamicVariables();
|
||||
// const {
|
||||
// data: queryKeySuggestions,
|
||||
// refetch: refetchQueryKeySuggestions,
|
||||
// } = useGetQueryKeySuggestions({
|
||||
// signal: dataSource,
|
||||
// name: searchText || '',
|
||||
// });
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: {
|
||||
@@ -977,25 +982,6 @@ function QuerySearch({
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
// Add dynamic variables suggestions for the current key
|
||||
const variableName = dynamicVariables.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
||||
)?.name;
|
||||
|
||||
if (variableName) {
|
||||
const variableValue = `$${variableName}`;
|
||||
const variableOption = {
|
||||
label: variableValue,
|
||||
type: 'variable',
|
||||
apply: variableValue,
|
||||
};
|
||||
|
||||
// Add variable suggestion at the beginning if it matches the search text
|
||||
if (variableValue.toLowerCase().includes(searchText.toLowerCase())) {
|
||||
options = [variableOption, ...options];
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger fetch only if needed
|
||||
const shouldFetch =
|
||||
// Fetch only if key is available
|
||||
@@ -1048,9 +1034,6 @@ function QuerySearch({
|
||||
} else if (option.type === 'array') {
|
||||
// Arrays are already formatted as arrays
|
||||
processedOption.apply = option.label;
|
||||
} else if (option.type === 'variable') {
|
||||
// Variables should be used as-is (they already have the $ prefix)
|
||||
processedOption.apply = option.label;
|
||||
}
|
||||
|
||||
return processedOption;
|
||||
@@ -1309,7 +1292,7 @@ function QuerySearch({
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(query);
|
||||
} else {
|
||||
handleRunQuery();
|
||||
handleRunQuery(true, true);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -26,9 +26,11 @@ export const QueryV2 = memo(function QueryV2({
|
||||
query,
|
||||
filterConfigs,
|
||||
isListViewPanel = false,
|
||||
showTraceOperator = false,
|
||||
version,
|
||||
showOnlyWhereClause = false,
|
||||
signalSource = '',
|
||||
isMultiQueryAllowed = false,
|
||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@@ -108,11 +110,15 @@ export const QueryV2 = memo(function QueryV2({
|
||||
ref={ref}
|
||||
>
|
||||
<div className="qb-content-section">
|
||||
{!showOnlyWhereClause && (
|
||||
{isMultiQueryAllowed && (
|
||||
<div className="qb-header-container">
|
||||
<div className="query-actions-container">
|
||||
<div className="query-actions-left-container">
|
||||
<QBEntityOptions
|
||||
hasTraceOperator={
|
||||
showTraceOperator ||
|
||||
(isListViewPanel && dataSource === DataSource.TRACES)
|
||||
}
|
||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||
showFunctions={
|
||||
(version && version === ENTITY_VERSION_V4) ||
|
||||
@@ -139,7 +145,30 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isListViewPanel && (
|
||||
{!isCollapsed &&
|
||||
(showTraceOperator ||
|
||||
(isListViewPanel && dataSource === DataSource.TRACES)) && (
|
||||
<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 && (
|
||||
<Dropdown
|
||||
className="query-actions-dropdown"
|
||||
menu={{
|
||||
@@ -181,28 +210,32 @@ export const QueryV2 = memo(function QueryV2({
|
||||
</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>
|
||||
{!showTraceOperator &&
|
||||
!(isListViewPanel && dataSource === DataSource.TRACES) && (
|
||||
<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} />
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
!isListViewPanel &&
|
||||
!showTraceOperator &&
|
||||
dataSource !== DataSource.METRICS && (
|
||||
<QueryAggregation
|
||||
dataSource={dataSource}
|
||||
@@ -225,7 +258,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && (
|
||||
{!showOnlyWhereClause && !isListViewPanel && !showTraceOperator && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
.qb-trace-operator {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&.non-list-view {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 12px;
|
||||
height: calc(100% - 48px);
|
||||
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: 50%;
|
||||
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: 50%;
|
||||
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;
|
||||
}
|
||||
|
||||
&-add-ons-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);
|
||||
|
||||
&::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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-add-ons-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
import './TraceOperator.styles.scss';
|
||||
|
||||
import { Button, Select, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo } 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';
|
||||
|
||||
export default function TraceOperator({
|
||||
traceOperator,
|
||||
isListViewPanel = false,
|
||||
}: {
|
||||
traceOperator: IBuilderTraceOperator;
|
||||
isListViewPanel?: boolean;
|
||||
}): JSX.Element {
|
||||
const { panelType, currentQuery, 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],
|
||||
);
|
||||
|
||||
const handleChangeSpanSource = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('returnSpansFrom', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const defaultSpanSource = useMemo(
|
||||
() =>
|
||||
traceOperator.returnSpansFrom ||
|
||||
currentQuery.builder.queryData[0].queryName ||
|
||||
'',
|
||||
[currentQuery.builder.queryData, traceOperator?.returnSpansFrom],
|
||||
);
|
||||
|
||||
const spanSourceOptions = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.map((query) => ({
|
||||
value: query.queryName,
|
||||
label: (
|
||||
<div className="qb-trace-operator-span-source-label">
|
||||
<span className="qb-trace-operator-span-source-label-query">Query</span>
|
||||
<p className="qb-trace-operator-span-source-label-query-name">
|
||||
{query.queryName}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
|
||||
<div className="qb-trace-operator-container">
|
||||
<InputWithLabel
|
||||
className={cx(
|
||||
'qb-trace-operator-input',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
initialValue={traceOperator?.expression || ''}
|
||||
label="TRACES MATCHING"
|
||||
placeholder="Add condition..."
|
||||
type="text"
|
||||
onChange={handleTraceOperatorChange}
|
||||
/>
|
||||
{!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 className="qb-trace-operator-add-ons-input">
|
||||
<Typography.Text className="label">Using spans from</Typography.Text>
|
||||
<Select
|
||||
bordered={false}
|
||||
defaultValue={defaultSpanSource}
|
||||
style={{ minWidth: 120 }}
|
||||
onChange={handleChangeSpanSource}
|
||||
options={spanSourceOptions}
|
||||
listItemHeight={24}
|
||||
/>
|
||||
</div>
|
||||
</QueryAddOns>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title="Remove Trace Operator" placement="topLeft">
|
||||
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,9 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
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: '' });
|
||||
@@ -543,229 +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'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -38,13 +38,6 @@ const isArrayOperator = (operator: string): boolean => {
|
||||
return arrayOperators.includes(operator);
|
||||
};
|
||||
|
||||
const isVariable = (value: string | string[] | number | boolean): boolean => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
|
||||
}
|
||||
return typeof value === 'string' && value.trim().startsWith('$');
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a value for the expression string
|
||||
* @param value - The value to format
|
||||
@@ -55,10 +48,6 @@ const formatValueForExpression = (
|
||||
value: string[] | string | number | boolean,
|
||||
operator?: string,
|
||||
): string => {
|
||||
if (isVariable(value)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// For IN operators, ensure value is always an array
|
||||
if (isArrayOperator(operator || '')) {
|
||||
const arrayValue = Array.isArray(value) ? value : [value];
|
||||
@@ -174,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,
|
||||
@@ -219,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;
|
||||
@@ -253,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;
|
||||
}
|
||||
|
||||
@@ -309,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 (
|
||||
@@ -326,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 (
|
||||
@@ -343,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
|
||||
}
|
||||
@@ -365,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
|
||||
}
|
||||
@@ -378,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ function OtherFilters({
|
||||
{
|
||||
key: filter.key,
|
||||
dataType: filter.dataType,
|
||||
isColumn: filter.isColumn,
|
||||
isJSON: filter.isJSON,
|
||||
type: filter.type,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
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 {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RadioChangeEvent } from 'antd/es/radio';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SignozRadioGroupProps {
|
||||
@@ -37,7 +38,10 @@ function SignozRadioGroup({
|
||||
value={option.value}
|
||||
className={value === option.value ? 'selected_view tab' : 'tab'}
|
||||
>
|
||||
{option.label}
|
||||
<div className="view-title-container">
|
||||
{option.icon && <div className="icon-container">{option.icon}</div>}
|
||||
{option.label}
|
||||
</div>
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -32,6 +32,4 @@ export enum LOCALSTORAGE {
|
||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
HavingForm,
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
@@ -50,13 +51,15 @@ import {
|
||||
export const MAX_FORMULAS = 20;
|
||||
export const MAX_QUERIES = 26;
|
||||
|
||||
export const TRACE_OPERATOR_QUERY_NAME = 'T1';
|
||||
|
||||
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',
|
||||
@@ -150,12 +153,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 = {
|
||||
@@ -187,7 +192,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
sourceNames: alphabet,
|
||||
}),
|
||||
disabled: false,
|
||||
stepInterval: null,
|
||||
stepInterval: undefined,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -235,7 +240,7 @@ export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
|
||||
sourceNames: alphabet,
|
||||
}),
|
||||
disabled: false,
|
||||
stepInterval: null,
|
||||
stepInterval: undefined,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -263,6 +268,11 @@ 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: '',
|
||||
@@ -280,6 +290,7 @@ export const initialClickHouseData: IClickHouseQuery = {
|
||||
export const initialQueryBuilderData: QueryBuilderData = {
|
||||
queryData: [initialQueryBuilderFormValues],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
|
||||
export const initialSingleQueryMap: Record<
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -58,6 +58,8 @@ function TopErrors({
|
||||
id: '92b8a1c1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'tag',
|
||||
},
|
||||
|
||||
@@ -99,6 +99,8 @@ function DomainList(): JSX.Element {
|
||||
key: 'kind_string',
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
|
||||
@@ -123,6 +123,8 @@ describe('AllEndPoints', () => {
|
||||
{
|
||||
key: 'http.status_code',
|
||||
dataType: 'string',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
type: '',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -43,6 +43,8 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
@@ -54,6 +56,8 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
@@ -65,6 +69,8 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
key: 'rpc.method',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
@@ -249,16 +255,22 @@ export const hardcodedAttributeKeys: BaseAutocompleteData[] = [
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'rpc.method',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -340,6 +352,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -352,6 +366,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
id: '4c57937c',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -378,6 +394,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
aggregateOperator: 'p99',
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -390,6 +408,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
id: '2cf675cd',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -417,6 +437,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -429,6 +450,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
id: '3db0f605',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -439,6 +462,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
id: '6096f745',
|
||||
key: {
|
||||
dataType: DataTypes.bool,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'has_error',
|
||||
type: '',
|
||||
},
|
||||
@@ -466,6 +491,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'timestamp------false',
|
||||
isColumn: false,
|
||||
key: 'timestamp',
|
||||
type: '',
|
||||
},
|
||||
@@ -478,6 +504,8 @@ export const getDomainMetricsQueryPayload = (
|
||||
id: '8ff8dea1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -611,6 +639,8 @@ export const getEndPointsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -624,6 +654,8 @@ export const getEndPointsQueryPayload = (
|
||||
id: 'ec316e57',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -636,6 +668,8 @@ export const getEndPointsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -658,6 +692,8 @@ export const getEndPointsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -671,6 +707,8 @@ export const getEndPointsQueryPayload = (
|
||||
id: '46d57857',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -683,6 +721,8 @@ export const getEndPointsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -706,6 +746,7 @@ export const getEndPointsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'timestamp------false',
|
||||
isColumn: false,
|
||||
key: 'timestamp',
|
||||
type: '',
|
||||
},
|
||||
@@ -719,6 +760,8 @@ export const getEndPointsQueryPayload = (
|
||||
id: '4a237616',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -731,6 +774,8 @@ export const getEndPointsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -753,6 +798,8 @@ export const getEndPointsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -766,6 +813,8 @@ export const getEndPointsQueryPayload = (
|
||||
id: 'f162de1e',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -776,6 +825,8 @@ export const getEndPointsQueryPayload = (
|
||||
id: '3df0ac1d',
|
||||
key: {
|
||||
dataType: DataTypes.bool,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'has_error',
|
||||
type: '',
|
||||
},
|
||||
@@ -788,6 +839,8 @@ export const getEndPointsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -866,7 +919,9 @@ export const getTopErrorsQueryPayload = (
|
||||
id: '------false',
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
isColumn: false,
|
||||
type: '',
|
||||
isJSON: false,
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
@@ -880,6 +935,8 @@ export const getTopErrorsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -890,6 +947,8 @@ export const getTopErrorsQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: 'exists',
|
||||
value: '',
|
||||
@@ -902,6 +961,8 @@ export const getTopErrorsQueryPayload = (
|
||||
key: 'status_message',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: 'exists',
|
||||
value: '',
|
||||
@@ -914,6 +975,8 @@ export const getTopErrorsQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: domainName,
|
||||
@@ -924,6 +987,8 @@ export const getTopErrorsQueryPayload = (
|
||||
key: 'has_error',
|
||||
dataType: DataTypes.bool,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: true,
|
||||
@@ -947,9 +1012,13 @@ export const getTopErrorsQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: '',
|
||||
id: 'response_status_code--string----true',
|
||||
@@ -958,6 +1027,8 @@ export const getTopErrorsQueryPayload = (
|
||||
key: 'status_message',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
@@ -1333,6 +1404,8 @@ export const getTopErrorsCoRelationQueryFilters = (
|
||||
key: 'http.url',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'http.url--string--tag--false',
|
||||
},
|
||||
op: '=',
|
||||
@@ -1344,6 +1417,8 @@ export const getTopErrorsCoRelationQueryFilters = (
|
||||
key: 'has_error',
|
||||
dataType: DataTypes.bool,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'true',
|
||||
@@ -1354,6 +1429,8 @@ export const getTopErrorsCoRelationQueryFilters = (
|
||||
key: 'net.peer.name',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: domainName,
|
||||
@@ -1364,6 +1441,8 @@ export const getTopErrorsCoRelationQueryFilters = (
|
||||
key: 'response_status_code',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'response_status_code--string----true',
|
||||
},
|
||||
op: '=',
|
||||
@@ -1472,6 +1551,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -1485,6 +1566,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '874562e1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1497,6 +1580,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1520,6 +1605,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -1533,6 +1620,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '0c5564e0',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1545,6 +1634,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1568,6 +1659,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -1581,6 +1674,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '0d656701',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1591,6 +1686,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '83ef9a1b',
|
||||
key: {
|
||||
dataType: DataTypes.bool,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'has_error',
|
||||
type: '',
|
||||
},
|
||||
@@ -1603,6 +1700,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1627,6 +1726,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'timestamp------false',
|
||||
isColumn: false,
|
||||
key: 'timestamp',
|
||||
type: '',
|
||||
},
|
||||
@@ -1640,6 +1740,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '918f5b99',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1652,6 +1754,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1675,6 +1779,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -1688,6 +1794,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: 'b355d1aa',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1700,6 +1808,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1764,6 +1874,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -1777,6 +1889,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '23450eb8',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1789,6 +1903,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1801,6 +1917,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: '',
|
||||
},
|
||||
@@ -1818,6 +1936,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -1831,6 +1951,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '2687dc18',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1843,6 +1965,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1855,6 +1979,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: '',
|
||||
},
|
||||
@@ -1876,6 +2002,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -1889,6 +2016,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'net.peer.name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'net.peer.name',
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -1901,6 +2030,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -1918,6 +2049,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: '',
|
||||
id: 'response_status_code--string----true',
|
||||
@@ -1967,6 +2100,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
isColumn: false,
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'count',
|
||||
@@ -1980,6 +2114,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: domainName,
|
||||
@@ -1990,6 +2126,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2010,6 +2148,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
],
|
||||
reduceTo: 'avg',
|
||||
@@ -2051,6 +2191,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -2064,6 +2206,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: 'b78ff216',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -2076,6 +2220,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2089,6 +2235,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'service.name--string--resource--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'service.name',
|
||||
type: 'resource',
|
||||
},
|
||||
@@ -2106,6 +2254,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -2119,6 +2269,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: 'a9024472',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -2131,6 +2283,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2144,6 +2298,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'service.name--string--resource--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'service.name',
|
||||
type: 'resource',
|
||||
},
|
||||
@@ -2162,6 +2318,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -2175,6 +2332,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: '1b6c062d',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -2187,6 +2346,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2200,6 +2361,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'service.name--string--resource--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'service.name',
|
||||
type: 'resource',
|
||||
},
|
||||
@@ -2217,6 +2380,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -2230,6 +2395,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: 'd14792a8',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -2239,6 +2406,9 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
id: '3212bf1a',
|
||||
key: {
|
||||
dataType: DataTypes.bool,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'has_error',
|
||||
type: '',
|
||||
},
|
||||
@@ -2251,6 +2421,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2264,6 +2436,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'service.name--string--resource--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'service.name',
|
||||
type: 'resource',
|
||||
},
|
||||
@@ -2323,6 +2497,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -2336,6 +2511,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: 'c6724407',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -2348,6 +2525,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2360,6 +2539,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: '',
|
||||
},
|
||||
@@ -2411,6 +2592,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -2424,6 +2607,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
id: 'aae93366',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -2436,6 +2621,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2448,6 +2635,8 @@ export const getEndPointDetailsQueryPayload = (
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: '',
|
||||
},
|
||||
@@ -2510,6 +2699,7 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
isColumn: false,
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'count',
|
||||
@@ -2523,6 +2713,8 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: domainName,
|
||||
@@ -2533,6 +2725,8 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -2552,6 +2746,8 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
],
|
||||
reduceTo: 'avg',
|
||||
@@ -3084,6 +3280,7 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -3097,6 +3294,8 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
id: 'c6724407',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3109,6 +3308,8 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
id: '8b1be6f0',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3226,6 +3427,8 @@ export const getAllEndpointsWidgetData = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -3239,6 +3442,8 @@ export const getAllEndpointsWidgetData = (
|
||||
id: 'ec316e57',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3251,6 +3456,8 @@ export const getAllEndpointsWidgetData = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -3276,6 +3483,8 @@ export const getAllEndpointsWidgetData = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -3289,6 +3498,8 @@ export const getAllEndpointsWidgetData = (
|
||||
id: '46d57857',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3301,6 +3512,8 @@ export const getAllEndpointsWidgetData = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -3327,6 +3540,7 @@ export const getAllEndpointsWidgetData = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'timestamp------false',
|
||||
isColumn: false,
|
||||
key: 'timestamp',
|
||||
type: '',
|
||||
},
|
||||
@@ -3340,6 +3554,8 @@ export const getAllEndpointsWidgetData = (
|
||||
id: '4a237616',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3352,6 +3568,8 @@ export const getAllEndpointsWidgetData = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -3377,6 +3595,8 @@ export const getAllEndpointsWidgetData = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
@@ -3390,6 +3610,8 @@ export const getAllEndpointsWidgetData = (
|
||||
id: 'f162de1e',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3400,11 +3622,13 @@ export const getAllEndpointsWidgetData = (
|
||||
id: '3df0ac1d',
|
||||
key: {
|
||||
dataType: DataTypes.bool,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'has_error',
|
||||
type: '',
|
||||
},
|
||||
op: '=',
|
||||
value: true,
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
id: '212678b9',
|
||||
@@ -3412,6 +3636,8 @@ export const getAllEndpointsWidgetData = (
|
||||
key: 'kind_string',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
@@ -3547,6 +3773,8 @@ export const getGroupByFiltersFromGroupByValues = (
|
||||
id: groupByAttribute?.id || v4(),
|
||||
key: {
|
||||
dataType: groupByAttribute?.dataType || DataTypes.String,
|
||||
isColumn: groupByAttribute?.isColumn || true,
|
||||
isJSON: groupByAttribute?.isJSON || false,
|
||||
key: groupByAttribute?.key || key,
|
||||
type: groupByAttribute?.type || '',
|
||||
},
|
||||
@@ -3582,6 +3810,7 @@ export const getRateOverTimeWidgetData = (
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
@@ -3595,6 +3824,8 @@ export const getRateOverTimeWidgetData = (
|
||||
id: '3c76fe0b',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3643,6 +3874,8 @@ export const getLatencyOverTimeWidgetData = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'duration_nano',
|
||||
type: '',
|
||||
},
|
||||
@@ -3656,6 +3889,8 @@ export const getLatencyOverTimeWidgetData = (
|
||||
id: '63adb3ff',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.SERVER_NAME,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3765,6 +4000,8 @@ export const getCustomFiltersForBarChart = (
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'response_status_code--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -3776,6 +4013,8 @@ export const getCustomFiltersForBarChart = (
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'response_status_code--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: 'tag',
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -142,6 +142,7 @@ function ChartPreview({
|
||||
return false;
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
query: query || initialQueriesMap.metrics,
|
||||
|
||||
@@ -54,6 +54,7 @@ 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) ||
|
||||
|
||||
@@ -150,6 +150,7 @@ function FormAlertRules({
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||
// TODO: Filter out queries who are used in trace operator
|
||||
[EQueryType.QUERY_BUILDER]: () => [
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||
@@ -864,7 +865,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}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const flattenLabels = (labels: Labels): ILabelRecord[] => {
|
||||
if (!hiddenLabels.includes(key)) {
|
||||
recs.push({
|
||||
key,
|
||||
value: labels[key] || '',
|
||||
value: labels[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@@ -21,13 +20,11 @@ import {
|
||||
convertHoursValueToRelevantUnit,
|
||||
SettingPeriod,
|
||||
TimeUnits,
|
||||
TimeUnitsValues,
|
||||
} from './utils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function Retention({
|
||||
type,
|
||||
retentionValue,
|
||||
setRetentionValue,
|
||||
text,
|
||||
@@ -53,9 +50,7 @@ function Retention({
|
||||
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
|
||||
}, [initialTimeUnitValue]);
|
||||
|
||||
const menuItems = TimeUnits.filter((option) =>
|
||||
type === 'logs' ? option.value !== TimeUnitsValues.hr : true,
|
||||
).map((option) => (
|
||||
const menuItems = TimeUnits.map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.key}
|
||||
</Option>
|
||||
@@ -129,7 +124,6 @@ function Retention({
|
||||
}
|
||||
|
||||
interface RetentionProps {
|
||||
type: TTTLType;
|
||||
retentionValue: number | null;
|
||||
text: string;
|
||||
setRetentionValue: Dispatch<SetStateAction<number | null>>;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Typography } from 'antd';
|
||||
import getDisks from 'api/disks/getDisks';
|
||||
import getRetentionPeriodApi from 'api/settings/getRetention';
|
||||
import getRetentionPeriodApiV2 from 'api/settings/getRetentionV2';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
|
||||
|
||||
@@ -17,10 +15,6 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
|
||||
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
|
||||
>;
|
||||
|
||||
type TRetentionAPIReturnV2<T extends TTTLType> = Promise<
|
||||
SuccessResponseV2<GetRetentionPeriodAPIPayloadProps<T>>
|
||||
>;
|
||||
|
||||
function GeneralSettings(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
const { user } = useAppContext();
|
||||
@@ -42,7 +36,7 @@ function GeneralSettings(): JSX.Element {
|
||||
queryKey: ['getRetentionPeriodApiTraces', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
queryFn: (): TRetentionAPIReturnV2<'logs'> => getRetentionPeriodApiV2(), // Only works for logs
|
||||
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
|
||||
queryKey: ['getRetentionPeriodApiLogs', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
@@ -76,7 +70,7 @@ function GeneralSettings(): JSX.Element {
|
||||
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
|
||||
return (
|
||||
<Typography>
|
||||
{(getRetentionPeriodLogsApiResponse.error as APIError).getErrorMessage() ||
|
||||
{getRetentionPeriodLogsApiResponse.data?.error ||
|
||||
getDisksResponse.data?.error ||
|
||||
t('something_went_wrong')}
|
||||
</Typography>
|
||||
@@ -92,7 +86,7 @@ function GeneralSettings(): JSX.Element {
|
||||
getRetentionPeriodTracesApiResponse.isLoading ||
|
||||
!getRetentionPeriodTracesApiResponse.data?.payload ||
|
||||
getRetentionPeriodLogsApiResponse.isLoading ||
|
||||
!getRetentionPeriodLogsApiResponse.data?.data
|
||||
!getRetentionPeriodLogsApiResponse.data?.payload
|
||||
) {
|
||||
return <Spinner tip="Loading.." height="70vh" />;
|
||||
}
|
||||
@@ -105,7 +99,7 @@ function GeneralSettings(): JSX.Element {
|
||||
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
||||
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
||||
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.data,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
|
||||
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,26 +5,19 @@ export interface ITimeUnit {
|
||||
key: string;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
export enum TimeUnitsValues {
|
||||
hr = 'hr',
|
||||
day = 'day',
|
||||
month = 'month',
|
||||
}
|
||||
|
||||
export const TimeUnits: ITimeUnit[] = [
|
||||
{
|
||||
value: TimeUnitsValues.hr,
|
||||
value: 'hr',
|
||||
key: 'Hours',
|
||||
multiplier: 1,
|
||||
},
|
||||
{
|
||||
value: TimeUnitsValues.day,
|
||||
value: 'day',
|
||||
key: 'Days',
|
||||
multiplier: 1 / 24,
|
||||
},
|
||||
{
|
||||
value: TimeUnitsValues.month,
|
||||
value: 'month',
|
||||
key: 'Months',
|
||||
multiplier: 1 / (24 * 30),
|
||||
},
|
||||
|
||||
@@ -65,6 +65,8 @@ const mockProps: WidgetGraphComponentProps = {
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user