mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-08 02:39:55 +00:00
Compare commits
249 Commits
improveTra
...
demo/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45d27304e1 | ||
|
|
535eed828d | ||
|
|
6b9ada2a1e | ||
|
|
d85c4cf9bd | ||
|
|
298647cf79 | ||
|
|
254d174962 | ||
|
|
ec2c9f3d0a | ||
|
|
ab26d6d3b2 | ||
|
|
3275af484b | ||
|
|
f2525fb293 | ||
|
|
41716e16a2 | ||
|
|
c6dcdb8ba8 | ||
|
|
b6b3b5d6a6 | ||
|
|
1a770f3b98 | ||
|
|
28129fbcaf | ||
|
|
dcba183872 | ||
|
|
ef0785fa69 | ||
|
|
4a2ea9907c | ||
|
|
2419927f02 | ||
|
|
d2c94a82d6 | ||
|
|
824576e176 | ||
|
|
f97001df91 | ||
|
|
448a2533bb | ||
|
|
b69583d017 | ||
|
|
f02ffeb4ff | ||
|
|
d22211443d | ||
|
|
b2cb00d993 | ||
|
|
abeadc7672 | ||
|
|
faadc60c74 | ||
|
|
0896ed9da9 | ||
|
|
d900076a77 | ||
|
|
56b153adc2 | ||
|
|
4ae881f1e2 | ||
|
|
439889400b | ||
|
|
d33d4b2a2e | ||
|
|
360e8309c8 | ||
|
|
7f3cf5e3c2 | ||
|
|
a28d94e790 | ||
|
|
b25d38e246 | ||
|
|
27580b62ba | ||
|
|
4ce41aa586 | ||
|
|
c9e7a19cc8 | ||
|
|
bcd21cee74 | ||
|
|
b59c5af060 | ||
|
|
2dbe0777f4 | ||
|
|
0cdb0253cd | ||
|
|
33f05d0745 | ||
|
|
13aa670972 | ||
|
|
1185a981c3 | ||
|
|
9a3feea008 | ||
|
|
14c1e522fa | ||
|
|
3a349d096a | ||
|
|
7602d863dd | ||
|
|
dfcbb40b62 | ||
|
|
7cf0d841ea | ||
|
|
020b6c79d3 | ||
|
|
2b67faa794 | ||
|
|
55509ad5c4 | ||
|
|
e7ab38e947 | ||
|
|
711b85f607 | ||
|
|
8c19228f87 | ||
|
|
68d9c6c3cc | ||
|
|
1632ad0396 | ||
|
|
e1c14a1dab | ||
|
|
10c6e1fac7 | ||
|
|
81e9b70842 | ||
|
|
45923f9a9c | ||
|
|
3999a64c64 | ||
|
|
82f81879c1 | ||
|
|
363fdfd646 | ||
|
|
31a917820c | ||
|
|
8fc9c09914 | ||
|
|
a1ca15fc81 | ||
|
|
729bfb31f1 | ||
|
|
052fb8b703 | ||
|
|
5d9247f591 | ||
|
|
8357716c0a | ||
|
|
ea54aae57a | ||
|
|
7ae2ca503f | ||
|
|
c0a9948146 | ||
|
|
a2f9eccc8b | ||
|
|
e355c944c8 | ||
|
|
a805bdb637 | ||
|
|
fcfaf152b2 | ||
|
|
3ad600c4df | ||
|
|
7e3d17ce5f | ||
|
|
bc888539e0 | ||
|
|
688867b708 | ||
|
|
23948e72eb | ||
|
|
f3569a9a02 | ||
|
|
0df1ed3b57 | ||
|
|
d0132f11ae | ||
|
|
f61e859901 | ||
|
|
4daec45d98 | ||
|
|
382d9d4a87 | ||
|
|
87ce197631 | ||
|
|
3cc5a24a4b | ||
|
|
9b8a892079 | ||
|
|
396e0cdc2d | ||
|
|
c838d7e2d4 | ||
|
|
1a193fb1a9 | ||
|
|
88dff3f552 | ||
|
|
5bb6d78c42 | ||
|
|
369f77977d | ||
|
|
836605def5 | ||
|
|
cc80923265 | ||
|
|
d0e5f6b478 | ||
|
|
8c75ba298a | ||
|
|
bc217a2aa3 | ||
|
|
6c0b5abbc0 | ||
|
|
0234829492 | ||
|
|
fba946bf78 | ||
|
|
55f96ca95f | ||
|
|
20a87db5bc | ||
|
|
ca774fe6a2 | ||
|
|
451c4bdeb7 | ||
|
|
dcd0de35a4 | ||
|
|
00829423bd | ||
|
|
585fadb867 | ||
|
|
4a1e786f4e | ||
|
|
0454a92b80 | ||
|
|
2d6d342ef0 | ||
|
|
fa06bac37b | ||
|
|
deb821617b | ||
|
|
f8b2bda431 | ||
|
|
6d95095d2f | ||
|
|
28a2ed4273 | ||
|
|
0fee724730 | ||
|
|
6f99d54a50 | ||
|
|
8dd3130701 | ||
|
|
8eaa609076 | ||
|
|
ac3c98b112 | ||
|
|
c0b96ed103 | ||
|
|
608d1565c0 | ||
|
|
e665d7c352 | ||
|
|
a5f9273743 | ||
|
|
7a79a16300 | ||
|
|
c39f48a41e | ||
|
|
d492d00976 | ||
|
|
4c0d2f0e6f | ||
|
|
02126b65b1 | ||
|
|
5235f65d9a | ||
|
|
43457eedc0 | ||
|
|
40c6458b31 | ||
|
|
f70f238b84 | ||
|
|
43d0cee5b5 | ||
|
|
33e7d852df | ||
|
|
5e968ec202 | ||
|
|
bbad7dca3e | ||
|
|
7ce278778f | ||
|
|
f09b79e04f | ||
|
|
1c72861290 | ||
|
|
9116c02e1c | ||
|
|
5d3254eeeb | ||
|
|
44a3fbfdd6 | ||
|
|
0dd41a07bd | ||
|
|
6f8de8da4c | ||
|
|
a5f57db0c7 | ||
|
|
83f46aeff6 | ||
|
|
7372bf0291 | ||
|
|
ee78805888 | ||
|
|
f6547210b2 | ||
|
|
7206bb82fe | ||
|
|
a1ad2b7835 | ||
|
|
4cb70ec07e | ||
|
|
0469233063 | ||
|
|
f3621e14bf | ||
|
|
fd035d885e | ||
|
|
c516825e41 | ||
|
|
188ff014d1 | ||
|
|
a2ab97a347 | ||
|
|
da7cdec01f | ||
|
|
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 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -42,3 +42,7 @@
|
||||
/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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23
|
||||
GO_VERSION: 1.24
|
||||
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.23"
|
||||
go-version: "1.24"
|
||||
- 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.23"
|
||||
go-version: "1.24"
|
||||
- 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.23"
|
||||
go-version: "1.24"
|
||||
|
||||
# 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.23"
|
||||
go-version: "1.24"
|
||||
- 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.23"
|
||||
go-version: "1.24"
|
||||
|
||||
# copy the caches from build
|
||||
- name: get-sha
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -86,6 +86,8 @@ queries.active
|
||||
.devenv/**/tmp/**
|
||||
.qodo
|
||||
|
||||
.dev
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -2,10 +2,11 @@ FROM node:18-bullseye AS build
|
||||
|
||||
WORKDIR /opt/
|
||||
COPY ./frontend/ ./
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
RUN CI=1 yarn install
|
||||
RUN CI=1 yarn build
|
||||
|
||||
FROM golang:1.23-bullseye
|
||||
FROM golang:1.24-bullseye
|
||||
|
||||
ARG OS="linux"
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
))
|
||||
}
|
||||
|
||||
password, err := types.NewFactorPassword(uuid.NewString())
|
||||
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
|
||||
|
||||
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
|
||||
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||
}
|
||||
|
||||
return integrationUser, nil
|
||||
return newUser, nil
|
||||
}
|
||||
|
||||
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
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: fmt.Errorf(s),
|
||||
Err: errors.New(s),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
|
||||
func InternalErrorStr(s string) *ApiError {
|
||||
return &ApiError{
|
||||
Typ: basemodel.ErrorInternal,
|
||||
Err: fmt.Errorf(s),
|
||||
Err: errors.New(s),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
*.typegen.ts
|
||||
i18-generate-hash.js
|
||||
i18-generate-hash.js
|
||||
src/parser/TraceOperatorParser/**
|
||||
@@ -10,4 +10,6 @@ public/
|
||||
**/*.json
|
||||
|
||||
# Ignore all files in parser folder:
|
||||
src/parser/**
|
||||
src/parser/**
|
||||
|
||||
src/TraceOperator/parser/**
|
||||
@@ -45,11 +45,13 @@
|
||||
"@sentry/webpack-plugin": "2.22.6",
|
||||
"@signozhq/badge": "0.0.2",
|
||||
"@signozhq/calendar": "0.0.0",
|
||||
"@signozhq/callout": "0.0.2",
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/table": "0.3.4",
|
||||
"@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",
|
||||
|
||||
25
frontend/src/api/settings/getRetentionV2.ts
Normal file
25
frontend/src/api/settings/getRetentionV2.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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 { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/settings/setRetention';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
|
||||
|
||||
const setRetention = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(
|
||||
const response = await axios.post<PayloadPropsV2>(
|
||||
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
|
||||
props.coldStorage
|
||||
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
|
||||
@@ -17,13 +17,11 @@ const setRetention = async (
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
32
frontend/src/api/settings/setRetentionV2.ts
Normal file
32
frontend/src/api/settings/setRetentionV2.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
@@ -92,6 +92,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery()],
|
||||
queryFormulas: [baseFormula()],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
@@ -215,7 +216,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
},
|
||||
],
|
||||
clickhouse_sql: [],
|
||||
builder: { queryData: [], queryFormulas: [] },
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
originalGraphType: PANEL_TYPES.TABLE,
|
||||
@@ -286,7 +287,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
legend: 'LC',
|
||||
},
|
||||
],
|
||||
builder: { queryData: [], queryFormulas: [] },
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
},
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
@@ -345,7 +346,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: { queryData: [], queryFormulas: [] },
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
@@ -386,6 +387,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery()],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
@@ -459,6 +461,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
builder: {
|
||||
queryData: [logsQuery],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
@@ -572,6 +575,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
@@ -332,6 +336,101 @@ export function convertBuilderQueriesToV5(
|
||||
);
|
||||
}
|
||||
|
||||
function createTraceOperatorBaseSpec(
|
||||
queryData: IBuilderTraceOperator,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): BaseBuilderQuery {
|
||||
const nonEmptySelectColumns = (queryData.selectColumns as (
|
||||
| BaseAutocompleteData
|
||||
| TelemetryFieldKey
|
||||
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||
|
||||
return {
|
||||
stepInterval: queryData?.stepInterval || undefined,
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
? queryData.limit || queryData.pageSize || undefined
|
||||
: queryData.limit || undefined,
|
||||
offset:
|
||||
requestType === 'raw' || requestType === 'trace'
|
||||
? queryData.offset
|
||||
: undefined,
|
||||
order:
|
||||
queryData.orderBy?.length > 0
|
||||
? queryData.orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
: nonEmptySelectColumns?.map(
|
||||
(column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function convertTraceOperatorToV5(
|
||||
traceOperator: Record<string, IBuilderTraceOperator>,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(traceOperator).map(
|
||||
([queryName, traceOperatorData]): QueryEnvelope => {
|
||||
const baseSpec = createTraceOperatorBaseSpec(
|
||||
traceOperatorData,
|
||||
requestType,
|
||||
panelType,
|
||||
);
|
||||
|
||||
// Skip aggregation for raw request type
|
||||
const aggregations =
|
||||
requestType === 'raw'
|
||||
? undefined
|
||||
: createAggregation(traceOperatorData, panelType);
|
||||
|
||||
const spec: QueryEnvelope['spec'] = {
|
||||
name: queryName,
|
||||
...baseSpec,
|
||||
expression: traceOperatorData.expression || '',
|
||||
aggregations: aggregations as TraceAggregation[],
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'builder_trace_operator' as QueryType,
|
||||
spec,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PromQL queries to V5 format
|
||||
*/
|
||||
@@ -413,14 +512,28 @@ 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',
|
||||
tableParams,
|
||||
);
|
||||
|
||||
// Combine legend maps
|
||||
legendMap = {
|
||||
...currentQueryData.newLegendMap,
|
||||
...currentFormulas.newLegendMap,
|
||||
...currentTraceOperator.newLegendMap,
|
||||
};
|
||||
|
||||
// Convert builder queries
|
||||
@@ -453,8 +566,14 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
}),
|
||||
);
|
||||
|
||||
// Combine both types
|
||||
queries = [...builderQueries, ...formulaQueries];
|
||||
const traceOperatorQueries = convertTraceOperatorToV5(
|
||||
currentTraceOperator.data,
|
||||
requestType,
|
||||
graphType,
|
||||
);
|
||||
|
||||
// Combine all query types
|
||||
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
&.bar-panel-container {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 80px);
|
||||
&.graph-panel-container {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,9 +84,11 @@
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
&.bar-panel-container {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,31 @@
|
||||
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;
|
||||
|
||||
@@ -59,7 +59,9 @@ interface CustomTimePickerProps {
|
||||
customDateTimeVisible?: boolean;
|
||||
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
|
||||
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
|
||||
handleGoLive?: () => void;
|
||||
showLiveLogs?: boolean;
|
||||
onGoLive?: () => void;
|
||||
onExitLiveLogs?: () => void;
|
||||
}
|
||||
|
||||
function CustomTimePicker({
|
||||
@@ -76,7 +78,9 @@ function CustomTimePicker({
|
||||
customDateTimeVisible,
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
handleGoLive,
|
||||
onGoLive,
|
||||
onExitLiveLogs,
|
||||
showLiveLogs,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
@@ -165,9 +169,13 @@ function CustomTimePicker({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}, [selectedTime, selectedValue]);
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
} else {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}
|
||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
||||
|
||||
const hide = (): void => {
|
||||
setOpen(false);
|
||||
@@ -338,6 +346,28 @@ function CustomTimePicker({
|
||||
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">
|
||||
@@ -357,7 +387,8 @@ function CustomTimePicker({
|
||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||
onSelectHandler={handleSelect}
|
||||
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||
onGoLive={defaultTo(onGoLive, noop)}
|
||||
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
||||
options={items}
|
||||
selectedTime={selectedTime}
|
||||
activeView={activeView}
|
||||
@@ -392,17 +423,7 @@ function CustomTimePicker({
|
||||
onBlur={handleBlur}
|
||||
onChange={handleInputChange}
|
||||
data-1p-ignore
|
||||
prefix={
|
||||
<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>
|
||||
}
|
||||
prefix={getInputPrefix()}
|
||||
suffix={
|
||||
<div className="time-input-suffix">
|
||||
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||
@@ -439,6 +460,8 @@ CustomTimePicker.defaultProps = {
|
||||
customDateTimeVisible: false,
|
||||
setCustomDTPickerVisible: noop,
|
||||
onCustomDateHandler: noop,
|
||||
handleGoLive: noop,
|
||||
onGoLive: noop,
|
||||
onCustomTimeStatusUpdate: noop,
|
||||
onExitLiveLogs: noop,
|
||||
showLiveLogs: false,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 {
|
||||
@@ -16,7 +17,14 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
import { Clock, PenLine } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
||||
|
||||
@@ -32,12 +40,13 @@ interface CustomTimePickerPopoverContentProps {
|
||||
lexicalContext?: LexicalContext,
|
||||
) => void;
|
||||
onSelectHandler: (label: string, value: string) => void;
|
||||
handleGoLive: () => void;
|
||||
onGoLive: () => void;
|
||||
selectedTime: string;
|
||||
activeView: 'datetime' | 'timezone';
|
||||
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||
isOpenedFromFooter: boolean;
|
||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||
onExitLiveLogs: () => void;
|
||||
}
|
||||
|
||||
interface RecentlyUsedDateTimeRange {
|
||||
@@ -56,12 +65,13 @@ function CustomTimePickerPopoverContent({
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
onSelectHandler,
|
||||
handleGoLive,
|
||||
onGoLive,
|
||||
selectedTime,
|
||||
activeView,
|
||||
setActiveView,
|
||||
isOpenedFromFooter,
|
||||
setIsOpenedFromFooter,
|
||||
onExitLiveLogs,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -69,6 +79,19 @@ function CustomTimePickerPopoverContent({
|
||||
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;
|
||||
|
||||
@@ -76,6 +99,12 @@ function CustomTimePickerPopoverContent({
|
||||
RecentlyUsedDateTimeRange[]
|
||||
>([]);
|
||||
|
||||
const handleExitLiveLogs = useCallback((): void => {
|
||||
if (isLogsExplorerPage) {
|
||||
onExitLiveLogs();
|
||||
}
|
||||
}, [isLogsExplorerPage, onExitLiveLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customDateTimeVisible) {
|
||||
const customTimeRanges = getCustomTimeRanges();
|
||||
@@ -107,6 +136,7 @@ function CustomTimePickerPopoverContent({
|
||||
className="time-btns"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
>
|
||||
@@ -140,12 +170,17 @@ function CustomTimePickerPopoverContent({
|
||||
);
|
||||
}
|
||||
|
||||
const handleGoLive = (): void => {
|
||||
onGoLive();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="date-time-popover">
|
||||
{!customDateTimeVisible && (
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && (
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||
Live
|
||||
</Button>
|
||||
@@ -155,6 +190,7 @@ function CustomTimePickerPopoverContent({
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
@@ -169,7 +205,6 @@ function CustomTimePickerPopoverContent({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'relative-date-time',
|
||||
@@ -199,12 +234,14 @@ function CustomTimePickerPopoverContent({
|
||||
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);
|
||||
}}
|
||||
|
||||
@@ -125,6 +125,7 @@ export const getHostTracesQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
|
||||
@@ -51,6 +51,7 @@ export const getHostLogsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
|
||||
@@ -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 {
|
||||
@@ -291,6 +295,13 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
.qb-trace-operator-button-container {
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +342,12 @@
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
&.has-trace-operator {
|
||||
&::before {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
@@ -347,7 +364,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,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
const isMultiQueryAllowed = useMemo(
|
||||
() => !isListViewPanel || showTraceOperator,
|
||||
[showTraceOperator, isListViewPanel],
|
||||
);
|
||||
|
||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
@@ -97,11 +106,60 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
listViewTracesFilterConfigs,
|
||||
]);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
|
||||
const hasAtLeastOneTraceQuery = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.some(
|
||||
(query) => query.dataSource === DataSource.TRACES,
|
||||
),
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const hasTraceOperator = useMemo(
|
||||
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
|
||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
||||
);
|
||||
|
||||
const shouldShowFooter = useMemo(
|
||||
() =>
|
||||
(!showOnlyWhereClause && !isListViewPanel) ||
|
||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
|
||||
);
|
||||
|
||||
const showQueryList = useMemo(
|
||||
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
|
||||
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
|
||||
);
|
||||
|
||||
const showFormula = useMemo(() => {
|
||||
if (currentDataSource === DataSource.TRACES) {
|
||||
return !isListViewPanel;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isListViewPanel, currentDataSource]);
|
||||
|
||||
const showAddTraceOperator = useMemo(
|
||||
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
|
||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
{isListViewPanel && (
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
@@ -109,15 +167,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
showTraceOperator={showTraceOperator}
|
||||
hasTraceOperator={hasTraceOperator}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isListViewPanel &&
|
||||
) : (
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
@@ -127,13 +186,17 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
isAvailableToDisable={false}
|
||||
showTraceOperator={showTraceOperator}
|
||||
hasTraceOperator={hasTraceOperator}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
signalSource={config?.signalSource || ''}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
@@ -158,15 +221,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
showAddFormula={showFormula}
|
||||
addNewBuilderQuery={addNewBuilderQuery}
|
||||
addNewFormula={addNewFormula}
|
||||
addTraceOperator={addTraceOperator}
|
||||
showAddTraceOperator={showAddTraceOperator}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasTraceOperator && (
|
||||
<TraceOperator
|
||||
isListViewPanel={isListViewPanel}
|
||||
traceOperator={traceOperator as IBuilderTraceOperator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
{showQueryList && (
|
||||
<div className="query-names-section">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<div key={query.queryName} className="query-name">
|
||||
|
||||
@@ -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,7 @@ function QueryAddOns({
|
||||
showReduceTo,
|
||||
panelType,
|
||||
index,
|
||||
isForTraceOperator = false,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
version: string;
|
||||
@@ -151,6 +152,7 @@ function QueryAddOns({
|
||||
showReduceTo: boolean;
|
||||
panelType: PANEL_TYPES | null;
|
||||
index: number;
|
||||
isForTraceOperator?: boolean;
|
||||
}): JSX.Element {
|
||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||
|
||||
@@ -160,6 +162,7 @@ function QueryAddOns({
|
||||
index,
|
||||
query,
|
||||
entityVersion: '',
|
||||
isForTraceOperator,
|
||||
});
|
||||
|
||||
const { handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { Plus, Sigma } from 'lucide-react';
|
||||
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
|
||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
||||
|
||||
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 +30,65 @@ 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-trace-operator-button-container">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add Trace Matching
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="add-trace-operator-button periscope-btn secondary"
|
||||
icon={<DraftingCompass size={16} />}
|
||||
onClick={(): void => addTraceOperator?.()}
|
||||
>
|
||||
<div className="qb-trace-operator-button-container-text">
|
||||
Add Trace Matching
|
||||
<BetaTag />
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { Dropdown } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
@@ -26,9 +27,12 @@ export const QueryV2 = memo(function QueryV2({
|
||||
query,
|
||||
filterConfigs,
|
||||
isListViewPanel = false,
|
||||
showTraceOperator = false,
|
||||
hasTraceOperator = false,
|
||||
version,
|
||||
showOnlyWhereClause = false,
|
||||
signalSource = '',
|
||||
isMultiQueryAllowed = false,
|
||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@@ -75,6 +79,15 @@ export const QueryV2 = memo(function QueryV2({
|
||||
dataSource,
|
||||
]);
|
||||
|
||||
const showInlineQuerySearch = useMemo(() => {
|
||||
if (!showTraceOperator) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
|
||||
);
|
||||
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
@@ -108,11 +121,12 @@ export const QueryV2 = memo(function QueryV2({
|
||||
ref={ref}
|
||||
>
|
||||
<div className="qb-content-section">
|
||||
{!showOnlyWhereClause && (
|
||||
{(!showOnlyWhereClause || showTraceOperator) && (
|
||||
<div className="qb-header-container">
|
||||
<div className="query-actions-container">
|
||||
<div className="query-actions-left-container">
|
||||
<QBEntityOptions
|
||||
hasTraceOperator={hasTraceOperator}
|
||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||
showFunctions={
|
||||
(version && version === ENTITY_VERSION_V4) ||
|
||||
@@ -122,6 +136,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
false
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
showTraceOperator={showTraceOperator}
|
||||
entityType="query"
|
||||
entityData={query}
|
||||
onToggleVisibility={handleToggleDisableQuery}
|
||||
@@ -139,7 +154,28 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isListViewPanel && (
|
||||
{!isCollapsed && showInlineQuerySearch && (
|
||||
<div className="qb-search-filter-container" style={{ flex: 1 }}>
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMultiQueryAllowed && (
|
||||
<Dropdown
|
||||
className="query-actions-dropdown"
|
||||
menu={{
|
||||
@@ -181,28 +217,31 @@ 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>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
{!showInlineQuerySearch && (
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
!isListViewPanel &&
|
||||
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
|
||||
dataSource !== DataSource.METRICS && (
|
||||
<QueryAggregation
|
||||
dataSource={dataSource}
|
||||
@@ -225,16 +264,17 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
version="v3"
|
||||
isListViewPanel={isListViewPanel}
|
||||
showReduceTo={showReduceTo}
|
||||
panelType={panelType}
|
||||
/>
|
||||
)}
|
||||
{!showOnlyWhereClause &&
|
||||
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
version="v3"
|
||||
isListViewPanel={isListViewPanel}
|
||||
showReduceTo={showReduceTo}
|
||||
panelType={panelType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
.qb-trace-operator {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&.non-list-view {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 12px;
|
||||
height: 88px;
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-span-source-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 24px;
|
||||
|
||||
&-query {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
&-query-name {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 2px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
background: rgba(242, 71, 105, 0.1);
|
||||
color: var(--Sakura-400, #f56c87);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
transform: translateY(-50%);
|
||||
left: -26px;
|
||||
height: 1px;
|
||||
width: 20px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: -10px;
|
||||
transform: translateY(-50%);
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-aggregation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-add-ons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&-label-with-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
.qb-trace-operator-editor-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--bg-vanilla-400);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0px 8px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.qb-trace-operator {
|
||||
&-arrow {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
&::after {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
&.non-list-view {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-label-with-input {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
.label {
|
||||
color: var(--bg-ink-500) !important;
|
||||
border-right: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
import './TraceOperator.styles.scss';
|
||||
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAddOns from '../QueryAddOns/QueryAddOns';
|
||||
import QueryAggregation from '../QueryAggregation/QueryAggregation';
|
||||
import TraceOperatorEditor from './TraceOperatorEditor';
|
||||
|
||||
export default function TraceOperator({
|
||||
traceOperator,
|
||||
isListViewPanel = false,
|
||||
}: {
|
||||
traceOperator: IBuilderTraceOperator;
|
||||
isListViewPanel?: boolean;
|
||||
}): JSX.Element {
|
||||
const { panelType, removeTraceOperator } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: traceOperator,
|
||||
entityVersion: '',
|
||||
isForTraceOperator: true,
|
||||
});
|
||||
|
||||
const handleTraceOperatorChange = useCallback(
|
||||
(traceOperatorExpression: string) => {
|
||||
handleChangeQueryData('expression', traceOperatorExpression);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregation = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('aggregations', [
|
||||
{
|
||||
expression: value,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
|
||||
<div className="qb-trace-operator-container">
|
||||
<div
|
||||
className={cx(
|
||||
'qb-trace-operator-label-with-input',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
>
|
||||
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
|
||||
<div className="qb-trace-operator-editor-container">
|
||||
<TraceOperatorEditor
|
||||
value={traceOperator?.expression || ''}
|
||||
traceOperator={traceOperator}
|
||||
onChange={handleTraceOperatorChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isListViewPanel && (
|
||||
<div className="qb-trace-operator-aggregation-container">
|
||||
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
|
||||
<QueryAggregation
|
||||
dataSource={DataSource.TRACES}
|
||||
key={`query-search-${traceOperator.queryName}`}
|
||||
panelType={panelType || undefined}
|
||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||
onChange={handleChangeAggregation}
|
||||
queryData={traceOperator}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
'qb-trace-operator-add-ons-container',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
>
|
||||
<QueryAddOns
|
||||
index={0}
|
||||
query={traceOperator}
|
||||
version="v3"
|
||||
isForTraceOperator
|
||||
isListViewPanel={false}
|
||||
showReduceTo={false}
|
||||
panelType={panelType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title="Remove Trace Operator" placement="topLeft">
|
||||
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
|
||||
import '../QuerySearch/QuerySearch.styles.scss';
|
||||
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
||||
import { Button, Popover } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
TRACE_OPERATOR_OPERATORS,
|
||||
TRACE_OPERATOR_OPERATORS_LABELS,
|
||||
TRACE_OPERATOR_OPERATORS_WITH_PRIORITY,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateTraceOperatorQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { getTraceOperatorContextAtCursor } from './utils/traceOperatorContextUtils';
|
||||
import { getInvolvedQueriesInTraceOperator } from './utils/utils';
|
||||
|
||||
// Custom extension to stop events
|
||||
const stopEventsExtension = EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
// Stop all keyboard events from propagating to global shortcuts
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
return false; // Important for CM to know you handled it
|
||||
},
|
||||
input: (event) => {
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
focus: (event) => {
|
||||
// Ensure focus events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
blur: (event) => {
|
||||
// Ensure blur events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
interface TraceOperatorEditorProps {
|
||||
value: string;
|
||||
traceOperator: IBuilderTraceOperator;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
onRun?: (query: string) => void;
|
||||
}
|
||||
|
||||
function TraceOperatorEditor({
|
||||
value,
|
||||
onChange,
|
||||
traceOperator,
|
||||
placeholder = 'Enter your trace operator query',
|
||||
onRun,
|
||||
}: TraceOperatorEditorProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
const [validation, setValidation] = useState<IValidationResult>({
|
||||
isValid: false,
|
||||
message: '',
|
||||
errors: [],
|
||||
});
|
||||
// Track if the query was changed externally (from props) vs internally (user input)
|
||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||
const [lastExternalValue, setLastExternalValue] = useState<string>('');
|
||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData
|
||||
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
|
||||
.map((query) => ({
|
||||
label: query.queryName,
|
||||
type: 'atom',
|
||||
apply: query.queryName,
|
||||
})),
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const toggleSuggestions = useCallback(
|
||||
(timeout?: number) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!editorRef.current) return;
|
||||
if (isFocused) {
|
||||
startCompletion(editorRef.current);
|
||||
} else {
|
||||
closeCompletion(editorRef.current);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
},
|
||||
[isFocused],
|
||||
);
|
||||
|
||||
const handleQueryValidation = (newQuery: string): void => {
|
||||
try {
|
||||
const validationResponse = validateTraceOperatorQuery(newQuery);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process trace operator',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Detect external value changes and mark for validation
|
||||
useEffect(() => {
|
||||
const newValue = value || '';
|
||||
if (newValue !== lastExternalValue) {
|
||||
setIsExternalQueryChange(true);
|
||||
setLastExternalValue(newValue);
|
||||
}
|
||||
}, [value, lastExternalValue]);
|
||||
|
||||
// Validate when the value changes externally (including on mount)
|
||||
useEffect(() => {
|
||||
if (isExternalQueryChange && value) {
|
||||
handleQueryValidation(value);
|
||||
setIsExternalQueryChange(false);
|
||||
}
|
||||
}, [isExternalQueryChange, value]);
|
||||
|
||||
// Enhanced autosuggestion function with context awareness
|
||||
function autoSuggestions(context: CompletionContext): CompletionResult | null {
|
||||
// This matches words before the cursor position
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
|
||||
if (word?.from === word?.to && !context.explicit) return null;
|
||||
|
||||
// Get the trace operator context at the cursor position
|
||||
const queryContext = getTraceOperatorContextAtCursor(value, cursorPos.ch);
|
||||
|
||||
// Define autocomplete options based on the context
|
||||
let options: {
|
||||
label: string;
|
||||
type: string;
|
||||
info?: string;
|
||||
apply:
|
||||
| string
|
||||
| ((view: EditorView, completion: any, from: number, to: number) => void);
|
||||
detail?: string;
|
||||
boost?: number;
|
||||
}[] = [];
|
||||
|
||||
// Helper function to add space after selection
|
||||
const addSpaceAfterSelection = (
|
||||
view: EditorView,
|
||||
completion: any,
|
||||
from: number,
|
||||
to: number,
|
||||
shouldAddSpace = true,
|
||||
): void => {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from,
|
||||
to,
|
||||
insert: shouldAddSpace ? `${completion.apply} ` : `${completion.apply}`,
|
||||
},
|
||||
selection: {
|
||||
anchor:
|
||||
from +
|
||||
(shouldAddSpace ? completion.apply.length + 1 : completion.apply.length),
|
||||
},
|
||||
});
|
||||
// Do not reopen here; onUpdate will handle reopening via toggleSuggestions
|
||||
};
|
||||
|
||||
// Helper function to add space after selection to options
|
||||
const addSpaceToOptions = (opts: typeof options): typeof options =>
|
||||
opts.map((option) => {
|
||||
const originalApply = option.apply || option.label;
|
||||
return {
|
||||
...option,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: any,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (queryContext.isInAtom) {
|
||||
// Suggest atoms (identifiers) for trace operators
|
||||
|
||||
const involvedQueries = getInvolvedQueriesInTraceOperator([traceOperator]);
|
||||
|
||||
options = queryOptions.map((option) => ({
|
||||
...option,
|
||||
boost: !involvedQueries.includes(option.apply as string) ? 100 : -99,
|
||||
}));
|
||||
|
||||
// Filter options based on what user is typing
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
options = options.filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
// Add space after selection for atoms
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
if (queryContext.isInOperator) {
|
||||
// Suggest operators for trace operators
|
||||
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
|
||||
options = operators.map((operator) => ({
|
||||
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
|
||||
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
|
||||
: operator,
|
||||
type: 'operator',
|
||||
apply: operator,
|
||||
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
|
||||
}));
|
||||
|
||||
// Add space after selection for operators
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
if (queryContext.isInParenthesis) {
|
||||
// Different suggestions based on the context within parenthesis
|
||||
const curChar = value.charAt(cursorPos.ch - 1) || '';
|
||||
|
||||
if (curChar === '(') {
|
||||
// Right after opening parenthesis, suggest atoms or nested expressions
|
||||
options = [
|
||||
{ label: '(', type: 'parenthesis', apply: '(' },
|
||||
...queryOptions,
|
||||
];
|
||||
|
||||
// Add space after selection for opening parenthesis context
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
if (curChar === ')') {
|
||||
// After closing parenthesis, suggest operators
|
||||
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
|
||||
options = operators.map((operator) => ({
|
||||
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
|
||||
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
|
||||
: operator,
|
||||
type: 'operator',
|
||||
apply: operator,
|
||||
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
|
||||
}));
|
||||
|
||||
// Add space after selection for closing parenthesis context
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default: suggest atoms if no specific context
|
||||
options = [
|
||||
...queryOptions,
|
||||
{
|
||||
label: '(',
|
||||
type: 'parenthesis',
|
||||
apply: '(',
|
||||
},
|
||||
];
|
||||
|
||||
// Filter options based on what user is typing
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
options = options.filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
// Add space after selection
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? context.pos,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
(viewUpdate: { view: EditorView }): void => {
|
||||
if (!editorRef.current) {
|
||||
editorRef.current = viewUpdate.view;
|
||||
}
|
||||
|
||||
const selection = viewUpdate.view.state.selection.main;
|
||||
const pos = selection.head;
|
||||
|
||||
const lineInfo = viewUpdate.view.state.doc.lineAt(pos);
|
||||
const newPos = {
|
||||
line: lineInfo.number,
|
||||
ch: pos - lineInfo.from,
|
||||
};
|
||||
|
||||
if (newPos.line !== cursorPos.line || newPos.ch !== cursorPos.ch) {
|
||||
setCursorPos(newPos);
|
||||
// Trigger suggestions on context update
|
||||
toggleSuggestions(10);
|
||||
}
|
||||
},
|
||||
[cursorPos, toggleSuggestions],
|
||||
);
|
||||
|
||||
const handleChange = (newValue: string): void => {
|
||||
// Mark as internal change to avoid triggering external validation
|
||||
setIsExternalQueryChange(false);
|
||||
setLastExternalValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const handleBlur = (): void => {
|
||||
handleQueryValidation(value);
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
// Effect to handle focus state and trigger suggestions on focus
|
||||
useEffect(() => {
|
||||
const clearTimeout = toggleSuggestions(10);
|
||||
return (): void => clearTimeout();
|
||||
}, [isFocused, toggleSuggestions]);
|
||||
|
||||
return (
|
||||
<div className="code-mirror-where-clause">
|
||||
<div className="query-where-clause-editor-container">
|
||||
<CodeMirror
|
||||
value={value}
|
||||
theme={isDarkMode ? copilot : githubLight}
|
||||
onChange={handleChange}
|
||||
onUpdate={handleUpdate}
|
||||
className={cx('query-where-clause-editor', {
|
||||
isValid: validation.isValid === true,
|
||||
hasErrors: validation.errors.length > 0,
|
||||
})}
|
||||
extensions={[
|
||||
autocompletion({
|
||||
override: [autoSuggestions],
|
||||
defaultKeymap: true,
|
||||
closeOnBlur: true,
|
||||
activateOnTyping: true,
|
||||
maxRenderedOptions: 50,
|
||||
}),
|
||||
javascript({ jsx: false, typescript: false }),
|
||||
EditorView.lineWrapping,
|
||||
stopEventsExtension,
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
{
|
||||
key: 'Escape',
|
||||
run: closeCompletion,
|
||||
},
|
||||
{
|
||||
key: 'Enter',
|
||||
preventDefault: true,
|
||||
// Prevent default behavior of Enter to add new line
|
||||
// and instead run a custom action
|
||||
run: (): boolean => true,
|
||||
},
|
||||
{
|
||||
key: 'Mod-Enter',
|
||||
preventDefault: true,
|
||||
run: (): boolean => {
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(value);
|
||||
} else {
|
||||
handleRunQuery();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Shift-Enter',
|
||||
preventDefault: true,
|
||||
// Prevent default behavior of Shift-Enter to add new line
|
||||
run: (): boolean => true,
|
||||
},
|
||||
]),
|
||||
),
|
||||
]}
|
||||
placeholder={placeholder}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setIsFocused(true);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{value && validation.isValid === false && !isFocused && (
|
||||
<div
|
||||
className={cx('query-status-container', {
|
||||
hasErrors: validation.errors.length > 0,
|
||||
})}
|
||||
>
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
content={
|
||||
<div className="query-status-content">
|
||||
<div className="query-status-content-header">
|
||||
<div className="query-validation">
|
||||
<div className="query-validation-errors">
|
||||
{validation.errors.map((error) => (
|
||||
<div key={error.message} className="query-validation-error">
|
||||
<div className="query-validation-error">
|
||||
{error.line}:{error.column} - {error.message}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
overlayClassName="query-status-popover"
|
||||
>
|
||||
{validation.isValid ? (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CheckCircleFilled />}
|
||||
className="periscope-btn ghost"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
|
||||
className="periscope-btn ghost"
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TraceOperatorEditor.defaultProps = {
|
||||
onRun: undefined,
|
||||
placeholder: 'Enter your trace operator query',
|
||||
};
|
||||
|
||||
export default TraceOperatorEditor;
|
||||
@@ -0,0 +1,425 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
|
||||
import { Token } from 'antlr4';
|
||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
||||
|
||||
import {
|
||||
createTraceOperatorContext,
|
||||
extractTraceExpressionPairs,
|
||||
getTraceOperatorContextAtCursor,
|
||||
} from '../utils/traceOperatorContextUtils';
|
||||
|
||||
describe('traceOperatorContextUtils', () => {
|
||||
describe('createTraceOperatorContext', () => {
|
||||
it('should create a context object with all required properties', () => {
|
||||
const mockToken = {
|
||||
type: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
} as Token;
|
||||
|
||||
const context = createTraceOperatorContext(
|
||||
mockToken,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
'atom',
|
||||
'operator',
|
||||
[],
|
||||
null,
|
||||
);
|
||||
|
||||
expect(context).toEqual({
|
||||
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
currentToken: 'test',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
atomToken: 'atom',
|
||||
operatorToken: 'operator',
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a context object with default values', () => {
|
||||
const mockToken = {
|
||||
type: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
} as Token;
|
||||
|
||||
const context = createTraceOperatorContext(
|
||||
mockToken,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(context).toEqual({
|
||||
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
currentToken: 'test',
|
||||
isInAtom: false,
|
||||
isInOperator: true,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
atomToken: undefined,
|
||||
operatorToken: undefined,
|
||||
expressionPairs: [],
|
||||
currentPair: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTraceExpressionPairs', () => {
|
||||
it('should extract simple expression pair', () => {
|
||||
const query = 'A => B';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].position.leftStart).toBe(0);
|
||||
expect(result[0].position.leftEnd).toBe(0);
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].position.operatorStart).toBe(2);
|
||||
expect(result[0].position.operatorEnd).toBe(3);
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
expect(result[0].position.rightStart).toBe(5);
|
||||
expect(result[0].position.rightEnd).toBe(5);
|
||||
expect(result[0].isComplete).toBe(true);
|
||||
});
|
||||
|
||||
it('should extract multiple expression pairs', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
// First pair: A => B
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
|
||||
// Second pair: C => D
|
||||
expect(result[1].leftAtom).toBe('C');
|
||||
expect(result[1].operator).toBe('=>');
|
||||
expect(result[1].rightAtom).toBe('D');
|
||||
});
|
||||
|
||||
it('should handle NOT operator', () => {
|
||||
const query = 'NOT A => B';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
});
|
||||
|
||||
it('should handle parentheses', () => {
|
||||
const query = '(A => B) && (C => D)';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
expect(result[1].leftAtom).toBe('C');
|
||||
expect(result[1].rightAtom).toBe('D');
|
||||
});
|
||||
|
||||
it('should handle incomplete expressions', () => {
|
||||
const query = 'A =>';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBeUndefined();
|
||||
expect(result[0].isComplete).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle complex nested expressions', () => {
|
||||
const query = 'A => B && (C => D || E => F)';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
expect(result[1].leftAtom).toBe('C');
|
||||
expect(result[1].rightAtom).toBe('D');
|
||||
expect(result[2].leftAtom).toBe('E');
|
||||
expect(result[2].rightAtom).toBe('F');
|
||||
});
|
||||
|
||||
it('should handle whitespace variations', () => {
|
||||
const query = 'A=>B';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
});
|
||||
|
||||
it('should handle error cases gracefully', () => {
|
||||
const query = 'invalid syntax @#$%';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
// Should return an array (even if empty or with partial results)
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTraceOperatorContextAtCursor', () => {
|
||||
beforeEach(() => {
|
||||
// Reset console.error mock
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return default context for empty query', () => {
|
||||
const result = getTraceOperatorContextAtCursor('', 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: 0,
|
||||
stop: 0,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default context for null query', () => {
|
||||
const result = getTraceOperatorContextAtCursor(null as any, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: 0,
|
||||
stop: 0,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default context for undefined query', () => {
|
||||
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: 0,
|
||||
stop: 0,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should identify atom context', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'A'
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.stop).toBe(0);
|
||||
});
|
||||
|
||||
it('should identify operator context', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 2); // cursor at '='
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(2);
|
||||
expect(result.stop).toBe(2);
|
||||
});
|
||||
|
||||
it('should identify parenthesis context', () => {
|
||||
const query = '(A => B)';
|
||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at '('
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(true);
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.stop).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle cursor at space', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at space
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle cursor at end of query', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at end
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(5);
|
||||
expect(result.stop).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle complex query', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 8); // cursor at '&'
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBe('&&');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(7);
|
||||
expect(result.stop).toBe(8);
|
||||
});
|
||||
|
||||
it('should identify operator position in complex query', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 10); // cursor at 'C'
|
||||
|
||||
expect(result.atomToken).toBe('C');
|
||||
expect(result.operatorToken).toBe('&&');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(10);
|
||||
expect(result.stop).toBe(10);
|
||||
});
|
||||
|
||||
it('should identify atom position in complex query', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 13); // cursor at '>'
|
||||
|
||||
expect(result.atomToken).toBe('C');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(12);
|
||||
expect(result.stop).toBe(13);
|
||||
});
|
||||
|
||||
it('should handle transition points', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 4); // cursor at 'B'
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(4);
|
||||
expect(result.stop).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle whitespace in complex queries', () => {
|
||||
const query = 'A=>B && C=>D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 6); // cursor at '&'
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBe('&&');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(5);
|
||||
expect(result.stop).toBe(6);
|
||||
});
|
||||
|
||||
it('should handle NOT operator context', () => {
|
||||
const query = 'NOT A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'N'
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle parentheses context', () => {
|
||||
const query = '(A => B)';
|
||||
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at 'A'
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(true);
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.stop).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle expression pairs context', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at 'A' in "&&"
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle various cursor positions', () => {
|
||||
const query = 'A => B';
|
||||
|
||||
// Test cursor at each position
|
||||
for (let i = 0; i < query.length; i++) {
|
||||
const result = getTraceOperatorContextAtCursor(query, i);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.start).toBe('number');
|
||||
expect(typeof result.stop).toBe('number');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';
|
||||
|
||||
const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
|
||||
(({ expression } as unknown) as IBuilderTraceOperator);
|
||||
|
||||
describe('getInvolvedQueriesInTraceOperator', () => {
|
||||
it('returns empty array for empty input', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts identifiers from expression', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([
|
||||
makeTraceOperator('A => B'),
|
||||
]);
|
||||
expect(result).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it('extracts identifiers from complex expression', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([
|
||||
makeTraceOperator('A => (NOT B || C)'),
|
||||
]);
|
||||
expect(result).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it('filters out querynames from complex expression', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([
|
||||
makeTraceOperator(
|
||||
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
|
||||
),
|
||||
]);
|
||||
expect(result).toEqual([
|
||||
'A1',
|
||||
'B2',
|
||||
'C3',
|
||||
'D4',
|
||||
'E5',
|
||||
'F6',
|
||||
'G7',
|
||||
'H8',
|
||||
'I9',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,562 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable no-continue */
|
||||
|
||||
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
|
||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
||||
import { IToken } from 'types/antlrQueryTypes';
|
||||
|
||||
// Trace Operator Context Interface
|
||||
export interface ITraceOperatorContext {
|
||||
tokenType: number;
|
||||
text: string;
|
||||
start: number;
|
||||
stop: number;
|
||||
currentToken: string;
|
||||
isInAtom: boolean;
|
||||
isInOperator: boolean;
|
||||
isInParenthesis: boolean;
|
||||
isInExpression: boolean;
|
||||
atomToken?: string;
|
||||
operatorToken?: string;
|
||||
expressionPairs: ITraceExpressionPair[];
|
||||
currentPair?: ITraceExpressionPair | null;
|
||||
}
|
||||
|
||||
// Trace Expression Pair Interface
|
||||
export interface ITraceExpressionPair {
|
||||
leftAtom: string;
|
||||
operator: string;
|
||||
rightAtom?: string;
|
||||
rightExpression?: string;
|
||||
position: {
|
||||
leftStart: number;
|
||||
leftEnd: number;
|
||||
operatorStart: number;
|
||||
operatorEnd: number;
|
||||
rightStart?: number;
|
||||
rightEnd?: number;
|
||||
};
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
// Helper functions to determine token types
|
||||
function isAtomToken(tokenType: number): boolean {
|
||||
return tokenType === TraceOperatorGrammarLexer.IDENTIFIER;
|
||||
}
|
||||
|
||||
function isOperatorToken(tokenType: number): boolean {
|
||||
return [
|
||||
TraceOperatorGrammarLexer.T__2, // '=>'
|
||||
TraceOperatorGrammarLexer.T__3, // '&&'
|
||||
TraceOperatorGrammarLexer.T__4, // '||'
|
||||
TraceOperatorGrammarLexer.T__5, // 'NOT'
|
||||
TraceOperatorGrammarLexer.T__6, // '->'
|
||||
].includes(tokenType);
|
||||
}
|
||||
|
||||
function isParenthesisToken(tokenType: number): boolean {
|
||||
return (
|
||||
tokenType === TraceOperatorGrammarLexer.T__0 ||
|
||||
tokenType === TraceOperatorGrammarLexer.T__1
|
||||
);
|
||||
}
|
||||
|
||||
function isOpeningParenthesis(tokenType: number): boolean {
|
||||
return tokenType === TraceOperatorGrammarLexer.T__0;
|
||||
}
|
||||
|
||||
function isClosingParenthesis(tokenType: number): boolean {
|
||||
return tokenType === TraceOperatorGrammarLexer.T__1;
|
||||
}
|
||||
|
||||
// Function to create a context object
|
||||
export function createTraceOperatorContext(
|
||||
token: Token,
|
||||
isInAtom: boolean,
|
||||
isInOperator: boolean,
|
||||
isInParenthesis: boolean,
|
||||
isInExpression: boolean,
|
||||
atomToken?: string,
|
||||
operatorToken?: string,
|
||||
expressionPairs?: ITraceExpressionPair[],
|
||||
currentPair?: ITraceExpressionPair | null,
|
||||
): ITraceOperatorContext {
|
||||
return {
|
||||
tokenType: token.type,
|
||||
text: token.text || '',
|
||||
start: token.start,
|
||||
stop: token.stop,
|
||||
currentToken: token.text || '',
|
||||
isInAtom,
|
||||
isInOperator,
|
||||
isInParenthesis,
|
||||
isInExpression,
|
||||
atomToken,
|
||||
operatorToken,
|
||||
expressionPairs: expressionPairs || [],
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to determine token context
|
||||
function determineTraceTokenContext(
|
||||
token: IToken,
|
||||
): {
|
||||
isInAtom: boolean;
|
||||
isInOperator: boolean;
|
||||
isInParenthesis: boolean;
|
||||
isInExpression: boolean;
|
||||
} {
|
||||
const tokenType = token.type;
|
||||
|
||||
return {
|
||||
isInAtom: isAtomToken(tokenType),
|
||||
isInOperator: isOperatorToken(tokenType),
|
||||
isInParenthesis: isParenthesisToken(tokenType),
|
||||
isInExpression: false, // Will be determined by broader context
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all expression pairs from a trace operator query string
|
||||
* This parses the query according to the TraceOperatorGrammar.g4 grammar
|
||||
*
|
||||
* @param query The trace operator query string to parse
|
||||
* @returns An array of ITraceExpressionPair objects representing the expression pairs
|
||||
*/
|
||||
export function extractTraceExpressionPairs(
|
||||
query: string,
|
||||
): ITraceExpressionPair[] {
|
||||
try {
|
||||
const input = query || '';
|
||||
const chars = CharStreams.fromString(input);
|
||||
const lexer = new TraceOperatorGrammarLexer(chars);
|
||||
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
tokenStream.fill();
|
||||
|
||||
const allTokens = tokenStream.tokens as IToken[];
|
||||
const expressionPairs: ITraceExpressionPair[] = [];
|
||||
let currentPair: Partial<ITraceExpressionPair> | null = null;
|
||||
|
||||
let i = 0;
|
||||
while (i < allTokens.length) {
|
||||
const token = allTokens[i];
|
||||
i++;
|
||||
|
||||
// Skip EOF and whitespace tokens
|
||||
if (token.type === TraceOperatorGrammarLexer.EOF || token.channel !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If token is an IDENTIFIER (atom), start or continue a pair
|
||||
if (isAtomToken(token.type)) {
|
||||
// If we don't have a current pair, start one
|
||||
if (!currentPair) {
|
||||
currentPair = {
|
||||
leftAtom: token.text,
|
||||
position: {
|
||||
leftStart: token.start,
|
||||
leftEnd: token.stop,
|
||||
operatorStart: 0,
|
||||
operatorEnd: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
// If we have a current pair but no operator yet, this is still the left atom
|
||||
else if (!currentPair.operator && currentPair.position) {
|
||||
currentPair.leftAtom = token.text;
|
||||
currentPair.position.leftStart = token.start;
|
||||
currentPair.position.leftEnd = token.stop;
|
||||
}
|
||||
// If we have an operator, this is the right atom
|
||||
else if (
|
||||
currentPair.operator &&
|
||||
!currentPair.rightAtom &&
|
||||
currentPair.position
|
||||
) {
|
||||
currentPair.rightAtom = token.text;
|
||||
currentPair.position.rightStart = token.start;
|
||||
currentPair.position.rightEnd = token.stop;
|
||||
currentPair.isComplete = true;
|
||||
|
||||
// Add the completed pair to the result
|
||||
expressionPairs.push(currentPair as ITraceExpressionPair);
|
||||
currentPair = null;
|
||||
}
|
||||
}
|
||||
// If token is an operator and we have a left atom
|
||||
else if (
|
||||
isOperatorToken(token.type) &&
|
||||
currentPair &&
|
||||
currentPair.leftAtom &&
|
||||
currentPair.position
|
||||
) {
|
||||
currentPair.operator = token.text;
|
||||
currentPair.position.operatorStart = token.start;
|
||||
currentPair.position.operatorEnd = token.stop;
|
||||
|
||||
// If this is a NOT operator, it might be followed by another operator
|
||||
if (token.type === TraceOperatorGrammarLexer.T__5 && i < allTokens.length) {
|
||||
// Look ahead for the next operator
|
||||
const nextToken = allTokens[i];
|
||||
if (isOperatorToken(nextToken.type) && nextToken.channel === 0) {
|
||||
currentPair.operator = `${token.text} ${nextToken.text}`;
|
||||
currentPair.position.operatorEnd = nextToken.stop;
|
||||
i++; // Skip the next token since we've consumed it
|
||||
}
|
||||
}
|
||||
}
|
||||
// If token is an opening parenthesis after an operator, this is a right expression
|
||||
else if (
|
||||
isOpeningParenthesis(token.type) &&
|
||||
currentPair &&
|
||||
currentPair.operator &&
|
||||
!currentPair.rightAtom &&
|
||||
currentPair.position
|
||||
) {
|
||||
// Find the matching closing parenthesis
|
||||
let parenCount = 1;
|
||||
let j = i;
|
||||
let rightExpression = '';
|
||||
const rightStart = token.start;
|
||||
let rightEnd = token.stop;
|
||||
|
||||
while (j < allTokens.length && parenCount > 0) {
|
||||
const parenToken = allTokens[j];
|
||||
if (parenToken.channel === 0) {
|
||||
if (isOpeningParenthesis(parenToken.type)) {
|
||||
parenCount++;
|
||||
} else if (isClosingParenthesis(parenToken.type)) {
|
||||
parenCount--;
|
||||
if (parenCount === 0) {
|
||||
rightEnd = parenToken.stop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
rightExpression += parenToken.text;
|
||||
j++;
|
||||
}
|
||||
|
||||
if (parenCount === 0) {
|
||||
currentPair.rightExpression = rightExpression;
|
||||
currentPair.position.rightStart = rightStart;
|
||||
currentPair.position.rightEnd = rightEnd;
|
||||
currentPair.isComplete = true;
|
||||
|
||||
// Add the completed pair to the result
|
||||
expressionPairs.push(currentPair as ITraceExpressionPair);
|
||||
currentPair = null;
|
||||
|
||||
// Skip to the end of the expression
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining incomplete pair
|
||||
if (currentPair && currentPair.leftAtom && currentPair.position) {
|
||||
expressionPairs.push({
|
||||
...currentPair,
|
||||
isComplete: !!(currentPair.leftAtom && currentPair.operator),
|
||||
} as ITraceExpressionPair);
|
||||
}
|
||||
|
||||
return expressionPairs;
|
||||
} catch (error) {
|
||||
console.error('Error in extractTraceExpressionPairs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current expression pair at the cursor position
|
||||
*
|
||||
* @param expressionPairs An array of ITraceExpressionPair objects
|
||||
* @param query The full query string
|
||||
* @param cursorIndex The position of the cursor in the query
|
||||
* @returns The expression pair at the cursor position, or null if not found
|
||||
*/
|
||||
export function getCurrentTraceExpressionPair(
|
||||
expressionPairs: ITraceExpressionPair[],
|
||||
cursorIndex: number,
|
||||
): ITraceExpressionPair | null {
|
||||
try {
|
||||
if (expressionPairs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the rightmost pair whose end position is before or at the cursor
|
||||
let bestMatch: ITraceExpressionPair | null = null;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const pair of expressionPairs) {
|
||||
const { position } = pair;
|
||||
const pairEnd =
|
||||
position.rightEnd || position.operatorEnd || position.leftEnd;
|
||||
const pairStart = position.leftStart;
|
||||
|
||||
// If this pair ends at or before the cursor, and it's further right than our previous best match
|
||||
if (
|
||||
pairStart <= cursorIndex &&
|
||||
cursorIndex <= pairEnd + 1 &&
|
||||
(!bestMatch ||
|
||||
pairEnd >
|
||||
(bestMatch.position.rightEnd ||
|
||||
bestMatch.position.operatorEnd ||
|
||||
bestMatch.position.leftEnd))
|
||||
) {
|
||||
bestMatch = pair;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
} catch (error) {
|
||||
console.error('Error in getCurrentTraceExpressionPair:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current trace operator context at the cursor position
|
||||
* This is useful for determining what kind of suggestions to show
|
||||
*
|
||||
* @param query The trace operator query string
|
||||
* @param cursorIndex The position of the cursor in the query
|
||||
* @returns The trace operator context at the cursor position
|
||||
*/
|
||||
export function getTraceOperatorContextAtCursor(
|
||||
query: string,
|
||||
cursorIndex: number,
|
||||
): ITraceOperatorContext {
|
||||
try {
|
||||
// Guard against infinite recursion
|
||||
const stackTrace = new Error().stack || '';
|
||||
const callCount = (stackTrace.match(/getTraceOperatorContextAtCursor/g) || [])
|
||||
.length;
|
||||
if (callCount > 3) {
|
||||
console.warn(
|
||||
'Potential infinite recursion detected in getTraceOperatorContextAtCursor',
|
||||
);
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Create input stream and lexer
|
||||
const input = query || '';
|
||||
const chars = CharStreams.fromString(input);
|
||||
const lexer = new TraceOperatorGrammarLexer(chars);
|
||||
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
tokenStream.fill();
|
||||
|
||||
const allTokens = tokenStream.tokens as IToken[];
|
||||
|
||||
// Get expression pairs information
|
||||
const expressionPairs = extractTraceExpressionPairs(query);
|
||||
const currentPair = getCurrentTraceExpressionPair(
|
||||
expressionPairs,
|
||||
cursorIndex,
|
||||
);
|
||||
|
||||
// Find the token at or just before the cursor
|
||||
let lastTokenBeforeCursor: IToken | null = null;
|
||||
for (let i = 0; i < allTokens.length; i++) {
|
||||
const token = allTokens[i];
|
||||
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
|
||||
|
||||
if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) {
|
||||
lastTokenBeforeCursor = token;
|
||||
}
|
||||
|
||||
if (token.start > cursorIndex) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find exact token at cursor
|
||||
let exactToken: IToken | null = null;
|
||||
for (let i = 0; i < allTokens.length; i++) {
|
||||
const token = allTokens[i];
|
||||
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
|
||||
|
||||
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
|
||||
exactToken = token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have any tokens, return default context
|
||||
if (!lastTokenBeforeCursor && !exactToken) {
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true, // Default to atom context when input is empty
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs,
|
||||
currentPair: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if cursor is at a space after a token (transition point)
|
||||
const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' ';
|
||||
const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' ';
|
||||
const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' ';
|
||||
const isTransitionPoint =
|
||||
(isAtSpace && isAfterToken) ||
|
||||
(cursorIndex === query.length && isAfterToken);
|
||||
|
||||
// If we're at a transition point after a token, progress the context
|
||||
if (
|
||||
lastTokenBeforeCursor &&
|
||||
(isAtSpace || isAfterSpace || isTransitionPoint)
|
||||
) {
|
||||
const lastTokenContext = determineTraceTokenContext(lastTokenBeforeCursor);
|
||||
|
||||
// Apply context progression: atom → operator → atom/expression → operator → atom
|
||||
if (lastTokenContext.isInAtom) {
|
||||
// After atom + space, move to operator context
|
||||
return {
|
||||
tokenType: lastTokenBeforeCursor.type,
|
||||
text: lastTokenBeforeCursor.text,
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: lastTokenBeforeCursor.text,
|
||||
isInAtom: false,
|
||||
isInOperator: true,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
atomToken: lastTokenBeforeCursor.text,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastTokenContext.isInOperator) {
|
||||
// After operator + space, move to atom/expression context
|
||||
return {
|
||||
tokenType: lastTokenBeforeCursor.type,
|
||||
text: lastTokenBeforeCursor.text,
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: lastTokenBeforeCursor.text,
|
||||
isInAtom: true, // Expecting an atom or expression after operator
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
operatorToken: lastTokenBeforeCursor.text,
|
||||
atomToken: currentPair?.leftAtom,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lastTokenContext.isInParenthesis &&
|
||||
isClosingParenthesis(lastTokenBeforeCursor.type)
|
||||
) {
|
||||
// After closing parenthesis, move to operator context
|
||||
return {
|
||||
tokenType: lastTokenBeforeCursor.type,
|
||||
text: lastTokenBeforeCursor.text,
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: lastTokenBeforeCursor.text,
|
||||
isInAtom: false,
|
||||
isInOperator: true,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If cursor is at the end of a token, return the current token context
|
||||
if (exactToken && cursorIndex === exactToken.stop + 1) {
|
||||
const tokenContext = determineTraceTokenContext(exactToken);
|
||||
|
||||
return {
|
||||
tokenType: exactToken.type,
|
||||
text: exactToken.text,
|
||||
start: exactToken.start,
|
||||
stop: exactToken.stop,
|
||||
currentToken: exactToken.text,
|
||||
...tokenContext,
|
||||
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
|
||||
operatorToken: tokenContext.isInOperator
|
||||
? exactToken.text
|
||||
: currentPair?.operator,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
// Regular token-based context detection
|
||||
if (exactToken?.channel === 0) {
|
||||
const tokenContext = determineTraceTokenContext(exactToken);
|
||||
|
||||
return {
|
||||
tokenType: exactToken.type,
|
||||
text: exactToken.text,
|
||||
start: exactToken.start,
|
||||
stop: exactToken.stop,
|
||||
currentToken: exactToken.text,
|
||||
...tokenContext,
|
||||
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
|
||||
operatorToken: tokenContext.isInOperator
|
||||
? exactToken.text
|
||||
: currentPair?.operator,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
// Default fallback to atom context
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getTraceOperatorContextAtCursor:', error);
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getInvolvedQueriesInTraceOperator = (
|
||||
traceOperators: IBuilderTraceOperator[],
|
||||
): string[] => {
|
||||
if (
|
||||
!traceOperators ||
|
||||
traceOperators.length === 0 ||
|
||||
traceOperators.length > 1
|
||||
)
|
||||
return [];
|
||||
|
||||
const currentTraceOperator = traceOperators[0];
|
||||
|
||||
// Match any word starting with letter or underscore
|
||||
const tokens =
|
||||
currentTraceOperator.expression.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || [];
|
||||
|
||||
// Filter out operator keywords
|
||||
const operators = new Set(['NOT']);
|
||||
return tokens.filter((t) => !operators.has(t));
|
||||
};
|
||||
@@ -1,10 +1,16 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
convertAggregationToExpression,
|
||||
convertFiltersToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from '../utils';
|
||||
@@ -769,3 +775,200 @@ describe('convertFiltersToExpression', () => {
|
||||
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertAggregationToExpression', () => {
|
||||
const mockAttribute: BaseAutocompleteData = {
|
||||
id: 'test-id',
|
||||
key: 'test_metric',
|
||||
type: 'string',
|
||||
dataType: DataTypes.String,
|
||||
};
|
||||
|
||||
it('should return undefined when no aggregateOperator is provided', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: '',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.METRICS,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should convert metrics aggregation with required temporality field', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'sum',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.METRICS,
|
||||
timeAggregation: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
alias: 'test_alias',
|
||||
reduceTo: 'sum',
|
||||
temporality: 'delta',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
metricName: 'test_metric',
|
||||
timeAggregation: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'sum',
|
||||
temporality: 'delta',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle noop operators by converting to count', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.METRICS,
|
||||
timeAggregation: 'noop',
|
||||
spaceAggregation: 'noop',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
metricName: 'test_metric',
|
||||
timeAggregation: 'count',
|
||||
spaceAggregation: 'count',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle missing attribute key gracefully', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'sum',
|
||||
aggregateAttribute: { ...mockAttribute, key: '' },
|
||||
dataSource: DataSource.METRICS,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
metricName: '',
|
||||
timeAggregation: 'sum',
|
||||
spaceAggregation: 'sum',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert traces aggregation to expression format', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.TRACES,
|
||||
alias: 'trace_alias',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
expression: 'count(test_metric)',
|
||||
alias: 'trace_alias',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert logs aggregation to expression format', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'avg',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.LOGS,
|
||||
alias: 'log_alias',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
expression: 'avg(test_metric)',
|
||||
alias: 'log_alias',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle aggregation without attribute key for traces/logs', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: { ...mockAttribute, key: '' },
|
||||
dataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
expression: 'count()',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle missing alias for traces/logs', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'sum',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.LOGS,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
expression: 'sum(test_metric)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use aggregateOperator as fallback for time and space aggregation', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'max',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.METRICS,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
metricName: 'test_metric',
|
||||
timeAggregation: 'max',
|
||||
spaceAggregation: 'max',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle undefined aggregateAttribute parameter with metrics', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'sum',
|
||||
aggregateAttribute: mockAttribute,
|
||||
dataSource: DataSource.METRICS,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
metricName: 'test_metric',
|
||||
timeAggregation: 'sum',
|
||||
spaceAggregation: 'sum',
|
||||
reduceTo: undefined,
|
||||
temporality: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle undefined aggregateAttribute parameter with traces', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: (undefined as unknown) as BaseAutocompleteData,
|
||||
dataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
expression: 'count()',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle undefined aggregateAttribute parameter with logs', () => {
|
||||
const result = convertAggregationToExpression({
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: (undefined as unknown) as BaseAutocompleteData,
|
||||
dataSource: DataSource.LOGS,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
expression: 'count()',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
@@ -580,14 +580,25 @@ export const convertHavingToExpression = (
|
||||
* @returns New aggregation format based on data source
|
||||
*
|
||||
*/
|
||||
export const convertAggregationToExpression = (
|
||||
aggregateOperator: string,
|
||||
aggregateAttribute: BaseAutocompleteData,
|
||||
dataSource: DataSource,
|
||||
timeAggregation?: string,
|
||||
spaceAggregation?: string,
|
||||
alias?: string,
|
||||
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||
export const convertAggregationToExpression = ({
|
||||
aggregateOperator,
|
||||
aggregateAttribute,
|
||||
dataSource,
|
||||
timeAggregation,
|
||||
spaceAggregation,
|
||||
alias,
|
||||
reduceTo,
|
||||
temporality,
|
||||
}: {
|
||||
aggregateOperator: string;
|
||||
aggregateAttribute: BaseAutocompleteData;
|
||||
dataSource: DataSource;
|
||||
timeAggregation?: string;
|
||||
spaceAggregation?: string;
|
||||
alias?: string;
|
||||
reduceTo?: ReduceOperators;
|
||||
temporality?: string;
|
||||
}): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||
// Skip if no operator or attribute key
|
||||
if (!aggregateOperator) {
|
||||
return undefined;
|
||||
@@ -605,7 +616,9 @@ export const convertAggregationToExpression = (
|
||||
if (dataSource === DataSource.METRICS) {
|
||||
return [
|
||||
{
|
||||
metricName: aggregateAttribute.key,
|
||||
metricName: aggregateAttribute?.key || '',
|
||||
reduceTo,
|
||||
temporality,
|
||||
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
||||
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
||||
} as MetricAggregation,
|
||||
@@ -613,7 +626,9 @@ export const convertAggregationToExpression = (
|
||||
}
|
||||
|
||||
// For traces and logs, use expression format
|
||||
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
|
||||
const expression = aggregateAttribute?.key
|
||||
? `${normalizedOperator}(${aggregateAttribute?.key})`
|
||||
: `${normalizedOperator}()`;
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,6 +17,27 @@ export const OPERATORS = {
|
||||
'<': '<',
|
||||
};
|
||||
|
||||
export const TRACE_OPERATOR_OPERATORS = {
|
||||
AND: '&&',
|
||||
OR: '||',
|
||||
NOT: 'NOT',
|
||||
DIRECT_DESCENDENT: '=>',
|
||||
INDIRECT_DESCENDENT: '->',
|
||||
};
|
||||
|
||||
export const TRACE_OPERATOR_OPERATORS_WITH_PRIORITY = {
|
||||
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 1,
|
||||
[TRACE_OPERATOR_OPERATORS.AND]: 2,
|
||||
[TRACE_OPERATOR_OPERATORS.OR]: 3,
|
||||
[TRACE_OPERATOR_OPERATORS.NOT]: 4,
|
||||
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 5,
|
||||
};
|
||||
|
||||
export const TRACE_OPERATOR_OPERATORS_LABELS = {
|
||||
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 'Direct Descendant',
|
||||
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 'Indirect Descendant',
|
||||
};
|
||||
|
||||
export const QUERY_BUILDER_FUNCTIONS = {
|
||||
HAS: 'has',
|
||||
HASANY: 'hasAny',
|
||||
|
||||
@@ -32,6 +32,7 @@ export enum LOCALSTORAGE {
|
||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
HavingForm,
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
@@ -50,6 +51,8 @@ import {
|
||||
export const MAX_FORMULAS = 20;
|
||||
export const MAX_QUERIES = 26;
|
||||
|
||||
export const TRACE_OPERATOR_QUERY_NAME = 'Trace Operator';
|
||||
|
||||
export const idDivider = '--';
|
||||
export const selectValueDivider = '__';
|
||||
|
||||
@@ -263,6 +266,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 +288,7 @@ export const initialClickHouseData: IClickHouseQuery = {
|
||||
export const initialQueryBuilderData: QueryBuilderData = {
|
||||
queryData: [initialQueryBuilderFormValues],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
|
||||
export const initialSingleQueryMap: Record<
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TRACE_OPERATOR_QUERY_NAME } from './queryBuilder';
|
||||
|
||||
export const FORMULA_REGEXP = /F\d+/;
|
||||
|
||||
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
|
||||
@@ -5,3 +7,5 @@ export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
|
||||
export const TYPE_ADDON_REGEXP = /_(.+)/;
|
||||
|
||||
export const SPLIT_FIRST_UNDERSCORE = /(?<!^)_/;
|
||||
|
||||
export const TRACE_OPERATOR_REGEXP = new RegExp(TRACE_OPERATOR_QUERY_NAME);
|
||||
|
||||
@@ -2,4 +2,5 @@ export const USER_PREFERENCES = {
|
||||
SIDENAV_PINNED: 'sidenav_pinned',
|
||||
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||
LAST_SEEN_CHANGELOG_VERSION: 'last_seen_changelog_version',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES: 'span_details_pinned_attributes',
|
||||
};
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
&__graph {
|
||||
&__alert-history-graph {
|
||||
margin-top: 80px;
|
||||
|
||||
.graph {
|
||||
.alert-history-graph {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
@@ -135,8 +135,8 @@ function StatsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="stats-card__graph">
|
||||
<div className="graph">
|
||||
<div className="stats-card__alert-history-graph">
|
||||
<div className="alert-history-graph">
|
||||
{!isEmpty && timeSeries.length > 1 && (
|
||||
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
|
||||
)}
|
||||
|
||||
@@ -507,6 +507,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -816,6 +817,7 @@ export const getEndPointsQueryPayload = (
|
||||
legend: 'error percentage',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -965,6 +967,7 @@ export const getTopErrorsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1729,6 +1732,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
legend: 'error percentage',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1928,6 +1932,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2016,6 +2021,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2287,6 +2293,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
legend: 'error percentage',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2376,6 +2383,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2464,6 +2472,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2558,6 +2567,7 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -3135,6 +3145,7 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -149,10 +150,17 @@ function FormAlertRules({
|
||||
]);
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
|
||||
currentQuery.builder.queryTraceOperator,
|
||||
);
|
||||
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||
[EQueryType.QUERY_BUILDER]: () => [
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData)?.filter(
|
||||
(option) =>
|
||||
!involvedQueriesInTraceOperator.includes(option.value as string),
|
||||
) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryTraceOperator) || []),
|
||||
],
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
|
||||
@@ -5,6 +5,7 @@ import getStep from 'lib/getStep';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -53,7 +54,11 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
||||
|
||||
export const getSelectedQueryOptions = (
|
||||
queries: Array<
|
||||
IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery
|
||||
| IBuilderQuery
|
||||
| IBuilderTraceOperator
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery
|
||||
>,
|
||||
): SelectProps['options'] =>
|
||||
queries
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
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, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ErrorResponse,
|
||||
ErrorResponseV2,
|
||||
SuccessResponse,
|
||||
SuccessResponseV2,
|
||||
} 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,
|
||||
@@ -127,7 +135,7 @@ function GeneralSettings({
|
||||
|
||||
useEffect(() => {
|
||||
if (logsCurrentTTLValues) {
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.default_ttl_days * 24);
|
||||
setLogsS3RetentionPeriod(
|
||||
logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
@@ -336,20 +344,40 @@ 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;
|
||||
if (setTTLResponse.statusCode === 409) {
|
||||
|
||||
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) {
|
||||
hasSetTTLFailed = true;
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_request_race_condition'),
|
||||
placement: 'topRight',
|
||||
});
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'metrics') {
|
||||
@@ -376,11 +404,14 @@ function GeneralSettings({
|
||||
logsTtlValuesRefetch();
|
||||
if (!hasSetTTLFailed)
|
||||
// Updates the currentTTL Values in order to avoid pushing the same values.
|
||||
setLogsCurrentTTLValues({
|
||||
setLogsCurrentTTLValues((prev) => ({
|
||||
...prev,
|
||||
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
|
||||
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
|
||||
status: '',
|
||||
});
|
||||
default_ttl_days: logsTotalRetentionPeriod
|
||||
? logsTotalRetentionPeriod / 24 // convert Hours to days
|
||||
: -1,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
@@ -399,6 +430,7 @@ function GeneralSettings({
|
||||
const renderConfig = [
|
||||
{
|
||||
name: 'Metrics',
|
||||
type: 'metrics',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -440,6 +472,7 @@ function GeneralSettings({
|
||||
},
|
||||
{
|
||||
name: 'Traces',
|
||||
type: 'traces',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -479,6 +512,7 @@ function GeneralSettings({
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
type: 'logs',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -537,6 +571,7 @@ function GeneralSettings({
|
||||
/>
|
||||
{category.retentionFields.map((retentionField) => (
|
||||
<Retention
|
||||
type={category.type as TTTLType}
|
||||
key={retentionField.name}
|
||||
text={retentionField.name}
|
||||
retentionValue={retentionField.value}
|
||||
@@ -625,7 +660,7 @@ interface GeneralSettingsProps {
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
|
||||
>['refetch'];
|
||||
logsTtlValuesRefetch: UseQueryResult<
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
|
||||
ErrorResponseV2 | SuccessResponseV2<GetRetentionPeriodLogsPayload>
|
||||
>['refetch'];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@@ -20,11 +21,13 @@ import {
|
||||
convertHoursValueToRelevantUnit,
|
||||
SettingPeriod,
|
||||
TimeUnits,
|
||||
TimeUnitsValues,
|
||||
} from './utils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function Retention({
|
||||
type,
|
||||
retentionValue,
|
||||
setRetentionValue,
|
||||
text,
|
||||
@@ -50,7 +53,9 @@ function Retention({
|
||||
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
|
||||
}, [initialTimeUnitValue]);
|
||||
|
||||
const menuItems = TimeUnits.map((option) => (
|
||||
const menuItems = TimeUnits.filter((option) =>
|
||||
type === 'logs' ? option.value !== TimeUnitsValues.hr : true,
|
||||
).map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.key}
|
||||
</Option>
|
||||
@@ -124,6 +129,7 @@ function Retention({
|
||||
}
|
||||
|
||||
interface RetentionProps {
|
||||
type: TTTLType;
|
||||
retentionValue: number | null;
|
||||
text: string;
|
||||
setRetentionValue: Dispatch<SetStateAction<number | null>>;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 } from 'types/api';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
|
||||
|
||||
@@ -15,6 +17,10 @@ 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();
|
||||
@@ -36,7 +42,7 @@ function GeneralSettings(): JSX.Element {
|
||||
queryKey: ['getRetentionPeriodApiTraces', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
|
||||
queryFn: (): TRetentionAPIReturnV2<'logs'> => getRetentionPeriodApiV2(), // Only works for logs
|
||||
queryKey: ['getRetentionPeriodApiLogs', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
@@ -70,7 +76,7 @@ function GeneralSettings(): JSX.Element {
|
||||
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
|
||||
return (
|
||||
<Typography>
|
||||
{getRetentionPeriodLogsApiResponse.data?.error ||
|
||||
{(getRetentionPeriodLogsApiResponse.error as APIError).getErrorMessage() ||
|
||||
getDisksResponse.data?.error ||
|
||||
t('something_went_wrong')}
|
||||
</Typography>
|
||||
@@ -86,7 +92,7 @@ function GeneralSettings(): JSX.Element {
|
||||
getRetentionPeriodTracesApiResponse.isLoading ||
|
||||
!getRetentionPeriodTracesApiResponse.data?.payload ||
|
||||
getRetentionPeriodLogsApiResponse.isLoading ||
|
||||
!getRetentionPeriodLogsApiResponse.data?.payload
|
||||
!getRetentionPeriodLogsApiResponse.data?.data
|
||||
) {
|
||||
return <Spinner tip="Loading.." height="70vh" />;
|
||||
}
|
||||
@@ -99,7 +105,7 @@ function GeneralSettings(): JSX.Element {
|
||||
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
||||
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
||||
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.data,
|
||||
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,19 +5,26 @@ export interface ITimeUnit {
|
||||
key: string;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
export enum TimeUnitsValues {
|
||||
hr = 'hr',
|
||||
day = 'day',
|
||||
month = 'month',
|
||||
}
|
||||
|
||||
export const TimeUnits: ITimeUnit[] = [
|
||||
{
|
||||
value: 'hr',
|
||||
value: TimeUnitsValues.hr,
|
||||
key: 'Hours',
|
||||
multiplier: 1,
|
||||
},
|
||||
{
|
||||
value: 'day',
|
||||
value: TimeUnitsValues.day,
|
||||
key: 'Days',
|
||||
multiplier: 1 / 24,
|
||||
},
|
||||
{
|
||||
value: 'month',
|
||||
value: TimeUnitsValues.month,
|
||||
key: 'Months',
|
||||
multiplier: 1 / (24 * 30),
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.list-graph-container {
|
||||
.full-view-graph-container {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ function FullView({
|
||||
className={cx('graph-container', {
|
||||
disabled: isDashboardLocked,
|
||||
'height-widget': widget?.mergeAllActiveQueries || widget?.stackedBarChart,
|
||||
'list-graph-container': isListView,
|
||||
'full-view-graph-container': isListView || isTablePanel,
|
||||
})}
|
||||
ref={fullViewRef}
|
||||
>
|
||||
|
||||
@@ -90,6 +90,7 @@ const mockProps: WidgetGraphComponentProps = {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -325,6 +325,7 @@ function WidgetGraphComponent({
|
||||
setHovered(false);
|
||||
}}
|
||||
id={widget.id}
|
||||
className="widget-graph-component-container"
|
||||
>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
@@ -396,7 +397,10 @@ function WidgetGraphComponent({
|
||||
)}
|
||||
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
|
||||
<div
|
||||
className={cx('widget-graph-container', widget.panelTypes)}
|
||||
className={cx(
|
||||
'widget-graph-container',
|
||||
`${widget.panelTypes}-panel-container`,
|
||||
)}
|
||||
ref={graphRef}
|
||||
>
|
||||
<PanelWrapper
|
||||
|
||||
@@ -41,9 +41,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-container {
|
||||
&.graph {
|
||||
height: 100%;
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
&.graph-panel-container {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,11 +91,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 30px);
|
||||
&.graph-panel-container {
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
promql: [],
|
||||
@@ -171,6 +172,7 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -195,6 +197,7 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -240,6 +243,7 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -268,6 +272,7 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
export const tableDataMultipleQueriesSuccessResponse = {
|
||||
columns: [
|
||||
{
|
||||
@@ -161,6 +162,7 @@ export const widgetQueryWithLegend = {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '48ad5a67-9a3c-49d4-a886-d7a34f8b875d',
|
||||
queryType: 'builder',
|
||||
@@ -210,3 +212,279 @@ export const expectedOutputWithLegends = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// QB v5 Aggregations Mock Data
|
||||
export const tableDataQBv5MultiAggregations = {
|
||||
columns: [
|
||||
{
|
||||
name: 'service.name',
|
||||
queryName: 'A',
|
||||
isValueColumn: false,
|
||||
id: 'service.name',
|
||||
},
|
||||
{
|
||||
name: 'host.name',
|
||||
queryName: 'A',
|
||||
isValueColumn: false,
|
||||
id: 'host.name',
|
||||
},
|
||||
{
|
||||
name: 'count()',
|
||||
queryName: 'A',
|
||||
isValueColumn: true,
|
||||
id: 'A.count()',
|
||||
},
|
||||
{
|
||||
name: 'count_distinct(app.ads.count)',
|
||||
queryName: 'A',
|
||||
isValueColumn: true,
|
||||
id: 'A.count_distinct(app.ads.count)',
|
||||
},
|
||||
{
|
||||
name: 'count()',
|
||||
queryName: 'B',
|
||||
isValueColumn: true,
|
||||
id: 'B.count()',
|
||||
},
|
||||
{
|
||||
name: 'count_distinct(app.ads.count)',
|
||||
queryName: 'B',
|
||||
isValueColumn: true,
|
||||
id: 'B.count_distinct(app.ads.count)',
|
||||
},
|
||||
{
|
||||
name: 'count()',
|
||||
queryName: 'C',
|
||||
isValueColumn: true,
|
||||
id: 'C.count()',
|
||||
},
|
||||
{
|
||||
name: 'count_distinct(app.ads.count)',
|
||||
queryName: 'C',
|
||||
isValueColumn: true,
|
||||
id: 'C.count_distinct(app.ads.count)',
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
data: {
|
||||
'service.name': 'frontend-proxy',
|
||||
'host.name': 'test-host.name',
|
||||
'A.count()': 144679,
|
||||
'A.count_distinct(app.ads.count)': 0,
|
||||
'B.count()': 144679,
|
||||
'B.count_distinct(app.ads.count)': 0,
|
||||
'C.count()': 144679,
|
||||
'C.count_distinct(app.ads.count)': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
'service.name': 'frontend',
|
||||
'host.name': 'test-host.name',
|
||||
'A.count()': 142311,
|
||||
'A.count_distinct(app.ads.count)': 0,
|
||||
'B.count()': 142311,
|
||||
'B.count_distinct(app.ads.count)': 0,
|
||||
'C.count()': 142311,
|
||||
'C.count_distinct(app.ads.count)': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const widgetQueryQBv5MultiAggregations = {
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: 'p99',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
name: 'B',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
name: 'C',
|
||||
legend: 'max',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: 'p99',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: 'B',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: 'C',
|
||||
query: '',
|
||||
legend: 'max',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
key: 'signoz_latency',
|
||||
type: 'ExponentialHistogram',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
key: 'service.name',
|
||||
type: 'tag',
|
||||
id: 'service.name--string--tag--false',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
key: 'host.name',
|
||||
type: 'tag',
|
||||
id: 'host.name--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: 'p99',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'B',
|
||||
aggregateOperator: 'rate',
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'system_disk_operations--float64--Sum--true',
|
||||
key: 'system_disk_operations',
|
||||
type: 'Sum',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'B',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
key: 'service.name',
|
||||
type: 'tag',
|
||||
id: 'service.name--string--tag--false',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
key: 'host.name',
|
||||
type: 'tag',
|
||||
id: 'host.name--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'C',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
dataType: 'float64',
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
key: 'signoz_latency',
|
||||
type: 'ExponentialHistogram',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'C',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: 'string',
|
||||
key: 'service.name',
|
||||
type: 'tag',
|
||||
id: 'service.name--string--tag--false',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
key: 'host.name',
|
||||
type: 'tag',
|
||||
id: 'host.name--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: 'max',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: 'qb-v5-multi-aggregations-test',
|
||||
queryType: 'builder',
|
||||
};
|
||||
|
||||
export const expectedOutputQBv5MultiAggregations = {
|
||||
dataSource: [
|
||||
{
|
||||
'service.name': 'frontend-proxy',
|
||||
'host.name': 'test-host.name',
|
||||
'A.count()': 144679,
|
||||
'A.count_distinct(app.ads.count)': 0,
|
||||
'B.count()': 144679,
|
||||
'B.count_distinct(app.ads.count)': 0,
|
||||
'C.count()': 144679,
|
||||
'C.count_distinct(app.ads.count)': 0,
|
||||
},
|
||||
{
|
||||
'service.name': 'frontend',
|
||||
'host.name': 'test-host.name',
|
||||
'A.count()': 142311,
|
||||
'A.count_distinct(app.ads.count)': 0,
|
||||
'B.count()': 142311,
|
||||
'B.count_distinct(app.ads.count)': 0,
|
||||
'C.count()': 142311,
|
||||
'C.count_distinct(app.ads.count)': 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,8 +6,11 @@ import {
|
||||
sortFunction,
|
||||
} from '../utils';
|
||||
import {
|
||||
expectedOutputQBv5MultiAggregations,
|
||||
expectedOutputWithLegends,
|
||||
tableDataMultipleQueriesSuccessResponse,
|
||||
tableDataQBv5MultiAggregations,
|
||||
widgetQueryQBv5MultiAggregations,
|
||||
widgetQueryWithLegend,
|
||||
} from './response';
|
||||
|
||||
@@ -67,6 +70,7 @@ describe('Table Panel utils', () => {
|
||||
isValueColumn: true,
|
||||
name: 'A',
|
||||
queryName: 'A',
|
||||
id: 'A',
|
||||
};
|
||||
// A has value and value is considered bigger than n/a hence 1
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(1);
|
||||
@@ -128,3 +132,96 @@ describe('Table Panel utils', () => {
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table Panel utils with QB v5 aggregations', () => {
|
||||
it('createColumnsAndDataSource function - QB v5 multi-aggregations', () => {
|
||||
const data = tableDataQBv5MultiAggregations;
|
||||
const query = widgetQueryQBv5MultiAggregations as Query;
|
||||
|
||||
const { columns, dataSource } = createColumnsAndDataSource(data, query);
|
||||
|
||||
// Verify column structure for multi-aggregations
|
||||
expect(columns).toHaveLength(8);
|
||||
expect(columns[0].title).toBe('service.name');
|
||||
expect(columns[1].title).toBe('host.name');
|
||||
// All columns with queryName 'A' get the legend 'p99'
|
||||
expect(columns[2].title).toBe('p99'); // A.count() uses legend from query A
|
||||
expect(columns[3].title).toBe('p99'); // A.count_distinct() uses legend from query A
|
||||
expect(columns[4].title).toBe('count()'); // B.count() uses column name (no legend)
|
||||
expect(columns[5].title).toBe('count_distinct(app.ads.count)'); // B.count_distinct() uses column name
|
||||
expect(columns[6].title).toBe('max'); // C.count() uses legend from query C
|
||||
expect(columns[7].title).toBe('max'); // C.count_distinct() uses legend from query C
|
||||
|
||||
// Verify dataIndex mapping
|
||||
expect((columns[0] as any).dataIndex).toBe('service.name');
|
||||
expect((columns[2] as any).dataIndex).toBe('A.count()');
|
||||
expect((columns[3] as any).dataIndex).toBe('A.count_distinct(app.ads.count)');
|
||||
|
||||
// Verify dataSource structure
|
||||
expect(dataSource).toStrictEqual(
|
||||
expectedOutputQBv5MultiAggregations.dataSource,
|
||||
);
|
||||
});
|
||||
|
||||
it('getQueryLegend function - QB v5 multi-query support', () => {
|
||||
const query = widgetQueryQBv5MultiAggregations as Query;
|
||||
|
||||
expect(getQueryLegend(query, 'A')).toBe('p99');
|
||||
expect(getQueryLegend(query, 'B')).toBeUndefined();
|
||||
expect(getQueryLegend(query, 'C')).toBe('max');
|
||||
expect(getQueryLegend(query, 'D')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sorter function - QB v5 multi-aggregation columns', () => {
|
||||
const item = {
|
||||
isValueColumn: true,
|
||||
name: 'count()',
|
||||
queryName: 'A',
|
||||
id: 'A.count()',
|
||||
};
|
||||
|
||||
// Test numeric sorting
|
||||
expect(
|
||||
sortFunction(
|
||||
{ 'A.count()': 100, key: '1', timestamp: 1000 },
|
||||
{ 'A.count()': 200, key: '2', timestamp: 1000 },
|
||||
item,
|
||||
),
|
||||
).toBe(-100);
|
||||
|
||||
// Test n/a handling
|
||||
expect(
|
||||
sortFunction(
|
||||
{ 'A.count()': 'n/a', key: '1', timestamp: 1000 },
|
||||
{ 'A.count()': 100, key: '2', timestamp: 1000 },
|
||||
item,
|
||||
),
|
||||
).toBe(-1);
|
||||
|
||||
expect(
|
||||
sortFunction(
|
||||
{ 'A.count()': 100, key: '1', timestamp: 1000 },
|
||||
{ 'A.count()': 'n/a', key: '2', timestamp: 1000 },
|
||||
item,
|
||||
),
|
||||
).toBe(1);
|
||||
|
||||
// Test string sorting
|
||||
expect(
|
||||
sortFunction(
|
||||
{ 'A.count()': 'read', key: '1', timestamp: 1000 },
|
||||
{ 'A.count()': 'write', key: '2', timestamp: 1000 },
|
||||
item,
|
||||
),
|
||||
).toBe(-1);
|
||||
|
||||
// Test equal values
|
||||
expect(
|
||||
sortFunction(
|
||||
{ 'A.count()': 'n/a', key: '1', timestamp: 1000 },
|
||||
{ 'A.count()': 'n/a', key: '2', timestamp: 1000 },
|
||||
item,
|
||||
),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,11 +150,14 @@ export function sortFunction(
|
||||
name: string;
|
||||
queryName: string;
|
||||
isValueColumn: boolean;
|
||||
id: string;
|
||||
},
|
||||
): number {
|
||||
const colId = item.id;
|
||||
const colName = item.name;
|
||||
// assumption :- number values is bigger than 'n/a'
|
||||
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
|
||||
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
|
||||
const valueA = Number(a[`${colId}_without_unit`] ?? a[colId] ?? a[colName]);
|
||||
const valueB = Number(b[`${colId}_without_unit`] ?? b[colId] ?? b[colName]);
|
||||
|
||||
// if both the values are numbers then return the difference here
|
||||
if (!isNaN(valueA) && !isNaN(valueB)) {
|
||||
@@ -172,10 +175,11 @@ export function sortFunction(
|
||||
}
|
||||
|
||||
// if both of them are strings do the localecompare
|
||||
return ((a[item.name] as string) || '').localeCompare(
|
||||
(b[item.name] as string) || '',
|
||||
return ((a[colId] as string) || (a[colName] as string) || '').localeCompare(
|
||||
(b[colId] as string) || (b[colName] as string) || '',
|
||||
);
|
||||
}
|
||||
|
||||
export function createColumnsAndDataSource(
|
||||
data: TableData,
|
||||
currentQuery: Query,
|
||||
@@ -198,7 +202,7 @@ export function createColumnsAndDataSource(
|
||||
// if no legend present then rely on the column name value
|
||||
title: !isNewAggregation && !isEmpty(legend) ? legend : item.name,
|
||||
width: QUERY_TABLE_CONFIG.width,
|
||||
render: renderColumnCell && renderColumnCell[item.name],
|
||||
render: renderColumnCell && renderColumnCell[item.id],
|
||||
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
||||
};
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -490,6 +491,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -575,6 +577,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -660,6 +663,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -797,6 +801,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1050,6 +1055,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1257,6 +1263,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1522,6 +1529,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -233,6 +233,7 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -416,6 +417,7 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -512,6 +514,7 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -608,6 +611,7 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -196,6 +196,7 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -346,6 +347,7 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -431,6 +433,7 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -516,6 +519,7 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -79,6 +79,7 @@ export const getEntityEventsOrLogsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
@@ -226,6 +227,7 @@ export const getEntityTracesQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
|
||||
@@ -108,6 +108,7 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -191,6 +192,7 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -287,6 +289,7 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -383,6 +386,7 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -309,6 +309,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -576,6 +577,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -655,6 +657,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -734,6 +737,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -819,6 +823,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -904,6 +909,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1075,6 +1081,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1212,6 +1219,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1429,6 +1437,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1561,6 +1570,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -341,6 +341,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -647,6 +648,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -810,6 +812,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -973,6 +976,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1052,6 +1056,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1131,6 +1136,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1216,6 +1222,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1301,6 +1308,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1451,6 +1459,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1569,6 +1578,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -335,6 +335,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -668,6 +669,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -851,6 +853,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1184,6 +1187,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1324,6 +1328,7 @@ export const getPodMetricsQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1407,6 +1412,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1497,6 +1503,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1714,6 +1721,7 @@ export const getPodMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1918,6 +1926,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2135,6 +2144,7 @@ export const getPodMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2231,6 +2241,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2327,6 +2338,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2510,6 +2522,7 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -246,6 +246,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -365,6 +366,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -534,6 +536,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -653,6 +656,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -735,6 +739,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -817,6 +822,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
|
||||
@@ -148,6 +148,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -239,6 +240,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -330,6 +332,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -421,6 +424,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -512,6 +516,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
|
||||
@@ -774,6 +774,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
),
|
||||
children: (
|
||||
<div className="ingestion-key-info-container">
|
||||
<Row>
|
||||
<Col span={6}> ID </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{APIKey.id}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={6}> Created on </Col>
|
||||
<Col span={12}>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useCallback } from 'react';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { SpinnerWrapper, Wrapper } from './styles';
|
||||
import { SpinnerWrapper } from './styles';
|
||||
|
||||
function ListViewPanel(): JSX.Element {
|
||||
const { config } = useOptionsMenu({
|
||||
@@ -42,7 +42,7 @@ function ListViewPanel(): JSX.Element {
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="live-logs-settings-panel">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
@@ -68,7 +68,7 @@ function ListViewPanel(): JSX.Element {
|
||||
<Spinner style={{ height: 'auto' }} />
|
||||
</SpinnerWrapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
.live-logs-chart-container {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.live-logs-settings-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--bg-ink-300);
|
||||
|
||||
.live-logs-frequency-chart-view-controller {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.live-logs-settings-panel {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,89 @@
|
||||
import { Col } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import './LiveLogsContainer.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import FiltersInput from 'container/LiveLogs/FiltersInput';
|
||||
import LiveLogsTopNav from 'container/LiveLogsTopNav';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { idObject } from '../constants';
|
||||
import ListViewPanel from '../ListViewPanel';
|
||||
import LiveLogsList from '../LiveLogsList';
|
||||
import { ILiveLogsLog } from '../LiveLogsList/types';
|
||||
import LiveLogsListChart from '../LiveLogsListChart';
|
||||
import { QueryHistoryState } from '../types';
|
||||
import { prepareQueryByFilter } from '../utils';
|
||||
import { ContentWrapper, LiveLogsChart, Wrapper } from './styles';
|
||||
|
||||
function LiveLogsContainer(): JSX.Element {
|
||||
const location = useLocation();
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [logs, setLogs] = useState<ILiveLogsLog[]>([]);
|
||||
const { currentQuery, stagedQuery } = useQueryBuilder();
|
||||
const [showLiveLogsFrequencyChart, setShowLiveLogsFrequencyChart] = useState(
|
||||
true,
|
||||
);
|
||||
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const queryLocationState = location.state as QueryHistoryState;
|
||||
|
||||
const batchedEventsRef = useRef<ILog[]>([]);
|
||||
const batchedEventsRef = useRef<ILiveLogsLog[]>([]);
|
||||
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const prevFilterExpressionRef = useRef<string | null>(null);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
data: {
|
||||
title: 'max lines per row',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Column',
|
||||
data: {
|
||||
title: 'columns',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleShowFormatOptions = (): void =>
|
||||
setShowFormatMenuItems(!showFormatMenuItems);
|
||||
|
||||
useClickOutside({
|
||||
ref: menuRef,
|
||||
onClickOutside: () => {
|
||||
if (showFormatMenuItems) {
|
||||
setShowFormatMenuItems(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
handleStartOpenConnection,
|
||||
@@ -53,7 +96,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const updateLogs = useCallback((newLogs: ILog[]) => {
|
||||
const updateLogs = useCallback((newLogs: ILiveLogsLog[]) => {
|
||||
setLogs((prevState) =>
|
||||
[...newLogs, ...prevState].slice(0, MAX_LOGS_LIST_SIZE),
|
||||
);
|
||||
@@ -67,7 +110,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
}, 500);
|
||||
|
||||
const batchLiveLog = useCallback(
|
||||
(log: ILog): void => {
|
||||
(log: ILiveLogsLog): void => {
|
||||
batchedEventsRef.current.push(log);
|
||||
|
||||
debouncedUpdateLogs();
|
||||
@@ -77,7 +120,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
|
||||
const handleGetLiveLogs = useCallback(
|
||||
(event: MessageEvent<string>) => {
|
||||
const data: ILog = JSON.parse(event.data);
|
||||
const data: ILiveLogsLog = JSON.parse(event?.data);
|
||||
|
||||
batchLiveLog(data);
|
||||
},
|
||||
@@ -91,72 +134,65 @@ function LiveLogsContainer(): JSX.Element {
|
||||
useEventSourceEvent('message', handleGetLiveLogs);
|
||||
useEventSourceEvent('error', handleError);
|
||||
|
||||
const getPreparedQuery = useCallback(
|
||||
(query: Query): Query => {
|
||||
const firstLogId: string | null = logs.length ? logs[0].id : null;
|
||||
|
||||
const preparedQuery: Query = prepareQueryByFilter(
|
||||
query,
|
||||
idObject,
|
||||
firstLogId,
|
||||
);
|
||||
|
||||
return preparedQuery;
|
||||
},
|
||||
[logs],
|
||||
);
|
||||
|
||||
const openConnection = useCallback(
|
||||
(query: Query) => {
|
||||
const { queryPayload } = prepareQueryRangePayload({
|
||||
query,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
});
|
||||
|
||||
const encodedQueryPayload = encodeURIComponent(JSON.stringify(queryPayload));
|
||||
const queryString = `q=${encodedQueryPayload}`;
|
||||
|
||||
handleStartOpenConnection({ queryString });
|
||||
(filterExpression?: string | null) => {
|
||||
handleStartOpenConnection(filterExpression || '');
|
||||
},
|
||||
[globalSelectedTime, handleStartOpenConnection],
|
||||
[handleStartOpenConnection],
|
||||
);
|
||||
|
||||
const handleStartNewConnection = useCallback(
|
||||
(query: Query) => {
|
||||
(filterExpression?: string | null) => {
|
||||
handleCloseConnection();
|
||||
|
||||
const preparedQuery = getPreparedQuery(query);
|
||||
|
||||
openConnection(preparedQuery);
|
||||
openConnection(filterExpression);
|
||||
},
|
||||
[getPreparedQuery, handleCloseConnection, openConnection],
|
||||
[handleCloseConnection, openConnection],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compositeQuery) return;
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
// Check if filterExpression has actually changed
|
||||
if (
|
||||
(initialLoading && !isConnectionLoading) ||
|
||||
compositeQuery.id !== stagedQuery?.id
|
||||
!prevFilterExpressionRef.current ||
|
||||
prevFilterExpressionRef.current !== currentFilterExpression
|
||||
) {
|
||||
handleStartNewConnection(compositeQuery);
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
setLogs([]);
|
||||
batchedEventsRef.current = [];
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
}
|
||||
|
||||
prevFilterExpressionRef.current = currentFilterExpression || null;
|
||||
}
|
||||
}, [
|
||||
compositeQuery,
|
||||
initialLoading,
|
||||
stagedQuery,
|
||||
isConnectionLoading,
|
||||
openConnection,
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
}, [currentQuery, handleStartNewConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoading && !isConnectionLoading) {
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
prevFilterExpressionRef.current = currentFilterExpression || null;
|
||||
} else {
|
||||
handleStartNewConnection(null);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialLoading, isConnectionLoading, handleStartNewConnection]);
|
||||
|
||||
useEffect((): (() => void) | undefined => {
|
||||
if (isConnectionError && reconnectDueToError && compositeQuery) {
|
||||
if (isConnectionError && reconnectDueToError) {
|
||||
// Small delay to prevent immediate reconnection attempts
|
||||
const reconnectTimer = setTimeout(() => {
|
||||
handleStartNewConnection(compositeQuery);
|
||||
handleStartNewConnection();
|
||||
}, 1000);
|
||||
|
||||
return (): void => clearTimeout(reconnectTimer);
|
||||
@@ -169,50 +205,70 @@ function LiveLogsContainer(): JSX.Element {
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const prefetchedList = queryLocationState?.listQueryPayload[0]?.list;
|
||||
// clean up the connection when the component unmounts
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
handleCloseConnection();
|
||||
},
|
||||
[handleCloseConnection],
|
||||
);
|
||||
|
||||
if (prefetchedList) {
|
||||
const prefetchedLogs: ILog[] = prefetchedList
|
||||
.map((item) => ({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
updateLogs(prefetchedLogs);
|
||||
}
|
||||
}, [queryLocationState, updateLogs]);
|
||||
const handleToggleFrequencyChart = useCallback(() => {
|
||||
setShowLiveLogsFrequencyChart(!showLiveLogsFrequencyChart);
|
||||
}, [showLiveLogsFrequencyChart]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LiveLogsTopNav />
|
||||
<ContentWrapper gutter={[0, 20]} style={{ color: themeColors.lightWhite }}>
|
||||
<Col span={24}>
|
||||
<FiltersInput />
|
||||
</Col>
|
||||
{initialLoading && logs.length === 0 ? (
|
||||
<Col span={24}>
|
||||
<Spinner style={{ height: 'auto' }} tip="Fetching Logs" />
|
||||
</Col>
|
||||
) : (
|
||||
<>
|
||||
<Col span={24}>
|
||||
<LiveLogsChart
|
||||
initialData={queryLocationState?.graphQueryPayload || null}
|
||||
<div className="live-logs-container">
|
||||
<div className="live-logs-content">
|
||||
<div className="live-logs-settings-panel">
|
||||
<div className="live-logs-frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showLiveLogsFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ListViewPanel />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<LiveLogsList logs={logs} />
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showLiveLogsFrequencyChart && (
|
||||
<div className="live-logs-chart-container">
|
||||
<LiveLogsListChart
|
||||
initialData={queryLocationState?.graphQueryPayload || null}
|
||||
className="live-logs-chart"
|
||||
isShowingLiveLogs
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<GoToTop />
|
||||
</ContentWrapper>
|
||||
</Wrapper>
|
||||
|
||||
<div className="live-logs-list-container">
|
||||
<LiveLogsList
|
||||
logs={logs}
|
||||
isLoading={initialLoading && logs.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GoToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
.live-logs-container {
|
||||
.live-logs-content {
|
||||
.live-logs-chart-container {
|
||||
padding: 0px 8px;
|
||||
|
||||
.logs-frequency-chart {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.live-logs-list {
|
||||
.live-logs-list-loading {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--text-vanilla-100);
|
||||
}
|
||||
|
||||
.live-logs-list-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.loading-live-logs-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-gif {
|
||||
height: 72px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.live-logs-list-loading {
|
||||
.loading-live-logs-content {
|
||||
.ant-typography {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
import './LiveLogsList.styles.scss';
|
||||
|
||||
import { Card, Typography } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
|
||||
import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
|
||||
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { Heading } from 'container/LogsTable/styles';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -26,11 +25,9 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { LiveLogsListProps } from './types';
|
||||
|
||||
function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const { t } = useTranslation(['logs']);
|
||||
|
||||
const { isConnectionLoading } = useEventSource();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
@@ -43,6 +40,12 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
onSetActiveLog,
|
||||
} = useActiveLog();
|
||||
|
||||
// get only data from the logs object
|
||||
const formattedLogs: ILog[] = useMemo(
|
||||
() => logs.map((log) => log?.data).flat(),
|
||||
[logs],
|
||||
);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
@@ -50,8 +53,8 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
});
|
||||
|
||||
const activeLogIndex = useMemo(
|
||||
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||
[logs, activeLogId],
|
||||
() => formattedLogs.findIndex(({ id }) => id === activeLogId),
|
||||
[formattedLogs, activeLogId],
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields([
|
||||
@@ -105,30 +108,39 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
});
|
||||
}, [activeLogId, activeLogIndex]);
|
||||
|
||||
const isLoadingList = isConnectionLoading && logs.length === 0;
|
||||
const isLoadingList = isConnectionLoading && formattedLogs.length === 0;
|
||||
|
||||
if (isLoadingList) {
|
||||
return <Spinner style={{ height: 'auto' }} tip="Fetching Logs" />;
|
||||
}
|
||||
const renderLoading = useCallback(
|
||||
() => (
|
||||
<div className="live-logs-list-loading">
|
||||
<div className="loading-live-logs-content">
|
||||
<img
|
||||
className="loading-gif"
|
||||
src="/Icons/loading-plane.gif"
|
||||
alt="wait-icon"
|
||||
/>
|
||||
|
||||
<Typography>Fetching live logs...</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.format !== OptionFormatTypes.TABLE && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
<div className="live-logs-list">
|
||||
{(formattedLogs.length === 0 || isLoading || isLoadingList) &&
|
||||
renderLoading()}
|
||||
|
||||
{logs.length === 0 && <Typography>{t('fetching_log_lines')}</Typography>}
|
||||
|
||||
{logs.length !== 0 && (
|
||||
{formattedLogs.length !== 0 && (
|
||||
<InfinityWrapperStyled>
|
||||
{options.format === OptionFormatTypes.TABLE ? (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={false}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
logs: formattedLogs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
@@ -142,8 +154,8 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
data={formattedLogs}
|
||||
totalCount={formattedLogs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
@@ -151,15 +163,18 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
)}
|
||||
</InfinityWrapperStyled>
|
||||
)}
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</>
|
||||
|
||||
{activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface ILiveLogsLog {
|
||||
data: ILog[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type LiveLogsListProps = {
|
||||
logs: ILog[];
|
||||
logs: ILiveLogsLog[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
@@ -9,24 +9,33 @@ import { useMemo } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { LiveLogsListChartProps } from './types';
|
||||
|
||||
function LiveLogsListChart({
|
||||
className,
|
||||
initialData,
|
||||
isShowingLiveLogs = false,
|
||||
}: LiveLogsListChartProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { isConnectionOpen } = useEventSource();
|
||||
|
||||
const listChartQuery: Query | null = useMemo(() => {
|
||||
if (!stagedQuery) return null;
|
||||
if (!currentQuery) return null;
|
||||
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (!validationResult.isValid) return null;
|
||||
|
||||
return {
|
||||
...stagedQuery,
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...stagedQuery.builder,
|
||||
queryData: stagedQuery.builder.queryData.map((item) => ({
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
disabled: false,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
@@ -39,7 +48,7 @@ function LiveLogsListChart({
|
||||
})),
|
||||
},
|
||||
};
|
||||
}, [stagedQuery]);
|
||||
}, [currentQuery]);
|
||||
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
@@ -62,12 +71,15 @@ function LiveLogsListChart({
|
||||
}, [data, initialData]);
|
||||
|
||||
return (
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
/>
|
||||
<div className="live-logs-chart-container">
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
isLogsExplorerViews={isShowingLiveLogs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ import { QueryData } from 'types/api/widgets/getQuery';
|
||||
export type LiveLogsListChartProps = {
|
||||
className?: string;
|
||||
initialData: QueryData[] | null;
|
||||
isShowingLiveLogs: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
function LiveLogsPauseResume(): JSX.Element {
|
||||
const {
|
||||
isConnectionOpen,
|
||||
isConnectionLoading,
|
||||
initialLoading,
|
||||
handleCloseConnection,
|
||||
handleStartOpenConnection,
|
||||
handleSetInitialLoading,
|
||||
} = useEventSource();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const isPlaying = isConnectionOpen || isConnectionLoading || initialLoading;
|
||||
|
||||
const openConnection = useCallback(
|
||||
(filterExpression?: string | null) => {
|
||||
handleStartOpenConnection(filterExpression || '');
|
||||
},
|
||||
[handleStartOpenConnection],
|
||||
);
|
||||
|
||||
const handleStartNewConnection = useCallback(
|
||||
(filterExpression?: string | null) => {
|
||||
handleCloseConnection();
|
||||
|
||||
openConnection(filterExpression);
|
||||
},
|
||||
[handleCloseConnection, openConnection],
|
||||
);
|
||||
|
||||
const onLiveButtonClick = useCallback(() => {
|
||||
if (initialLoading) {
|
||||
handleSetInitialLoading(false);
|
||||
}
|
||||
|
||||
if ((!isConnectionOpen && isConnectionLoading) || isConnectionOpen) {
|
||||
handleCloseConnection();
|
||||
} else {
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
} else {
|
||||
handleStartNewConnection(null);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialLoading,
|
||||
isConnectionOpen,
|
||||
isConnectionLoading,
|
||||
currentQuery,
|
||||
handleSetInitialLoading,
|
||||
handleCloseConnection,
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
// clean up the connection when the component unmounts
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
handleCloseConnection();
|
||||
},
|
||||
[handleCloseConnection],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="live-logs-pause-resume">
|
||||
<Button
|
||||
icon={isPlaying ? <PauseCircleFilled /> : <PlayCircleFilled />}
|
||||
danger={isPlaying}
|
||||
onClick={onLiveButtonClick}
|
||||
type="primary"
|
||||
className={`periscope-btn ${isPlaying ? 'warning' : 'success'}`}
|
||||
>
|
||||
{isPlaying ? 'Pause' : 'Resume'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveLogsPauseResume;
|
||||
@@ -58,6 +58,7 @@ export const mockQuery: Query = {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
id: 'test-query-id',
|
||||
|
||||
@@ -121,6 +121,7 @@ export const getPodQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '9b92756a-b445-45f8-90f4-d26f3ef28f8f',
|
||||
@@ -197,6 +198,7 @@ export const getPodQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'a22c1e03-4876-4b3e-9a96-a3c3a28f9c0f',
|
||||
@@ -337,6 +339,7 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '7bb3a6f5-d1c6-4f2e-9cc9-7dcc46db398f',
|
||||
@@ -477,6 +480,7 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '6d5ccd81-0ea1-4fb9-a66b-7f0fe2f15165',
|
||||
@@ -624,6 +628,7 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '4d03a0ff-4fa5-4b19-b397-97f80ba9e0ac',
|
||||
@@ -772,6 +777,7 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'ad491f19-0f83-4dd4-bb8f-bec295c18d1b',
|
||||
@@ -920,6 +926,7 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '16908d4e-1565-4847-8d87-01ebb8fc494a',
|
||||
@@ -1001,6 +1008,7 @@ export const getPodQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '4b255d6d-4cde-474d-8866-f4418583c18b',
|
||||
@@ -1177,6 +1185,7 @@ export const getNodeQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '259295b5-774d-4b2e-8a4f-e5dd63e6c38d',
|
||||
@@ -1314,6 +1323,7 @@ export const getNodeQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '486af4da-2a1a-4b8f-992c-eba098d3a6f9',
|
||||
@@ -1409,6 +1419,7 @@ export const getNodeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'b56143c0-7d2f-4425-97c5-65ad6fc87366',
|
||||
@@ -1557,6 +1568,7 @@ export const getNodeQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '57eeac15-615c-4a71-9c61-8e0c0c76b045',
|
||||
@@ -1718,6 +1730,7 @@ export const getHostQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
|
||||
@@ -1786,6 +1799,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '40218bfb-a9b7-4974-aead-5bf666e139bf',
|
||||
@@ -1928,6 +1942,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '8e6485ea-7018-43b0-ab27-b210f77b59ad',
|
||||
@@ -2009,6 +2024,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '47173220-44df-4ef6-87f4-31e333c180c7',
|
||||
@@ -2084,6 +2100,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '62eedbc6-c8ad-4d13-80a8-129396e1d1dc',
|
||||
@@ -2159,6 +2176,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '5ddb1b38-53bb-46f5-b4fe-fe832d6b9b24',
|
||||
@@ -2234,6 +2252,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'a849bcce-7684-4852-9134-530b45419b8f',
|
||||
@@ -2309,6 +2328,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'ab685a3d-fa4c-4663-8d94-c452e59038f3',
|
||||
@@ -2369,6 +2389,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '9bd40b51-0790-4cdd-9718-551b2ded5926',
|
||||
@@ -2450,6 +2471,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '9c6d18ad-89ff-4e38-a15a-440e72ed6ca8',
|
||||
@@ -2524,6 +2546,7 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'f4cfc2a5-78fc-42cc-8f4a-194c8c916132',
|
||||
|
||||
@@ -6,4 +6,5 @@ export type LogsExplorerChartProps = {
|
||||
isLogsExplorerViews?: boolean;
|
||||
isLabelEnabled?: boolean;
|
||||
className?: string;
|
||||
isShowingLiveLogs?: boolean;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ function LogsExplorerChart({
|
||||
isLabelEnabled = true,
|
||||
className,
|
||||
isLogsExplorerViews = false,
|
||||
isShowingLiveLogs = false,
|
||||
}: LogsExplorerChartProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
@@ -55,6 +56,11 @@ function LogsExplorerChart({
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
// Do not allow dragging on live logs chart
|
||||
if (isShowingLiveLogs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
@@ -75,7 +81,7 @@ function LogsExplorerChart({
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
},
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery],
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery, isShowingLiveLogs],
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ColumnDef, DataTable, Row } from '@signozhq/table';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
@@ -5,15 +6,18 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorTypeForTable } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface ColumnViewProps {
|
||||
@@ -47,6 +51,8 @@ function ColumnView({
|
||||
onGroupByAttribute: handleGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const [showActiveLog, setShowActiveLog] = useState<boolean>(false);
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
@@ -60,15 +66,18 @@ function ColumnView({
|
||||
| undefined
|
||||
>();
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useEffect(() => {
|
||||
if (activeLogId) {
|
||||
const log = logs.find(({ id }) => id === activeLogId);
|
||||
|
||||
if (log) {
|
||||
handleSetActiveLog(log);
|
||||
setShowActiveLog(true);
|
||||
}
|
||||
}
|
||||
}, [activeLogId, logs, handleSetActiveLog]);
|
||||
}, []);
|
||||
|
||||
const tableViewProps = {
|
||||
logs,
|
||||
@@ -82,7 +91,6 @@ function ColumnView({
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: handleSetActiveLog,
|
||||
onOpenLogsContext: handleClearActiveLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onColumnOrderChange } = useDragColumns<
|
||||
@@ -134,7 +142,7 @@ function ColumnView({
|
||||
enableInfiniteScroll: true,
|
||||
enableScrollRestoration: false,
|
||||
fixedHeight: isFrequencyChartVisible ? 560 : 760,
|
||||
enableDynamicRowHeight: false,
|
||||
enableDynamicRowHeight: true,
|
||||
};
|
||||
|
||||
const selectedColumns = useMemo(
|
||||
@@ -148,8 +156,10 @@ function ColumnView({
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
size: field.key === 'state-indicator' ? 4 : 180,
|
||||
minSize: field.key === 'state-indicator' ? 4 : 120,
|
||||
maxSize: field.key === 'state-indicator' ? 4 : 1080,
|
||||
pin: field.key === 'state-indicator' ? 'left' : 'none',
|
||||
maxSize: field.key === 'state-indicator' ? 4 : Number.MAX_SAFE_INTEGER,
|
||||
disableReorder: field.key === 'state-indicator',
|
||||
disableDropBefore: field.key === 'state-indicator',
|
||||
disableResizing: field.key === 'state-indicator',
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
cell: ({
|
||||
row,
|
||||
@@ -165,13 +175,28 @@ function ColumnView({
|
||||
return <LogStateIndicator type={type} fontSize={fontSize} />;
|
||||
}
|
||||
|
||||
const isTimestamp = field.key === 'timestamp';
|
||||
const cellContent = getValue();
|
||||
|
||||
if (isTimestamp) {
|
||||
const formattedTimestamp = dayjs(cellContent as string).tz(
|
||||
timezone.value,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="table-cell-content">
|
||||
{formattedTimestamp.format(DATE_TIME_FORMATS.ISO_DATETIME_MS)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`table-cell-content ${
|
||||
row.original.id === activeLog?.id ? 'active-log' : ''
|
||||
}`}
|
||||
>
|
||||
{getValue()}
|
||||
{cellContent}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -199,9 +224,22 @@ function ColumnView({
|
||||
const handleRowClick = (row: Row<Record<string, unknown>>): void => {
|
||||
const currentLog = logs.find(({ id }) => id === row.original.id);
|
||||
|
||||
setShowActiveLog(true);
|
||||
handleSetActiveLog(currentLog as ILog);
|
||||
};
|
||||
|
||||
const removeQueryParam = (key: string): void => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete(key);
|
||||
window.history.replaceState({}, '', url);
|
||||
};
|
||||
|
||||
const handleLogDetailClose = (): void => {
|
||||
removeQueryParam(QueryParams.activeLogId);
|
||||
handleClearActiveLog();
|
||||
setShowActiveLog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`logs-list-table-view-container ${
|
||||
@@ -223,11 +261,11 @@ function ColumnView({
|
||||
scrollToIndexRef={scrollToIndexRef}
|
||||
/>
|
||||
|
||||
{activeLog && (
|
||||
{showActiveLog && activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={handleClearActiveLog}
|
||||
onClose={handleLogDetailClose}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleAddToQuery}
|
||||
onGroupByAttribute={handleGroupByAttribute}
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
color: white !important;
|
||||
|
||||
.cursor-col-resize {
|
||||
width: 2px !important;
|
||||
width: 3px !important;
|
||||
cursor: col-resize !important;
|
||||
opacity: 0.5 !important;
|
||||
background-color: var(--bg-ink-500) !important;
|
||||
border: 1px solid var(--bg-ink-500) !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
|
||||
@@ -141,6 +141,7 @@ describe('LogsExplorerList - empty states', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
@@ -205,6 +206,7 @@ describe('LogsExplorerList - empty states', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import NoLogs from '../NoLogs/NoLogs';
|
||||
import ColumnView from './ColumnView/ColumnView';
|
||||
import InfinityTableView from './InfinityTableView';
|
||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||
import { InfinityWrapperStyled } from './styles';
|
||||
import {
|
||||
@@ -48,7 +48,6 @@ function LogsExplorerList({
|
||||
isError,
|
||||
error,
|
||||
isFilterApplied,
|
||||
isFrequencyChartVisible,
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
@@ -130,6 +129,75 @@ function LogsExplorerList({
|
||||
],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
Footer,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={isLoading}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
appendTo: 'end',
|
||||
activeLogIndex,
|
||||
}}
|
||||
infitiyTableProps={{ onEndReached }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
options.format,
|
||||
options.maxLines,
|
||||
options.fontSize,
|
||||
activeLogIndex,
|
||||
logs,
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
]);
|
||||
|
||||
const isTraceToLogsNavigation = useMemo(() => {
|
||||
if (!currentStagedQueryData) return false;
|
||||
return isTraceToLogsQuery(currentStagedQueryData);
|
||||
@@ -169,83 +237,6 @@ function LogsExplorerList({
|
||||
return getEmptyLogsListConfig(handleClearFilters);
|
||||
}, [isTraceToLogsNavigation, handleClearFilters]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (isLoading || isFetching) return;
|
||||
|
||||
onEndReached(logs.length);
|
||||
}, [isLoading, isFetching, onEndReached, logs.length]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
Footer,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<ColumnView
|
||||
logs={logs}
|
||||
onLoadMore={handleLoadMore}
|
||||
selectedFields={selectedFields}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
options={{
|
||||
maxLinesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
}}
|
||||
isFrequencyChartVisible={isFrequencyChartVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
</InfinityWrapperStyled>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
activeLogIndex,
|
||||
handleLoadMore,
|
||||
isFetching,
|
||||
logs,
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
isFrequencyChartVisible,
|
||||
options,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="logs-list-view-container">
|
||||
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
|
||||
@@ -274,7 +265,9 @@ function LogsExplorerList({
|
||||
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<>
|
||||
{renderContent}
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
{renderContent}
|
||||
</InfinityWrapperStyled>
|
||||
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import { WsDataEvent } from 'api/common/getQueryStats';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Download from 'container/DownloadV2/DownloadV2';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsActionsContainer({
|
||||
listQuery,
|
||||
queryStats,
|
||||
selectedPanelType,
|
||||
showFrequencyChart,
|
||||
handleToggleFrequencyChart,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
flattenLogData,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
}: {
|
||||
listQuery: any;
|
||||
selectedPanelType: PANEL_TYPES;
|
||||
showFrequencyChart: boolean;
|
||||
handleToggleFrequencyChart: () => void;
|
||||
orderBy: string;
|
||||
setOrderBy: (value: string) => void;
|
||||
flattenLogData: any;
|
||||
isFetching: boolean;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isSuccess: boolean;
|
||||
queryStats: WsDataEvent | undefined;
|
||||
}): JSX.Element {
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
data: {
|
||||
title: 'max lines per row',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Column',
|
||||
data: {
|
||||
title: 'columns',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleShowFormatOptions = (): void =>
|
||||
setShowFormatMenuItems(!showFormatMenuItems);
|
||||
|
||||
useClickOutside({
|
||||
ref: menuRef,
|
||||
onClickOutside: () => {
|
||||
if (showFormatMenuItems) {
|
||||
setShowFormatMenuItems(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="logs-actions-container">
|
||||
<div className="tab-options">
|
||||
<div className="tab-options-left">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tab-options-right">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<>
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={(value): void => setOrderBy(value)}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>
|
||||
</div>
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsActionsContainer;
|
||||
@@ -1,14 +1,10 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -22,20 +18,18 @@ import {
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import Download from 'container/DownloadV2/DownloadV2';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import {} from 'container/LiveLogs/constants';
|
||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||
import LogsExplorerList from 'container/LogsExplorerList';
|
||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
@@ -49,7 +43,7 @@ import {
|
||||
omit,
|
||||
set,
|
||||
} from 'lodash-es';
|
||||
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||
import LiveLogs from 'pages/LiveLogs';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
@@ -77,16 +71,12 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import {
|
||||
DataSource,
|
||||
LogsAggregatorOperator,
|
||||
StringOperators,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
import LogsActionsContainer from './LogsActionsContainer';
|
||||
|
||||
function LogsExplorerViewsContainer({
|
||||
selectedView,
|
||||
@@ -94,6 +84,7 @@ function LogsExplorerViewsContainer({
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
setWarning,
|
||||
showLiveLogs,
|
||||
}: {
|
||||
selectedView: ExplorerViews;
|
||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@@ -102,6 +93,7 @@ function LogsExplorerViewsContainer({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
chartQueryKeyRef: MutableRefObject<any>;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
showLiveLogs: boolean;
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const dispatch = useDispatch();
|
||||
@@ -149,7 +141,6 @@ function LogsExplorerViewsContainer({
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
@@ -162,12 +153,6 @@ function LogsExplorerViewsContainer({
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData?.length > 1 ||
|
||||
@@ -603,41 +588,6 @@ function LogsExplorerViewsContainer({
|
||||
return isGroupByExist ? data.payload.data.result : firstPayloadQueryArray;
|
||||
}, [stagedQuery, panelType, data, listChartData, listQuery]);
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
data: {
|
||||
title: 'max lines per row',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Column',
|
||||
data: {
|
||||
title: 'columns',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleShowFormatOptions = (): void =>
|
||||
setShowFormatMenuItems(!showFormatMenuItems);
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickOutside({
|
||||
ref: menuRef,
|
||||
onClickOutside: () => {
|
||||
if (showFormatMenuItems) {
|
||||
setShowFormatMenuItems(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isLoading ||
|
||||
@@ -695,104 +645,40 @@ function LogsExplorerViewsContainer({
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
<div className="logs-explorer-views-types">
|
||||
<div className="logs-actions-container">
|
||||
<div className="tab-options">
|
||||
<div className="tab-options-left">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tab-options-right">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<>
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={(value): void => setOrderBy(value)}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>
|
||||
</div>
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
|
||||
<div className="logs-frequency-chart-container">
|
||||
<LogsExplorerChart
|
||||
className="logs-frequency-chart"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
</div>
|
||||
{!showLiveLogs && (
|
||||
<LogsActionsContainer
|
||||
listQuery={listQuery}
|
||||
queryStats={queryStats}
|
||||
selectedPanelType={selectedPanelType}
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
handleToggleFrequencyChart={handleToggleFrequencyChart}
|
||||
orderBy={orderBy}
|
||||
setOrderBy={setOrderBy}
|
||||
flattenLogData={flattenLogData}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST &&
|
||||
showFrequencyChart &&
|
||||
!showLiveLogs && (
|
||||
<div className="logs-frequency-chart-container">
|
||||
<LogsExplorerChart
|
||||
className="logs-frequency-chart"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-type-content">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
{showLiveLogs && <LiveLogs />}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && !showLiveLogs && (
|
||||
<LogsExplorerList
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
@@ -805,7 +691,8 @@ function LogsExplorerViewsContainer({
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
/>
|
||||
)}
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
data={data}
|
||||
@@ -817,7 +704,7 @@ function LogsExplorerViewsContainer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && (
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
|
||||
<LogsExplorerTable
|
||||
data={
|
||||
(data?.payload?.data?.newResult?.data?.result ||
|
||||
|
||||
@@ -174,6 +174,7 @@ const renderer = (): RenderResult =>
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
@@ -235,6 +236,7 @@ describe('LogsExplorerViews -', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
|
||||
@@ -178,6 +178,10 @@ export const mockQueryBuilderContextValue = {
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isEnabledQuery: false,
|
||||
lastUsedQuery: 0,
|
||||
handleSetTraceOperatorData: noop,
|
||||
removeAllQueryBuilderEntities: noop,
|
||||
removeTraceOperator: noop,
|
||||
addTraceOperator: noop,
|
||||
setLastUsedQuery: noop,
|
||||
handleSetQueryData: noop,
|
||||
handleSetFormulaData: noop,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user