mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-08 18:59:56 +00:00
Compare commits
8 Commits
demo/trace
...
improveTra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
334b717f23 | ||
|
|
d08b3313bb | ||
|
|
606f7c1272 | ||
|
|
fa8cc92105 | ||
|
|
47a03e8f7e | ||
|
|
ba68efd60f | ||
|
|
dce7ded49b | ||
|
|
3f66c20316 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -42,7 +42,3 @@
|
||||
/pkg/telemetrymetadata/ @srikanthccv
|
||||
/pkg/telemetrymetrics/ @srikanthccv
|
||||
/pkg/telemetrytraces/ @srikanthccv
|
||||
|
||||
# AuthN / AuthZ Owners
|
||||
|
||||
/pkg/authz/ @vikrantgupta25 @grandwizard28
|
||||
|
||||
2
.github/workflows/build-community.yaml
vendored
2
.github/workflows/build-community.yaml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
GO_NAME: signoz-community
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
|
||||
2
.github/workflows/build-enterprise.yaml
vendored
2
.github/workflows/build-enterprise.yaml
vendored
@@ -93,7 +93,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||
|
||||
2
.github/workflows/build-staging.yaml
vendored
2
.github/workflows/build-staging.yaml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||
|
||||
10
.github/workflows/goci.yaml
vendored
10
.github/workflows/goci.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_TEST_CONTEXT: ./...
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
fmt:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
lint:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
deps:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.24
|
||||
GO_VERSION: 1.23
|
||||
build:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.23"
|
||||
- name: qemu-install
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: aarch64-install
|
||||
|
||||
4
.github/workflows/gor-signoz-community.yaml
vendored
4
.github/workflows/gor-signoz-community.yaml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.23"
|
||||
- name: cross-compilation-tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.23"
|
||||
|
||||
# copy the caches from build
|
||||
- name: get-sha
|
||||
|
||||
4
.github/workflows/gor-signoz.yaml
vendored
4
.github/workflows/gor-signoz.yaml
vendored
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.23"
|
||||
- name: cross-compilation-tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.23"
|
||||
|
||||
# copy the caches from build
|
||||
- name: get-sha
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -86,8 +86,6 @@ queries.active
|
||||
.devenv/**/tmp/**
|
||||
.qodo
|
||||
|
||||
.dev
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -2,11 +2,10 @@ FROM node:18-bullseye AS build
|
||||
|
||||
WORKDIR /opt/
|
||||
COPY ./frontend/ ./
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
RUN CI=1 yarn install
|
||||
RUN CI=1 yarn build
|
||||
|
||||
FROM golang:1.24-bullseye
|
||||
FROM golang:1.23-bullseye
|
||||
|
||||
ARG OS="linux"
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
))
|
||||
}
|
||||
|
||||
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
|
||||
password, err := types.NewFactorPassword(uuid.NewString())
|
||||
|
||||
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
|
||||
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||
}
|
||||
|
||||
return newUser, nil
|
||||
return integrationUser, nil
|
||||
}
|
||||
|
||||
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
)
|
||||
@@ -57,7 +57,7 @@ func Unauthorized(err error) *ApiError {
|
||||
func BadRequestStr(s string) *ApiError {
|
||||
return &ApiError{
|
||||
Typ: basemodel.ErrorBadData,
|
||||
Err: errors.New(s),
|
||||
Err: fmt.Errorf(s),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
|
||||
func InternalErrorStr(s string) *ApiError {
|
||||
return &ApiError{
|
||||
Typ: basemodel.ErrorInternal,
|
||||
Err: errors.New(s),
|
||||
Err: fmt.Errorf(s),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
*.typegen.ts
|
||||
i18-generate-hash.js
|
||||
src/parser/TraceOperatorParser/**
|
||||
i18-generate-hash.js
|
||||
@@ -10,6 +10,4 @@ public/
|
||||
**/*.json
|
||||
|
||||
# Ignore all files in parser folder:
|
||||
src/parser/**
|
||||
|
||||
src/TraceOperator/parser/**
|
||||
src/parser/**
|
||||
@@ -45,13 +45,11 @@
|
||||
"@sentry/webpack-plugin": "2.22.6",
|
||||
"@signozhq/badge": "0.0.2",
|
||||
"@signozhq/calendar": "0.0.0",
|
||||
"@signozhq/callout": "0.0.2",
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@signozhq/table": "0.3.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/settings/getRetention';
|
||||
|
||||
// Only works for logs
|
||||
const getRetentionV2 = async (): Promise<
|
||||
SuccessResponseV2<PayloadProps<'logs'>>
|
||||
> => {
|
||||
try {
|
||||
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
|
||||
`/settings/ttl`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getRetentionV2;
|
||||
@@ -1,14 +1,14 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/settings/setRetention';
|
||||
|
||||
const setRetention = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadPropsV2>(
|
||||
const response = await axios.post<PayloadProps>(
|
||||
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
|
||||
props.coldStorage
|
||||
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
|
||||
@@ -17,11 +17,13 @@ const setRetention = async (
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
|
||||
|
||||
const setRetentionV2 = async ({
|
||||
type,
|
||||
defaultTTLDays,
|
||||
coldStorageVolume,
|
||||
coldStorageDuration,
|
||||
ttlConditions,
|
||||
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
||||
try {
|
||||
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
|
||||
type,
|
||||
defaultTTLDays,
|
||||
coldStorageVolume,
|
||||
coldStorageDuration,
|
||||
ttlConditions,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default setRetentionV2;
|
||||
@@ -92,7 +92,6 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery()],
|
||||
queryFormulas: [baseFormula()],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
@@ -216,7 +215,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
},
|
||||
],
|
||||
clickhouse_sql: [],
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
builder: { queryData: [], queryFormulas: [] },
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
originalGraphType: PANEL_TYPES.TABLE,
|
||||
@@ -287,7 +286,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
legend: 'LC',
|
||||
},
|
||||
],
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
builder: { queryData: [], queryFormulas: [] },
|
||||
},
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
@@ -346,7 +345,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
builder: { queryData: [], queryFormulas: [] },
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
@@ -387,7 +386,6 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery()],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
@@ -461,7 +459,6 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
builder: {
|
||||
queryData: [logsQuery],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
@@ -575,7 +572,6 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
@@ -336,101 +332,6 @@ export function convertBuilderQueriesToV5(
|
||||
);
|
||||
}
|
||||
|
||||
function createTraceOperatorBaseSpec(
|
||||
queryData: IBuilderTraceOperator,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): BaseBuilderQuery {
|
||||
const nonEmptySelectColumns = (queryData.selectColumns as (
|
||||
| BaseAutocompleteData
|
||||
| TelemetryFieldKey
|
||||
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||
|
||||
return {
|
||||
stepInterval: queryData?.stepInterval || undefined,
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
? queryData.limit || queryData.pageSize || undefined
|
||||
: queryData.limit || undefined,
|
||||
offset:
|
||||
requestType === 'raw' || requestType === 'trace'
|
||||
? queryData.offset
|
||||
: undefined,
|
||||
order:
|
||||
queryData.orderBy?.length > 0
|
||||
? queryData.orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
: nonEmptySelectColumns?.map(
|
||||
(column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function convertTraceOperatorToV5(
|
||||
traceOperator: Record<string, IBuilderTraceOperator>,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(traceOperator).map(
|
||||
([queryName, traceOperatorData]): QueryEnvelope => {
|
||||
const baseSpec = createTraceOperatorBaseSpec(
|
||||
traceOperatorData,
|
||||
requestType,
|
||||
panelType,
|
||||
);
|
||||
|
||||
// Skip aggregation for raw request type
|
||||
const aggregations =
|
||||
requestType === 'raw'
|
||||
? undefined
|
||||
: createAggregation(traceOperatorData, panelType);
|
||||
|
||||
const spec: QueryEnvelope['spec'] = {
|
||||
name: queryName,
|
||||
...baseSpec,
|
||||
expression: traceOperatorData.expression || '',
|
||||
aggregations: aggregations as TraceAggregation[],
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'builder_trace_operator' as QueryType,
|
||||
spec,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PromQL queries to V5 format
|
||||
*/
|
||||
@@ -512,28 +413,14 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
|
||||
switch (query.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||
const { queryData: data, queryFormulas } = query.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
const filteredTraceOperator =
|
||||
queryTraceOperator && queryTraceOperator.length > 0
|
||||
? queryTraceOperator.filter((traceOperator) =>
|
||||
Boolean(traceOperator.expression.trim()),
|
||||
)
|
||||
: [];
|
||||
|
||||
const currentTraceOperator = mapQueryDataToApi(
|
||||
filteredTraceOperator,
|
||||
'queryName',
|
||||
tableParams,
|
||||
);
|
||||
|
||||
// Combine legend maps
|
||||
legendMap = {
|
||||
...currentQueryData.newLegendMap,
|
||||
...currentFormulas.newLegendMap,
|
||||
...currentTraceOperator.newLegendMap,
|
||||
};
|
||||
|
||||
// Convert builder queries
|
||||
@@ -566,14 +453,8 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const traceOperatorQueries = convertTraceOperatorToV5(
|
||||
currentTraceOperator.data,
|
||||
requestType,
|
||||
graphType,
|
||||
);
|
||||
|
||||
// Combine all query types
|
||||
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
|
||||
// Combine both types
|
||||
queries = [...builderQueries, ...formulaQueries];
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
|
||||
@@ -20,15 +20,13 @@
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
&.bar-panel-container {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
|
||||
&.graph-panel-container {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
&.graph {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,11 +82,9 @@
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
&.bar-panel-container {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,31 +174,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-input-prefix {
|
||||
.live-dot-icon {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-forest-500);
|
||||
animation: ripple 1s infinite;
|
||||
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.time-input-suffix-icon-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -59,9 +59,7 @@ interface CustomTimePickerProps {
|
||||
customDateTimeVisible?: boolean;
|
||||
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
|
||||
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
|
||||
showLiveLogs?: boolean;
|
||||
onGoLive?: () => void;
|
||||
onExitLiveLogs?: () => void;
|
||||
handleGoLive?: () => void;
|
||||
}
|
||||
|
||||
function CustomTimePicker({
|
||||
@@ -78,9 +76,7 @@ function CustomTimePicker({
|
||||
customDateTimeVisible,
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
onGoLive,
|
||||
onExitLiveLogs,
|
||||
showLiveLogs,
|
||||
handleGoLive,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
@@ -169,13 +165,9 @@ function CustomTimePicker({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
} else {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}
|
||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}, [selectedTime, selectedValue]);
|
||||
|
||||
const hide = (): void => {
|
||||
setOpen(false);
|
||||
@@ -346,28 +338,6 @@ 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">
|
||||
@@ -387,8 +357,7 @@ function CustomTimePicker({
|
||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||
onSelectHandler={handleSelect}
|
||||
onGoLive={defaultTo(onGoLive, noop)}
|
||||
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
||||
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||
options={items}
|
||||
selectedTime={selectedTime}
|
||||
activeView={activeView}
|
||||
@@ -423,7 +392,17 @@ function CustomTimePicker({
|
||||
onBlur={handleBlur}
|
||||
onChange={handleInputChange}
|
||||
data-1p-ignore
|
||||
prefix={getInputPrefix()}
|
||||
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>
|
||||
}
|
||||
suffix={
|
||||
<div className="time-input-suffix">
|
||||
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||
@@ -460,8 +439,6 @@ CustomTimePicker.defaultProps = {
|
||||
customDateTimeVisible: false,
|
||||
setCustomDTPickerVisible: noop,
|
||||
onCustomDateHandler: noop,
|
||||
onGoLive: noop,
|
||||
handleGoLive: noop,
|
||||
onCustomTimeStatusUpdate: noop,
|
||||
onExitLiveLogs: noop,
|
||||
showLiveLogs: false,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 {
|
||||
@@ -17,14 +16,7 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
import { Clock, PenLine } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
||||
|
||||
@@ -40,13 +32,12 @@ interface CustomTimePickerPopoverContentProps {
|
||||
lexicalContext?: LexicalContext,
|
||||
) => void;
|
||||
onSelectHandler: (label: string, value: string) => void;
|
||||
onGoLive: () => void;
|
||||
handleGoLive: () => void;
|
||||
selectedTime: string;
|
||||
activeView: 'datetime' | 'timezone';
|
||||
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||
isOpenedFromFooter: boolean;
|
||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||
onExitLiveLogs: () => void;
|
||||
}
|
||||
|
||||
interface RecentlyUsedDateTimeRange {
|
||||
@@ -65,13 +56,12 @@ function CustomTimePickerPopoverContent({
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
onSelectHandler,
|
||||
onGoLive,
|
||||
handleGoLive,
|
||||
selectedTime,
|
||||
activeView,
|
||||
setActiveView,
|
||||
isOpenedFromFooter,
|
||||
setIsOpenedFromFooter,
|
||||
onExitLiveLogs,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -79,19 +69,6 @@ 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;
|
||||
|
||||
@@ -99,12 +76,6 @@ function CustomTimePickerPopoverContent({
|
||||
RecentlyUsedDateTimeRange[]
|
||||
>([]);
|
||||
|
||||
const handleExitLiveLogs = useCallback((): void => {
|
||||
if (isLogsExplorerPage) {
|
||||
onExitLiveLogs();
|
||||
}
|
||||
}, [isLogsExplorerPage, onExitLiveLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customDateTimeVisible) {
|
||||
const customTimeRanges = getCustomTimeRanges();
|
||||
@@ -136,7 +107,6 @@ function CustomTimePickerPopoverContent({
|
||||
className="time-btns"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
>
|
||||
@@ -170,17 +140,12 @@ function CustomTimePickerPopoverContent({
|
||||
);
|
||||
}
|
||||
|
||||
const handleGoLive = (): void => {
|
||||
onGoLive();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="date-time-popover">
|
||||
{!customDateTimeVisible && (
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
{isLogsExplorerPage && (
|
||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||
Live
|
||||
</Button>
|
||||
@@ -190,7 +155,6 @@ function CustomTimePickerPopoverContent({
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
@@ -205,6 +169,7 @@ function CustomTimePickerPopoverContent({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'relative-date-time',
|
||||
@@ -234,14 +199,12 @@ 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,7 +125,6 @@ export const getHostTracesQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
|
||||
@@ -51,7 +51,6 @@ export const getHostLogsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
flex: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
.qb-trace-view-selector-container {
|
||||
padding: 12px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-content-section {
|
||||
@@ -183,7 +179,7 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
margin-left: 26px;
|
||||
margin-left: 32px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
|
||||
@@ -199,8 +195,8 @@
|
||||
}
|
||||
|
||||
.formula-container {
|
||||
padding: 8px;
|
||||
margin-left: 74px;
|
||||
margin-left: 82px;
|
||||
padding: 4px 0px;
|
||||
|
||||
.ant-col {
|
||||
&::before {
|
||||
@@ -295,13 +291,6 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
.qb-trace-operator-button-container {
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,12 +331,6 @@
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
&.has-trace-operator {
|
||||
&::before {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
@@ -364,7 +347,7 @@
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 128px;
|
||||
height: 65px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -5,13 +5,11 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||
import { QueryV2 } from './QueryV2/QueryV2';
|
||||
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
|
||||
|
||||
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
config,
|
||||
@@ -20,7 +18,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryComponents,
|
||||
isListViewPanel = false,
|
||||
showOnlyWhereClause = false,
|
||||
showTraceOperator = false,
|
||||
version,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
@@ -28,7 +25,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
handleSetConfig,
|
||||
addTraceOperator,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
} = useQueryBuilder();
|
||||
@@ -58,11 +54,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
const isMultiQueryAllowed = useMemo(
|
||||
() => !isListViewPanel || showTraceOperator,
|
||||
[showTraceOperator, isListViewPanel],
|
||||
);
|
||||
|
||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
@@ -106,60 +97,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
listViewTracesFilterConfigs,
|
||||
]);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
|
||||
const hasAtLeastOneTraceQuery = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.some(
|
||||
(query) => query.dataSource === DataSource.TRACES,
|
||||
),
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const hasTraceOperator = useMemo(
|
||||
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
|
||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
||||
);
|
||||
|
||||
const shouldShowFooter = useMemo(
|
||||
() =>
|
||||
(!showOnlyWhereClause && !isListViewPanel) ||
|
||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
||||
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
|
||||
);
|
||||
|
||||
const showQueryList = useMemo(
|
||||
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
|
||||
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
|
||||
);
|
||||
|
||||
const showFormula = useMemo(() => {
|
||||
if (currentDataSource === DataSource.TRACES) {
|
||||
return !isListViewPanel;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [isListViewPanel, currentDataSource]);
|
||||
|
||||
const showAddTraceOperator = useMemo(
|
||||
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
|
||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
{!isMultiQueryAllowed ? (
|
||||
{isListViewPanel && (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
@@ -167,16 +109,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
showTraceOperator={showTraceOperator}
|
||||
hasTraceOperator={hasTraceOperator}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{!isListViewPanel &&
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
@@ -186,17 +127,13 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
isAvailableToDisable={false}
|
||||
showTraceOperator={showTraceOperator}
|
||||
hasTraceOperator={hasTraceOperator}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
signalSource={config?.signalSource || ''}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
@@ -221,25 +158,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
<QueryFooter
|
||||
showAddFormula={showFormula}
|
||||
addNewBuilderQuery={addNewBuilderQuery}
|
||||
addNewFormula={addNewFormula}
|
||||
addTraceOperator={addTraceOperator}
|
||||
showAddTraceOperator={showAddTraceOperator}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasTraceOperator && (
|
||||
<TraceOperator
|
||||
isListViewPanel={isListViewPanel}
|
||||
traceOperator={traceOperator as IBuilderTraceOperator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showQueryList && (
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
<div className="query-names-section">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<div key={query.queryName} className="query-name">
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
.query-add-ons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-ons-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.add-ons-tabs {
|
||||
display: flex;
|
||||
|
||||
@@ -144,7 +144,6 @@ function QueryAddOns({
|
||||
showReduceTo,
|
||||
panelType,
|
||||
index,
|
||||
isForTraceOperator = false,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
version: string;
|
||||
@@ -152,7 +151,6 @@ function QueryAddOns({
|
||||
showReduceTo: boolean;
|
||||
panelType: PANEL_TYPES | null;
|
||||
index: number;
|
||||
isForTraceOperator?: boolean;
|
||||
}): JSX.Element {
|
||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||
|
||||
@@ -162,7 +160,6 @@ function QueryAddOns({
|
||||
index,
|
||||
query,
|
||||
entityVersion: '',
|
||||
isForTraceOperator,
|
||||
});
|
||||
|
||||
const { handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
@@ -4,10 +4,7 @@ import { Tooltip } from 'antd';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||
@@ -23,7 +20,7 @@ function QueryAggregationOptions({
|
||||
panelType?: string;
|
||||
onAggregationIntervalChange: (value: number) => void;
|
||||
onChange?: (value: string) => void;
|
||||
queryData: IBuilderQuery | IBuilderTraceOperator;
|
||||
queryData: IBuilderQuery;
|
||||
}): JSX.Element {
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
|
||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
||||
import { Plus, Sigma } from 'lucide-react';
|
||||
|
||||
export default function QueryFooter({
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
addTraceOperator,
|
||||
showAddFormula = true,
|
||||
showAddTraceOperator = false,
|
||||
}: {
|
||||
addNewBuilderQuery: () => void;
|
||||
addNewFormula: () => void;
|
||||
addTraceOperator?: () => void;
|
||||
showAddTraceOperator: boolean;
|
||||
showAddFormula?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="qb-footer">
|
||||
@@ -30,65 +22,32 @@ export default function QueryFooter({
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{showAddFormula && (
|
||||
<div className="qb-add-formula">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add New Formula
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
<div className="qb-add-formula">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add New Formula
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={addNewFormula}
|
||||
>
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={addNewFormula}
|
||||
>
|
||||
Add Formula
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{showAddTraceOperator && (
|
||||
<div className="qb-trace-operator-button-container">
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add Trace Matching
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{' '}
|
||||
<br />
|
||||
Learn more
|
||||
</Typography.Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="add-trace-operator-button periscope-btn secondary"
|
||||
icon={<DraftingCompass size={16} />}
|
||||
onClick={(): void => addTraceOperator?.()}
|
||||
>
|
||||
<div className="qb-trace-operator-button-container-text">
|
||||
Add Trace Matching
|
||||
<BetaTag />
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
Add Formula
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { Dropdown } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
@@ -27,12 +26,9 @@ export const QueryV2 = memo(function QueryV2({
|
||||
query,
|
||||
filterConfigs,
|
||||
isListViewPanel = false,
|
||||
showTraceOperator = false,
|
||||
hasTraceOperator = false,
|
||||
version,
|
||||
showOnlyWhereClause = false,
|
||||
signalSource = '',
|
||||
isMultiQueryAllowed = false,
|
||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@@ -79,15 +75,6 @@ export const QueryV2 = memo(function QueryV2({
|
||||
dataSource,
|
||||
]);
|
||||
|
||||
const showInlineQuerySearch = useMemo(() => {
|
||||
if (!showTraceOperator) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
|
||||
);
|
||||
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
@@ -121,12 +108,11 @@ export const QueryV2 = memo(function QueryV2({
|
||||
ref={ref}
|
||||
>
|
||||
<div className="qb-content-section">
|
||||
{(!showOnlyWhereClause || showTraceOperator) && (
|
||||
{!showOnlyWhereClause && (
|
||||
<div className="qb-header-container">
|
||||
<div className="query-actions-container">
|
||||
<div className="query-actions-left-container">
|
||||
<QBEntityOptions
|
||||
hasTraceOperator={hasTraceOperator}
|
||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||
showFunctions={
|
||||
(version && version === ENTITY_VERSION_V4) ||
|
||||
@@ -136,7 +122,6 @@ export const QueryV2 = memo(function QueryV2({
|
||||
false
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
showTraceOperator={showTraceOperator}
|
||||
entityType="query"
|
||||
entityData={query}
|
||||
onToggleVisibility={handleToggleDisableQuery}
|
||||
@@ -154,28 +139,7 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && showInlineQuerySearch && (
|
||||
<div className="qb-search-filter-container" style={{ flex: 1 }}>
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMultiQueryAllowed && (
|
||||
{!isListViewPanel && (
|
||||
<Dropdown
|
||||
className="query-actions-dropdown"
|
||||
menu={{
|
||||
@@ -217,31 +181,28 @@ export const QueryV2 = memo(function QueryV2({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showInlineQuerySearch && (
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
signalSource={signalSource}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
!isListViewPanel &&
|
||||
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
|
||||
dataSource !== DataSource.METRICS && (
|
||||
<QueryAggregation
|
||||
dataSource={dataSource}
|
||||
@@ -264,17 +225,16 @@ export const QueryV2 = memo(function QueryV2({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
version="v3"
|
||||
isListViewPanel={isListViewPanel}
|
||||
showReduceTo={showReduceTo}
|
||||
panelType={panelType}
|
||||
/>
|
||||
)}
|
||||
{!showOnlyWhereClause && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
version="v3"
|
||||
isListViewPanel={isListViewPanel}
|
||||
showReduceTo={showReduceTo}
|
||||
panelType={panelType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
.qb-trace-operator {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&.non-list-view {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 12px;
|
||||
height: 88px;
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-span-source-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 24px;
|
||||
|
||||
&-query {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
&-query-name {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 2px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
background: rgba(242, 71, 105, 0.1);
|
||||
color: var(--Sakura-400, #f56c87);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
transform: translateY(-50%);
|
||||
left: -26px;
|
||||
height: 1px;
|
||||
width: 20px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: -10px;
|
||||
transform: translateY(-50%);
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-aggregation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-add-ons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&-label-with-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
.qb-trace-operator-editor-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--bg-vanilla-400);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0px 8px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.qb-trace-operator {
|
||||
&-arrow {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
&::after {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
&.non-list-view {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-label-with-input {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
.label {
|
||||
color: var(--bg-ink-500) !important;
|
||||
border-right: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
import './TraceOperator.styles.scss';
|
||||
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAddOns from '../QueryAddOns/QueryAddOns';
|
||||
import QueryAggregation from '../QueryAggregation/QueryAggregation';
|
||||
import TraceOperatorEditor from './TraceOperatorEditor';
|
||||
|
||||
export default function TraceOperator({
|
||||
traceOperator,
|
||||
isListViewPanel = false,
|
||||
}: {
|
||||
traceOperator: IBuilderTraceOperator;
|
||||
isListViewPanel?: boolean;
|
||||
}): JSX.Element {
|
||||
const { panelType, removeTraceOperator } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: traceOperator,
|
||||
entityVersion: '',
|
||||
isForTraceOperator: true,
|
||||
});
|
||||
|
||||
const handleTraceOperatorChange = useCallback(
|
||||
(traceOperatorExpression: string) => {
|
||||
handleChangeQueryData('expression', traceOperatorExpression);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregation = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('aggregations', [
|
||||
{
|
||||
expression: value,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
|
||||
<div className="qb-trace-operator-container">
|
||||
<div
|
||||
className={cx(
|
||||
'qb-trace-operator-label-with-input',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
>
|
||||
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
|
||||
<div className="qb-trace-operator-editor-container">
|
||||
<TraceOperatorEditor
|
||||
value={traceOperator?.expression || ''}
|
||||
traceOperator={traceOperator}
|
||||
onChange={handleTraceOperatorChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isListViewPanel && (
|
||||
<div className="qb-trace-operator-aggregation-container">
|
||||
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
|
||||
<QueryAggregation
|
||||
dataSource={DataSource.TRACES}
|
||||
key={`query-search-${traceOperator.queryName}`}
|
||||
panelType={panelType || undefined}
|
||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||
onChange={handleChangeAggregation}
|
||||
queryData={traceOperator}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
'qb-trace-operator-add-ons-container',
|
||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||
)}
|
||||
>
|
||||
<QueryAddOns
|
||||
index={0}
|
||||
query={traceOperator}
|
||||
version="v3"
|
||||
isForTraceOperator
|
||||
isListViewPanel={false}
|
||||
showReduceTo={false}
|
||||
panelType={panelType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title="Remove Trace Operator" placement="topLeft">
|
||||
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
|
||||
import '../QuerySearch/QuerySearch.styles.scss';
|
||||
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
||||
import { Button, Popover } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
TRACE_OPERATOR_OPERATORS,
|
||||
TRACE_OPERATOR_OPERATORS_LABELS,
|
||||
TRACE_OPERATOR_OPERATORS_WITH_PRIORITY,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateTraceOperatorQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { getTraceOperatorContextAtCursor } from './utils/traceOperatorContextUtils';
|
||||
import { getInvolvedQueriesInTraceOperator } from './utils/utils';
|
||||
|
||||
// Custom extension to stop events
|
||||
const stopEventsExtension = EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
// Stop all keyboard events from propagating to global shortcuts
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
return false; // Important for CM to know you handled it
|
||||
},
|
||||
input: (event) => {
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
focus: (event) => {
|
||||
// Ensure focus events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
blur: (event) => {
|
||||
// Ensure blur events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
interface TraceOperatorEditorProps {
|
||||
value: string;
|
||||
traceOperator: IBuilderTraceOperator;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
onRun?: (query: string) => void;
|
||||
}
|
||||
|
||||
function TraceOperatorEditor({
|
||||
value,
|
||||
onChange,
|
||||
traceOperator,
|
||||
placeholder = 'Enter your trace operator query',
|
||||
onRun,
|
||||
}: TraceOperatorEditorProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
const [validation, setValidation] = useState<IValidationResult>({
|
||||
isValid: false,
|
||||
message: '',
|
||||
errors: [],
|
||||
});
|
||||
// Track if the query was changed externally (from props) vs internally (user input)
|
||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||
const [lastExternalValue, setLastExternalValue] = useState<string>('');
|
||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData
|
||||
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
|
||||
.map((query) => ({
|
||||
label: query.queryName,
|
||||
type: 'atom',
|
||||
apply: query.queryName,
|
||||
})),
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const toggleSuggestions = useCallback(
|
||||
(timeout?: number) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!editorRef.current) return;
|
||||
if (isFocused) {
|
||||
startCompletion(editorRef.current);
|
||||
} else {
|
||||
closeCompletion(editorRef.current);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
},
|
||||
[isFocused],
|
||||
);
|
||||
|
||||
const handleQueryValidation = (newQuery: string): void => {
|
||||
try {
|
||||
const validationResponse = validateTraceOperatorQuery(newQuery);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process trace operator',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Detect external value changes and mark for validation
|
||||
useEffect(() => {
|
||||
const newValue = value || '';
|
||||
if (newValue !== lastExternalValue) {
|
||||
setIsExternalQueryChange(true);
|
||||
setLastExternalValue(newValue);
|
||||
}
|
||||
}, [value, lastExternalValue]);
|
||||
|
||||
// Validate when the value changes externally (including on mount)
|
||||
useEffect(() => {
|
||||
if (isExternalQueryChange && value) {
|
||||
handleQueryValidation(value);
|
||||
setIsExternalQueryChange(false);
|
||||
}
|
||||
}, [isExternalQueryChange, value]);
|
||||
|
||||
// Enhanced autosuggestion function with context awareness
|
||||
function autoSuggestions(context: CompletionContext): CompletionResult | null {
|
||||
// This matches words before the cursor position
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
|
||||
if (word?.from === word?.to && !context.explicit) return null;
|
||||
|
||||
// Get the trace operator context at the cursor position
|
||||
const queryContext = getTraceOperatorContextAtCursor(value, cursorPos.ch);
|
||||
|
||||
// Define autocomplete options based on the context
|
||||
let options: {
|
||||
label: string;
|
||||
type: string;
|
||||
info?: string;
|
||||
apply:
|
||||
| string
|
||||
| ((view: EditorView, completion: any, from: number, to: number) => void);
|
||||
detail?: string;
|
||||
boost?: number;
|
||||
}[] = [];
|
||||
|
||||
// Helper function to add space after selection
|
||||
const addSpaceAfterSelection = (
|
||||
view: EditorView,
|
||||
completion: any,
|
||||
from: number,
|
||||
to: number,
|
||||
shouldAddSpace = true,
|
||||
): void => {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from,
|
||||
to,
|
||||
insert: shouldAddSpace ? `${completion.apply} ` : `${completion.apply}`,
|
||||
},
|
||||
selection: {
|
||||
anchor:
|
||||
from +
|
||||
(shouldAddSpace ? completion.apply.length + 1 : completion.apply.length),
|
||||
},
|
||||
});
|
||||
// Do not reopen here; onUpdate will handle reopening via toggleSuggestions
|
||||
};
|
||||
|
||||
// Helper function to add space after selection to options
|
||||
const addSpaceToOptions = (opts: typeof options): typeof options =>
|
||||
opts.map((option) => {
|
||||
const originalApply = option.apply || option.label;
|
||||
return {
|
||||
...option,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: any,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (queryContext.isInAtom) {
|
||||
// Suggest atoms (identifiers) for trace operators
|
||||
|
||||
const involvedQueries = getInvolvedQueriesInTraceOperator([traceOperator]);
|
||||
|
||||
options = queryOptions.map((option) => ({
|
||||
...option,
|
||||
boost: !involvedQueries.includes(option.apply as string) ? 100 : -99,
|
||||
}));
|
||||
|
||||
// Filter options based on what user is typing
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
options = options.filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
// Add space after selection for atoms
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
if (queryContext.isInOperator) {
|
||||
// Suggest operators for trace operators
|
||||
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
|
||||
options = operators.map((operator) => ({
|
||||
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
|
||||
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
|
||||
: operator,
|
||||
type: 'operator',
|
||||
apply: operator,
|
||||
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
|
||||
}));
|
||||
|
||||
// Add space after selection for operators
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
if (queryContext.isInParenthesis) {
|
||||
// Different suggestions based on the context within parenthesis
|
||||
const curChar = value.charAt(cursorPos.ch - 1) || '';
|
||||
|
||||
if (curChar === '(') {
|
||||
// Right after opening parenthesis, suggest atoms or nested expressions
|
||||
options = [
|
||||
{ label: '(', type: 'parenthesis', apply: '(' },
|
||||
...queryOptions,
|
||||
];
|
||||
|
||||
// Add space after selection for opening parenthesis context
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
if (curChar === ')') {
|
||||
// After closing parenthesis, suggest operators
|
||||
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
|
||||
options = operators.map((operator) => ({
|
||||
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
|
||||
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
|
||||
: operator,
|
||||
type: 'operator',
|
||||
apply: operator,
|
||||
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
|
||||
}));
|
||||
|
||||
// Add space after selection for closing parenthesis context
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default: suggest atoms if no specific context
|
||||
options = [
|
||||
...queryOptions,
|
||||
{
|
||||
label: '(',
|
||||
type: 'parenthesis',
|
||||
apply: '(',
|
||||
},
|
||||
];
|
||||
|
||||
// Filter options based on what user is typing
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
options = options.filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
// Add space after selection
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? context.pos,
|
||||
options: optionsWithSpace,
|
||||
};
|
||||
}
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
(viewUpdate: { view: EditorView }): void => {
|
||||
if (!editorRef.current) {
|
||||
editorRef.current = viewUpdate.view;
|
||||
}
|
||||
|
||||
const selection = viewUpdate.view.state.selection.main;
|
||||
const pos = selection.head;
|
||||
|
||||
const lineInfo = viewUpdate.view.state.doc.lineAt(pos);
|
||||
const newPos = {
|
||||
line: lineInfo.number,
|
||||
ch: pos - lineInfo.from,
|
||||
};
|
||||
|
||||
if (newPos.line !== cursorPos.line || newPos.ch !== cursorPos.ch) {
|
||||
setCursorPos(newPos);
|
||||
// Trigger suggestions on context update
|
||||
toggleSuggestions(10);
|
||||
}
|
||||
},
|
||||
[cursorPos, toggleSuggestions],
|
||||
);
|
||||
|
||||
const handleChange = (newValue: string): void => {
|
||||
// Mark as internal change to avoid triggering external validation
|
||||
setIsExternalQueryChange(false);
|
||||
setLastExternalValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const handleBlur = (): void => {
|
||||
handleQueryValidation(value);
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
// Effect to handle focus state and trigger suggestions on focus
|
||||
useEffect(() => {
|
||||
const clearTimeout = toggleSuggestions(10);
|
||||
return (): void => clearTimeout();
|
||||
}, [isFocused, toggleSuggestions]);
|
||||
|
||||
return (
|
||||
<div className="code-mirror-where-clause">
|
||||
<div className="query-where-clause-editor-container">
|
||||
<CodeMirror
|
||||
value={value}
|
||||
theme={isDarkMode ? copilot : githubLight}
|
||||
onChange={handleChange}
|
||||
onUpdate={handleUpdate}
|
||||
className={cx('query-where-clause-editor', {
|
||||
isValid: validation.isValid === true,
|
||||
hasErrors: validation.errors.length > 0,
|
||||
})}
|
||||
extensions={[
|
||||
autocompletion({
|
||||
override: [autoSuggestions],
|
||||
defaultKeymap: true,
|
||||
closeOnBlur: true,
|
||||
activateOnTyping: true,
|
||||
maxRenderedOptions: 50,
|
||||
}),
|
||||
javascript({ jsx: false, typescript: false }),
|
||||
EditorView.lineWrapping,
|
||||
stopEventsExtension,
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
{
|
||||
key: 'Escape',
|
||||
run: closeCompletion,
|
||||
},
|
||||
{
|
||||
key: 'Enter',
|
||||
preventDefault: true,
|
||||
// Prevent default behavior of Enter to add new line
|
||||
// and instead run a custom action
|
||||
run: (): boolean => true,
|
||||
},
|
||||
{
|
||||
key: 'Mod-Enter',
|
||||
preventDefault: true,
|
||||
run: (): boolean => {
|
||||
if (onRun && typeof onRun === 'function') {
|
||||
onRun(value);
|
||||
} else {
|
||||
handleRunQuery();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Shift-Enter',
|
||||
preventDefault: true,
|
||||
// Prevent default behavior of Shift-Enter to add new line
|
||||
run: (): boolean => true,
|
||||
},
|
||||
]),
|
||||
),
|
||||
]}
|
||||
placeholder={placeholder}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setIsFocused(true);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{value && validation.isValid === false && !isFocused && (
|
||||
<div
|
||||
className={cx('query-status-container', {
|
||||
hasErrors: validation.errors.length > 0,
|
||||
})}
|
||||
>
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
content={
|
||||
<div className="query-status-content">
|
||||
<div className="query-status-content-header">
|
||||
<div className="query-validation">
|
||||
<div className="query-validation-errors">
|
||||
{validation.errors.map((error) => (
|
||||
<div key={error.message} className="query-validation-error">
|
||||
<div className="query-validation-error">
|
||||
{error.line}:{error.column} - {error.message}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
overlayClassName="query-status-popover"
|
||||
>
|
||||
{validation.isValid ? (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CheckCircleFilled />}
|
||||
className="periscope-btn ghost"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
|
||||
className="periscope-btn ghost"
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TraceOperatorEditor.defaultProps = {
|
||||
onRun: undefined,
|
||||
placeholder: 'Enter your trace operator query',
|
||||
};
|
||||
|
||||
export default TraceOperatorEditor;
|
||||
@@ -1,425 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
|
||||
import { Token } from 'antlr4';
|
||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
||||
|
||||
import {
|
||||
createTraceOperatorContext,
|
||||
extractTraceExpressionPairs,
|
||||
getTraceOperatorContextAtCursor,
|
||||
} from '../utils/traceOperatorContextUtils';
|
||||
|
||||
describe('traceOperatorContextUtils', () => {
|
||||
describe('createTraceOperatorContext', () => {
|
||||
it('should create a context object with all required properties', () => {
|
||||
const mockToken = {
|
||||
type: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
} as Token;
|
||||
|
||||
const context = createTraceOperatorContext(
|
||||
mockToken,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
'atom',
|
||||
'operator',
|
||||
[],
|
||||
null,
|
||||
);
|
||||
|
||||
expect(context).toEqual({
|
||||
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
currentToken: 'test',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
atomToken: 'atom',
|
||||
operatorToken: 'operator',
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a context object with default values', () => {
|
||||
const mockToken = {
|
||||
type: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
} as Token;
|
||||
|
||||
const context = createTraceOperatorContext(
|
||||
mockToken,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(context).toEqual({
|
||||
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
|
||||
text: 'test',
|
||||
start: 0,
|
||||
stop: 3,
|
||||
currentToken: 'test',
|
||||
isInAtom: false,
|
||||
isInOperator: true,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
atomToken: undefined,
|
||||
operatorToken: undefined,
|
||||
expressionPairs: [],
|
||||
currentPair: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTraceExpressionPairs', () => {
|
||||
it('should extract simple expression pair', () => {
|
||||
const query = 'A => B';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].position.leftStart).toBe(0);
|
||||
expect(result[0].position.leftEnd).toBe(0);
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].position.operatorStart).toBe(2);
|
||||
expect(result[0].position.operatorEnd).toBe(3);
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
expect(result[0].position.rightStart).toBe(5);
|
||||
expect(result[0].position.rightEnd).toBe(5);
|
||||
expect(result[0].isComplete).toBe(true);
|
||||
});
|
||||
|
||||
it('should extract multiple expression pairs', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
// First pair: A => B
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
|
||||
// Second pair: C => D
|
||||
expect(result[1].leftAtom).toBe('C');
|
||||
expect(result[1].operator).toBe('=>');
|
||||
expect(result[1].rightAtom).toBe('D');
|
||||
});
|
||||
|
||||
it('should handle NOT operator', () => {
|
||||
const query = 'NOT A => B';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
});
|
||||
|
||||
it('should handle parentheses', () => {
|
||||
const query = '(A => B) && (C => D)';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
expect(result[1].leftAtom).toBe('C');
|
||||
expect(result[1].rightAtom).toBe('D');
|
||||
});
|
||||
|
||||
it('should handle incomplete expressions', () => {
|
||||
const query = 'A =>';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBeUndefined();
|
||||
expect(result[0].isComplete).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle complex nested expressions', () => {
|
||||
const query = 'A => B && (C => D || E => F)';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
expect(result[1].leftAtom).toBe('C');
|
||||
expect(result[1].rightAtom).toBe('D');
|
||||
expect(result[2].leftAtom).toBe('E');
|
||||
expect(result[2].rightAtom).toBe('F');
|
||||
});
|
||||
|
||||
it('should handle whitespace variations', () => {
|
||||
const query = 'A=>B';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].leftAtom).toBe('A');
|
||||
expect(result[0].operator).toBe('=>');
|
||||
expect(result[0].rightAtom).toBe('B');
|
||||
});
|
||||
|
||||
it('should handle error cases gracefully', () => {
|
||||
const query = 'invalid syntax @#$%';
|
||||
const result = extractTraceExpressionPairs(query);
|
||||
|
||||
// Should return an array (even if empty or with partial results)
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTraceOperatorContextAtCursor', () => {
|
||||
beforeEach(() => {
|
||||
// Reset console.error mock
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return default context for empty query', () => {
|
||||
const result = getTraceOperatorContextAtCursor('', 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: 0,
|
||||
stop: 0,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default context for null query', () => {
|
||||
const result = getTraceOperatorContextAtCursor(null as any, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: 0,
|
||||
stop: 0,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default context for undefined query', () => {
|
||||
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: 0,
|
||||
stop: 0,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should identify atom context', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'A'
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.stop).toBe(0);
|
||||
});
|
||||
|
||||
it('should identify operator context', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 2); // cursor at '='
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(2);
|
||||
expect(result.stop).toBe(2);
|
||||
});
|
||||
|
||||
it('should identify parenthesis context', () => {
|
||||
const query = '(A => B)';
|
||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at '('
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(true);
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.stop).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle cursor at space', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at space
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle cursor at end of query', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at end
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(5);
|
||||
expect(result.stop).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle complex query', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 8); // cursor at '&'
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBe('&&');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(7);
|
||||
expect(result.stop).toBe(8);
|
||||
});
|
||||
|
||||
it('should identify operator position in complex query', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 10); // cursor at 'C'
|
||||
|
||||
expect(result.atomToken).toBe('C');
|
||||
expect(result.operatorToken).toBe('&&');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(10);
|
||||
expect(result.stop).toBe(10);
|
||||
});
|
||||
|
||||
it('should identify atom position in complex query', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 13); // cursor at '>'
|
||||
|
||||
expect(result.atomToken).toBe('C');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(12);
|
||||
expect(result.stop).toBe(13);
|
||||
});
|
||||
|
||||
it('should handle transition points', () => {
|
||||
const query = 'A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 4); // cursor at 'B'
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(4);
|
||||
expect(result.stop).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle whitespace in complex queries', () => {
|
||||
const query = 'A=>B && C=>D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 6); // cursor at '&'
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBe('&&');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(true);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
expect(result.start).toBe(5);
|
||||
expect(result.stop).toBe(6);
|
||||
});
|
||||
|
||||
it('should handle NOT operator context', () => {
|
||||
const query = 'NOT A => B';
|
||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'N'
|
||||
|
||||
expect(result.atomToken).toBeUndefined();
|
||||
expect(result.operatorToken).toBeUndefined();
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle parentheses context', () => {
|
||||
const query = '(A => B)';
|
||||
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at 'A'
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(false);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(true);
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.stop).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle expression pairs context', () => {
|
||||
const query = 'A => B && C => D';
|
||||
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at 'A' in "&&"
|
||||
|
||||
expect(result.atomToken).toBe('A');
|
||||
expect(result.operatorToken).toBe('=>');
|
||||
expect(result.isInAtom).toBe(true);
|
||||
expect(result.isInOperator).toBe(false);
|
||||
expect(result.isInParenthesis).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle various cursor positions', () => {
|
||||
const query = 'A => B';
|
||||
|
||||
// Test cursor at each position
|
||||
for (let i = 0; i < query.length; i++) {
|
||||
const result = getTraceOperatorContextAtCursor(query, i);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.start).toBe('number');
|
||||
expect(typeof result.stop).toBe('number');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';
|
||||
|
||||
const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
|
||||
(({ expression } as unknown) as IBuilderTraceOperator);
|
||||
|
||||
describe('getInvolvedQueriesInTraceOperator', () => {
|
||||
it('returns empty array for empty input', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts identifiers from expression', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([
|
||||
makeTraceOperator('A => B'),
|
||||
]);
|
||||
expect(result).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it('extracts identifiers from complex expression', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([
|
||||
makeTraceOperator('A => (NOT B || C)'),
|
||||
]);
|
||||
expect(result).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it('filters out querynames from complex expression', () => {
|
||||
const result = getInvolvedQueriesInTraceOperator([
|
||||
makeTraceOperator(
|
||||
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
|
||||
),
|
||||
]);
|
||||
expect(result).toEqual([
|
||||
'A1',
|
||||
'B2',
|
||||
'C3',
|
||||
'D4',
|
||||
'E5',
|
||||
'F6',
|
||||
'G7',
|
||||
'H8',
|
||||
'I9',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,562 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable no-continue */
|
||||
|
||||
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
|
||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
||||
import { IToken } from 'types/antlrQueryTypes';
|
||||
|
||||
// Trace Operator Context Interface
|
||||
export interface ITraceOperatorContext {
|
||||
tokenType: number;
|
||||
text: string;
|
||||
start: number;
|
||||
stop: number;
|
||||
currentToken: string;
|
||||
isInAtom: boolean;
|
||||
isInOperator: boolean;
|
||||
isInParenthesis: boolean;
|
||||
isInExpression: boolean;
|
||||
atomToken?: string;
|
||||
operatorToken?: string;
|
||||
expressionPairs: ITraceExpressionPair[];
|
||||
currentPair?: ITraceExpressionPair | null;
|
||||
}
|
||||
|
||||
// Trace Expression Pair Interface
|
||||
export interface ITraceExpressionPair {
|
||||
leftAtom: string;
|
||||
operator: string;
|
||||
rightAtom?: string;
|
||||
rightExpression?: string;
|
||||
position: {
|
||||
leftStart: number;
|
||||
leftEnd: number;
|
||||
operatorStart: number;
|
||||
operatorEnd: number;
|
||||
rightStart?: number;
|
||||
rightEnd?: number;
|
||||
};
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
// Helper functions to determine token types
|
||||
function isAtomToken(tokenType: number): boolean {
|
||||
return tokenType === TraceOperatorGrammarLexer.IDENTIFIER;
|
||||
}
|
||||
|
||||
function isOperatorToken(tokenType: number): boolean {
|
||||
return [
|
||||
TraceOperatorGrammarLexer.T__2, // '=>'
|
||||
TraceOperatorGrammarLexer.T__3, // '&&'
|
||||
TraceOperatorGrammarLexer.T__4, // '||'
|
||||
TraceOperatorGrammarLexer.T__5, // 'NOT'
|
||||
TraceOperatorGrammarLexer.T__6, // '->'
|
||||
].includes(tokenType);
|
||||
}
|
||||
|
||||
function isParenthesisToken(tokenType: number): boolean {
|
||||
return (
|
||||
tokenType === TraceOperatorGrammarLexer.T__0 ||
|
||||
tokenType === TraceOperatorGrammarLexer.T__1
|
||||
);
|
||||
}
|
||||
|
||||
function isOpeningParenthesis(tokenType: number): boolean {
|
||||
return tokenType === TraceOperatorGrammarLexer.T__0;
|
||||
}
|
||||
|
||||
function isClosingParenthesis(tokenType: number): boolean {
|
||||
return tokenType === TraceOperatorGrammarLexer.T__1;
|
||||
}
|
||||
|
||||
// Function to create a context object
|
||||
export function createTraceOperatorContext(
|
||||
token: Token,
|
||||
isInAtom: boolean,
|
||||
isInOperator: boolean,
|
||||
isInParenthesis: boolean,
|
||||
isInExpression: boolean,
|
||||
atomToken?: string,
|
||||
operatorToken?: string,
|
||||
expressionPairs?: ITraceExpressionPair[],
|
||||
currentPair?: ITraceExpressionPair | null,
|
||||
): ITraceOperatorContext {
|
||||
return {
|
||||
tokenType: token.type,
|
||||
text: token.text || '',
|
||||
start: token.start,
|
||||
stop: token.stop,
|
||||
currentToken: token.text || '',
|
||||
isInAtom,
|
||||
isInOperator,
|
||||
isInParenthesis,
|
||||
isInExpression,
|
||||
atomToken,
|
||||
operatorToken,
|
||||
expressionPairs: expressionPairs || [],
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to determine token context
|
||||
function determineTraceTokenContext(
|
||||
token: IToken,
|
||||
): {
|
||||
isInAtom: boolean;
|
||||
isInOperator: boolean;
|
||||
isInParenthesis: boolean;
|
||||
isInExpression: boolean;
|
||||
} {
|
||||
const tokenType = token.type;
|
||||
|
||||
return {
|
||||
isInAtom: isAtomToken(tokenType),
|
||||
isInOperator: isOperatorToken(tokenType),
|
||||
isInParenthesis: isParenthesisToken(tokenType),
|
||||
isInExpression: false, // Will be determined by broader context
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all expression pairs from a trace operator query string
|
||||
* This parses the query according to the TraceOperatorGrammar.g4 grammar
|
||||
*
|
||||
* @param query The trace operator query string to parse
|
||||
* @returns An array of ITraceExpressionPair objects representing the expression pairs
|
||||
*/
|
||||
export function extractTraceExpressionPairs(
|
||||
query: string,
|
||||
): ITraceExpressionPair[] {
|
||||
try {
|
||||
const input = query || '';
|
||||
const chars = CharStreams.fromString(input);
|
||||
const lexer = new TraceOperatorGrammarLexer(chars);
|
||||
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
tokenStream.fill();
|
||||
|
||||
const allTokens = tokenStream.tokens as IToken[];
|
||||
const expressionPairs: ITraceExpressionPair[] = [];
|
||||
let currentPair: Partial<ITraceExpressionPair> | null = null;
|
||||
|
||||
let i = 0;
|
||||
while (i < allTokens.length) {
|
||||
const token = allTokens[i];
|
||||
i++;
|
||||
|
||||
// Skip EOF and whitespace tokens
|
||||
if (token.type === TraceOperatorGrammarLexer.EOF || token.channel !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If token is an IDENTIFIER (atom), start or continue a pair
|
||||
if (isAtomToken(token.type)) {
|
||||
// If we don't have a current pair, start one
|
||||
if (!currentPair) {
|
||||
currentPair = {
|
||||
leftAtom: token.text,
|
||||
position: {
|
||||
leftStart: token.start,
|
||||
leftEnd: token.stop,
|
||||
operatorStart: 0,
|
||||
operatorEnd: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
// If we have a current pair but no operator yet, this is still the left atom
|
||||
else if (!currentPair.operator && currentPair.position) {
|
||||
currentPair.leftAtom = token.text;
|
||||
currentPair.position.leftStart = token.start;
|
||||
currentPair.position.leftEnd = token.stop;
|
||||
}
|
||||
// If we have an operator, this is the right atom
|
||||
else if (
|
||||
currentPair.operator &&
|
||||
!currentPair.rightAtom &&
|
||||
currentPair.position
|
||||
) {
|
||||
currentPair.rightAtom = token.text;
|
||||
currentPair.position.rightStart = token.start;
|
||||
currentPair.position.rightEnd = token.stop;
|
||||
currentPair.isComplete = true;
|
||||
|
||||
// Add the completed pair to the result
|
||||
expressionPairs.push(currentPair as ITraceExpressionPair);
|
||||
currentPair = null;
|
||||
}
|
||||
}
|
||||
// If token is an operator and we have a left atom
|
||||
else if (
|
||||
isOperatorToken(token.type) &&
|
||||
currentPair &&
|
||||
currentPair.leftAtom &&
|
||||
currentPair.position
|
||||
) {
|
||||
currentPair.operator = token.text;
|
||||
currentPair.position.operatorStart = token.start;
|
||||
currentPair.position.operatorEnd = token.stop;
|
||||
|
||||
// If this is a NOT operator, it might be followed by another operator
|
||||
if (token.type === TraceOperatorGrammarLexer.T__5 && i < allTokens.length) {
|
||||
// Look ahead for the next operator
|
||||
const nextToken = allTokens[i];
|
||||
if (isOperatorToken(nextToken.type) && nextToken.channel === 0) {
|
||||
currentPair.operator = `${token.text} ${nextToken.text}`;
|
||||
currentPair.position.operatorEnd = nextToken.stop;
|
||||
i++; // Skip the next token since we've consumed it
|
||||
}
|
||||
}
|
||||
}
|
||||
// If token is an opening parenthesis after an operator, this is a right expression
|
||||
else if (
|
||||
isOpeningParenthesis(token.type) &&
|
||||
currentPair &&
|
||||
currentPair.operator &&
|
||||
!currentPair.rightAtom &&
|
||||
currentPair.position
|
||||
) {
|
||||
// Find the matching closing parenthesis
|
||||
let parenCount = 1;
|
||||
let j = i;
|
||||
let rightExpression = '';
|
||||
const rightStart = token.start;
|
||||
let rightEnd = token.stop;
|
||||
|
||||
while (j < allTokens.length && parenCount > 0) {
|
||||
const parenToken = allTokens[j];
|
||||
if (parenToken.channel === 0) {
|
||||
if (isOpeningParenthesis(parenToken.type)) {
|
||||
parenCount++;
|
||||
} else if (isClosingParenthesis(parenToken.type)) {
|
||||
parenCount--;
|
||||
if (parenCount === 0) {
|
||||
rightEnd = parenToken.stop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
rightExpression += parenToken.text;
|
||||
j++;
|
||||
}
|
||||
|
||||
if (parenCount === 0) {
|
||||
currentPair.rightExpression = rightExpression;
|
||||
currentPair.position.rightStart = rightStart;
|
||||
currentPair.position.rightEnd = rightEnd;
|
||||
currentPair.isComplete = true;
|
||||
|
||||
// Add the completed pair to the result
|
||||
expressionPairs.push(currentPair as ITraceExpressionPair);
|
||||
currentPair = null;
|
||||
|
||||
// Skip to the end of the expression
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining incomplete pair
|
||||
if (currentPair && currentPair.leftAtom && currentPair.position) {
|
||||
expressionPairs.push({
|
||||
...currentPair,
|
||||
isComplete: !!(currentPair.leftAtom && currentPair.operator),
|
||||
} as ITraceExpressionPair);
|
||||
}
|
||||
|
||||
return expressionPairs;
|
||||
} catch (error) {
|
||||
console.error('Error in extractTraceExpressionPairs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current expression pair at the cursor position
|
||||
*
|
||||
* @param expressionPairs An array of ITraceExpressionPair objects
|
||||
* @param query The full query string
|
||||
* @param cursorIndex The position of the cursor in the query
|
||||
* @returns The expression pair at the cursor position, or null if not found
|
||||
*/
|
||||
export function getCurrentTraceExpressionPair(
|
||||
expressionPairs: ITraceExpressionPair[],
|
||||
cursorIndex: number,
|
||||
): ITraceExpressionPair | null {
|
||||
try {
|
||||
if (expressionPairs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the rightmost pair whose end position is before or at the cursor
|
||||
let bestMatch: ITraceExpressionPair | null = null;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const pair of expressionPairs) {
|
||||
const { position } = pair;
|
||||
const pairEnd =
|
||||
position.rightEnd || position.operatorEnd || position.leftEnd;
|
||||
const pairStart = position.leftStart;
|
||||
|
||||
// If this pair ends at or before the cursor, and it's further right than our previous best match
|
||||
if (
|
||||
pairStart <= cursorIndex &&
|
||||
cursorIndex <= pairEnd + 1 &&
|
||||
(!bestMatch ||
|
||||
pairEnd >
|
||||
(bestMatch.position.rightEnd ||
|
||||
bestMatch.position.operatorEnd ||
|
||||
bestMatch.position.leftEnd))
|
||||
) {
|
||||
bestMatch = pair;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
} catch (error) {
|
||||
console.error('Error in getCurrentTraceExpressionPair:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current trace operator context at the cursor position
|
||||
* This is useful for determining what kind of suggestions to show
|
||||
*
|
||||
* @param query The trace operator query string
|
||||
* @param cursorIndex The position of the cursor in the query
|
||||
* @returns The trace operator context at the cursor position
|
||||
*/
|
||||
export function getTraceOperatorContextAtCursor(
|
||||
query: string,
|
||||
cursorIndex: number,
|
||||
): ITraceOperatorContext {
|
||||
try {
|
||||
// Guard against infinite recursion
|
||||
const stackTrace = new Error().stack || '';
|
||||
const callCount = (stackTrace.match(/getTraceOperatorContextAtCursor/g) || [])
|
||||
.length;
|
||||
if (callCount > 3) {
|
||||
console.warn(
|
||||
'Potential infinite recursion detected in getTraceOperatorContextAtCursor',
|
||||
);
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Create input stream and lexer
|
||||
const input = query || '';
|
||||
const chars = CharStreams.fromString(input);
|
||||
const lexer = new TraceOperatorGrammarLexer(chars);
|
||||
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
tokenStream.fill();
|
||||
|
||||
const allTokens = tokenStream.tokens as IToken[];
|
||||
|
||||
// Get expression pairs information
|
||||
const expressionPairs = extractTraceExpressionPairs(query);
|
||||
const currentPair = getCurrentTraceExpressionPair(
|
||||
expressionPairs,
|
||||
cursorIndex,
|
||||
);
|
||||
|
||||
// Find the token at or just before the cursor
|
||||
let lastTokenBeforeCursor: IToken | null = null;
|
||||
for (let i = 0; i < allTokens.length; i++) {
|
||||
const token = allTokens[i];
|
||||
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
|
||||
|
||||
if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) {
|
||||
lastTokenBeforeCursor = token;
|
||||
}
|
||||
|
||||
if (token.start > cursorIndex) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find exact token at cursor
|
||||
let exactToken: IToken | null = null;
|
||||
for (let i = 0; i < allTokens.length; i++) {
|
||||
const token = allTokens[i];
|
||||
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
|
||||
|
||||
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
|
||||
exactToken = token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have any tokens, return default context
|
||||
if (!lastTokenBeforeCursor && !exactToken) {
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true, // Default to atom context when input is empty
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs,
|
||||
currentPair: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if cursor is at a space after a token (transition point)
|
||||
const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' ';
|
||||
const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' ';
|
||||
const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' ';
|
||||
const isTransitionPoint =
|
||||
(isAtSpace && isAfterToken) ||
|
||||
(cursorIndex === query.length && isAfterToken);
|
||||
|
||||
// If we're at a transition point after a token, progress the context
|
||||
if (
|
||||
lastTokenBeforeCursor &&
|
||||
(isAtSpace || isAfterSpace || isTransitionPoint)
|
||||
) {
|
||||
const lastTokenContext = determineTraceTokenContext(lastTokenBeforeCursor);
|
||||
|
||||
// Apply context progression: atom → operator → atom/expression → operator → atom
|
||||
if (lastTokenContext.isInAtom) {
|
||||
// After atom + space, move to operator context
|
||||
return {
|
||||
tokenType: lastTokenBeforeCursor.type,
|
||||
text: lastTokenBeforeCursor.text,
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: lastTokenBeforeCursor.text,
|
||||
isInAtom: false,
|
||||
isInOperator: true,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
atomToken: lastTokenBeforeCursor.text,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastTokenContext.isInOperator) {
|
||||
// After operator + space, move to atom/expression context
|
||||
return {
|
||||
tokenType: lastTokenBeforeCursor.type,
|
||||
text: lastTokenBeforeCursor.text,
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: lastTokenBeforeCursor.text,
|
||||
isInAtom: true, // Expecting an atom or expression after operator
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
operatorToken: lastTokenBeforeCursor.text,
|
||||
atomToken: currentPair?.leftAtom,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lastTokenContext.isInParenthesis &&
|
||||
isClosingParenthesis(lastTokenBeforeCursor.type)
|
||||
) {
|
||||
// After closing parenthesis, move to operator context
|
||||
return {
|
||||
tokenType: lastTokenBeforeCursor.type,
|
||||
text: lastTokenBeforeCursor.text,
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: lastTokenBeforeCursor.text,
|
||||
isInAtom: false,
|
||||
isInOperator: true,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If cursor is at the end of a token, return the current token context
|
||||
if (exactToken && cursorIndex === exactToken.stop + 1) {
|
||||
const tokenContext = determineTraceTokenContext(exactToken);
|
||||
|
||||
return {
|
||||
tokenType: exactToken.type,
|
||||
text: exactToken.text,
|
||||
start: exactToken.start,
|
||||
stop: exactToken.stop,
|
||||
currentToken: exactToken.text,
|
||||
...tokenContext,
|
||||
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
|
||||
operatorToken: tokenContext.isInOperator
|
||||
? exactToken.text
|
||||
: currentPair?.operator,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
// Regular token-based context detection
|
||||
if (exactToken?.channel === 0) {
|
||||
const tokenContext = determineTraceTokenContext(exactToken);
|
||||
|
||||
return {
|
||||
tokenType: exactToken.type,
|
||||
text: exactToken.text,
|
||||
start: exactToken.start,
|
||||
stop: exactToken.stop,
|
||||
currentToken: exactToken.text,
|
||||
...tokenContext,
|
||||
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
|
||||
operatorToken: tokenContext.isInOperator
|
||||
? exactToken.text
|
||||
: currentPair?.operator,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
}
|
||||
|
||||
// Default fallback to atom context
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs,
|
||||
currentPair,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getTraceOperatorContextAtCursor:', error);
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: '',
|
||||
isInAtom: true,
|
||||
isInOperator: false,
|
||||
isInParenthesis: false,
|
||||
isInExpression: false,
|
||||
expressionPairs: [],
|
||||
currentPair: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getInvolvedQueriesInTraceOperator = (
|
||||
traceOperators: IBuilderTraceOperator[],
|
||||
): string[] => {
|
||||
if (
|
||||
!traceOperators ||
|
||||
traceOperators.length === 0 ||
|
||||
traceOperators.length > 1
|
||||
)
|
||||
return [];
|
||||
|
||||
const currentTraceOperator = traceOperators[0];
|
||||
|
||||
// Match any word starting with letter or underscore
|
||||
const tokens =
|
||||
currentTraceOperator.expression.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || [];
|
||||
|
||||
// Filter out operator keywords
|
||||
const operators = new Set(['NOT']);
|
||||
return tokens.filter((t) => !operators.has(t));
|
||||
};
|
||||
@@ -1,16 +1,10 @@
|
||||
/* 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';
|
||||
@@ -775,200 +769,3 @@ 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, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
@@ -580,25 +580,14 @@ export const convertHavingToExpression = (
|
||||
* @returns New aggregation format based on data source
|
||||
*
|
||||
*/
|
||||
export const convertAggregationToExpression = ({
|
||||
aggregateOperator,
|
||||
aggregateAttribute,
|
||||
dataSource,
|
||||
timeAggregation,
|
||||
spaceAggregation,
|
||||
alias,
|
||||
reduceTo,
|
||||
temporality,
|
||||
}: {
|
||||
aggregateOperator: string;
|
||||
aggregateAttribute: BaseAutocompleteData;
|
||||
dataSource: DataSource;
|
||||
timeAggregation?: string;
|
||||
spaceAggregation?: string;
|
||||
alias?: string;
|
||||
reduceTo?: ReduceOperators;
|
||||
temporality?: string;
|
||||
}): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||
export const convertAggregationToExpression = (
|
||||
aggregateOperator: string,
|
||||
aggregateAttribute: BaseAutocompleteData,
|
||||
dataSource: DataSource,
|
||||
timeAggregation?: string,
|
||||
spaceAggregation?: string,
|
||||
alias?: string,
|
||||
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||
// Skip if no operator or attribute key
|
||||
if (!aggregateOperator) {
|
||||
return undefined;
|
||||
@@ -616,9 +605,7 @@ export const convertAggregationToExpression = ({
|
||||
if (dataSource === DataSource.METRICS) {
|
||||
return [
|
||||
{
|
||||
metricName: aggregateAttribute?.key || '',
|
||||
reduceTo,
|
||||
temporality,
|
||||
metricName: aggregateAttribute.key,
|
||||
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
||||
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
||||
} as MetricAggregation,
|
||||
@@ -626,9 +613,7 @@ export const convertAggregationToExpression = ({
|
||||
}
|
||||
|
||||
// For traces and logs, use expression format
|
||||
const expression = aggregateAttribute?.key
|
||||
? `${normalizedOperator}(${aggregateAttribute?.key})`
|
||||
: `${normalizedOperator}()`;
|
||||
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return [
|
||||
|
||||
@@ -17,19 +17,6 @@
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.view-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
&:hover {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { RadioChangeEvent } from 'antd/es/radio';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SignozRadioGroupProps {
|
||||
@@ -38,10 +37,7 @@ function SignozRadioGroup({
|
||||
value={option.value}
|
||||
className={value === option.value ? 'selected_view tab' : 'tab'}
|
||||
>
|
||||
<div className="view-title-container">
|
||||
{option.icon && <div className="icon-container">{option.icon}</div>}
|
||||
{option.label}
|
||||
</div>
|
||||
{option.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
@@ -17,27 +17,6 @@ export const OPERATORS = {
|
||||
'<': '<',
|
||||
};
|
||||
|
||||
export const TRACE_OPERATOR_OPERATORS = {
|
||||
AND: '&&',
|
||||
OR: '||',
|
||||
NOT: 'NOT',
|
||||
DIRECT_DESCENDENT: '=>',
|
||||
INDIRECT_DESCENDENT: '->',
|
||||
};
|
||||
|
||||
export const TRACE_OPERATOR_OPERATORS_WITH_PRIORITY = {
|
||||
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 1,
|
||||
[TRACE_OPERATOR_OPERATORS.AND]: 2,
|
||||
[TRACE_OPERATOR_OPERATORS.OR]: 3,
|
||||
[TRACE_OPERATOR_OPERATORS.NOT]: 4,
|
||||
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 5,
|
||||
};
|
||||
|
||||
export const TRACE_OPERATOR_OPERATORS_LABELS = {
|
||||
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 'Direct Descendant',
|
||||
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 'Indirect Descendant',
|
||||
};
|
||||
|
||||
export const QUERY_BUILDER_FUNCTIONS = {
|
||||
HAS: 'has',
|
||||
HASANY: 'hasAny',
|
||||
|
||||
@@ -32,7 +32,6 @@ 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,7 +12,6 @@ import {
|
||||
HavingForm,
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
@@ -51,8 +50,6 @@ 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 = '__';
|
||||
|
||||
@@ -266,11 +263,6 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
|
||||
legend: '',
|
||||
};
|
||||
|
||||
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
|
||||
...initialQueryBuilderFormTracesValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
};
|
||||
|
||||
export const initialQueryPromQLData: IPromQLQuery = {
|
||||
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||
query: '',
|
||||
@@ -288,7 +280,6 @@ export const initialClickHouseData: IClickHouseQuery = {
|
||||
export const initialQueryBuilderData: QueryBuilderData = {
|
||||
queryData: [initialQueryBuilderFormValues],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
|
||||
export const initialSingleQueryMap: Record<
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TRACE_OPERATOR_QUERY_NAME } from './queryBuilder';
|
||||
|
||||
export const FORMULA_REGEXP = /F\d+/;
|
||||
|
||||
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
|
||||
@@ -7,5 +5,3 @@ export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
|
||||
export const TYPE_ADDON_REGEXP = /_(.+)/;
|
||||
|
||||
export const SPLIT_FIRST_UNDERSCORE = /(?<!^)_/;
|
||||
|
||||
export const TRACE_OPERATOR_REGEXP = new RegExp(TRACE_OPERATOR_QUERY_NAME);
|
||||
|
||||
@@ -2,5 +2,4 @@ export const USER_PREFERENCES = {
|
||||
SIDENAV_PINNED: 'sidenav_pinned',
|
||||
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||
LAST_SEEN_CHANGELOG_VERSION: 'last_seen_changelog_version',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES: 'span_details_pinned_attributes',
|
||||
};
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
&__alert-history-graph {
|
||||
&__graph {
|
||||
margin-top: 80px;
|
||||
|
||||
.alert-history-graph {
|
||||
.graph {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
@@ -135,8 +135,8 @@ function StatsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="stats-card__alert-history-graph">
|
||||
<div className="alert-history-graph">
|
||||
<div className="stats-card__graph">
|
||||
<div className="graph">
|
||||
{!isEmpty && timeSeries.length > 1 && (
|
||||
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
|
||||
)}
|
||||
|
||||
@@ -507,7 +507,6 @@ export const getDomainMetricsQueryPayload = (
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -817,7 +816,6 @@ export const getEndPointsQueryPayload = (
|
||||
legend: 'error percentage',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -967,7 +965,6 @@ export const getTopErrorsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1732,7 +1729,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
legend: 'error percentage',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1932,7 +1928,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2021,7 +2016,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2293,7 +2287,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
legend: 'error percentage',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2383,7 +2376,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2472,7 +2464,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2567,7 +2558,6 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -3145,7 +3135,6 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -54,7 +54,6 @@ function QuerySection({
|
||||
queryVariant: 'static',
|
||||
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||
}}
|
||||
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
|
||||
showFunctions={
|
||||
(alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||
alertDef.version === ENTITY_VERSION_V4) ||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -150,17 +149,10 @@ function FormAlertRules({
|
||||
]);
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
|
||||
currentQuery.builder.queryTraceOperator,
|
||||
);
|
||||
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||
[EQueryType.QUERY_BUILDER]: () => [
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData)?.filter(
|
||||
(option) =>
|
||||
!involvedQueriesInTraceOperator.includes(option.value as string),
|
||||
) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryTraceOperator) || []),
|
||||
],
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
|
||||
@@ -5,7 +5,6 @@ import getStep from 'lib/getStep';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderTraceOperator,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -54,11 +53,7 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
||||
|
||||
export const getSelectedQueryOptions = (
|
||||
queries: Array<
|
||||
| IBuilderQuery
|
||||
| IBuilderTraceOperator
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery
|
||||
IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery
|
||||
>,
|
||||
): SelectProps['options'] =>
|
||||
queries
|
||||
|
||||
@@ -2,30 +2,22 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
|
||||
import setRetentionApi from 'api/settings/setRetention';
|
||||
import setRetentionApiV2 from 'api/settings/setRetentionV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import find from 'lodash-es/find';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useInterval } from 'react-use';
|
||||
import {
|
||||
ErrorResponse,
|
||||
ErrorResponseV2,
|
||||
SuccessResponse,
|
||||
SuccessResponseV2,
|
||||
} from 'types/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IDiskType,
|
||||
PayloadProps as GetDisksPayload,
|
||||
} from 'types/api/disks/getDisks';
|
||||
import APIError from 'types/api/error';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import {
|
||||
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
|
||||
@@ -135,7 +127,7 @@ function GeneralSettings({
|
||||
|
||||
useEffect(() => {
|
||||
if (logsCurrentTTLValues) {
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.default_ttl_days * 24);
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
|
||||
setLogsS3RetentionPeriod(
|
||||
logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
@@ -344,40 +336,20 @@ function GeneralSettings({
|
||||
}
|
||||
try {
|
||||
onPostApiLoadingHandler(type);
|
||||
const setTTLResponse = await setRetentionApi({
|
||||
type,
|
||||
totalDuration: `${apiCallTotalRetention || -1}h`,
|
||||
coldStorage: s3Enabled ? 's3' : null,
|
||||
toColdDuration: `${apiCallS3Retention || -1}h`,
|
||||
});
|
||||
let hasSetTTLFailed = false;
|
||||
|
||||
try {
|
||||
if (type === 'logs') {
|
||||
await setRetentionApiV2({
|
||||
type,
|
||||
defaultTTLDays: apiCallTotalRetention ? apiCallTotalRetention / 24 : -1, // convert Hours to days
|
||||
coldStorageVolume: '',
|
||||
coldStorageDuration: 0,
|
||||
ttlConditions: [],
|
||||
});
|
||||
} else {
|
||||
await setRetentionApi({
|
||||
type,
|
||||
totalDuration: `${apiCallTotalRetention || -1}h`,
|
||||
coldStorage: s3Enabled ? 's3' : null,
|
||||
toColdDuration: `${apiCallS3Retention || -1}h`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (setTTLResponse.statusCode === 409) {
|
||||
hasSetTTLFailed = true;
|
||||
if ((error as APIError).getHttpStatusCode() === StatusCodes.CONFLICT) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_request_race_condition'),
|
||||
placement: 'topRight',
|
||||
});
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
placement: 'topRight',
|
||||
});
|
||||
}
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_request_race_condition'),
|
||||
placement: 'topRight',
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'metrics') {
|
||||
@@ -404,14 +376,11 @@ function GeneralSettings({
|
||||
logsTtlValuesRefetch();
|
||||
if (!hasSetTTLFailed)
|
||||
// Updates the currentTTL Values in order to avoid pushing the same values.
|
||||
setLogsCurrentTTLValues((prev) => ({
|
||||
...prev,
|
||||
setLogsCurrentTTLValues({
|
||||
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
|
||||
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
|
||||
default_ttl_days: logsTotalRetentionPeriod
|
||||
? logsTotalRetentionPeriod / 24 // convert Hours to days
|
||||
: -1,
|
||||
}));
|
||||
status: '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
@@ -430,7 +399,6 @@ function GeneralSettings({
|
||||
const renderConfig = [
|
||||
{
|
||||
name: 'Metrics',
|
||||
type: 'metrics',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -472,7 +440,6 @@ function GeneralSettings({
|
||||
},
|
||||
{
|
||||
name: 'Traces',
|
||||
type: 'traces',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -512,7 +479,6 @@ function GeneralSettings({
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
type: 'logs',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -571,7 +537,6 @@ function GeneralSettings({
|
||||
/>
|
||||
{category.retentionFields.map((retentionField) => (
|
||||
<Retention
|
||||
type={category.type as TTTLType}
|
||||
key={retentionField.name}
|
||||
text={retentionField.name}
|
||||
retentionValue={retentionField.value}
|
||||
@@ -660,7 +625,7 @@ interface GeneralSettingsProps {
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
|
||||
>['refetch'];
|
||||
logsTtlValuesRefetch: UseQueryResult<
|
||||
ErrorResponseV2 | SuccessResponseV2<GetRetentionPeriodLogsPayload>
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
|
||||
>['refetch'];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@@ -21,13 +20,11 @@ import {
|
||||
convertHoursValueToRelevantUnit,
|
||||
SettingPeriod,
|
||||
TimeUnits,
|
||||
TimeUnitsValues,
|
||||
} from './utils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function Retention({
|
||||
type,
|
||||
retentionValue,
|
||||
setRetentionValue,
|
||||
text,
|
||||
@@ -53,9 +50,7 @@ function Retention({
|
||||
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
|
||||
}, [initialTimeUnitValue]);
|
||||
|
||||
const menuItems = TimeUnits.filter((option) =>
|
||||
type === 'logs' ? option.value !== TimeUnitsValues.hr : true,
|
||||
).map((option) => (
|
||||
const menuItems = TimeUnits.map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.key}
|
||||
</Option>
|
||||
@@ -129,7 +124,6 @@ function Retention({
|
||||
}
|
||||
|
||||
interface RetentionProps {
|
||||
type: TTTLType;
|
||||
retentionValue: number | null;
|
||||
text: string;
|
||||
setRetentionValue: Dispatch<SetStateAction<number | null>>;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Typography } from 'antd';
|
||||
import getDisks from 'api/disks/getDisks';
|
||||
import getRetentionPeriodApi from 'api/settings/getRetention';
|
||||
import getRetentionPeriodApiV2 from 'api/settings/getRetentionV2';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
|
||||
|
||||
@@ -17,10 +15,6 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
|
||||
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
|
||||
>;
|
||||
|
||||
type TRetentionAPIReturnV2<T extends TTTLType> = Promise<
|
||||
SuccessResponseV2<GetRetentionPeriodAPIPayloadProps<T>>
|
||||
>;
|
||||
|
||||
function GeneralSettings(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
const { user } = useAppContext();
|
||||
@@ -42,7 +36,7 @@ function GeneralSettings(): JSX.Element {
|
||||
queryKey: ['getRetentionPeriodApiTraces', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
queryFn: (): TRetentionAPIReturnV2<'logs'> => getRetentionPeriodApiV2(), // Only works for logs
|
||||
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
|
||||
queryKey: ['getRetentionPeriodApiLogs', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
@@ -76,7 +70,7 @@ function GeneralSettings(): JSX.Element {
|
||||
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
|
||||
return (
|
||||
<Typography>
|
||||
{(getRetentionPeriodLogsApiResponse.error as APIError).getErrorMessage() ||
|
||||
{getRetentionPeriodLogsApiResponse.data?.error ||
|
||||
getDisksResponse.data?.error ||
|
||||
t('something_went_wrong')}
|
||||
</Typography>
|
||||
@@ -92,7 +86,7 @@ function GeneralSettings(): JSX.Element {
|
||||
getRetentionPeriodTracesApiResponse.isLoading ||
|
||||
!getRetentionPeriodTracesApiResponse.data?.payload ||
|
||||
getRetentionPeriodLogsApiResponse.isLoading ||
|
||||
!getRetentionPeriodLogsApiResponse.data?.data
|
||||
!getRetentionPeriodLogsApiResponse.data?.payload
|
||||
) {
|
||||
return <Spinner tip="Loading.." height="70vh" />;
|
||||
}
|
||||
@@ -105,7 +99,7 @@ function GeneralSettings(): JSX.Element {
|
||||
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
||||
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
||||
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.data,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
|
||||
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,26 +5,19 @@ export interface ITimeUnit {
|
||||
key: string;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
export enum TimeUnitsValues {
|
||||
hr = 'hr',
|
||||
day = 'day',
|
||||
month = 'month',
|
||||
}
|
||||
|
||||
export const TimeUnits: ITimeUnit[] = [
|
||||
{
|
||||
value: TimeUnitsValues.hr,
|
||||
value: 'hr',
|
||||
key: 'Hours',
|
||||
multiplier: 1,
|
||||
},
|
||||
{
|
||||
value: TimeUnitsValues.day,
|
||||
value: 'day',
|
||||
key: 'Days',
|
||||
multiplier: 1 / 24,
|
||||
},
|
||||
{
|
||||
value: TimeUnitsValues.month,
|
||||
value: 'month',
|
||||
key: 'Months',
|
||||
multiplier: 1 / (24 * 30),
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.full-view-graph-container {
|
||||
.list-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,
|
||||
'full-view-graph-container': isListView || isTablePanel,
|
||||
'list-graph-container': isListView,
|
||||
})}
|
||||
ref={fullViewRef}
|
||||
>
|
||||
|
||||
@@ -90,7 +90,6 @@ const mockProps: WidgetGraphComponentProps = {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -325,7 +325,6 @@ function WidgetGraphComponent({
|
||||
setHovered(false);
|
||||
}}
|
||||
id={widget.id}
|
||||
className="widget-graph-component-container"
|
||||
>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
@@ -397,10 +396,7 @@ function WidgetGraphComponent({
|
||||
)}
|
||||
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
|
||||
<div
|
||||
className={cx(
|
||||
'widget-graph-container',
|
||||
`${widget.panelTypes}-panel-container`,
|
||||
)}
|
||||
className={cx('widget-graph-container', widget.panelTypes)}
|
||||
ref={graphRef}
|
||||
>
|
||||
<PanelWrapper
|
||||
|
||||
@@ -41,11 +41,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
&.graph-panel-container {
|
||||
height: 100%;
|
||||
}
|
||||
.widget-graph-container {
|
||||
&.graph {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,13 +89,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-component-container {
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
|
||||
&.graph-panel-container {
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
&.graph {
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,6 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
promql: [],
|
||||
@@ -172,7 +171,6 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -197,7 +195,6 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -243,7 +240,6 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -272,7 +268,6 @@ describe('GridCardLayout Utils', () => {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
export const tableDataMultipleQueriesSuccessResponse = {
|
||||
columns: [
|
||||
{
|
||||
@@ -162,7 +161,6 @@ export const widgetQueryWithLegend = {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '48ad5a67-9a3c-49d4-a886-d7a34f8b875d',
|
||||
queryType: 'builder',
|
||||
@@ -212,279 +210,3 @@ 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,11 +6,8 @@ import {
|
||||
sortFunction,
|
||||
} from '../utils';
|
||||
import {
|
||||
expectedOutputQBv5MultiAggregations,
|
||||
expectedOutputWithLegends,
|
||||
tableDataMultipleQueriesSuccessResponse,
|
||||
tableDataQBv5MultiAggregations,
|
||||
widgetQueryQBv5MultiAggregations,
|
||||
widgetQueryWithLegend,
|
||||
} from './response';
|
||||
|
||||
@@ -70,7 +67,6 @@ 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);
|
||||
@@ -132,96 +128,3 @@ 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,14 +150,11 @@ 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[`${colId}_without_unit`] ?? a[colId] ?? a[colName]);
|
||||
const valueB = Number(b[`${colId}_without_unit`] ?? b[colId] ?? b[colName]);
|
||||
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
|
||||
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
|
||||
|
||||
// if both the values are numbers then return the difference here
|
||||
if (!isNaN(valueA) && !isNaN(valueB)) {
|
||||
@@ -175,11 +172,10 @@ export function sortFunction(
|
||||
}
|
||||
|
||||
// if both of them are strings do the localecompare
|
||||
return ((a[colId] as string) || (a[colName] as string) || '').localeCompare(
|
||||
(b[colId] as string) || (b[colName] as string) || '',
|
||||
return ((a[item.name] as string) || '').localeCompare(
|
||||
(b[item.name] as string) || '',
|
||||
);
|
||||
}
|
||||
|
||||
export function createColumnsAndDataSource(
|
||||
data: TableData,
|
||||
currentQuery: Query,
|
||||
@@ -202,7 +198,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.id],
|
||||
render: renderColumnCell && renderColumnCell[item.name],
|
||||
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
||||
};
|
||||
|
||||
|
||||
@@ -301,7 +301,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -491,7 +490,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -577,7 +575,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -663,7 +660,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -801,7 +797,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1055,7 +1050,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1263,7 +1257,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1529,7 +1522,6 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -233,7 +233,6 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -417,7 +416,6 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -514,7 +512,6 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -611,7 +608,6 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -196,7 +196,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -347,7 +346,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -433,7 +431,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -519,7 +516,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -79,7 +79,6 @@ export const getEntityEventsOrLogsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
@@ -227,7 +226,6 @@ export const getEntityTracesQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
|
||||
@@ -108,7 +108,6 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -192,7 +191,6 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -289,7 +287,6 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -386,7 +383,6 @@ export const getJobMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -309,7 +309,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -577,7 +576,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -657,7 +655,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -737,7 +734,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -823,7 +819,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -909,7 +904,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1081,7 +1075,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1219,7 +1212,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1437,7 +1429,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1570,7 +1561,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -341,7 +341,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -648,7 +647,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -812,7 +810,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -976,7 +973,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1056,7 +1052,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1136,7 +1131,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1222,7 +1216,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1308,7 +1301,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1459,7 +1451,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1578,7 +1569,6 @@ export const getNodeMetricsQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -335,7 +335,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -669,7 +668,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -853,7 +851,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1187,7 +1184,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1328,7 +1324,6 @@ export const getPodMetricsQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1412,7 +1407,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1503,7 +1497,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1721,7 +1714,6 @@ export const getPodMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -1926,7 +1918,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2144,7 +2135,6 @@ export const getPodMetricsQueryPayload = (
|
||||
queryName: 'F2',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2241,7 +2231,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2338,7 +2327,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
@@ -2522,7 +2510,6 @@ export const getPodMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
|
||||
@@ -246,7 +246,6 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -366,7 +365,6 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -536,7 +534,6 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -656,7 +653,6 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -739,7 +735,6 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -822,7 +817,6 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
|
||||
@@ -148,7 +148,6 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -240,7 +239,6 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -332,7 +330,6 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -424,7 +421,6 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
@@ -516,7 +512,6 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: v4(),
|
||||
|
||||
@@ -774,13 +774,6 @@ 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 } from './styles';
|
||||
import { SpinnerWrapper, Wrapper } from './styles';
|
||||
|
||||
function ListViewPanel(): JSX.Element {
|
||||
const { config } = useOptionsMenu({
|
||||
@@ -42,7 +42,7 @@ function ListViewPanel(): JSX.Element {
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div className="live-logs-settings-panel">
|
||||
<Wrapper>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
@@ -68,7 +68,7 @@ function ListViewPanel(): JSX.Element {
|
||||
<Spinner style={{ height: 'auto' }} />
|
||||
</SpinnerWrapper>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.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,89 +1,46 @@
|
||||
import './LiveLogsContainer.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { Col } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import FiltersInput from 'container/LiveLogs/FiltersInput';
|
||||
import LiveLogsTopNav from 'container/LiveLogsTopNav';
|
||||
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 { Sliders } from 'lucide-react';
|
||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
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 { 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<ILiveLogsLog[]>([]);
|
||||
const { currentQuery, stagedQuery } = useQueryBuilder();
|
||||
const [showLiveLogsFrequencyChart, setShowLiveLogsFrequencyChart] = useState(
|
||||
true,
|
||||
);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const queryLocationState = location.state as QueryHistoryState;
|
||||
|
||||
const batchedEventsRef = useRef<ILiveLogsLog[]>([]);
|
||||
const batchedEventsRef = useRef<ILog[]>([]);
|
||||
|
||||
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 { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const {
|
||||
handleStartOpenConnection,
|
||||
@@ -96,7 +53,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const updateLogs = useCallback((newLogs: ILiveLogsLog[]) => {
|
||||
const updateLogs = useCallback((newLogs: ILog[]) => {
|
||||
setLogs((prevState) =>
|
||||
[...newLogs, ...prevState].slice(0, MAX_LOGS_LIST_SIZE),
|
||||
);
|
||||
@@ -110,7 +67,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
}, 500);
|
||||
|
||||
const batchLiveLog = useCallback(
|
||||
(log: ILiveLogsLog): void => {
|
||||
(log: ILog): void => {
|
||||
batchedEventsRef.current.push(log);
|
||||
|
||||
debouncedUpdateLogs();
|
||||
@@ -120,7 +77,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
|
||||
const handleGetLiveLogs = useCallback(
|
||||
(event: MessageEvent<string>) => {
|
||||
const data: ILiveLogsLog = JSON.parse(event?.data);
|
||||
const data: ILog = JSON.parse(event.data);
|
||||
|
||||
batchLiveLog(data);
|
||||
},
|
||||
@@ -134,65 +91,72 @@ function LiveLogsContainer(): JSX.Element {
|
||||
useEventSourceEvent('message', handleGetLiveLogs);
|
||||
useEventSourceEvent('error', handleError);
|
||||
|
||||
const openConnection = useCallback(
|
||||
(filterExpression?: string | null) => {
|
||||
handleStartOpenConnection(filterExpression || '');
|
||||
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;
|
||||
},
|
||||
[handleStartOpenConnection],
|
||||
[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 });
|
||||
},
|
||||
[globalSelectedTime, handleStartOpenConnection],
|
||||
);
|
||||
|
||||
const handleStartNewConnection = useCallback(
|
||||
(filterExpression?: string | null) => {
|
||||
(query: Query) => {
|
||||
handleCloseConnection();
|
||||
|
||||
openConnection(filterExpression);
|
||||
const preparedQuery = getPreparedQuery(query);
|
||||
|
||||
openConnection(preparedQuery);
|
||||
},
|
||||
[handleCloseConnection, openConnection],
|
||||
[getPreparedQuery, handleCloseConnection, openConnection],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
if (!compositeQuery) return;
|
||||
|
||||
// Check if filterExpression has actually changed
|
||||
if (
|
||||
!prevFilterExpressionRef.current ||
|
||||
prevFilterExpressionRef.current !== currentFilterExpression
|
||||
(initialLoading && !isConnectionLoading) ||
|
||||
compositeQuery.id !== stagedQuery?.id
|
||||
) {
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
setLogs([]);
|
||||
batchedEventsRef.current = [];
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
}
|
||||
|
||||
prevFilterExpressionRef.current = currentFilterExpression || null;
|
||||
handleStartNewConnection(compositeQuery);
|
||||
}
|
||||
}, [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]);
|
||||
}, [
|
||||
compositeQuery,
|
||||
initialLoading,
|
||||
stagedQuery,
|
||||
isConnectionLoading,
|
||||
openConnection,
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
useEffect((): (() => void) | undefined => {
|
||||
if (isConnectionError && reconnectDueToError) {
|
||||
if (isConnectionError && reconnectDueToError && compositeQuery) {
|
||||
// Small delay to prevent immediate reconnection attempts
|
||||
const reconnectTimer = setTimeout(() => {
|
||||
handleStartNewConnection();
|
||||
handleStartNewConnection(compositeQuery);
|
||||
}, 1000);
|
||||
|
||||
return (): void => clearTimeout(reconnectTimer);
|
||||
@@ -205,70 +169,50 @@ function LiveLogsContainer(): JSX.Element {
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
// clean up the connection when the component unmounts
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
handleCloseConnection();
|
||||
},
|
||||
[handleCloseConnection],
|
||||
);
|
||||
useEffect(() => {
|
||||
const prefetchedList = queryLocationState?.listQueryPayload[0]?.list;
|
||||
|
||||
const handleToggleFrequencyChart = useCallback(() => {
|
||||
setShowLiveLogsFrequencyChart(!showLiveLogsFrequencyChart);
|
||||
}, [showLiveLogsFrequencyChart]);
|
||||
if (prefetchedList) {
|
||||
const prefetchedLogs: ILog[] = prefetchedList
|
||||
.map((item) => ({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
updateLogs(prefetchedLogs);
|
||||
}
|
||||
}, [queryLocationState, updateLogs]);
|
||||
|
||||
return (
|
||||
<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}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{showLiveLogsFrequencyChart && (
|
||||
<div className="live-logs-chart-container">
|
||||
<LiveLogsListChart
|
||||
initialData={queryLocationState?.graphQueryPayload || null}
|
||||
className="live-logs-chart"
|
||||
isShowingLiveLogs
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ListViewPanel />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<LiveLogsList logs={logs} />
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="live-logs-list-container">
|
||||
<LiveLogsList
|
||||
logs={logs}
|
||||
isLoading={initialLoading && logs.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GoToTop />
|
||||
</div>
|
||||
<GoToTop />
|
||||
</ContentWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
.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,23 +1,24 @@
|
||||
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';
|
||||
@@ -25,9 +26,11 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { LiveLogsListProps } from './types';
|
||||
|
||||
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const { t } = useTranslation(['logs']);
|
||||
|
||||
const { isConnectionLoading } = useEventSource();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
@@ -40,12 +43,6 @@ function LiveLogsList({ logs, isLoading }: 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,
|
||||
@@ -53,8 +50,8 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
});
|
||||
|
||||
const activeLogIndex = useMemo(
|
||||
() => formattedLogs.findIndex(({ id }) => id === activeLogId),
|
||||
[formattedLogs, activeLogId],
|
||||
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields([
|
||||
@@ -108,39 +105,30 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
});
|
||||
}, [activeLogId, activeLogIndex]);
|
||||
|
||||
const isLoadingList = isConnectionLoading && formattedLogs.length === 0;
|
||||
const isLoadingList = isConnectionLoading && logs.length === 0;
|
||||
|
||||
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
|
||||
[],
|
||||
);
|
||||
if (isLoadingList) {
|
||||
return <Spinner style={{ height: 'auto' }} tip="Fetching Logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="live-logs-list">
|
||||
{(formattedLogs.length === 0 || isLoading || isLoadingList) &&
|
||||
renderLoading()}
|
||||
<>
|
||||
{options.format !== OptionFormatTypes.TABLE && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{formattedLogs.length !== 0 && (
|
||||
{logs.length === 0 && <Typography>{t('fetching_log_lines')}</Typography>}
|
||||
|
||||
{logs.length !== 0 && (
|
||||
<InfinityWrapperStyled>
|
||||
{options.format === OptionFormatTypes.TABLE ? (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={false}
|
||||
tableViewProps={{
|
||||
logs: formattedLogs,
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
@@ -154,8 +142,8 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={formattedLogs}
|
||||
totalCount={formattedLogs.length}
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
@@ -163,18 +151,15 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
)}
|
||||
</InfinityWrapperStyled>
|
||||
)}
|
||||
|
||||
{activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface ILiveLogsLog {
|
||||
data: ILog[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type LiveLogsListProps = {
|
||||
logs: ILiveLogsLog[];
|
||||
isLoading: boolean;
|
||||
logs: ILog[];
|
||||
};
|
||||
|
||||
@@ -9,33 +9,24 @@ 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 { currentQuery } = useQueryBuilder();
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { isConnectionOpen } = useEventSource();
|
||||
|
||||
const listChartQuery: Query | null = useMemo(() => {
|
||||
if (!currentQuery) return null;
|
||||
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (!validationResult.isValid) return null;
|
||||
if (!stagedQuery) return null;
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
...stagedQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...stagedQuery.builder,
|
||||
queryData: stagedQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
disabled: false,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
@@ -48,7 +39,7 @@ function LiveLogsListChart({
|
||||
})),
|
||||
},
|
||||
};
|
||||
}, [currentQuery]);
|
||||
}, [stagedQuery]);
|
||||
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
@@ -71,15 +62,12 @@ function LiveLogsListChart({
|
||||
}, [data, initialData]);
|
||||
|
||||
return (
|
||||
<div className="live-logs-chart-container">
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
isLogsExplorerViews={isShowingLiveLogs}
|
||||
/>
|
||||
</div>
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,4 @@ import { QueryData } from 'types/api/widgets/getQuery';
|
||||
export type LiveLogsListChartProps = {
|
||||
className?: string;
|
||||
initialData: QueryData[] | null;
|
||||
isShowingLiveLogs: boolean;
|
||||
};
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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,7 +58,6 @@ export const mockQuery: Query = {
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
id: 'test-query-id',
|
||||
|
||||
@@ -121,7 +121,6 @@ export const getPodQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '9b92756a-b445-45f8-90f4-d26f3ef28f8f',
|
||||
@@ -198,7 +197,6 @@ export const getPodQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'a22c1e03-4876-4b3e-9a96-a3c3a28f9c0f',
|
||||
@@ -339,7 +337,6 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '7bb3a6f5-d1c6-4f2e-9cc9-7dcc46db398f',
|
||||
@@ -480,7 +477,6 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '6d5ccd81-0ea1-4fb9-a66b-7f0fe2f15165',
|
||||
@@ -628,7 +624,6 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '4d03a0ff-4fa5-4b19-b397-97f80ba9e0ac',
|
||||
@@ -777,7 +772,6 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'ad491f19-0f83-4dd4-bb8f-bec295c18d1b',
|
||||
@@ -926,7 +920,6 @@ export const getPodQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '16908d4e-1565-4847-8d87-01ebb8fc494a',
|
||||
@@ -1008,7 +1001,6 @@ export const getPodQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '4b255d6d-4cde-474d-8866-f4418583c18b',
|
||||
@@ -1185,7 +1177,6 @@ export const getNodeQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '259295b5-774d-4b2e-8a4f-e5dd63e6c38d',
|
||||
@@ -1323,7 +1314,6 @@ export const getNodeQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '486af4da-2a1a-4b8f-992c-eba098d3a6f9',
|
||||
@@ -1419,7 +1409,6 @@ export const getNodeQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'b56143c0-7d2f-4425-97c5-65ad6fc87366',
|
||||
@@ -1568,7 +1557,6 @@ export const getNodeQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '57eeac15-615c-4a71-9c61-8e0c0c76b045',
|
||||
@@ -1730,7 +1718,6 @@ export const getHostQueryPayload = (
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
|
||||
@@ -1799,7 +1786,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '40218bfb-a9b7-4974-aead-5bf666e139bf',
|
||||
@@ -1942,7 +1928,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '8e6485ea-7018-43b0-ab27-b210f77b59ad',
|
||||
@@ -2024,7 +2009,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '47173220-44df-4ef6-87f4-31e333c180c7',
|
||||
@@ -2100,7 +2084,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '62eedbc6-c8ad-4d13-80a8-129396e1d1dc',
|
||||
@@ -2176,7 +2159,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '5ddb1b38-53bb-46f5-b4fe-fe832d6b9b24',
|
||||
@@ -2252,7 +2234,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'a849bcce-7684-4852-9134-530b45419b8f',
|
||||
@@ -2328,7 +2309,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'ab685a3d-fa4c-4663-8d94-c452e59038f3',
|
||||
@@ -2389,7 +2369,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '9bd40b51-0790-4cdd-9718-551b2ded5926',
|
||||
@@ -2471,7 +2450,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: '9c6d18ad-89ff-4e38-a15a-440e72ed6ca8',
|
||||
@@ -2546,7 +2524,6 @@ export const getHostQueryPayload = (
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'f4cfc2a5-78fc-42cc-8f4a-194c8c916132',
|
||||
|
||||
@@ -6,5 +6,4 @@ export type LogsExplorerChartProps = {
|
||||
isLogsExplorerViews?: boolean;
|
||||
isLabelEnabled?: boolean;
|
||||
className?: string;
|
||||
isShowingLiveLogs?: boolean;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ function LogsExplorerChart({
|
||||
isLabelEnabled = true,
|
||||
className,
|
||||
isLogsExplorerViews = false,
|
||||
isShowingLiveLogs = false,
|
||||
}: LogsExplorerChartProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
@@ -56,11 +55,6 @@ 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);
|
||||
|
||||
@@ -81,7 +75,7 @@ function LogsExplorerChart({
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
},
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery, isShowingLiveLogs],
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery],
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ColumnDef, DataTable, Row } from '@signozhq/table';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
@@ -6,18 +5,15 @@ 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 { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface ColumnViewProps {
|
||||
@@ -51,8 +47,6 @@ function ColumnView({
|
||||
onGroupByAttribute: handleGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const [showActiveLog, setShowActiveLog] = useState<boolean>(false);
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
@@ -66,18 +60,15 @@ 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,
|
||||
@@ -91,6 +82,7 @@ function ColumnView({
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: handleSetActiveLog,
|
||||
onOpenLogsContext: handleClearActiveLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onColumnOrderChange } = useDragColumns<
|
||||
@@ -142,7 +134,7 @@ function ColumnView({
|
||||
enableInfiniteScroll: true,
|
||||
enableScrollRestoration: false,
|
||||
fixedHeight: isFrequencyChartVisible ? 560 : 760,
|
||||
enableDynamicRowHeight: true,
|
||||
enableDynamicRowHeight: false,
|
||||
};
|
||||
|
||||
const selectedColumns = useMemo(
|
||||
@@ -156,10 +148,8 @@ 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 : Number.MAX_SAFE_INTEGER,
|
||||
disableReorder: field.key === 'state-indicator',
|
||||
disableDropBefore: field.key === 'state-indicator',
|
||||
disableResizing: field.key === 'state-indicator',
|
||||
maxSize: field.key === 'state-indicator' ? 4 : 1080,
|
||||
pin: field.key === 'state-indicator' ? 'left' : 'none',
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
cell: ({
|
||||
row,
|
||||
@@ -175,28 +165,13 @@ 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' : ''
|
||||
}`}
|
||||
>
|
||||
{cellContent}
|
||||
{getValue()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -224,22 +199,9 @@ 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 ${
|
||||
@@ -261,11 +223,11 @@ function ColumnView({
|
||||
scrollToIndexRef={scrollToIndexRef}
|
||||
/>
|
||||
|
||||
{showActiveLog && activeLog && (
|
||||
{activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={handleLogDetailClose}
|
||||
onClose={handleClearActiveLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleAddToQuery}
|
||||
onGroupByAttribute={handleGroupByAttribute}
|
||||
|
||||
@@ -24,11 +24,9 @@
|
||||
color: white !important;
|
||||
|
||||
.cursor-col-resize {
|
||||
width: 3px !important;
|
||||
width: 2px !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,7 +141,6 @@ describe('LogsExplorerList - empty states', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
@@ -206,7 +205,6 @@ 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 InfinityTableView from './InfinityTableView';
|
||||
import ColumnView from './ColumnView/ColumnView';
|
||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||
import { InfinityWrapperStyled } from './styles';
|
||||
import {
|
||||
@@ -48,6 +48,7 @@ function LogsExplorerList({
|
||||
isError,
|
||||
error,
|
||||
isFilterApplied,
|
||||
isFrequencyChartVisible,
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
@@ -129,75 +130,6 @@ 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);
|
||||
@@ -237,6 +169,83 @@ 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 />}
|
||||
@@ -265,9 +274,7 @@ function LogsExplorerList({
|
||||
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<>
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
{renderContent}
|
||||
</InfinityWrapperStyled>
|
||||
{renderContent}
|
||||
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
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,10 +1,14 @@
|
||||
/* 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';
|
||||
@@ -18,18 +22,20 @@ 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';
|
||||
@@ -43,7 +49,7 @@ import {
|
||||
omit,
|
||||
set,
|
||||
} from 'lodash-es';
|
||||
import LiveLogs from 'pages/LiveLogs';
|
||||
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
@@ -71,12 +77,16 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import {
|
||||
DataSource,
|
||||
LogsAggregatorOperator,
|
||||
StringOperators,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import LogsActionsContainer from './LogsActionsContainer';
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsExplorerViewsContainer({
|
||||
selectedView,
|
||||
@@ -84,7 +94,6 @@ function LogsExplorerViewsContainer({
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
setWarning,
|
||||
showLiveLogs,
|
||||
}: {
|
||||
selectedView: ExplorerViews;
|
||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@@ -93,7 +102,6 @@ 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();
|
||||
@@ -141,6 +149,7 @@ 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);
|
||||
@@ -153,6 +162,12 @@ 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 ||
|
||||
@@ -588,6 +603,41 @@ 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 ||
|
||||
@@ -645,40 +695,104 @@ function LogsExplorerViewsContainer({
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
<div className="logs-explorer-views-types">
|
||||
{!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}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{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">
|
||||
{showLiveLogs && <LiveLogs />}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && !showLiveLogs && (
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<LogsExplorerList
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
@@ -691,8 +805,7 @@ function LogsExplorerViewsContainer({
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
data={data}
|
||||
@@ -704,7 +817,7 @@ function LogsExplorerViewsContainer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && (
|
||||
<LogsExplorerTable
|
||||
data={
|
||||
(data?.payload?.data?.newResult?.data?.result ||
|
||||
|
||||
@@ -174,7 +174,6 @@ const renderer = (): RenderResult =>
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
@@ -236,7 +235,6 @@ describe('LogsExplorerViews -', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
|
||||
@@ -178,10 +178,6 @@ 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