diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 82c53c6fa34d..aebd30b3b64e 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -3,6 +3,7 @@ package app import ( "context" "fmt" + "log/slog" "net" "net/http" _ "net/http/pprof" // http profiler @@ -18,6 +19,7 @@ import ( "github.com/SigNoz/signoz/pkg/http/middleware" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/prometheus" + "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" @@ -104,6 +106,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) signoz.TelemetryStore, signoz.Prometheus, signoz.Modules.OrgGetter, + signoz.Querier, + signoz.Instrumentation.Logger(), ) if err != nil { @@ -421,6 +425,8 @@ func makeRulesManager( telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, + querier querier.Querier, + logger *slog.Logger, ) (*baserules.Manager, error) { // create manager opts managerOpts := &baserules.ManagerOptions{ @@ -429,6 +435,8 @@ func makeRulesManager( Context: context.Background(), Logger: zap.L(), Reader: ch, + Querier: querier, + SLogger: logger, Cache: cache, EvalDelay: baseconst.GetEvalDelay(), PrepareTaskFunc: rules.PrepareTaskFunc, diff --git a/ee/query-service/rules/anomaly.go b/ee/query-service/rules/anomaly.go index 78801f1f1c59..52226bd08b7f 100644 --- a/ee/query-service/rules/anomaly.go +++ b/ee/query-service/rules/anomaly.go @@ -4,17 +4,17 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "math" "strings" "sync" "time" - "go.uber.org/zap" - "github.com/SigNoz/signoz/ee/query-service/anomaly" "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/query-service/common" "github.com/SigNoz/signoz/pkg/query-service/model" + "github.com/SigNoz/signoz/pkg/transition" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" @@ -30,6 +30,11 @@ import ( baserules "github.com/SigNoz/signoz/pkg/query-service/rules" + querierV5 "github.com/SigNoz/signoz/pkg/querier" + + anomalyV2 "github.com/SigNoz/signoz/ee/anomaly" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" yaml "gopkg.in/yaml.v2" ) @@ -47,7 +52,14 @@ type AnomalyRule struct { // querierV2 is used for alerts created after the introduction of new metrics query builder querierV2 interfaces.Querier - provider anomaly.Provider + // querierV5 is used for alerts migrated after the introduction of new query builder + querierV5 querierV5.Querier + + provider anomaly.Provider + providerV2 anomalyV2.Provider + + version string + logger *slog.Logger seasonality anomaly.Seasonality } @@ -57,11 +69,15 @@ func NewAnomalyRule( orgID valuer.UUID, p *ruletypes.PostableRule, reader interfaces.Reader, + querierV5 querierV5.Querier, + logger *slog.Logger, cache cache.Cache, opts ...baserules.RuleOption, ) (*AnomalyRule, error) { - zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts)) + logger.Info("creating new AnomalyRule", "rule_id", id) + + opts = append(opts, baserules.WithLogger(logger)) if p.RuleCondition.CompareOp == ruletypes.ValueIsBelow { target := -1 * *p.RuleCondition.Target @@ -88,7 +104,7 @@ func NewAnomalyRule( t.seasonality = anomaly.SeasonalityDaily } - zap.L().Info("using seasonality", zap.String("seasonality", t.seasonality.String())) + logger.Info("using seasonality", "seasonality", t.seasonality.String()) querierOptsV2 := querierV2.QuerierOptions{ Reader: reader, @@ -117,6 +133,27 @@ func NewAnomalyRule( anomaly.WithReader[*anomaly.WeeklyProvider](reader), ) } + + if t.seasonality == anomaly.SeasonalityHourly { + t.providerV2 = anomalyV2.NewHourlyProvider( + anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](querierV5), + anomalyV2.WithLogger[*anomalyV2.HourlyProvider](logger), + ) + } else if t.seasonality == anomaly.SeasonalityDaily { + t.providerV2 = anomalyV2.NewDailyProvider( + anomalyV2.WithQuerier[*anomalyV2.DailyProvider](querierV5), + anomalyV2.WithLogger[*anomalyV2.DailyProvider](logger), + ) + } else if t.seasonality == anomaly.SeasonalityWeekly { + t.providerV2 = anomalyV2.NewWeeklyProvider( + anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](querierV5), + anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](logger), + ) + } + + t.querierV5 = querierV5 + t.version = p.Version + t.logger = logger return &t, nil } @@ -124,9 +161,11 @@ func (r *AnomalyRule) Type() ruletypes.RuleType { return RuleTypeAnomaly } -func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, error) { +func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.QueryRangeParamsV3, error) { - zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.EvalWindow().Milliseconds()), zap.Int64("evalDelay", r.EvalDelay().Milliseconds())) + r.logger.InfoContext( + ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds(), + ) start := ts.Add(-time.Duration(r.EvalWindow())).UnixMilli() end := ts.UnixMilli() @@ -156,13 +195,33 @@ func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, e }, nil } +func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) { + + r.logger.InfoContext(ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds()) + + startTs, endTs := r.Timestamps(ts) + start, end := startTs.UnixMilli(), endTs.UnixMilli() + + req := &qbtypes.QueryRangeRequest{ + Start: uint64(start), + End: uint64(end), + RequestType: qbtypes.RequestTypeTimeSeries, + CompositeQuery: qbtypes.CompositeQuery{ + Queries: make([]qbtypes.QueryEnvelope, 0), + }, + NoCache: true, + } + copy(r.Condition().CompositeQuery.Queries, req.CompositeQuery.Queries) + return req, nil +} + func (r *AnomalyRule) GetSelectedQuery() string { return r.Condition().GetSelectedQueryName() } func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) { - params, err := r.prepareQueryRange(ts) + params, err := r.prepareQueryRange(ctx, ts) if err != nil { return nil, err } @@ -190,7 +249,50 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t var resultVector ruletypes.Vector scoresJSON, _ := json.Marshal(queryResult.AnomalyScores) - zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON))) + r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON)) + + for _, series := range queryResult.AnomalyScores { + smpl, shouldAlert := r.ShouldAlert(*series) + if shouldAlert { + resultVector = append(resultVector, smpl) + } + } + return resultVector, nil +} + +func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) { + + params, err := r.prepareQueryRangeV5(ctx, ts) + if err != nil { + return nil, err + } + + anomalies, err := r.providerV2.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{ + Params: *params, + Seasonality: anomalyV2.Seasonality{String: valuer.NewString(r.seasonality.String())}, + }) + if err != nil { + return nil, err + } + + var qbResult *qbtypes.TimeSeriesData + for _, result := range anomalies.Results { + if result.QueryName == r.GetSelectedQuery() { + qbResult = result + break + } + } + + if qbResult == nil { + r.logger.WarnContext(ctx, "nil qb result", "ts", ts.UnixMilli()) + } + + queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult) + + var resultVector ruletypes.Vector + + scoresJSON, _ := json.Marshal(queryResult.AnomalyScores) + r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON)) for _, series := range queryResult.AnomalyScores { smpl, shouldAlert := r.ShouldAlert(*series) @@ -206,8 +308,17 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro prevState := r.State() valueFormatter := formatter.FromUnit(r.Unit()) - res, err := r.buildAndRunQuery(ctx, r.OrgID(), ts) + var res ruletypes.Vector + var err error + + if r.version == "v5" { + r.logger.InfoContext(ctx, "running v5 query") + res, err = r.buildAndRunQueryV5(ctx, r.OrgID(), ts) + } else { + r.logger.InfoContext(ctx, "running v4 query") + res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts) + } if err != nil { return nil, err } @@ -226,7 +337,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro value := valueFormatter.Format(smpl.V, r.Unit()) threshold := valueFormatter.Format(r.TargetVal(), r.Unit()) - zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold)) + r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold) tmplData := ruletypes.AlertTemplateData(l, value, threshold) // Inject some convenience variables that are easier to remember for users @@ -247,7 +358,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro result, err := tmpl.Expand() if err != nil { result = fmt.Sprintf("", err) - zap.L().Error("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData)) + r.logger.ErrorContext(ctx, "Expanding alert template failed", "error", err, "data", tmplData, "rule_name", r.Name()) } return result } @@ -276,7 +387,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro resultFPs[h] = struct{}{} if _, ok := alerts[h]; ok { - zap.L().Error("the alert query returns duplicate records", zap.String("ruleid", r.ID()), zap.Any("alert", alerts[h])) + r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h]) err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels") return nil, err } @@ -294,7 +405,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro } } - zap.L().Info("number of alerts found", zap.String("name", r.Name()), zap.Int("count", len(alerts))) + r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts)) // alerts[h] is ready, add or update active list now for h, a := range alerts { @@ -317,7 +428,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro for fp, a := range r.Active { labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { - zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels)) + r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "labels", a.Labels) } if _, ok := resultFPs[fp]; !ok { // If the alert was previously firing, keep it around for a given diff --git a/ee/query-service/rules/manager.go b/ee/query-service/rules/manager.go index 48550558dc03..bf5cbbbec117 100644 --- a/ee/query-service/rules/manager.go +++ b/ee/query-service/rules/manager.go @@ -27,6 +27,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) opts.OrgID, opts.Rule, opts.Reader, + opts.Querier, + opts.SLogger, baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay), baserules.WithSQLStore(opts.SQLStore), ) @@ -47,7 +49,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) ruleId, opts.OrgID, opts.Rule, - opts.Logger, + opts.SLogger, opts.Reader, opts.ManagerOpts.Prometheus, baserules.WithSQLStore(opts.SQLStore), @@ -69,6 +71,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) opts.OrgID, opts.Rule, opts.Reader, + opts.Querier, + opts.SLogger, opts.Cache, baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay), baserules.WithSQLStore(opts.SQLStore), @@ -126,6 +130,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap opts.OrgID, parsedRule, opts.Reader, + opts.Querier, + opts.SLogger, baserules.WithSendAlways(), baserules.WithSendUnmatched(), baserules.WithSQLStore(opts.SQLStore), @@ -143,7 +149,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap alertname, opts.OrgID, parsedRule, - opts.Logger, + opts.SLogger, opts.Reader, opts.ManagerOpts.Prometheus, baserules.WithSendAlways(), @@ -162,6 +168,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap opts.OrgID, parsedRule, opts.Reader, + opts.Querier, + opts.SLogger, opts.Cache, baserules.WithSendAlways(), baserules.WithSendUnmatched(), diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index f0f5a7330a61..25ff23f209e4 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + ignorePatterns: ['src/parser/*.ts'], env: { browser: true, es2021: true, diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 30573bc2829d..a3a90bf3c62e 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -8,3 +8,6 @@ public/ # Ignore all JSON files: **/*.json + +# Ignore all files in parser folder: +src/parser/** \ No newline at end of file diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 4f215d82e952..29cf32d131fb 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -25,7 +25,7 @@ const config: Config.InitialOptions = { '^.+\\.(js|jsx)$': 'babel-jest', }, transformIgnorePatterns: [ - 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)', + 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)', ], setupFilesAfterEnv: ['jest.setup.ts'], testPathIgnorePatterns: ['/node_modules/', '/public/'], diff --git a/frontend/package.json b/frontend/package.json index c179cd1cb1c7..dd5a9c16e38e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,8 @@ "dependencies": { "@ant-design/colors": "6.0.0", "@ant-design/icons": "4.8.0", + "@codemirror/autocomplete": "6.18.6", + "@codemirror/lang-javascript": "6.2.3", "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", @@ -44,6 +46,9 @@ "@signozhq/design-tokens": "1.1.4", "@tanstack/react-table": "8.20.6", "@tanstack/react-virtual": "3.11.2", + "@uiw/codemirror-theme-github": "4.24.1", + "@uiw/codemirror-theme-copilot": "4.23.11", + "@uiw/react-codemirror": "4.23.10", "@uiw/react-md-editor": "3.23.5", "@visx/group": "3.3.0", "@visx/hierarchy": "3.12.0", @@ -53,6 +58,7 @@ "ansi-to-html": "0.7.2", "antd": "5.11.0", "antd-table-saveas-excel": "2.2.1", + "antlr4": "4.13.2", "axios": "1.8.2", "babel-eslint": "^10.1.0", "babel-jest": "^29.6.4", diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts index abd7d701a4c8..f7f15f0cce72 100644 --- a/frontend/src/api/apiV1.ts +++ b/frontend/src/api/apiV1.ts @@ -3,6 +3,7 @@ const apiV1 = '/api/v1/'; export const apiV2 = '/api/v2/'; export const apiV3 = '/api/v3/'; export const apiV4 = '/api/v4/'; +export const apiV5 = '/api/v5/'; export const gatewayApiV1 = '/api/gateway/v1/'; export const gatewayApiV2 = '/api/gateway/v2/'; export const apiAlertManager = '/api/alertmanager/'; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a5e62ae78942..9e78b9022129 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -19,6 +19,7 @@ import apiV1, { apiV2, apiV3, apiV4, + apiV5, gatewayApiV1, gatewayApiV2, } from './apiV1'; @@ -171,6 +172,18 @@ ApiV4Instance.interceptors.response.use( ApiV4Instance.interceptors.request.use(interceptorsRequestResponse); // +// axios V5 +export const ApiV5Instance = axios.create({ + baseURL: `${ENVIRONMENT.baseURL}${apiV5}`, +}); + +ApiV5Instance.interceptors.response.use( + interceptorsResponse, + interceptorRejected, +); +ApiV5Instance.interceptors.request.use(interceptorsRequestResponse); +// + // axios Base export const ApiBaseInstance = axios.create({ baseURL: `${ENVIRONMENT.baseURL}${apiV1}`, diff --git a/frontend/src/api/querySuggestions/getKeySuggestions.ts b/frontend/src/api/querySuggestions/getKeySuggestions.ts new file mode 100644 index 000000000000..01f737fb4011 --- /dev/null +++ b/frontend/src/api/querySuggestions/getKeySuggestions.ts @@ -0,0 +1,28 @@ +import axios from 'api'; +import { AxiosResponse } from 'axios'; +import { + QueryKeyRequestProps, + QueryKeySuggestionsResponseProps, +} from 'types/api/querySuggestions/types'; + +export const getKeySuggestions = ( + props: QueryKeyRequestProps, +): Promise> => { + const { + signal = '', + searchText = '', + metricName = '', + fieldContext = '', + fieldDataType = '', + } = props; + + const encodedSignal = encodeURIComponent(signal); + const encodedSearchText = encodeURIComponent(searchText); + const encodedMetricName = encodeURIComponent(metricName); + const encodedFieldContext = encodeURIComponent(fieldContext); + const encodedFieldDataType = encodeURIComponent(fieldDataType); + + return axios.get( + `/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}`, + ); +}; diff --git a/frontend/src/api/querySuggestions/getValueSuggestion.ts b/frontend/src/api/querySuggestions/getValueSuggestion.ts new file mode 100644 index 000000000000..cbd2ea46245a --- /dev/null +++ b/frontend/src/api/querySuggestions/getValueSuggestion.ts @@ -0,0 +1,20 @@ +import axios from 'api'; +import { AxiosResponse } from 'axios'; +import { + QueryKeyValueRequestProps, + QueryKeyValueSuggestionsResponseProps, +} from 'types/api/querySuggestions/types'; + +export const getValueSuggestions = ( + props: QueryKeyValueRequestProps, +): Promise> => { + const { signal, key, searchText } = props; + + const encodedSignal = encodeURIComponent(signal); + const encodedKey = encodeURIComponent(key); + const encodedSearchText = encodeURIComponent(searchText); + + return axios.get( + `/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`, + ); +}; diff --git a/frontend/src/api/v5/queryRange/constants.ts b/frontend/src/api/v5/queryRange/constants.ts new file mode 100644 index 000000000000..8a5ec95f7e2b --- /dev/null +++ b/frontend/src/api/v5/queryRange/constants.ts @@ -0,0 +1,168 @@ +// V5 Query Range Constants + +import { ENTITY_VERSION_V5 } from 'constants/app'; +import { + FunctionName, + RequestType, + SignalType, + Step, +} from 'types/api/v5/queryRange'; + +// ===================== Schema and Version Constants ===================== + +export const SCHEMA_VERSION_V5 = ENTITY_VERSION_V5; +export const API_VERSION_V5 = 'v5'; + +// ===================== Default Values ===================== + +export const DEFAULT_STEP_INTERVAL: Step = '60s'; +export const DEFAULT_LIMIT = 100; +export const DEFAULT_OFFSET = 0; + +// ===================== Request Type Constants ===================== + +export const REQUEST_TYPES: Record = { + SCALAR: 'scalar', + TIME_SERIES: 'time_series', + RAW: 'raw', + DISTRIBUTION: 'distribution', +} as const; + +// ===================== Signal Type Constants ===================== + +export const SIGNAL_TYPES: Record = { + TRACES: 'traces', + LOGS: 'logs', + METRICS: 'metrics', +} as const; + +// ===================== Common Aggregation Expressions ===================== + +export const TRACE_AGGREGATIONS = { + COUNT: 'count()', + COUNT_DISTINCT_TRACE_ID: 'count_distinct(traceID)', + AVG_DURATION: 'avg(duration_nano)', + P50_DURATION: 'p50(duration_nano)', + P95_DURATION: 'p95(duration_nano)', + P99_DURATION: 'p99(duration_nano)', + MAX_DURATION: 'max(duration_nano)', + MIN_DURATION: 'min(duration_nano)', + SUM_DURATION: 'sum(duration_nano)', +} as const; + +export const LOG_AGGREGATIONS = { + COUNT: 'count()', + COUNT_DISTINCT_HOST: 'count_distinct(host.name)', + COUNT_DISTINCT_SERVICE: 'count_distinct(service.name)', + COUNT_DISTINCT_CONTAINER: 'count_distinct(container.name)', +} as const; + +// ===================== Common Filter Expressions ===================== + +export const COMMON_FILTERS = { + // Trace filters + SERVER_SPANS: "kind_string = 'Server'", + CLIENT_SPANS: "kind_string = 'Client'", + INTERNAL_SPANS: "kind_string = 'Internal'", + ERROR_SPANS: 'http.status_code >= 400', + SUCCESS_SPANS: 'http.status_code < 400', + + // Common service filters + EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'", + HTTP_REQUESTS: "http.method != ''", + + // Log filters + ERROR_LOGS: "severity_text = 'ERROR'", + WARN_LOGS: "severity_text = 'WARN'", + INFO_LOGS: "severity_text = 'INFO'", + DEBUG_LOGS: "severity_text = 'DEBUG'", +} as const; + +// ===================== Common Group By Fields ===================== + +export const COMMON_GROUP_BY_FIELDS = { + SERVICE_NAME: { + name: 'service.name', + fieldDataType: 'string' as const, + fieldContext: 'resource' as const, + }, + HTTP_METHOD: { + name: 'http.method', + fieldDataType: 'string' as const, + fieldContext: 'attribute' as const, + }, + HTTP_ROUTE: { + name: 'http.route', + fieldDataType: 'string' as const, + fieldContext: 'attribute' as const, + }, + HTTP_STATUS_CODE: { + name: 'http.status_code', + fieldDataType: 'int64' as const, + fieldContext: 'attribute' as const, + }, + HOST_NAME: { + name: 'host.name', + fieldDataType: 'string' as const, + fieldContext: 'resource' as const, + }, + CONTAINER_NAME: { + name: 'container.name', + fieldDataType: 'string' as const, + fieldContext: 'resource' as const, + }, +} as const; + +// ===================== Function Names ===================== + +export const FUNCTION_NAMES: Record = { + CUT_OFF_MIN: 'cutOffMin', + CUT_OFF_MAX: 'cutOffMax', + CLAMP_MIN: 'clampMin', + CLAMP_MAX: 'clampMax', + ABSOLUTE: 'absolute', + RUNNING_DIFF: 'runningDiff', + LOG2: 'log2', + LOG10: 'log10', + CUM_SUM: 'cumSum', + EWMA3: 'ewma3', + EWMA5: 'ewma5', + EWMA7: 'ewma7', + MEDIAN3: 'median3', + MEDIAN5: 'median5', + MEDIAN7: 'median7', + TIME_SHIFT: 'timeShift', + ANOMALY: 'anomaly', +} as const; + +// ===================== Common Step Intervals ===================== + +export const STEP_INTERVALS = { + FIFTEEN_SECONDS: '15s', + THIRTY_SECONDS: '30s', + ONE_MINUTE: '60s', + FIVE_MINUTES: '300s', + TEN_MINUTES: '600s', + FIFTEEN_MINUTES: '900s', + THIRTY_MINUTES: '1800s', + ONE_HOUR: '3600s', + TWO_HOURS: '7200s', + SIX_HOURS: '21600s', + TWELVE_HOURS: '43200s', + ONE_DAY: '86400s', +} as const; + +// ===================== Time Range Presets ===================== + +export const TIME_RANGE_PRESETS = { + LAST_5_MINUTES: 5 * 60 * 1000, + LAST_15_MINUTES: 15 * 60 * 1000, + LAST_30_MINUTES: 30 * 60 * 1000, + LAST_HOUR: 60 * 60 * 1000, + LAST_3_HOURS: 3 * 60 * 60 * 1000, + LAST_6_HOURS: 6 * 60 * 60 * 1000, + LAST_12_HOURS: 12 * 60 * 60 * 1000, + LAST_24_HOURS: 24 * 60 * 60 * 1000, + LAST_3_DAYS: 3 * 24 * 60 * 60 * 1000, + LAST_7_DAYS: 7 * 24 * 60 * 60 * 1000, +} as const; diff --git a/frontend/src/api/v5/queryRange/convertV5Response.ts b/frontend/src/api/v5/queryRange/convertV5Response.ts new file mode 100644 index 000000000000..37ff2bf8af88 --- /dev/null +++ b/frontend/src/api/v5/queryRange/convertV5Response.ts @@ -0,0 +1,423 @@ +import { cloneDeep, isEmpty } from 'lodash-es'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange'; +import { + DistributionData, + MetricRangePayloadV5, + QueryRangeRequestV5, + RawData, + ScalarData, + TimeSeriesData, +} from 'types/api/v5/queryRange'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; + +function getColName( + col: ScalarData['columns'][number], + legendMap: Record, + aggregationPerQuery: Record, +): string { + if (col.columnType === 'group') { + return col.name; + } + + const aggregation = + aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex]; + const legend = legendMap[col.queryName]; + const alias = aggregation?.alias; + const expression = aggregation?.expression || ''; + const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0; + const isSingleAggregation = aggregationsCount === 1; + + // Single aggregation: Priority is alias > legend > expression + if (isSingleAggregation) { + return alias || legend || expression; + } + + // Multiple aggregations: Each follows single rules BUT never shows legend + // Priority: alias > expression (legend is ignored for multiple aggregations) + return alias || expression; +} + +function getColId( + col: ScalarData['columns'][number], + aggregationPerQuery: Record, +): string { + if (col.columnType === 'group') { + return col.name; + } + const aggregation = + aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex]; + const expression = aggregation?.expression || ''; + return `${col.queryName}.${expression}`; +} + +/** + * Converts V5 TimeSeriesData to legacy format + */ +function convertTimeSeriesData( + timeSeriesData: TimeSeriesData, + legendMap: Record, +): QueryDataV3 { + // Convert V5 time series format to legacy QueryDataV3 format + + // Helper function to process series data + const processSeriesData = ( + aggregations: any[], + seriesKey: + | 'series' + | 'predictedSeries' + | 'upperBoundSeries' + | 'lowerBoundSeries' + | 'anomalyScores', + ): any[] => + aggregations?.flatMap((aggregation) => { + const { index, alias } = aggregation; + const seriesData = aggregation[seriesKey]; + + if (!seriesData || !seriesData.length) { + return []; + } + + return seriesData.map((series: any) => ({ + labels: series.labels + ? Object.fromEntries( + series.labels.map((label: any) => [label.key.name, label.value]), + ) + : {}, + labelsArray: series.labels + ? series.labels.map((label: any) => ({ [label.key.name]: label.value })) + : [], + values: series.values.map((value: any) => ({ + timestamp: value.timestamp, + value: String(value.value), + })), + metaData: { + alias, + index, + queryName: timeSeriesData.queryName, + }, + })); + }); + + return { + queryName: timeSeriesData.queryName, + legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName, + series: processSeriesData(timeSeriesData?.aggregations, 'series'), + predictedSeries: processSeriesData( + timeSeriesData?.aggregations, + 'predictedSeries', + ), + upperBoundSeries: processSeriesData( + timeSeriesData?.aggregations, + 'upperBoundSeries', + ), + lowerBoundSeries: processSeriesData( + timeSeriesData?.aggregations, + 'lowerBoundSeries', + ), + anomalyScores: processSeriesData( + timeSeriesData?.aggregations, + 'anomalyScores', + ), + list: null, + }; +} + +/** + * Converts V5 ScalarData array to legacy format with table structure + */ +function convertScalarDataArrayToTable( + scalarDataArray: ScalarData[], + legendMap: Record, + aggregationPerQuery: Record, +): QueryDataV3[] { + // If no scalar data, return empty structure + + if (!scalarDataArray || scalarDataArray.length === 0) { + return []; + } + + // Process each scalar data separately to maintain query separation + return scalarDataArray?.map((scalarData) => { + // Get query name from the first column + const queryName = scalarData?.columns?.[0]?.queryName || ''; + + if ((scalarData as any)?.aggregations?.length > 0) { + return { + ...convertTimeSeriesData(scalarData as any, legendMap), + table: { + columns: [], + rows: [], + }, + list: null, + }; + } + + // Collect columns for this specific query + const columns = scalarData?.columns?.map((col) => ({ + name: getColName(col, legendMap, aggregationPerQuery), + queryName: col.queryName, + isValueColumn: col.columnType === 'aggregation', + id: getColId(col, aggregationPerQuery), + })); + + // Process rows for this specific query + const rows = scalarData?.data?.map((dataRow) => { + const rowData: Record = {}; + + scalarData?.columns?.forEach((col, colIndex) => { + const columnName = getColName(col, legendMap, aggregationPerQuery); + const columnId = getColId(col, aggregationPerQuery); + rowData[columnId || columnName] = dataRow[colIndex]; + }); + + return { data: rowData }; + }); + + return { + queryName, + legend: legendMap[queryName] || '', + series: null, + list: null, + table: { + columns, + rows, + }, + }; + }); +} + +function convertScalarWithFormatForWeb( + scalarDataArray: ScalarData[], + legendMap: Record, + aggregationPerQuery: Record, +): QueryDataV3[] { + if (!scalarDataArray || scalarDataArray.length === 0) { + return []; + } + + return scalarDataArray.map((scalarData) => { + const columns = + scalarData.columns?.map((col) => { + const colName = getColName(col, legendMap, aggregationPerQuery); + + return { + name: colName, + queryName: col.queryName, + isValueColumn: col.columnType === 'aggregation', + id: getColId(col, aggregationPerQuery), + }; + }) || []; + + const rows = + scalarData.data?.map((dataRow) => { + const rowData: Record = {}; + columns?.forEach((col, colIndex) => { + rowData[col.id || col.name] = dataRow[colIndex]; + }); + return { data: rowData }; + }) || []; + + const queryName = scalarData.columns?.[0]?.queryName || ''; + + return { + queryName, + legend: legendMap[queryName] || queryName, + series: null, + list: null, + table: { + columns, + rows, + }, + }; + }); +} + +/** + * Converts V5 RawData to legacy format + */ +function convertRawData( + rawData: RawData, + legendMap: Record, +): QueryDataV3 { + // Convert V5 raw format to legacy QueryDataV3 format + return { + queryName: rawData.queryName, + legend: legendMap[rawData.queryName] || rawData.queryName, + series: null, + list: rawData.rows?.map((row) => ({ + timestamp: row.timestamp, + data: { + // Map raw data to ILog structure - spread row.data first to include all properties + ...row.data, + date: row.timestamp, + } as any, + })), + }; +} + +/** + * Converts V5 DistributionData to legacy format + */ +function convertDistributionData( + distributionData: DistributionData, + legendMap: Record, +): any { + // eslint-disable-line @typescript-eslint/no-explicit-any + // Convert V5 distribution format to legacy histogram format + return { + ...distributionData, + legendMap, + }; +} + +/** + * Helper function to convert V5 data based on type + */ +function convertV5DataByType( + v5Data: any, + legendMap: Record, + aggregationPerQuery: Record, +): MetricRangePayloadV3['data'] { + switch (v5Data?.type) { + case 'time_series': { + const timeSeriesData = v5Data.data.results as TimeSeriesData[]; + return { + resultType: 'time_series', + result: timeSeriesData.map((timeSeries) => + convertTimeSeriesData(timeSeries, legendMap), + ), + }; + } + case 'scalar': { + const scalarData = v5Data.data.results as ScalarData[]; + // For scalar data, combine all results into separate table entries + const combinedTables = convertScalarDataArrayToTable( + scalarData, + legendMap, + aggregationPerQuery, + ); + return { + resultType: 'scalar', + result: combinedTables, + }; + } + case 'raw': { + const rawData = v5Data.data.results as RawData[]; + return { + resultType: 'raw', + result: rawData.map((raw) => convertRawData(raw, legendMap)), + }; + } + case 'trace': { + const traceData = v5Data.data.results as RawData[]; + return { + resultType: 'trace', + result: traceData.map((trace) => convertRawData(trace, legendMap)), + }; + } + case 'distribution': { + const distributionData = v5Data.data.results as DistributionData[]; + return { + resultType: 'distribution', + result: distributionData.map((distribution) => + convertDistributionData(distribution, legendMap), + ), + }; + } + default: + return { + resultType: '', + result: [], + }; + } +} + +/** + * Converts V5 API response to legacy format expected by frontend components + */ +// eslint-disable-next-line sonarjs/cognitive-complexity +export function convertV5ResponseToLegacy( + v5Response: SuccessResponse, + legendMap: Record, + formatForWeb?: boolean, +): SuccessResponse { + const { payload, params } = v5Response; + const v5Data = payload?.data; + + const aggregationPerQuery = + (params as QueryRangeRequestV5)?.compositeQuery?.queries + ?.filter((query) => query.type === 'builder_query') + .reduce((acc, query) => { + if ( + query.type === 'builder_query' && + 'aggregations' in query.spec && + query.spec.name + ) { + acc[query.spec.name] = query.spec.aggregations; + } + return acc; + }, {} as Record) || {}; + + // If formatForWeb is true, return as-is (like existing logic) + if (formatForWeb && v5Data?.type === 'scalar') { + const scalarData = v5Data.data.results as ScalarData[]; + const webTables = convertScalarWithFormatForWeb( + scalarData, + legendMap, + aggregationPerQuery, + ); + return { + ...v5Response, + payload: { + data: { + resultType: 'scalar', + result: webTables, + }, + }, + }; + } + + // Convert based on V5 response type + const convertedData = convertV5DataByType( + v5Data, + legendMap, + aggregationPerQuery, + ); + + // Create legacy-compatible response structure + const legacyResponse: SuccessResponse = { + ...v5Response, + payload: { + data: convertedData, + }, + }; + + // Apply legend mapping (similar to existing logic) + if (legacyResponse.payload?.data?.result) { + legacyResponse.payload.data.result = legacyResponse.payload.data.result.map( + (queryData: any) => { + // eslint-disable-line @typescript-eslint/no-explicit-any + const newQueryData = cloneDeep(queryData); + newQueryData.legend = legendMap[queryData.queryName]; + + // If metric names is an empty object + if (isEmpty(queryData.metric)) { + // If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query. + if (newQueryData.legend === undefined || newQueryData.legend === null) { + newQueryData.legend = queryData.queryName; + } + // If name of the query and the legend if inserted is same then add the same to the metrics object. + if (queryData.queryName === newQueryData.legend) { + newQueryData.metric = newQueryData.metric || {}; + newQueryData.metric[queryData.queryName] = queryData.queryName; + } + } + + return newQueryData; + }, + ); + } + + return legacyResponse; +} diff --git a/frontend/src/api/v5/queryRange/getQueryRange.ts b/frontend/src/api/v5/queryRange/getQueryRange.ts new file mode 100644 index 000000000000..35aa474e6cbd --- /dev/null +++ b/frontend/src/api/v5/queryRange/getQueryRange.ts @@ -0,0 +1,45 @@ +import { ApiV5Instance } from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ENTITY_VERSION_V5 } from 'constants/app'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { + MetricRangePayloadV5, + QueryRangePayloadV5, +} from 'types/api/v5/queryRange'; + +export const getQueryRangeV5 = async ( + props: QueryRangePayloadV5, + version: string, + signal: AbortSignal, + headers?: Record, +): Promise> => { + try { + if (version && version === ENTITY_VERSION_V5) { + const response = await ApiV5Instance.post('/query_range', props, { + signal, + headers, + }); + + return { + httpStatusCode: response.status, + data: response.data, + }; + } + + // Default V5 behavior + const response = await ApiV5Instance.post('/query_range', props, { + signal, + headers, + }); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default getQueryRangeV5; diff --git a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts new file mode 100644 index 000000000000..f71edb1936f0 --- /dev/null +++ b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts @@ -0,0 +1,447 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +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, + QueryFunctionProps, +} from 'types/api/queryBuilder/queryBuilderData'; +import { + BaseBuilderQuery, + FieldContext, + FieldDataType, + FunctionName, + GroupByKey, + Having, + LogAggregation, + MetricAggregation, + OrderBy, + QueryEnvelope, + QueryFunction, + QueryRangePayloadV5, + QueryType, + RequestType, + TelemetryFieldKey, + TraceAggregation, + VariableItem, +} from 'types/api/v5/queryRange'; +import { EQueryType } from 'types/common/dashboard'; +import { DataSource } from 'types/common/queryBuilder'; + +type PrepareQueryRangePayloadV5Result = { + queryPayload: QueryRangePayloadV5; + legendMap: Record; +}; + +/** + * Maps panel types to V5 request types + */ +export function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType { + switch (panelType) { + case PANEL_TYPES.TIME_SERIES: + case PANEL_TYPES.BAR: + return 'time_series'; + case PANEL_TYPES.TABLE: + case PANEL_TYPES.PIE: + case PANEL_TYPES.VALUE: + return 'scalar'; + case PANEL_TYPES.TRACE: + return 'trace'; + case PANEL_TYPES.LIST: + return 'raw'; + case PANEL_TYPES.HISTOGRAM: + return 'distribution'; + default: + return ''; + } +} + +/** + * Gets signal type from data source + */ +function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' { + if (dataSource === 'traces') return 'traces'; + if (dataSource === 'logs') return 'logs'; + return 'metrics'; +} + +/** + * Creates base spec for builder queries + */ +function createBaseSpec( + queryData: IBuilderQuery, + 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, + disabled: queryData.disabled, + filter: queryData?.filter?.expression ? queryData.filter : 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), + functions: isEmpty(queryData.functions) + ? undefined + : queryData.functions.map( + (func: QueryFunctionProps): QueryFunction => ({ + name: func.name as FunctionName, + args: isEmpty(func.namedArgs) + ? func.args.map((arg) => ({ + value: arg, + })) + : Object.entries(func.namedArgs).map(([name, value]) => ({ + name, + value, + })), + }), + ), + 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, + }), + ), + }; +} +// Utility to parse aggregation expressions with optional alias +export function parseAggregations( + expression: string, +): { expression: string; alias?: string }[] { + const result: { expression: string; alias?: string }[] = []; + // Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'" + // Handles quoted ('alias'), dash-separated (field-name), and unquoted values after "as" keyword + const regex = /([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+((?:'[^']*'|"[^"]*"|[a-zA-Z0-9_-]+)))?/g; + let match = regex.exec(expression); + while (match !== null) { + const expr = match[1]; + let alias = match[2]; + if (alias) { + // Remove quotes if present + alias = alias.replace(/^['"]|['"]$/g, ''); + result.push({ expression: expr, alias }); + } else { + result.push({ expression: expr }); + } + match = regex.exec(expression); + } + return result; +} + +export function createAggregation( + queryData: any, + panelType?: PANEL_TYPES, +): TraceAggregation[] | LogAggregation[] | MetricAggregation[] { + if (!queryData) { + return []; + } + + const haveReduceTo = + queryData.dataSource === DataSource.METRICS && + panelType && + (panelType === PANEL_TYPES.TABLE || + panelType === PANEL_TYPES.PIE || + panelType === PANEL_TYPES.VALUE); + + if (queryData.dataSource === DataSource.METRICS) { + return [ + { + metricName: + queryData?.aggregations?.[0]?.metricName || + queryData?.aggregateAttribute?.key, + temporality: + queryData?.aggregations?.[0]?.temporality || + queryData?.aggregateAttribute?.temporality, + timeAggregation: + queryData?.aggregations?.[0]?.timeAggregation || + queryData?.timeAggregation, + spaceAggregation: + queryData?.aggregations?.[0]?.spaceAggregation || + queryData?.spaceAggregation, + reduceTo: haveReduceTo + ? queryData?.aggregations?.[0]?.reduceTo || queryData?.reduceTo + : undefined, + }, + ]; + } + + if (queryData.aggregations?.length > 0) { + return isEmpty(parseAggregations(queryData.aggregations?.[0].expression)) + ? [{ expression: 'count()' }] + : parseAggregations(queryData.aggregations?.[0].expression); + } + + return [{ expression: 'count()' }]; +} + +/** + * Converts query builder data to V5 builder queries + */ +export function convertBuilderQueriesToV5( + builderQueries: Record, // eslint-disable-line @typescript-eslint/no-explicit-any + requestType: RequestType, + panelType?: PANEL_TYPES, +): QueryEnvelope[] { + return Object.entries(builderQueries).map( + ([queryName, queryData]): QueryEnvelope => { + const signal = getSignalType(queryData.dataSource); + const baseSpec = createBaseSpec(queryData, requestType, panelType); + let spec: QueryEnvelope['spec']; + + // Skip aggregation for raw request type + const aggregations = + requestType === 'raw' ? undefined : createAggregation(queryData, panelType); + + switch (signal) { + case 'traces': + spec = { + name: queryName, + signal: 'traces' as const, + ...baseSpec, + aggregations: aggregations as TraceAggregation[], + }; + break; + case 'logs': + spec = { + name: queryName, + signal: 'logs' as const, + ...baseSpec, + aggregations: aggregations as LogAggregation[], + }; + break; + case 'metrics': + default: + spec = { + name: queryName, + signal: 'metrics' as const, + ...baseSpec, + aggregations: aggregations as MetricAggregation[], + // reduceTo: queryData.reduceTo, + }; + break; + } + + return { + type: 'builder_query' as QueryType, + spec, + }; + }, + ); +} + +/** + * Converts PromQL queries to V5 format + */ +export function convertPromQueriesToV5( + promQueries: Record, // eslint-disable-line @typescript-eslint/no-explicit-any +): QueryEnvelope[] { + return Object.entries(promQueries).map( + ([queryName, queryData]): QueryEnvelope => ({ + type: 'promql' as QueryType, + spec: { + name: queryName, + query: queryData.query, + disabled: queryData.disabled || false, + step: queryData?.stepInterval, + legend: isEmpty(queryData.legend) ? undefined : queryData.legend, + stats: false, // PromQL specific field + }, + }), + ); +} + +/** + * Converts ClickHouse queries to V5 format + */ +export function convertClickHouseQueriesToV5( + chQueries: Record, // eslint-disable-line @typescript-eslint/no-explicit-any +): QueryEnvelope[] { + return Object.entries(chQueries).map( + ([queryName, queryData]): QueryEnvelope => ({ + type: 'clickhouse_sql' as QueryType, + spec: { + name: queryName, + query: queryData.query, + disabled: queryData.disabled || false, + legend: isEmpty(queryData.legend) ? undefined : queryData.legend, + // ClickHouse doesn't have step or stats like PromQL + }, + }), + ); +} + +/** + * Helper function to reduce query arrays to objects + */ +function reduceQueriesToObject( + queryArray: any[], // eslint-disable-line @typescript-eslint/no-explicit-any +): { queries: Record; legends: Record } { + // eslint-disable-line @typescript-eslint/no-explicit-any + const legends: Record = {}; + const queries = queryArray.reduce((acc, queryItem) => { + if (!queryItem.query) return acc; + acc[queryItem.name] = queryItem; + legends[queryItem.name] = queryItem.legend; + return acc; + }, {} as Record); // eslint-disable-line @typescript-eslint/no-explicit-any + + return { queries, legends }; +} + +/** + * Prepares V5 query range payload from GetQueryResultsProps + */ +export const prepareQueryRangePayloadV5 = ({ + query, + globalSelectedInterval, + graphType, + selectedTime, + tableParams, + variables = {}, + start: startTime, + end: endTime, + formatForWeb, + originalGraphType, + fillGaps, +}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => { + let legendMap: Record = {}; + const requestType = mapPanelTypeToRequestType(graphType); + let queries: QueryEnvelope[] = []; + + switch (query.queryType) { + case EQueryType.QUERY_BUILDER: { + const { queryData: data, queryFormulas } = query.builder; + const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams); + const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName'); + + // Combine legend maps + legendMap = { + ...currentQueryData.newLegendMap, + ...currentFormulas.newLegendMap, + }; + + // Convert builder queries + const builderQueries = convertBuilderQueriesToV5( + currentQueryData.data, + requestType, + graphType, + ); + + // Convert formulas as separate query type + const formulaQueries = Object.entries(currentFormulas.data).map( + ([queryName, formulaData]): QueryEnvelope => ({ + type: 'builder_formula' as const, + spec: { + name: queryName, + expression: formulaData.expression || '', + disabled: formulaData.disabled, + limit: formulaData.limit ?? undefined, + legend: isEmpty(formulaData.legend) ? undefined : formulaData.legend, + order: formulaData.orderBy?.map( + // eslint-disable-next-line sonarjs/no-identical-functions + (order: any): OrderBy => ({ + key: { + name: order.columnName, + }, + direction: order.order, + }), + ), + }, + }), + ); + + // Combine both types + queries = [...builderQueries, ...formulaQueries]; + break; + } + case EQueryType.PROM: { + const promQueries = reduceQueriesToObject(query[query.queryType]); + queries = convertPromQueriesToV5(promQueries.queries); + legendMap = promQueries.legends; + break; + } + case EQueryType.CLICKHOUSE: { + const chQueries = reduceQueriesToObject(query[query.queryType]); + queries = convertClickHouseQueriesToV5(chQueries.queries); + legendMap = chQueries.legends; + break; + } + default: + break; + } + + // Calculate time range + const { start, end } = getStartEndRangeTime({ + type: selectedTime, + interval: globalSelectedInterval, + }); + + // Create V5 payload + const queryPayload: QueryRangePayloadV5 = { + schemaVersion: 'v1', + start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3, + end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3, + requestType, + compositeQuery: { + queries, + }, + formatOptions: { + formatTableResultForUI: + !!formatForWeb || + (originalGraphType + ? originalGraphType === PANEL_TYPES.TABLE + : graphType === PANEL_TYPES.TABLE), + fillGaps: fillGaps || false, + }, + variables: Object.entries(variables).reduce((acc, [key, value]) => { + acc[key] = { value }; + return acc; + }, {} as Record), + }; + + return { legendMap, queryPayload }; +}; diff --git a/frontend/src/api/v5/v5.ts b/frontend/src/api/v5/v5.ts new file mode 100644 index 000000000000..44d71a74104f --- /dev/null +++ b/frontend/src/api/v5/v5.ts @@ -0,0 +1,8 @@ +// V5 API exports +export * from './queryRange/constants'; +export { convertV5ResponseToLegacy } from './queryRange/convertV5Response'; +export { getQueryRangeV5 } from './queryRange/getQueryRange'; +export { prepareQueryRangePayloadV5 } from './queryRange/prepareQueryRangePayloadV5'; + +// Export types from proper location +export * from 'types/api/v5/queryRange'; diff --git a/frontend/src/components/CeleryTask/CeleryUtils.ts b/frontend/src/components/CeleryTask/CeleryUtils.ts index 1a2a4bfb4878..8e3eb19acc11 100644 --- a/frontend/src/components/CeleryTask/CeleryUtils.ts +++ b/frontend/src/components/CeleryTask/CeleryUtils.ts @@ -64,7 +64,8 @@ export function applyCeleryFilterOnWidgetData( ...queryItem, filters: { ...queryItem.filters, - items: [...queryItem.filters.items, ...filters], + items: [...(queryItem.filters?.items || []), ...filters], + op: queryItem.filters?.op || 'AND', }, } : queryItem, diff --git a/frontend/src/components/CeleryTask/useNavigateToExplorer.ts b/frontend/src/components/CeleryTask/useNavigateToExplorer.ts index 60bd88be52a6..8baa59de2df9 100644 --- a/frontend/src/components/CeleryTask/useNavigateToExplorer.ts +++ b/frontend/src/components/CeleryTask/useNavigateToExplorer.ts @@ -41,7 +41,8 @@ export function useNavigateToExplorer(): ( aggregateOperator: MetricAggregateOperator.NOOP, filters: { ...item.filters, - items: selectedFilters, + items: [...(item.filters?.items || []), ...selectedFilters], + op: item.filters?.op || 'AND', }, groupBy: [], disabled: false, diff --git a/frontend/src/components/ErrorModal/components/ErrorContent.tsx b/frontend/src/components/ErrorModal/components/ErrorContent.tsx index 3817b0d82ce2..6b2f914617c1 100644 --- a/frontend/src/components/ErrorModal/components/ErrorContent.tsx +++ b/frontend/src/components/ErrorModal/components/ErrorContent.tsx @@ -18,7 +18,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element { errors: errorMessages, code: errorCode, message: errorMessage, - } = error.error.error; + } = error?.error?.error || {}; return (
{/* Summary Header */} diff --git a/frontend/src/components/ExplorerCard/utils.ts b/frontend/src/components/ExplorerCard/utils.ts index 0f90435f6fb8..f9fb61d5993c 100644 --- a/frontend/src/components/ExplorerCard/utils.ts +++ b/frontend/src/components/ExplorerCard/utils.ts @@ -43,13 +43,13 @@ export const omitIdFromQuery = (query: Query | null): any => ({ builder: { ...query?.builder, queryData: query?.builder.queryData.map((queryData) => { - const { id, ...rest } = queryData.aggregateAttribute; + const { id, ...rest } = queryData.aggregateAttribute || {}; const newAggregateAttribute = rest; const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => { const { id, ...rest } = groupByAttribute; return rest; }); - const newItems = queryData.filters.items.map((item) => { + const newItems = queryData.filters?.items?.map((item) => { const { id, ...newItem } = item; if (item.key) { const { id, ...rest } = item.key; diff --git a/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx b/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx index 482e19f9b93f..e8e1f6bd1e2d 100644 --- a/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx +++ b/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx @@ -74,16 +74,16 @@ function HostMetricTraces({ ...currentQuery.builder.queryData[0].aggregateAttribute, }, filters: { - items: tracesFilters.items.filter( - (item) => item.key?.key !== 'host.name', - ), + items: + tracesFilters?.items?.filter((item) => item.key?.key !== 'host.name') || + [], op: 'AND', }, }, ], }, }), - [currentQuery, tracesFilters.items], + [currentQuery, tracesFilters?.items], ); const query = updatedCurrentQuery?.builder?.queryData[0] || null; @@ -140,7 +140,8 @@ function HostMetricTraces({ const isDataEmpty = !isLoading && !isFetching && !isError && traces.length === 0; - const hasAdditionalFilters = tracesFilters.items.length > 1; + const hasAdditionalFilters = + tracesFilters?.items && tracesFilters?.items?.length > 1; const totalCount = data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0; @@ -158,7 +159,7 @@ function HostMetricTraces({
{query && ( handleChangeTracesFilters(value, VIEWS.TRACES) } diff --git a/frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx b/frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx index 1c72ef9fa81a..5e2ebe291841 100644 --- a/frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx +++ b/frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx @@ -216,15 +216,17 @@ function HostMetricsDetails({ const handleChangeLogFilters = useCallback( (value: IBuilderQuery['filters'], view: VIEWS) => { setLogFilters((prevFilters) => { - const hostNameFilter = prevFilters.items.find( + const hostNameFilter = prevFilters?.items?.find( (item) => item.key?.key === 'host.name', ); - const paginationFilter = value.items.find((item) => item.key?.key === 'id'); - const newFilters = value.items.filter( + const paginationFilter = value?.items?.find( + (item) => item.key?.key === 'id', + ); + const newFilters = value?.items?.filter( (item) => item.key?.key !== 'id' && item.key?.key !== 'host.name', ); - if (newFilters.length > 0) { + if (newFilters && newFilters?.length > 0) { logEvent(InfraMonitoringEvents.FilterApplied, { entity: InfraMonitoringEvents.HostEntity, view: InfraMonitoringEvents.LogsView, @@ -236,7 +238,7 @@ function HostMetricsDetails({ op: 'AND', items: [ hostNameFilter, - ...newFilters, + ...(newFilters || []), ...(paginationFilter ? [paginationFilter] : []), ].filter((item): item is TagFilterItem => item !== undefined), }; @@ -258,11 +260,11 @@ function HostMetricsDetails({ const handleChangeTracesFilters = useCallback( (value: IBuilderQuery['filters'], view: VIEWS) => { setTracesFilters((prevFilters) => { - const hostNameFilter = prevFilters.items.find( + const hostNameFilter = prevFilters?.items?.find( (item) => item.key?.key === 'host.name', ); - if (value.items.length > 0) { + if (value?.items && value?.items?.length > 0) { logEvent(InfraMonitoringEvents.FilterApplied, { entity: InfraMonitoringEvents.HostEntity, view: InfraMonitoringEvents.TracesView, @@ -274,7 +276,7 @@ function HostMetricsDetails({ op: 'AND', items: [ hostNameFilter, - ...value.items.filter((item) => item.key?.key !== 'host.name'), + ...(value?.items?.filter((item) => item.key?.key !== 'host.name') || []), ].filter((item): item is TagFilterItem => item !== undefined), }; @@ -311,7 +313,7 @@ function HostMetricsDetails({ if (selectedView === VIEW_TYPES.LOGS) { const filtersWithoutPagination = { ...logFilters, - items: logFilters.items.filter((item) => item.key?.key !== 'id'), + items: logFilters?.items?.filter((item) => item.key?.key !== 'id') || [], }; const compositeQuery = { diff --git a/frontend/src/components/HostMetricsDetail/HostMetricsLogs/HostMetricLogsDetailedView.tsx b/frontend/src/components/HostMetricsDetail/HostMetricsLogs/HostMetricLogsDetailedView.tsx index 7fe06a641e45..669f86975527 100644 --- a/frontend/src/components/HostMetricsDetail/HostMetricsLogs/HostMetricLogsDetailedView.tsx +++ b/frontend/src/components/HostMetricsDetail/HostMetricsLogs/HostMetricLogsDetailedView.tsx @@ -52,14 +52,16 @@ function HostMetricLogsDetailedView({ ...currentQuery.builder.queryData[0].aggregateAttribute, }, filters: { - items: logFilters.items.filter((item) => item.key?.key !== 'host.name'), + items: + logFilters?.items?.filter((item) => item.key?.key !== 'host.name') || + [], op: 'AND', }, }, ], }, }), - [currentQuery, logFilters.items], + [currentQuery, logFilters?.items], ); const query = updatedCurrentQuery?.builder?.queryData[0] || null; @@ -70,7 +72,7 @@ function HostMetricLogsDetailedView({
{query && ( handleChangeLogFilters(value, VIEWS.LOGS)} disableNavigationShortcuts /> diff --git a/frontend/src/components/HostMetricsDetail/HostMetricsLogs/__tests__/HostMetricsLogs.test.tsx b/frontend/src/components/HostMetricsDetail/HostMetricsLogs/__tests__/HostMetricsLogs.test.tsx deleted file mode 100644 index 36dc148400de..000000000000 --- a/frontend/src/components/HostMetricsDetail/HostMetricsLogs/__tests__/HostMetricsLogs.test.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { ENVIRONMENT } from 'constants/env'; -import { - verifyFiltersAndOrderBy, - verifyPayload, -} from 'container/LogsExplorerViews/tests/LogsExplorerPagination.test'; -import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range'; -import { server } from 'mocks-server/server'; -import { rest } from 'msw'; -import { VirtuosoMockContext } from 'react-virtuoso'; -import { - act, - fireEvent, - render, - RenderResult, - waitFor, -} from 'tests/test-utils'; -import { QueryRangePayload } from 'types/api/metrics/getQueryRange'; -import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; - -import HostMetricsLogs from '../HostMetricsLogs'; - -jest.mock('uplot', () => { - const paths = { - spline: jest.fn(), - bars: jest.fn(), - }; - const uplotMock = jest.fn(() => ({ - paths, - })); - return { - paths, - default: uplotMock, - }; -}); - -jest.mock( - 'components/OverlayScrollbar/OverlayScrollbar', - () => - function MockOverlayScrollbar({ - children, - }: { - children: React.ReactNode; - }): JSX.Element { - return
{children}
; - }, -); - -describe.skip('HostMetricsLogs', () => { - let capturedQueryRangePayloads: QueryRangePayload[] = []; - const itemHeight = 100; - beforeEach(() => { - server.use( - rest.post( - `${ENVIRONMENT.baseURL}/api/v3/query_range`, - async (req, res, ctx) => { - capturedQueryRangePayloads.push(await req.json()); - - const lastPayload = - capturedQueryRangePayloads[capturedQueryRangePayloads.length - 1]; - - const queryData = lastPayload?.compositeQuery.builderQueries - ?.A as IBuilderQuery; - - const offset = queryData?.offset ?? 0; - - return res( - ctx.status(200), - ctx.json(logsPaginationQueryRangeSuccessResponse({ offset })), - ); - }, - ), - ); - capturedQueryRangePayloads = []; - }); - it('should check if host logs pagination flows work properly', async () => { - let renderResult: RenderResult; - let scrollableElement: HTMLElement; - - await act(async () => { - renderResult = render( - - - , - ); - }); - - await waitFor(() => { - expect(capturedQueryRangePayloads.length).toBe(1); - }); - - await waitFor(async () => { - // Find the Virtuoso scroller element by its data-test-id - scrollableElement = renderResult.container.querySelector( - '[data-test-id="virtuoso-scroller"]', - ) as HTMLElement; - - // Ensure the element exists - expect(scrollableElement).not.toBeNull(); - - if (scrollableElement) { - // Set the scrollTop property to simulate scrolling to the calculated end position - scrollableElement.scrollTop = 99 * itemHeight; - - act(() => { - fireEvent.scroll(scrollableElement); - }); - } - }); - - await waitFor(() => { - expect(capturedQueryRangePayloads.length).toBe(2); - }); - - const firstPayload = capturedQueryRangePayloads[0]; - verifyPayload({ - payload: firstPayload, - expectedOffset: 0, - }); - - // Store the time range from the first payload, which should be consistent in subsequent requests - const initialTimeRange = { - start: firstPayload.start, - end: firstPayload.end, - }; - - const secondPayload = capturedQueryRangePayloads[1]; - const secondQueryData = verifyPayload({ - payload: secondPayload, - expectedOffset: 100, - initialTimeRange, - }); - verifyFiltersAndOrderBy(secondQueryData); - - await waitFor(async () => { - // Find the Virtuoso scroller element by its data-test-id - scrollableElement = renderResult.container.querySelector( - '[data-test-id="virtuoso-scroller"]', - ) as HTMLElement; - - // Ensure the element exists - expect(scrollableElement).not.toBeNull(); - - if (scrollableElement) { - // Set the scrollTop property to simulate scrolling to the calculated end position - scrollableElement.scrollTop = 199 * itemHeight; - - act(() => { - fireEvent.scroll(scrollableElement); - }); - } - }); - - await waitFor(() => { - expect(capturedQueryRangePayloads.length).toBeGreaterThanOrEqual(3); - }); - - const thirdPayload = capturedQueryRangePayloads[2]; - const thirdQueryData = verifyPayload({ - payload: thirdPayload, - expectedOffset: 200, - initialTimeRange, - }); - verifyFiltersAndOrderBy(thirdQueryData); - }); -}); diff --git a/frontend/src/components/HostMetricsDetail/Metrics/Metrics.tsx b/frontend/src/components/HostMetricsDetail/Metrics/Metrics.tsx index 0f5f7b7728b7..064d34c6882b 100644 --- a/frontend/src/components/HostMetricsDetail/Metrics/Metrics.tsx +++ b/frontend/src/components/HostMetricsDetail/Metrics/Metrics.tsx @@ -13,6 +13,7 @@ import { CustomTimeType, Time, } from 'container/TopNav/DateTimeSelectionV2/config'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useResizeObserver } from 'hooks/useDimensions'; import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver'; @@ -86,6 +87,7 @@ function Metrics({ const isDarkMode = useIsDarkMode(); const graphRef = useRef(null); const dimensions = useResizeObserver(graphRef); + const { currentQuery } = useQueryBuilder(); const chartData = useMemo( () => queries.map(({ data }) => getUPlotChartData(data?.payload)), @@ -144,9 +146,17 @@ function Metrics({ minTimeScale: graphTimeIntervals[idx].start, maxTimeScale: graphTimeIntervals[idx].end, onDragSelect: (start, end) => onDragSelect(start, end, idx), + query: currentQuery, }), ), - [queries, isDarkMode, dimensions, graphTimeIntervals, onDragSelect], + [ + queries, + isDarkMode, + dimensions, + graphTimeIntervals, + onDragSelect, + currentQuery, + ], ); const renderCardContent = ( diff --git a/frontend/src/components/InputWithLabel/InputWithLabel.styles.scss b/frontend/src/components/InputWithLabel/InputWithLabel.styles.scss new file mode 100644 index 000000000000..c459b54b2c3c --- /dev/null +++ b/frontend/src/components/InputWithLabel/InputWithLabel.styles.scss @@ -0,0 +1,101 @@ +.input-with-label { + display: flex; + flex-direction: row; + + border-radius: 2px 0px 0px 2px; + + .label { + color: var(--bg-vanilla-400); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: 0.56px; + + max-width: 150px; + min-width: 60px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + padding: 0px 8px; + + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + display: flex; + justify-content: flex-start; + align-items: center; + font-weight: var(--font-weight-light); + } + + .input { + flex: 1; + min-width: 150px; + font-family: 'Space Mono', monospace !important; + + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + border-right: none; + border-left: none; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + + .close-btn { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + height: 38px; + width: 38px; + } + + &.labelAfter { + .input { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + } + + .label { + border-left: none; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + } +} + +.lightMode { + .input-with-label { + .label { + color: var(--bg-ink-500) !important; + + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + + .input { + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + + .close-btn { + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + + &.labelAfter { + .input { + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + } + } +} diff --git a/frontend/src/components/InputWithLabel/InputWithLabel.tsx b/frontend/src/components/InputWithLabel/InputWithLabel.tsx new file mode 100644 index 000000000000..6fef16100d0e --- /dev/null +++ b/frontend/src/components/InputWithLabel/InputWithLabel.tsx @@ -0,0 +1,74 @@ +import './InputWithLabel.styles.scss'; + +import { Button, Input, Typography } from 'antd'; +import cx from 'classnames'; +import { X } from 'lucide-react'; +import { useState } from 'react'; + +function InputWithLabel({ + label, + initialValue, + placeholder, + type, + onClose, + labelAfter, + onChange, + className, + closeIcon, +}: { + label: string; + initialValue?: string | number; + placeholder: string; + type?: string; + onClose?: () => void; + labelAfter?: boolean; + onChange: (value: string) => void; + className?: string; + closeIcon?: React.ReactNode; +}): JSX.Element { + const [inputValue, setInputValue] = useState( + initialValue ? initialValue.toString() : '', + ); + + const handleChange = (e: React.ChangeEvent): void => { + setInputValue(e.target.value); + onChange?.(e.target.value); + }; + + return ( +
+ {!labelAfter && {label}} + + {labelAfter && {label}} + {onClose && ( +
+ ); +} + +InputWithLabel.defaultProps = { + type: 'text', + onClose: undefined, + labelAfter: false, + initialValue: undefined, + className: undefined, + closeIcon: undefined, +}; + +export default InputWithLabel; diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2c56d58fd1db..d7b14d4f03f4 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -20,3 +20,7 @@ export type LogDetailProps = { } & Pick & Partial> & Pick; + +export type LogDetailInnerProps = LogDetailProps & { + log: NonNullable; +}; diff --git a/frontend/src/components/LogDetail/LogDetails.styles.scss b/frontend/src/components/LogDetail/LogDetails.styles.scss index 458de97b3612..37902394f74d 100644 --- a/frontend/src/components/LogDetail/LogDetails.styles.scss +++ b/frontend/src/components/LogDetail/LogDetails.styles.scss @@ -62,6 +62,10 @@ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); } + &-query-container { + margin-bottom: 16px; + } + .log-detail-drawer__log { width: 100%; display: flex; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index ed9a025c056b..6fd66e20b503 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -6,6 +6,8 @@ import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import { RadioChangeEvent } from 'antd/lib'; import cx from 'classnames'; import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator'; +import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch'; +import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils'; import { LOCALSTORAGE } from 'constants/localStorage'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; @@ -19,19 +21,20 @@ import { getSanitizedLogBody, removeEscapeCharacters, } from 'container/LogDetailedView/utils'; +import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery'; import { useOptionsMenu } from 'container/OptionsMenu'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import createQueryParams from 'lib/createQueryParams'; +import { cloneDeep } from 'lodash-es'; import { BarChart2, Braces, Compass, Copy, Filter, - HardHat, Table, TextSelect, X, @@ -45,10 +48,9 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants'; -import { LogDetailProps } from './LogDetail.interfaces'; -import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper'; +import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces'; -function LogDetail({ +function LogDetailInner({ log, onClose, onAddToQuery, @@ -57,13 +59,16 @@ function LogDetail({ selectedTab, isListViewPanel = false, listViewPanelSelectedFields, -}: LogDetailProps): JSX.Element { +}: LogDetailInnerProps): JSX.Element { + const initialContextQuery = useInitialQuery(log); + const [contextQuery, setContextQuery] = useState( + initialContextQuery, + ); const [, copyToClipboard] = useCopyToClipboard(); const [selectedView, setSelectedView] = useState(selectedTab); - const [isFilterVisibile, setIsFilterVisible] = useState(false); + const [isFilterVisible, setIsFilterVisible] = useState(false); - const [contextQuery, setContextQuery] = useState(); const [filters, setFilters] = useState(null); const [isEdit, setIsEdit] = useState(false); const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder(); @@ -98,7 +103,7 @@ function LogDetail({ }; const handleFilterVisible = (): void => { - setIsFilterVisible(!isFilterVisibile); + setIsFilterVisible(!isFilterVisible); setIsEdit(!isEdit); }; @@ -141,6 +146,44 @@ function LogDetail({ safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`); }; + const handleRunQuery = (expression: string): void => { + let updatedContextQuery = cloneDeep(contextQuery); + + if (!updatedContextQuery || !updatedContextQuery.builder) { + return; + } + + const newFilters: TagFilter = { + items: expression ? convertExpressionToFilters(expression) : [], + op: 'AND', + }; + + updatedContextQuery = { + ...updatedContextQuery, + builder: { + ...updatedContextQuery?.builder, + queryData: updatedContextQuery?.builder.queryData.map((queryData) => ({ + ...queryData, + filter: { + ...queryData.filter, + expression, + }, + filters: { + ...queryData.filters, + ...newFilters, + op: queryData.filters?.op ?? 'AND', + }, + })), + }, + }; + + setContextQuery(updatedContextQuery); + + if (newFilters) { + setFilters(newFilters); + } + }; + // Only show when opened from infra monitoring page const showOpenInExplorerBtn = useMemo( () => location.pathname?.includes('/infrastructure-monitoring'), @@ -148,11 +191,6 @@ function LogDetail({ [], ); - if (!log) { - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>; - } - const logType = log?.attributes_string?.log_level || LogType.INFO; return ( @@ -268,18 +306,16 @@ function LogDetail({ /> )}
- - - } - /> + {isFilterVisible && contextQuery?.builder.queryData[0] && ( +
+ {}} + dataSource={DataSource.LOGS} + queryData={contextQuery?.builder.queryData[0]} + onRun={handleRunQuery} + /> +
+ )} {selectedView === VIEW_TYPES.OVERVIEW && ( ; + } + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + export default LogDetail; diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx index 91d6aa30aae2..3dfe0869504f 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx @@ -410,18 +410,18 @@ export default function LogsFormatOptionsMenu({ )}
- {addColumn?.value?.map(({ key, id }) => ( -
+ {addColumn?.value?.map(({ name }) => ( +
- - {key} + + {name}
{addColumn?.value?.length > 1 && ( addColumn.onRemove(id as string)} + onClick={(): void => addColumn.onRemove(name)} /> )}
diff --git a/frontend/src/components/OrderBy/ListViewOrderBy.styles.scss b/frontend/src/components/OrderBy/ListViewOrderBy.styles.scss new file mode 100644 index 000000000000..9210406a5539 --- /dev/null +++ b/frontend/src/components/OrderBy/ListViewOrderBy.styles.scss @@ -0,0 +1,7 @@ +.order-by-loading-container { + padding: 4px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/components/OrderBy/ListViewOrderBy.tsx b/frontend/src/components/OrderBy/ListViewOrderBy.tsx new file mode 100644 index 000000000000..d207cba5976b --- /dev/null +++ b/frontend/src/components/OrderBy/ListViewOrderBy.tsx @@ -0,0 +1,115 @@ +import './ListViewOrderBy.styles.scss'; + +import { Select, Spin } from 'antd'; +import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions'; +import { useEffect, useRef, useState } from 'react'; +import { useQuery } from 'react-query'; +import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types'; +import { DataSource } from 'types/common/queryBuilder'; + +interface ListViewOrderByProps { + value: string; + onChange: (value: string) => void; + dataSource: DataSource; +} + +// Loader component for the dropdown when loading or no results +function Loader({ isLoading }: { isLoading: boolean }): JSX.Element { + return ( +
+ {isLoading ? : 'No results found'} +
+ ); +} + +function ListViewOrderBy({ + value, + onChange, + dataSource, +}: ListViewOrderByProps): JSX.Element { + const [searchInput, setSearchInput] = useState(''); + const [debouncedInput, setDebouncedInput] = useState(''); + const [selectOptions, setSelectOptions] = useState< + { label: string; value: string }[] + >([]); + const debounceTimer = useRef | null>(null); + + // Fetch key suggestions based on debounced input + const { data, isLoading } = useQuery({ + queryKey: ['orderByKeySuggestions', dataSource, debouncedInput], + queryFn: async () => { + const response = await getKeySuggestions({ + signal: dataSource, + searchText: debouncedInput, + }); + return response.data; + }, + }); + + useEffect( + () => (): void => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + }, + [], + ); + + // Update options when API data changes + useEffect(() => { + const rawKeys: QueryKeyDataSuggestionsProps[] = data?.data?.keys + ? Object.values(data.data?.keys).flat() + : []; + + const keyNames = rawKeys.map((key) => key.name); + const uniqueKeys = [ + ...new Set(searchInput ? keyNames : ['timestamp', ...keyNames]), + ]; + + const updatedOptions = uniqueKeys.flatMap((key) => [ + { label: `${key} (desc)`, value: `${key}:desc` }, + { label: `${key} (asc)`, value: `${key}:asc` }, + ]); + + setSelectOptions(updatedOptions); + }, [data, searchInput]); + + // Handle search input with debounce + const handleSearch = (input: string): void => { + setSearchInput(input); + + // Filter current options for instant client-side match + const filteredOptions = selectOptions.filter((option) => + option.value.toLowerCase().includes(input.trim().toLowerCase()), + ); + + // If no match found or input is empty, trigger debounced fetch + if (filteredOptions.length === 0 || input === '') { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + + debounceTimer.current = setTimeout(() => { + setDebouncedInput(input); + }, 100); + } + }; + + return ( + )} + {isQBV2 && ( + +
+
+
Order By
+
+
); } + +SpaceAggregationOptions.defaultProps = { + qbVersion: 'v2', +}; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx index 691f53da589b..695a08d8ece3 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx @@ -1,35 +1,31 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import './ToolbarActions.styles.scss'; import { FilterOutlined } from '@ant-design/icons'; -import { Button, Switch, Tooltip, Typography } from 'antd'; +import { Button, Tooltip } from 'antd'; import cx from 'classnames'; -import { Atom, SquareMousePointer, Terminal } from 'lucide-react'; -import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react'; +import { ExplorerViews } from 'pages/LogsExplorer/utils'; interface LeftToolbarActionsProps { items: any; selectedView: string; - onToggleHistrogramVisibility: () => void; - onChangeSelectedView: (view: SELECTED_VIEWS) => void; - showFrequencyChart: boolean; + onChangeSelectedView: (view: ExplorerViews) => void; showFilter: boolean; handleFilterVisibilityChange: () => void; } const activeTab = 'active-tab'; -const actionBtn = 'action-btn'; -export const queryBuilder = 'query-builder'; export default function LeftToolbarActions({ items, selectedView, - onToggleHistrogramVisibility, + onChangeSelectedView, - showFrequencyChart, showFilter, handleFilterVisibilityChange, }: LeftToolbarActionsProps): JSX.Element { - const { clickhouse, search, queryBuilder: QB } = items; + const { clickhouse, list, timeseries, table, trace } = items; return (
@@ -41,56 +37,90 @@ export default function LeftToolbarActions({ )}
- - - - - - + {list?.show && ( + + + + )} + + {trace?.show && ( + + + + )} + + {timeseries?.show && ( + + + + )} {clickhouse?.show && ( - + + + )} -
-
- Frequency chart - + {table?.show && ( + + + + )}
); diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss index e6bcb83a53dd..6f95b8c80308 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss @@ -19,32 +19,51 @@ border: 1px solid var(--bg-slate-400); background: var(--bg-ink-300); flex-direction: row; + border-bottom: none; + margin-bottom: -1px; .prom-ql-icon { height: 14px; width: 14px; } - .ant-btn { + .explorer-view-option { display: flex; align-items: center; justify-content: center; + flex-direction: row; border: none; padding: 9px; box-shadow: none; - border-radius: 0; + border-radius: 0px; + border-left: 1px solid var(--bg-slate-400); + border-bottom: 1px solid var(--bg-slate-400); + + gap: 8px; &.active-tab { - background-color: var(--bg-slate-400); + background-color: var(--bg-ink-500); + border-bottom: 1px solid var(--bg-ink-500); + + &:hover { + background-color: var(--bg-ink-500) !important; + } } &:disabled { - background-color: #121317; + background-color: var(--bg-ink-300); opacity: 0.6; } - } - .action-btn + .action-btn { - border-left: 1px solid var(--bg-slate-400); + + &:first-child { + border-left: 1px solid transparent; + } + + &:hover { + background-color: transparent !important; + border-left: 1px solid transparent !important; + color: var(--bg-vanilla-100); + } } } @@ -108,32 +127,36 @@ .lightMode { .left-toolbar { + .filter-btn { + border-color: var(--bg-vanilla-300) !important; + } + .left-toolbar-query-actions { - border-color: var(--bg-vanilla-300); - background: var(--bg-vanilla-100); + border-color: var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; .ant-btn { - border-color: var(--bg-vanilla-300); - background: var(--bg-vanilla-100); - color: var(--bg-ink-200); + border-color: var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + color: var(--bg-ink-200) !important; &.active-tab { - background-color: var(--bg-robin-100); + background-color: var(--bg-robin-100) !important; } } } } .loading-container { .loading-btn { - background: var(--bg-vanilla-300); + background: var(--bg-vanilla-300) !important; } .cancel-run { - color: var(--bg-vanilla-100); + color: var(--bg-vanilla-100) !important; } .cancel-run:hover { - background-color: #ff7875; + background-color: #ff7875 !important; } } } diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx index 1fcfddc531ad..cb5ecb7abb87 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx @@ -1,42 +1,48 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import { ExplorerViews } from 'pages/LogsExplorer/utils'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import LeftToolbarActions from '../LeftToolbarActions'; import RightToolbarActions from '../RightToolbarActions'; describe('ToolbarActions', () => { + const mockHandleFilterVisibilityChange = (): void => {}; + + const defaultItems = { + list: { + name: 'list', + label: 'List View', + disabled: false, + show: true, + key: ExplorerViews.LIST, + }, + timeseries: { + name: 'timeseries', + label: 'Time Series', + disabled: false, + show: true, + key: ExplorerViews.TIMESERIES, + }, + clickhouse: { + name: 'clickhouse', + label: 'Clickhouse', + disabled: false, + show: false, + key: 'clickhouse', + }, + }; + it('LeftToolbarActions - renders correctly with default props', async () => { const handleChangeSelectedView = jest.fn(); - const handleToggleShowFrequencyChart = jest.fn(); const { queryByTestId } = render( {}} + handleFilterVisibilityChange={mockHandleFilterVisibilityChange} />, ); expect(screen.getByTestId('search-view')).toBeInTheDocument(); @@ -52,37 +58,20 @@ describe('ToolbarActions', () => { expect(handleChangeSelectedView).toBeCalled(); }); - it('renders - clickhouse view and test histogram toggle', async () => { + it('renders - clickhouse view and test view switching', async () => { const handleChangeSelectedView = jest.fn(); - const handleToggleShowFrequencyChart = jest.fn(); - const { queryByTestId, getByRole } = render( + const clickhouseItems = { + ...defaultItems, + list: { ...defaultItems.list, show: false }, + clickhouse: { ...defaultItems.clickhouse, show: true }, + }; + const { queryByTestId } = render( {}} + handleFilterVisibilityChange={mockHandleFilterVisibilityChange} />, ); @@ -92,8 +81,12 @@ describe('ToolbarActions', () => { await userEvent.click(clickHouseView as HTMLElement); expect(handleChangeSelectedView).toBeCalled(); - await userEvent.click(getByRole('switch')); - expect(handleToggleShowFrequencyChart).toBeCalled(); + // Test that timeseries view is also present and clickable + const timeseriesView = queryByTestId('query-builder-view'); + expect(timeseriesView).toBeInTheDocument(); + + await userEvent.click(timeseriesView as HTMLElement); + expect(handleChangeSelectedView).toBeCalled(); }); it('RightToolbarActions - render correctly with props', async () => { diff --git a/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx index 658987fd350a..6bdf4f09493e 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx @@ -22,14 +22,14 @@ function AggregateEveryFilter({ }; const isDisabled = - (isMetricsDataSource && !query.aggregateAttribute.key) || disabled; + (isMetricsDataSource && !query.aggregateAttribute?.key) || disabled; return ( diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx index c06e85b0376e..5f56efe6f3bc 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx @@ -20,6 +20,7 @@ import { BaseAutocompleteData, IQueryAutocompleteResponse, } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { MetricAggregation } from 'types/api/v5/queryRange'; import { DataSource } from 'types/common/queryBuilder'; import { ExtendedSelectOption } from 'types/common/select'; import { popupContainer } from 'utils/selectPopupContainer'; @@ -42,6 +43,12 @@ export const AggregatorFilter = memo(function AggregatorFilter({ const [optionsData, setOptionsData] = useState([]); const [searchText, setSearchText] = useState(''); + // this function is only relevant for metrics and now operators are part of aggregations + const queryAggregation = useMemo( + () => query.aggregations?.[0] as MetricAggregation, + [query.aggregations], + ); + const debouncedSearchText = useMemo(() => { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars const [_, value] = getAutocompleteValueAndType(searchText); @@ -54,19 +61,19 @@ export const AggregatorFilter = memo(function AggregatorFilter({ [ QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, debouncedValue, - query.aggregateOperator, + queryAggregation.timeAggregation, query.dataSource, ], async () => getAggregateAttribute({ searchText: debouncedValue, - aggregateOperator: query.aggregateOperator, + aggregateOperator: queryAggregation.timeAggregation, dataSource: query.dataSource, }), { enabled: query.dataSource === DataSource.METRICS || - (!!query.aggregateOperator && !!query.dataSource), + (!!queryAggregation.timeAggregation && !!query.dataSource), onSuccess: (data) => { const options: ExtendedSelectOption[] = data?.payload?.attributeKeys?.map(({ id: _, ...item }) => ({ @@ -115,10 +122,15 @@ export const AggregatorFilter = memo(function AggregatorFilter({ queryClient.getQueryData>([ QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, debouncedValue, - query.aggregateOperator, + queryAggregation.timeAggregation, query.dataSource, ])?.payload?.attributeKeys || [], - [debouncedValue, query.aggregateOperator, query.dataSource, queryClient], + [ + debouncedValue, + queryAggregation.timeAggregation, + query.dataSource, + queryClient, + ], ); const getResponseAttributes = useCallback(async () => { @@ -126,19 +138,24 @@ export const AggregatorFilter = memo(function AggregatorFilter({ [ QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, searchText, - query.aggregateOperator, + queryAggregation.timeAggregation, query.dataSource, ], async () => getAggregateAttribute({ searchText, - aggregateOperator: query.aggregateOperator, + aggregateOperator: queryAggregation.timeAggregation, dataSource: query.dataSource, }), ); return response.payload?.attributeKeys || []; - }, [query.aggregateOperator, query.dataSource, queryClient, searchText]); + }, [ + queryAggregation.timeAggregation, + query.dataSource, + queryClient, + searchText, + ]); const handleChangeCustomValue = useCallback( async (value: string, attributes: BaseAutocompleteData[]) => { @@ -208,12 +225,15 @@ export const AggregatorFilter = memo(function AggregatorFilter({ const value = removePrefix( transformStringWithPrefix({ - str: query.aggregateAttribute.key, - prefix: query.aggregateAttribute.type || '', - condition: !query.aggregateAttribute.isColumn, + str: + (query.aggregations?.[0] as MetricAggregation)?.metricName || + query.aggregateAttribute?.key || + '', + prefix: query.aggregateAttribute?.type || '', + condition: !query.aggregateAttribute?.isColumn, }), - !query.aggregateAttribute.isColumn && query.aggregateAttribute.type - ? query.aggregateAttribute.type + !query.aggregateAttribute?.isColumn && query.aggregateAttribute?.type + ? query.aggregateAttribute?.type : '', ); diff --git a/frontend/src/container/QueryBuilder/filters/Formula/OrderBy/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/Formula/OrderBy/OrderByFilter.tsx index ead02a31a953..1c7c92b41b28 100644 --- a/frontend/src/container/QueryBuilder/filters/Formula/OrderBy/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/Formula/OrderBy/OrderByFilter.tsx @@ -29,13 +29,13 @@ function OrderByFilter({ const { data, isFetching } = useGetAggregateKeys( { - aggregateAttribute: query.aggregateAttribute.key, + aggregateAttribute: query.aggregateAttribute?.key || '', dataSource: query.dataSource, - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', searchText: debouncedSearchText, }, { - enabled: !!query.aggregateAttribute.key, + enabled: !!query.aggregateAttribute?.key, keepPreviousData: true, }, ); @@ -59,7 +59,7 @@ function OrderByFilter({ ]); const isDisabledSelect = - !query.aggregateAttribute.key || + !query.aggregateAttribute?.key || query.aggregateOperator === MetricAggregateOperator.NOOP; return ( diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx index 476bf71f217d..937e1007082a 100644 --- a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx @@ -40,9 +40,9 @@ export const GroupByFilter = memo(function GroupByFilter({ const { isFetching } = useGetAggregateKeys( { - aggregateAttribute: query.aggregateAttribute.key, + aggregateAttribute: query.aggregateAttribute?.key || '', dataSource: query.dataSource, - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', searchText: debouncedValue, }, { @@ -94,9 +94,9 @@ export const GroupByFilter = memo(function GroupByFilter({ [QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText, isFocused], async () => getAggregateKeys({ - aggregateAttribute: query.aggregateAttribute.key, + aggregateAttribute: query.aggregateAttribute?.key || '', dataSource: query.dataSource, - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', searchText, }), ); @@ -104,7 +104,7 @@ export const GroupByFilter = memo(function GroupByFilter({ return response.payload?.attributeKeys || []; }, [ isFocused, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, query.aggregateOperator, query.dataSource, queryClient, diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/GroupByFilter/utils.ts index 0fb85a7e3089..60dd5938395a 100644 --- a/frontend/src/container/QueryBuilder/filters/GroupByFilter/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/utils.ts @@ -5,13 +5,13 @@ export function removePrefix(str: string, type: string): string { const resourcePrefix = `${MetricsType.Resource}_`; const scopePrefix = `${MetricsType.Scope}_`; - if (str.startsWith(tagPrefix)) { + if (str?.startsWith(tagPrefix)) { return str.slice(tagPrefix.length); } - if (str.startsWith(resourcePrefix)) { + if (str?.startsWith(resourcePrefix)) { return str.slice(resourcePrefix.length); } - if (str.startsWith(scopePrefix) && type === MetricsType.Scope) { + if (str?.startsWith(scopePrefix) && type === MetricsType.Scope) { return str.slice(scopePrefix.length); } diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx index 3eab3e50ee63..a06a699783ec 100644 --- a/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx @@ -45,9 +45,9 @@ export function HavingFilter({ const aggregatorAttribute = useMemo( () => transformStringWithPrefix({ - str: query.aggregateAttribute.key, - prefix: query.aggregateAttribute.type || '', - condition: !query.aggregateAttribute.isColumn, + str: query.aggregateAttribute?.key || '', + prefix: query.aggregateAttribute?.type || '', + condition: !query.aggregateAttribute?.isColumn, }), [query], ); @@ -62,7 +62,9 @@ export function HavingFilter({ return `${query.spaceAggregation.toUpperCase()}(${aggregatorAttribute})`; } - return `${query.aggregateOperator.toUpperCase()}(${aggregatorAttribute})`; + return `${ + query.aggregateOperator?.toUpperCase() || '' + }(${aggregatorAttribute})`; }, [query, aggregatorAttribute, entityVersion]); const aggregatorOptions: SelectOption[] = useMemo( @@ -228,7 +230,7 @@ export function HavingFilter({ }, [searchText, parseSearchText]); useEffect(() => { - setLocalValues(transformHavingToStringValue(having)); + setLocalValues(transformHavingToStringValue(having as Having[])); }, [having]); const isMetricsDataSource = query.dataSource === DataSource.METRICS; @@ -244,7 +246,7 @@ export function HavingFilter({ tagRender={tagRender} value={localValues} data-testid="havingSelect" - disabled={isMetricsDataSource && !query.aggregateAttribute.key} + disabled={isMetricsDataSource && !query.aggregateAttribute?.key} style={{ width: '100%' }} notFoundContent={currentFormValue.value.length === 0 ? undefined : null} placeholder="GroupBy(operation) > 5" diff --git a/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx index 081aed365ded..d87c6d490768 100644 --- a/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx @@ -8,7 +8,7 @@ import { handleKeyDownLimitFilter } from '../utils'; function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element { const isMetricsDataSource = query.dataSource === DataSource.METRICS; - const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key; + const isDisabled = isMetricsDataSource && !query.aggregateAttribute?.key; return ( ); }); diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts index 323531f6f1a7..ce8f4a4bd900 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts @@ -8,6 +8,7 @@ export type OrderByFilterProps = { onChange: (values: OrderByPayload[]) => void; isListViewPanel?: boolean; entityVersion?: string; + isNewQueryV2?: boolean; }; export type OrderByFilterValue = { diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx index 32aefe490a8d..d88fb1dd2693 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx @@ -2,6 +2,7 @@ import { Select, Spin } from 'antd'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useMemo } from 'react'; import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder'; +import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter'; import { popupContainer } from 'utils/selectPopupContainer'; import { selectStyle } from '../QueryBuilderSearch/config'; @@ -13,6 +14,7 @@ export function OrderByFilter({ onChange, isListViewPanel = false, entityVersion, + isNewQueryV2 = false, }: OrderByFilterProps): JSX.Element { const { debouncedSearchText, @@ -26,37 +28,50 @@ export function OrderByFilter({ const { data, isFetching } = useGetAggregateKeys( { - aggregateAttribute: query.aggregateAttribute.key, + aggregateAttribute: query.aggregateAttribute?.key || '', dataSource: query.dataSource, - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', searchText: debouncedSearchText, }, { - enabled: !!query.aggregateAttribute.key || isListViewPanel, + enabled: !!query.aggregateAttribute?.key || isListViewPanel, keepPreviousData: true, }, ); + // Get parsed aggregation options using createAggregation only for QueryV2 + const parsedAggregationOptions = useMemo( + () => (isNewQueryV2 ? getParsedAggregationOptionsForOrderBy(query) : []), + [query, isNewQueryV2], + ); + const optionsData = useMemo(() => { const keyOptions = createOptions(data?.payload?.attributeKeys || []); const groupByOptions = createOptions(query.groupBy); + const aggregationOptionsFromParsed = createOptions(parsedAggregationOptions); + const options = query.aggregateOperator === MetricAggregateOperator.NOOP ? keyOptions - : [...groupByOptions, ...aggregationOptions]; + : [ + ...groupByOptions, + ...(isNewQueryV2 ? aggregationOptionsFromParsed : aggregationOptions), + ]; return generateOptions(options); }, [ - aggregationOptions, createOptions, data?.payload?.attributeKeys, - generateOptions, - query.aggregateOperator, query.groupBy, + query.aggregateOperator, + parsedAggregationOptions, + aggregationOptions, + generateOptions, + isNewQueryV2, ]); const isDisabledSelect = - !query.aggregateAttribute.key || + !query.aggregateAttribute?.key || query.aggregateOperator === MetricAggregateOperator.NOOP; const isMetricsDataSource = query.dataSource === DataSource.METRICS; diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/useOrderByFilter.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/useOrderByFilter.ts index dbd1909a488f..76fae4b9fcd2 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/useOrderByFilter.ts +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/useOrderByFilter.ts @@ -128,13 +128,13 @@ export const useOrderByFilter = ({ { label: `${ entityVersion === 'v4' ? query.spaceAggregation : query.aggregateOperator - }(${query.aggregateAttribute.key}) ${ORDERBY_FILTERS.ASC}`, + }(${query.aggregateAttribute?.key}) ${ORDERBY_FILTERS.ASC}`, value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`, }, { label: `${ entityVersion === 'v4' ? query.spaceAggregation : query.aggregateOperator - }(${query.aggregateAttribute.key}) ${ORDERBY_FILTERS.DESC}`, + }(${query.aggregateAttribute?.key}) ${ORDERBY_FILTERS.DESC}`, value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`, }, ], diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts index 4523a07eff3d..31147cfb580c 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts @@ -20,7 +20,7 @@ export const transformToOrderByStringValues = ( return { label: `${ entityVersion === 'v4' ? query.spaceAggregation : query.aggregateOperator - }(${query.aggregateAttribute.key}) ${item.order}`, + }(${query.aggregateAttribute?.key || ''}) ${item.order}`, value: `${item.columnName}${orderByValueDelimiter}${item.order}`, }; } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts index a93171168fa9..677705831be8 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts @@ -1 +1,5 @@ -export const selectStyle = { width: '100%', minWidth: '7.7rem' }; +export const selectStyle = { + width: '100%', + minWidth: '7.7rem', + height: '100%', +}; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index 14b8b5852fb0..326cb27f0a47 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -262,13 +262,13 @@ function QueryBuilderSearch({ }; const queryTags = useMemo(() => { - if (!query.aggregateAttribute.key && isMetricsDataSource) return []; + if (!query.aggregateAttribute?.key && isMetricsDataSource) return []; return tags; - }, [isMetricsDataSource, query.aggregateAttribute.key, tags]); + }, [isMetricsDataSource, query.aggregateAttribute?.key, tags]); useEffect(() => { const initialTagFilters: TagFilter = { items: [], op: 'AND' }; - const initialSourceKeys = query.filters.items?.map( + const initialSourceKeys = query.filters?.items?.map( (item) => item.key as BaseAutocompleteData, ); @@ -277,9 +277,10 @@ function QueryBuilderSearch({ const { tagKey, tagOperator, tagValue } = getTagToken(tag); - const filterAttribute = [...initialSourceKeys, ...sourceKeys].find( - (key) => key?.key === getRemovePrefixFromKey(tagKey), - ); + const filterAttribute = [ + ...(initialSourceKeys || []), + ...(sourceKeys || []), + ].find((key) => key?.key === getRemovePrefixFromKey(tagKey)); const computedTagValue = tagValue && Array.isArray(tagValue) && tagValue[tagValue.length - 1] === '' @@ -385,7 +386,7 @@ function QueryBuilderSearch({ !showAllFilters && options.length > 3 && !key ? 'hide-scroll' : '', )} rootClassName="query-builder-search" - disabled={isMetricsDataSource && !query.aggregateAttribute.key} + disabled={isMetricsDataSource && !query.aggregateAttribute?.key} style={selectStyle} onSearch={handleSearch} onChange={onChangeHandler} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts index cf683b749801..b74072a6e095 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts @@ -58,31 +58,31 @@ export function getOperatorValue(op: string): string { case 'IN': return 'in'; case 'NOT_IN': - return 'nin'; + return 'not in'; case OPERATORS.REGEX: return 'regex'; case OPERATORS.HAS: return 'has'; case OPERATORS.NHAS: - return 'nhas'; + return 'not has'; case OPERATORS.NREGEX: - return 'nregex'; + return 'not regex'; case 'LIKE': return 'like'; + case 'ILIKE': + return 'ilike'; case 'NOT_LIKE': - return 'nlike'; + return 'not like'; + case 'NOT_ILIKE': + return 'not ilike'; case 'EXISTS': return 'exists'; case 'NOT_EXISTS': - return 'nexists'; + return 'not exists'; case 'CONTAINS': return 'contains'; case 'NOT_CONTAINS': - return 'ncontains'; - case 'ILIKE': - return 'ilike'; - case 'NOT_ILIKE': - return 'notilike'; + return 'not contains'; default: return op; } @@ -92,27 +92,27 @@ export function getOperatorFromValue(op: string): string { switch (op) { case 'in': return 'IN'; - case 'nin': + case 'not in': return 'NOT_IN'; case 'like': return 'LIKE'; case 'regex': return OPERATORS.REGEX; - case 'nregex': + case 'not regex': return OPERATORS.NREGEX; - case 'nlike': + case 'not like': return 'NOT_LIKE'; case 'exists': return 'EXISTS'; - case 'nexists': + case 'not exists': return 'NOT_EXISTS'; case 'contains': return 'CONTAINS'; - case 'ncontains': + case 'not contains': return 'NOT_CONTAINS'; case 'has': return OPERATORS.HAS; - case 'nhas': + case 'not has': return OPERATORS.NHAS; default: return op; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss index 8295918e1043..079d46644a44 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss @@ -17,6 +17,7 @@ .query-builder-search-v2 { display: flex; gap: 4px; + flex: 1; .ant-select-dropdown { padding: 0px; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx index 7edad1757fe2..2505e70eb7ab 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx @@ -113,12 +113,14 @@ export enum DropdownState { } function getInitTags(query: IBuilderQuery): ITag[] { - return query.filters.items.map((item) => ({ - id: item.id, - key: item.key as BaseAutocompleteData, - op: getOperatorFromValue(item.op), - value: item.value, - })); + return ( + query.filters?.items?.map((item) => ({ + id: item.id, + key: item.key as BaseAutocompleteData, + op: getOperatorFromValue(item.op), + value: item.value, + })) || [] + ); } function QueryBuilderSearchV2( @@ -175,20 +177,20 @@ function QueryBuilderSearchV2( searchValue, query.dataSource, query.aggregateOperator, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, ], [ searchValue, query.dataSource, query.aggregateOperator, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, ], ); const queryFiltersWithoutId = useMemo( () => ({ ...query.filters, - items: query.filters.items.map((item) => { + items: query.filters?.items.map((item) => { const filterWithoutId = cloneDeep(item); unset(filterWithoutId, 'id'); return filterWithoutId; @@ -206,7 +208,7 @@ function QueryBuilderSearchV2( () => [ query.aggregateOperator, query.dataSource, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, currentFilterItem?.key?.key || '', currentFilterItem?.key?.dataType, currentFilterItem?.key?.type ?? '', @@ -217,7 +219,7 @@ function QueryBuilderSearchV2( [ query.aggregateOperator, query.dataSource, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, currentFilterItem?.key?.key, currentFilterItem?.key?.dataType, currentFilterItem?.key?.type, @@ -237,19 +239,20 @@ function QueryBuilderSearchV2( const isQueryEnabled = useMemo(() => { if (currentState === DropdownState.ATTRIBUTE_KEY) { return query.dataSource === DataSource.METRICS - ? !!query.dataSource && !!query.aggregateAttribute.dataType + ? !!query.dataSource && !!query.aggregateAttribute?.dataType : true; } + return false; - }, [currentState, query.aggregateAttribute.dataType, query.dataSource]); + }, [currentState, query.aggregateAttribute?.dataType, query.dataSource]); const { data, isFetching } = useGetAggregateKeys( { searchText: searchValue?.split(' ')[0], dataSource: query.dataSource, - aggregateOperator: query.aggregateOperator, - aggregateAttribute: query.aggregateAttribute.key, - tagType: query.aggregateAttribute.type ?? null, + aggregateOperator: query.aggregateOperator || '', + aggregateAttribute: query.aggregateAttribute?.key || '', + tagType: query.aggregateAttribute?.type ?? null, }, { queryKey: [searchParams], @@ -264,7 +267,7 @@ function QueryBuilderSearchV2( { searchText: searchValue?.split(' ')[0], dataSource: query.dataSource, - filters: query.filters, + filters: query.filters || { items: [], op: 'AND' }, }, { queryKey: [suggestionsParams], @@ -277,9 +280,9 @@ function QueryBuilderSearchV2( isFetching: isFetchingAttributeValues, } = useGetAggregateValues( { - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', dataSource: query.dataSource, - aggregateAttribute: query.aggregateAttribute.key, + aggregateAttribute: query.aggregateAttribute?.key || '', attributeKey: currentFilterItem?.key?.key || '', filterAttributeKeyDataType: currentFilterItem?.key?.dataType ?? DataTypes.EMPTY, @@ -452,7 +455,7 @@ function QueryBuilderSearchV2( if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); - handleRunQuery(); + handleRunQuery(false, true); setIsOpen(false); } }, @@ -914,7 +917,7 @@ function QueryBuilderSearchV2( setSearchValue(''); setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails))); if (triggerOnChangeOnClose) { - onChange(query.filters); + onChange(query.filters || { items: [], op: 'AND' }); } }; @@ -992,7 +995,7 @@ function QueryBuilderSearchV2( className, )} rootClassName={cx('query-builder-search', rootClassName)} - disabled={isMetricsDataSource && !query.aggregateAttribute.key} + disabled={isMetricsDataSource && !query.aggregateAttribute?.key} style={selectStyle} onSearch={handleSearch} onSelect={handleDropdownSelect} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector.tsx index 287d06a27311..4ff2170e104a 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector.tsx @@ -1,4 +1,5 @@ import { Select } from 'antd'; +import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { cloneDeep } from 'lodash-es'; import { useEffect, useState } from 'react'; @@ -119,21 +120,32 @@ function SpanScopeSelector({ return [...nonScopeFilters, ...newScopeFilter]; }; + const keysToRemove = Object.values(SPAN_FILTER_CONFIG) + .map((config) => config?.key) + .filter((key): key is string => typeof key === 'string'); + newQuery.builder.queryData = newQuery.builder.queryData.map((item) => ({ ...item, + filter: { + expression: removeKeysFromExpression( + item.filter?.expression ?? '', + keysToRemove, + ), + }, filters: { ...item.filters, items: getUpdatedFilters( item.filters?.items, item.queryName === query?.queryName, ), + op: item.filters?.op || 'AND', }, })); if (skipQueryBuilderRedirect && onChange && query) { onChange({ - ...query.filters, - items: getUpdatedFilters([...query.filters.items], true), + ...(query.filters || { items: [], op: 'AND' }), + items: getUpdatedFilters([...(query.filters?.items || [])], true) || [], }); setSelectedScope(newScope); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/__test__/SpanScopeSelector.test.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/__test__/SpanScopeSelector.test.tsx index d1930c88eec8..9e02758f92d1 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/__test__/SpanScopeSelector.test.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/__test__/SpanScopeSelector.test.tsx @@ -139,7 +139,7 @@ describe('SpanScopeSelector', () => { updatedQuery: Query, expectedKey: string, ): void => { - const filters = updatedQuery.builder.queryData[0].filters.items; + const filters = updatedQuery.builder.queryData[0].filters?.items || []; expect(filters).toContainEqual( expect.objectContaining({ key: expect.objectContaining({ @@ -429,7 +429,9 @@ describe('SpanScopeSelector', () => { const redirectQueryArg = mockRedirectWithQueryBuilderData.mock .calls[0][0] as Query; - const { items } = redirectQueryArg.builder.queryData[0].filters; + const { items } = redirectQueryArg.builder.queryData[0].filters || { + items: [], + }; // Count non-scope filters const nonScopeFilters = items.filter( (filter) => filter.key?.type !== 'spanSearchScope', diff --git a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx index 4b1984bb48f9..11f72183d095 100644 --- a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx @@ -1,6 +1,7 @@ import { Select } from 'antd'; import { REDUCE_TO_VALUES } from 'constants/queryBuilder'; import { memo } from 'react'; +import { MetricAggregation } from 'types/api/v5/queryRange'; // ** Types import { ReduceOperators } from 'types/common/queryBuilder'; import { SelectOption } from 'types/common/select'; @@ -11,8 +12,11 @@ export const ReduceToFilter = memo(function ReduceToFilter({ query, onChange, }: ReduceToFilterProps): JSX.Element { + const reduceToValue = + (query.aggregations?.[0] as MetricAggregation)?.reduceTo || query.reduceTo; + const currentValue = - REDUCE_TO_VALUES.find((option) => option.value === query.reduceTo) || + REDUCE_TO_VALUES.find((option) => option.value === reduceToValue) || REDUCE_TO_VALUES[0]; const handleChange = ( diff --git a/frontend/src/container/ResourceAttributeFilterV2/ResourceAttributesFilterV2.tsx b/frontend/src/container/ResourceAttributeFilterV2/ResourceAttributesFilterV2.tsx index 3a0be5d24ef3..2366dddf9ebe 100644 --- a/frontend/src/container/ResourceAttributeFilterV2/ResourceAttributesFilterV2.tsx +++ b/frontend/src/container/ResourceAttributeFilterV2/ResourceAttributesFilterV2.tsx @@ -21,21 +21,26 @@ function ResourceAttributesFilter(): JSX.Element | null { // initialise tab with default query. useShareBuilderUrl({ - ...initialQueriesMap.traces, - builder: { - ...initialQueriesMap.traces.builder, - queryData: [ - { - ...initialQueriesMap.traces.builder.queryData[0], - dataSource: DataSource.TRACES, - aggregateOperator: 'noop', - aggregateAttribute: { - ...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute, - type: 'resource', + defaultValue: { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueriesMap.traces.builder.queryData[0], + dataSource: DataSource.TRACES, + aggregateOperator: 'noop', + aggregateAttribute: { + ...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute, + key: + initialQueriesMap.traces.builder.queryData[0].aggregateAttribute?.key || + '', + type: 'resource', + }, + queryName: '', }, - queryName: '', - }, - ], + ], + }, }, }); diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetrics.test.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetrics.test.tsx index bb5901b8f7f8..7ae0057345b4 100644 --- a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetrics.test.tsx +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetrics.test.tsx @@ -1,48 +1,193 @@ +/* eslint-disable sonarjs/no-identical-functions */ +import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; import { act, render, screen } from 'tests/test-utils'; import ServicesUsingMetrics from './index'; +// Mock the useGetTopLevelOperations hook +jest.mock('hooks/useGetTopLevelOperations', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const mockUseGetTopLevelOperations = useGetTopLevelOperations as jest.MockedFunction< + typeof useGetTopLevelOperations +>; + describe('ServicesUsingMetrics', () => { + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); + }); + test('should render the ServicesUsingMetrics component', async () => { - await act(() => { + // Mock successful API response + mockUseGetTopLevelOperations.mockReturnValue({ + data: { + SampleApp: ['GET'], + TestApp: ['POST'], + }, + isLoading: false, + isError: false, + error: null, + isIdle: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: true, + status: 'success', + dataUpdatedAt: 0, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + isFetching: false, + isFetchingNextPage: false, + isFetchingPreviousPage: false, + isPlaceholderData: false, + isPreviousData: false, + isStale: false, + refetch: jest.fn(), + remove: jest.fn(), + } as any); + + // Mock the query range API responses + server.use( + rest.post('*/api/v1/query_range', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'A' }, + values: [[Date.now() / 1000, '100']], + }, + { + metric: { __name__: 'D' }, + values: [[Date.now() / 1000, '50']], + }, + { + metric: { __name__: 'F1' }, + values: [[Date.now() / 1000, '2.5']], + }, + ], + }, + }), + ), + ), + ); + + await act(async () => { render(); }); - const applicationHeader = await screen.findByText(/application/i); - expect(applicationHeader).toBeInTheDocument(); - const p99LatencyHeader = await screen.findByText(/p99 latency \(in ns\)/i); - expect(p99LatencyHeader).toBeInTheDocument(); - const errorRateHeader = await screen.findByText(/error rate \(% of total\)/i); - expect(errorRateHeader).toBeInTheDocument(); + + // Wait for the component to load and render + await screen.findByText(/application/i); + expect(screen.getByText(/p99 latency \(in ns\)/i)).toBeInTheDocument(); + expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument(); }); test('should render the ServicesUsingMetrics component with loading', async () => { - await act(() => { + // Mock loading state + mockUseGetTopLevelOperations.mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + error: null, + isIdle: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: false, + status: 'loading', + dataUpdatedAt: 0, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + isFetching: true, + isFetchingNextPage: false, + isFetchingPreviousPage: false, + isPlaceholderData: false, + isPreviousData: false, + isStale: false, + refetch: jest.fn(), + remove: jest.fn(), + } as any); + + await act(async () => { render(); }); - const loadingText = await screen.findByText(/Testapp/i); - expect(loadingText).toBeInTheDocument(); + + // Should show loading spinner + expect(screen.getByLabelText(/loading/i)).toBeInTheDocument(); }); - test('should not render is the data is not prsent', async () => { + test('should not render if the data is not present', async () => { + // Mock successful API response with data + mockUseGetTopLevelOperations.mockReturnValue({ + data: { + SampleApp: ['GET'], + TestApp: ['GET'], + }, + isLoading: false, + isError: false, + error: null, + isIdle: false, + isLoadingError: false, + isRefetchError: false, + isSuccess: true, + status: 'success', + dataUpdatedAt: 0, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + isFetching: false, + isFetchingNextPage: false, + isFetchingPreviousPage: false, + isPlaceholderData: false, + isPreviousData: false, + isStale: false, + refetch: jest.fn(), + remove: jest.fn(), + } as any); + + // Mock the query range API responses server.use( - rest.post( - 'http://localhost/api/v1/service/top_level_operations', - (req, res, ctx) => - res( - ctx.status(200), - ctx.json({ - SampleApp: ['GET'], - TestApp: ['GET'], - }), - ), + rest.post('*/api/v1/query_range', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'A' }, + values: [[Date.now() / 1000, '100']], + }, + { + metric: { __name__: 'D' }, + values: [[Date.now() / 1000, '50']], + }, + { + metric: { __name__: 'F1' }, + values: [[Date.now() / 1000, '2.5']], + }, + ], + }, + }), + ), ), ); - render(); - const sampleAppText = await screen.findByText(/SampleApp/i); - expect(sampleAppText).toBeInTheDocument(); - const testAppText = await screen.findByText(/TestApp/i); - expect(testAppText).toBeInTheDocument(); + + await act(async () => { + render(); + }); + + // Wait for the services to be rendered + await screen.findByText(/SampleApp/i); + expect(screen.getByText(/TestApp/i)).toBeInTheDocument(); }); }); diff --git a/frontend/src/container/ServiceApplication/utils.ts b/frontend/src/container/ServiceApplication/utils.ts index aef4cbb5f61d..25309ab61736 100644 --- a/frontend/src/container/ServiceApplication/utils.ts +++ b/frontend/src/container/ServiceApplication/utils.ts @@ -15,10 +15,10 @@ import { } from './types'; export function getSeriesValue( - queryArray: QueryDataV3[], + queryArray: QueryDataV3[] | undefined, queryName: string, ): string { - const queryObject = queryArray.find((item) => item.queryName === queryName); + const queryObject = queryArray?.find((item) => item?.queryName === queryName); const series = queryObject ? queryObject.series : 0; return series ? series[0].values[0].value : '0'; } diff --git a/frontend/src/container/TimeSeriesView/TimeSeriesView.styles.scss b/frontend/src/container/TimeSeriesView/TimeSeriesView.styles.scss index e69de29bb2d1..afaa0942d880 100644 --- a/frontend/src/container/TimeSeriesView/TimeSeriesView.styles.scss +++ b/frontend/src/container/TimeSeriesView/TimeSeriesView.styles.scss @@ -0,0 +1,11 @@ +.time-series-view { + height: 50vh; + min-height: 350px; + padding: 0px 12px; + + .ant-card-body { + height: 50vh; + min-height: 350px; + padding: 0px 12px; + } +} diff --git a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx index 4a351b44190f..7ba3e12ab179 100644 --- a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx +++ b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx @@ -11,6 +11,7 @@ import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/Metrics import NoLogs from 'container/NoLogs/NoLogs'; import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useResizeObserver } from 'hooks/useDimensions'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -33,8 +34,6 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import uPlot from 'uplot'; import { getTimeRange } from 'utils/getTimeRange'; -import { Container } from './styles'; - function TimeSeriesView({ data, isLoading, @@ -48,6 +47,7 @@ function TimeSeriesView({ const dispatch = useDispatch(); const urlQuery = useUrlQuery(); const location = useLocation(); + const { currentQuery } = useQueryBuilder(); const chartData = useMemo(() => getUPlotChartData(data?.payload), [ data?.payload, @@ -159,10 +159,12 @@ function TimeSeriesView({ tzDate: (timestamp: number) => uPlot.tzDate(new Date(timestamp * 1e3), timezone.value), timezone: timezone.value, + currentQuery, + query: currentQuery, }); return ( - +
{isError && }
}
- +
); } diff --git a/frontend/src/container/TimeSeriesView/index.tsx b/frontend/src/container/TimeSeriesView/index.tsx index de8038847dab..8b27c31a9272 100644 --- a/frontend/src/container/TimeSeriesView/index.tsx +++ b/frontend/src/container/TimeSeriesView/index.tsx @@ -1,4 +1,6 @@ -import { ENTITY_VERSION_V4 } from 'constants/app'; +import './TimeSeriesView.styles.scss'; + +import { ENTITY_VERSION_V5 } from 'constants/app'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; @@ -29,8 +31,8 @@ function TimeSeriesViewContainer({ currentQuery.builder.queryData.forEach( ({ aggregateAttribute, aggregateOperator }) => { const isExistDurationNanoAttribute = - aggregateAttribute.key === 'durationNano' || - aggregateAttribute.key === 'duration_nano'; + aggregateAttribute?.key === 'durationNano' || + aggregateAttribute?.key === 'duration_nano'; const isCountOperator = aggregateOperator === 'count' || aggregateOperator === 'count_distinct'; @@ -52,7 +54,8 @@ function TimeSeriesViewContainer({ dataSource, }, }, - ENTITY_VERSION_V4, + // ENTITY_VERSION_V4, + ENTITY_VERSION_V5, { queryKey: [ REACT_QUERY_KEY.GET_QUERY_RANGE, diff --git a/frontend/src/container/Toolbar/Toolbar.styles.scss b/frontend/src/container/Toolbar/Toolbar.styles.scss index 472b1bd7598d..54d92dfc85f7 100644 --- a/frontend/src/container/Toolbar/Toolbar.styles.scss +++ b/frontend/src/container/Toolbar/Toolbar.styles.scss @@ -1,12 +1,23 @@ .toolbar { - display: grid; - grid-template-columns: 3fr auto auto; - padding: 8px 16px; + display: flex; + justify-content: space-between; + align-items: flex-end; + + border-top: 1px solid var(--bg-slate-400); + border-bottom: 1px solid var(--bg-slate-400); + + padding: 0px 8px; gap: 8px; + .rightActions { + margin: 4px 0; + display: flex; + align-items: center; + gap: 4px; + } + .timeRange { display: flex; - padding-right: 8px; align-items: center; gap: 16px; } @@ -27,3 +38,10 @@ } } } + +.lightMode { + .toolbar { + border-top: 1px solid var(--bg-vanilla-300); + border-bottom: 1px solid var(--bg-vanilla-300); + } +} diff --git a/frontend/src/container/Toolbar/Toolbar.tsx b/frontend/src/container/Toolbar/Toolbar.tsx index 85d6cd346745..d85e383af5ce 100644 --- a/frontend/src/container/Toolbar/Toolbar.tsx +++ b/frontend/src/container/Toolbar/Toolbar.tsx @@ -32,14 +32,19 @@ export default function Toolbar({ return (
{leftActions}
-
- {showOldCTA && } - + +
+
+ {showOldCTA && } + +
+ + {rightActions}
-
{rightActions}
); } diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss b/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss index 601b513f8fc4..0cdc9bafd57b 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss @@ -31,6 +31,10 @@ } } + .refresh-text-container { + display: none; + } + .refresh-actions { display: flex; flex-direction: row; @@ -309,3 +313,11 @@ border-color: var(--bg-vanilla-300); } } + +@media (min-width: 1400px) { + .date-time-selector { + .refresh-text-container { + display: block; + } + } +} diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index 3ca773569fbb..d17dde4ba266 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -27,7 +27,7 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax'; import getTimeString from 'lib/getTimeString'; -import { isObject } from 'lodash-es'; +import { cloneDeep, isObject } from 'lodash-es'; import { Check, Copy, Info, Send, Undo } from 'lucide-react'; import { useTimezone } from 'providers/Timezone'; import { useCallback, useEffect, useState } from 'react'; @@ -45,6 +45,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { GlobalReducer } from 'types/reducer/globalTime'; import { normalizeTimeToMs } from 'utils/timeUtils'; +import { v4 as uuid } from 'uuid'; import AutoRefresh from '../AutoRefreshV2'; import { DateTimeRangeType } from '../CustomDateTimeModal'; @@ -185,7 +186,12 @@ function DateTimeSelection({ false, ); - const { stagedQuery, initQueryBuilderData, panelType } = useQueryBuilder(); + const { + stagedQuery, + currentQuery, + initQueryBuilderData, + panelType, + } = useQueryBuilder(); const handleGoLive = useCallback(() => { if (!stagedQuery) return; @@ -345,6 +351,30 @@ function DateTimeSelection({ return `Refreshed ${secondsDiff} sec ago`; }, [maxTime, minTime, selectedTime]); + const getUpdatedCompositeQuery = useCallback((): string => { + let updatedCompositeQuery = cloneDeep(currentQuery); + updatedCompositeQuery.id = uuid(); + // Remove the filters + updatedCompositeQuery = { + ...updatedCompositeQuery, + builder: { + ...updatedCompositeQuery.builder, + queryData: updatedCompositeQuery.builder.queryData.map((item) => ({ + ...item, + filter: { + ...item.filter, + expression: item.filter?.expression?.trim() || '', + }, + filters: { + items: [], + op: 'AND', + }, + })), + }, + }; + return JSON.stringify(updatedCompositeQuery); + }, [currentQuery]); + const onSelectHandler = useCallback( (value: Time | CustomTimeType): void => { if (isModalTimeSelection) { @@ -380,26 +410,21 @@ function DateTimeSelection({ // Remove Hidden Filters from URL query parameters on time change urlQuery.delete(QueryParams.activeLogId); + const updatedCompositeQuery = getUpdatedCompositeQuery(); + urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; safeNavigate(generatedUrl); - // For logs explorer - time range handling is managed in useCopyLogLink.ts:52 - - if (!stagedQuery) { - return; - } - // the second boolean param directs the qb about the time change so to merge the query and retain the current state - // we removed update step interval to stop auto updating the value on time change - initQueryBuilderData(stagedQuery, true); + // // For logs explorer - time range handling is managed in useCopyLogLink.ts:52 }, [ - initQueryBuilderData, isModalTimeSelection, location.pathname, onTimeChange, refreshButtonHidden, safeNavigate, - stagedQuery, + getUpdatedCompositeQuery, updateLocalStorageForRoutes, updateTimeInterval, urlQuery, @@ -462,6 +487,10 @@ function DateTimeSelection({ ); urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString()); urlQuery.delete(QueryParams.relativeTime); + + const updatedCompositeQuery = getUpdatedCompositeQuery(); + urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; safeNavigate(generatedUrl); } @@ -782,18 +811,22 @@ function DateTimeSelection({ )} + {showOldExplorerCTA && (
)} + {!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && ( - +
+ +
)}
(BASE_FILTER_QUERY.filters); + const [filters, setFilters] = useState( + BASE_FILTER_QUERY.filters || { items: [], op: 'AND' }, + ); const [noData, setNoData] = useState(false); const [filteredSpanIds, setFilteredSpanIds] = useState([]); const handleFilterChange = (value: TagFilter): void => { diff --git a/frontend/src/container/TracesExplorer/ListView/ListView.styles.scss b/frontend/src/container/TracesExplorer/ListView/ListView.styles.scss new file mode 100644 index 000000000000..26549c6a220b --- /dev/null +++ b/frontend/src/container/TracesExplorer/ListView/ListView.styles.scss @@ -0,0 +1,33 @@ +.trace-explorer-controls { + display: flex; + justify-content: flex-end; + gap: 8px; + + .order-by-container { + display: flex; + align-items: center; + gap: 8px; + + .order-by-label { + color: var(--text-vanilla-400); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + + display: flex; + align-items: center; + gap: 4px; + } + + .order-by-select { + width: 100px; + + .ant-select-selector { + border: none; + box-shadow: none; + background-color: transparent; + } + } + } +} diff --git a/frontend/src/container/TracesExplorer/ListView/configs.tsx b/frontend/src/container/TracesExplorer/ListView/configs.tsx index 308ca1995e4a..298a938057df 100644 --- a/frontend/src/container/TracesExplorer/ListView/configs.tsx +++ b/frontend/src/container/TracesExplorer/ListView/configs.tsx @@ -1,11 +1,12 @@ import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination'; export const defaultSelectedColumns: string[] = [ - 'serviceName', + 'service.name', 'name', - 'durationNano', - 'httpMethod', - 'responseStatusCode', + 'duration_nano', + 'http_method', + 'response_status_code', + 'timestamp', ]; export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS]; diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx index 78d239453f3f..a08953ded204 100644 --- a/frontend/src/container/TracesExplorer/ListView/index.tsx +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -1,6 +1,9 @@ +import './ListView.styles.scss'; + import logEvent from 'api/common/logEvent'; +import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy'; import { ResizeTable } from 'components/ResizeTable'; -import { ENTITY_VERSION_V4 } from 'constants/app'; +import { ENTITY_VERSION_V5 } from 'constants/app'; import { LOCALSTORAGE } from 'constants/localStorage'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; @@ -18,8 +21,10 @@ import useDragColumns from 'hooks/useDragColumns'; import { getDraggedColumns } from 'hooks/useDragColumns/utils'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { cloneDeep } from 'lodash-es'; +import { ArrowUp10, Minus } from 'lucide-react'; import { useTimezone } from 'providers/Timezone'; -import { memo, useCallback, useEffect, useMemo } from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { DataSource } from 'types/common/queryBuilder'; @@ -42,6 +47,8 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST; + const [orderBy, setOrderBy] = useState('timestamp:desc'); + const { selectedTime: globalSelectedTime, maxTime, @@ -68,6 +75,23 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { const paginationConfig = paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS); + const requestQuery = useMemo(() => { + const query = stagedQuery + ? cloneDeep(stagedQuery) + : cloneDeep(initialQueriesMap.traces); + + if (query.builder.queryData[0]) { + query.builder.queryData[0].orderBy = [ + { + columnName: orderBy.split(':')[0], + order: orderBy.split(':')[1] as 'asc' | 'desc', + }, + ]; + } + + return query; + }, [stagedQuery, orderBy]); + const queryKey = useMemo( () => [ REACT_QUERY_KEY.GET_QUERY_RANGE, @@ -78,6 +102,7 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { panelType, paginationConfig, options?.selectColumns, + orderBy, ], [ stagedQuery, @@ -87,12 +112,13 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { options?.selectColumns, maxTime, minTime, + orderBy, ], ); const { data, isFetching, isLoading, isError } = useGetQueryRange( { - query: stagedQuery || initialQueriesMap.traces, + query: requestQuery, graphType: panelType, selectedTime: 'GLOBAL_TIME' as const, globalSelectedInterval: globalSelectedTime as CustomTimeType, @@ -104,7 +130,8 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { selectColumns: options?.selectColumns, }, }, - ENTITY_VERSION_V4, + // ENTITY_VERSION_V4, + ENTITY_VERSION_V5, { queryKey, enabled: @@ -146,6 +173,10 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { [columns, onDragColumns], ); + const handleOrderChange = useCallback((value: string) => { + setOrderBy(value); + }, []); + const isDataAbsent = !isLoading && !isFetching && @@ -167,12 +198,26 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { return ( {transformedQueryTableData.length !== 0 && ( - +
+
+
+ Order by +
+ + +
+ + +
)} {isError && {data?.error || 'Something went wrong'}} diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx index 1a0a43d4cab3..c2ffba8c7ba1 100644 --- a/frontend/src/container/TracesExplorer/ListView/utils.tsx +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -1,5 +1,6 @@ import { Tag, Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import ROUTES from 'constants/routes'; import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util'; @@ -9,7 +10,6 @@ import { RowData } from 'lib/query/createTableColumnsFromQuery'; import LineClampedText from 'periscope/components/LineClampedText/LineClampedText'; import { Link } from 'react-router-dom'; import { ILog } from 'types/api/logs/log'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; export function BlockLink({ @@ -40,14 +40,14 @@ export const transformDataWithDate = ( []; export const getTraceLink = (record: RowData): string => - `${ROUTES.TRACE}/${record.traceID}${formUrlParams({ - spanId: record.spanID, + `${ROUTES.TRACE}/${record.traceID || record.trace_id}${formUrlParams({ + spanId: record.spanID || record.span_id, levelUp: 0, levelDown: 0, })}`; export const getListColumns = ( - selectedColumns: BaseAutocompleteData[], + selectedColumns: TelemetryFieldKey[], formatTimezoneAdjustedTimestamp: ( input: TimestampInput, format?: string, @@ -80,48 +80,58 @@ export const getListColumns = ( ]; const columns: ColumnsType = - selectedColumns.map(({ dataType, key, type }) => ({ - title: key, - dataIndex: key, - key: `${key}-${dataType}-${type}`, - width: 145, - render: (value, item): JSX.Element => { - if (value === '') { + selectedColumns.map((props) => { + const name = props?.name || (props as any)?.key; + const fieldDataType = props?.fieldDataType || (props as any)?.dataType; + const fieldContext = props?.fieldContext || (props as any)?.type; + return { + title: name, + dataIndex: name, + key: `${name}-${fieldDataType}-${fieldContext}`, + width: 145, + render: (value, item): JSX.Element => { + if (value === '') { + return ( + + N/A + + ); + } + + if ( + name === 'httpMethod' || + name === 'responseStatusCode' || + name === 'response_status_code' || + name === 'http_method' + ) { + return ( + + + {value} + + + ); + } + + if (name === 'durationNano' || name === 'duration_nano') { + return ( + + {getMs(value)}ms + + ); + } + return ( - N/A + + + ); - } - - if (key === 'httpMethod' || key === 'responseStatusCode') { - return ( - - - {value} - - - ); - } - - if (key === 'durationNano' || key === 'duration_nano') { - return ( - - {getMs(value)}ms - - ); - } - - return ( - - - - - - ); - }, - responsive: ['md'], - })) || []; + }, + responsive: ['md'], + }; + }) || []; return [...initialColumns, ...columns]; }; diff --git a/frontend/src/container/TracesExplorer/QuerySection/index.tsx b/frontend/src/container/TracesExplorer/QuerySection/index.tsx index 8f5d2606f055..96f11fd77849 100644 --- a/frontend/src/container/TracesExplorer/QuerySection/index.tsx +++ b/frontend/src/container/TracesExplorer/QuerySection/index.tsx @@ -1,19 +1,13 @@ -import { Button } from 'antd'; +import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2'; import { PANEL_TYPES } from 'constants/queryBuilder'; import ExplorerOrderBy from 'container/ExplorerOrderBy'; -import { QueryBuilder } from 'container/QueryBuilder'; import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces'; import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; -import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { memo, useCallback, useMemo } from 'react'; import { DataSource } from 'types/common/queryBuilder'; -import { ButtonWrapper, Container } from './styles'; - function QuerySection(): JSX.Element { - const { handleRunQuery } = useQueryBuilder(); - const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { @@ -44,25 +38,19 @@ function QuerySection(): JSX.Element { }, [panelTypes, renderOrderBy]); return ( - - - - - } - /> - + ); } diff --git a/frontend/src/container/TracesExplorer/TableView/index.tsx b/frontend/src/container/TracesExplorer/TableView/index.tsx index 98cb98dd086d..a6de39409ece 100644 --- a/frontend/src/container/TracesExplorer/TableView/index.tsx +++ b/frontend/src/container/TracesExplorer/TableView/index.tsx @@ -1,13 +1,14 @@ import { Space } from 'antd'; -import { ENTITY_VERSION_V4 } from 'constants/app'; +import { ENTITY_VERSION_V5 } from 'constants/app'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { QueryTable } from 'container/QueryTable'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; import { GlobalReducer } from 'types/reducer/globalTime'; function TableView(): JSX.Element { @@ -28,7 +29,8 @@ function TableView(): JSX.Element { dataSource: 'traces', }, }, - ENTITY_VERSION_V4, + // ENTITY_VERSION_V4, + ENTITY_VERSION_V5, { queryKey: [ REACT_QUERY_KEY.GET_QUERY_RANGE, @@ -41,11 +43,19 @@ function TableView(): JSX.Element { }, ); + const queryTableData = useMemo( + () => + data?.payload?.data?.newResult?.data?.result || + data?.payload.data.result || + [], + [data], + ); + return ( diff --git a/frontend/src/container/TracesExplorer/TracesView/configs.tsx b/frontend/src/container/TracesExplorer/TracesView/configs.tsx index 202603b680d7..c15bfc43808f 100644 --- a/frontend/src/container/TracesExplorer/TracesView/configs.tsx +++ b/frontend/src/container/TracesExplorer/TracesView/configs.tsx @@ -11,17 +11,17 @@ export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS]; export const columns: ColumnsType = [ { title: 'Root Service Name', - dataIndex: 'subQuery.serviceName', + dataIndex: 'service.name', key: 'serviceName', }, { title: 'Root Operation Name', - dataIndex: 'subQuery.name', + dataIndex: 'name', key: 'name', }, { title: 'Root Duration (in ms)', - dataIndex: 'subQuery.durationNano', + dataIndex: 'duration_nano', key: 'durationNano', render: (duration: number): JSX.Element => ( {getMs(String(duration))}ms @@ -34,7 +34,7 @@ export const columns: ColumnsType = [ }, { title: 'TraceID', - dataIndex: 'traceID', + dataIndex: 'trace_id', key: 'traceID', render: (traceID: string): JSX.Element => ( ('timestamp:desc'); const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< AppState, @@ -45,14 +48,18 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element { transformBuilderQueryFields(stagedQuery || initialQueriesMap.traces, { orderBy: [ { - columnName: 'timestamp', - order: 'desc', + columnName: orderBy.split(':')[0], + order: orderBy.split(':')[1] as 'asc' | 'desc', }, ], }), - [stagedQuery], + [stagedQuery, orderBy], ); + const handleOrderChange = useCallback((value: string) => { + setOrderBy(value); + }, []); + const { data, isLoading, isFetching, isError } = useGetQueryRange( { query: transformedQuery, @@ -66,7 +73,7 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element { pagination: paginationQueryData, }, }, - ENTITY_VERSION_V4, + ENTITY_VERSION_V5, { queryKey: [ REACT_QUERY_KEY.GET_QUERY_RANGE, @@ -76,6 +83,7 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element { stagedQuery, panelType, paginationQueryData, + orderBy, ], enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE, }, @@ -106,11 +114,26 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element { here - + +
+
+
+ Order by +
+ + +
+ + +
)} diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts index 1f602d95d5c3..6b7874366d20 100644 --- a/frontend/src/hooks/dashboard/utils.ts +++ b/frontend/src/hooks/dashboard/utils.ts @@ -1,8 +1,8 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils'; import { placeWidgetAtBottom } from 'container/NewWidget/utils'; import { Dashboard } from 'types/api/dashboard/getAll'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; const baseLogsSelectedColumns = { @@ -16,7 +16,7 @@ export const addEmptyWidgetInDashboardJSONWithQuery = ( query: Query, widgetId: string, panelType?: PANEL_TYPES, - selectedColumns?: BaseAutocompleteData[] | null, + selectedColumns?: TelemetryFieldKey[] | null, ): Dashboard => { const logsSelectedColumns = [ baseLogsSelectedColumns, diff --git a/frontend/src/hooks/hotkeys/useKeyboardHotkeys.tsx b/frontend/src/hooks/hotkeys/useKeyboardHotkeys.tsx index 12b70fa68e07..311d6b88b526 100644 --- a/frontend/src/hooks/hotkeys/useKeyboardHotkeys.tsx +++ b/frontend/src/hooks/hotkeys/useKeyboardHotkeys.tsx @@ -31,7 +31,7 @@ const KeyboardHotkeysContext = createContext( }, ); -const IGNORE_INPUTS = ['input', 'textarea']; // Inputs in which hotkey events will be ignored +const IGNORE_INPUTS = ['input', 'textarea', 'cm-editor']; // Inputs in which hotkey events will be ignored const useKeyboardHotkeys = (): KeyboardHotkeysContextReturnValue => { const context = useContext(KeyboardHotkeysContext); @@ -54,7 +54,13 @@ function KeyboardHotkeysProvider({ const handleKeyPress = (event: KeyboardEvent): void => { const { key, ctrlKey, altKey, shiftKey, metaKey, target } = event; - if (IGNORE_INPUTS.includes((target as HTMLElement).tagName.toLowerCase())) { + const isCodeMirrorEditor = + (target as HTMLElement).closest('.cm-editor') !== null; + + if ( + IGNORE_INPUTS.includes((target as HTMLElement).tagName.toLowerCase()) || + isCodeMirrorEditor + ) { return; } diff --git a/frontend/src/hooks/infraMonitoring/useHandleLogsPagination.tsx b/frontend/src/hooks/infraMonitoring/useHandleLogsPagination.tsx index ccf88985ebf5..061ea8657c26 100644 --- a/frontend/src/hooks/infraMonitoring/useHandleLogsPagination.tsx +++ b/frontend/src/hooks/infraMonitoring/useHandleLogsPagination.tsx @@ -60,7 +60,7 @@ export const useHandleLogsPagination = ({ const [isPaginating, setIsPaginating] = useState(false); const { shouldResetPage, newRestFilters } = useMemo(() => { - const newRestFilters = filters.items.filter((item) => { + const newRestFilters = filters?.items?.filter((item) => { const keyToCheck = item.key?.key ?? ''; return ( !queryKeyFilters.includes(keyToCheck) && @@ -132,7 +132,7 @@ export const useHandleLogsPagination = ({ } setPrevTimeRange(timeRange); - setRestFilters(newRestFilters); + setRestFilters(newRestFilters || []); // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldResetPage, timeRange]); diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts index be5d882f31d6..4e8fe6137bc5 100644 --- a/frontend/src/hooks/logs/useActiveLog.ts +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -35,6 +35,7 @@ export function getOldLogsOperatorFromNew(operator: string): string { return operator; } } +// eslint-disable-next-line sonarjs/cognitive-complexity export const useActiveLog = (): UseActiveLog => { const dispatch = useDispatch(); @@ -91,10 +92,11 @@ export const useActiveLog = (): UseActiveLog => { async () => getAggregateKeys({ searchText: fieldKey, - aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + aggregateOperator: + currentQuery.builder.queryData[0].aggregateOperator || '', dataSource: currentQuery.builder.queryData[0].dataSource, aggregateAttribute: - currentQuery.builder.queryData[0].aggregateAttribute.key, + currentQuery.builder.queryData[0].aggregateAttribute?.key || '', }), ); @@ -120,7 +122,7 @@ export const useActiveLog = (): UseActiveLog => { filters: { ...item.filters, items: [ - ...item.filters.items, + ...(item.filters?.items || []), { id: uuid(), key: existAutocompleteKey, @@ -128,6 +130,7 @@ export const useActiveLog = (): UseActiveLog => { value: fieldValue, }, ], + op: item.filters?.op || 'AND', }, })), }, @@ -154,10 +157,11 @@ export const useActiveLog = (): UseActiveLog => { async () => getAggregateKeys({ searchText: fieldKey, - aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + aggregateOperator: + currentQuery.builder.queryData[0].aggregateOperator || '', dataSource: currentQuery.builder.queryData[0].dataSource, aggregateAttribute: - currentQuery.builder.queryData[0].aggregateAttribute.key, + currentQuery.builder.queryData[0].aggregateAttribute?.key || '', }), ); diff --git a/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx b/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx index e4a36ebcbe10..0a27c25cd5f6 100644 --- a/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx +++ b/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx @@ -63,6 +63,7 @@ const useCreateAlerts = ( graphType: getGraphType(widget.panelTypes), selectedTime: widget.timePreferance, variables: getDashboardVariables(selectedDashboard?.data.variables), + originalGraphType: widget.panelTypes, }); queryRangeMutation.mutate(queryPayload, { onSuccess: (data) => { diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index a690afe91f9c..cb6ab591b26e 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -68,13 +68,13 @@ export const useFetchKeysAndValues = ( searchKey, query.dataSource, query.aggregateOperator, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, ], [ searchKey, query.dataSource, query.aggregateOperator, - query.aggregateAttribute.key, + query.aggregateAttribute?.key, ], ); @@ -107,12 +107,12 @@ export const useFetchKeysAndValues = ( query.dataSource === DataSource.METRICS && !isInfraMonitoring && !isMetricsExplorer - ? !!query.dataSource && !!query.aggregateAttribute.dataType + ? !!query.dataSource && !!query.aggregateAttribute?.dataType : true, [ isInfraMonitoring, isMetricsExplorer, - query.aggregateAttribute.dataType, + query.aggregateAttribute?.dataType, query.dataSource, ], ); @@ -121,12 +121,12 @@ export const useFetchKeysAndValues = ( { searchText: searchKey, dataSource: query.dataSource, - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', aggregateAttribute: isInfraMonitoring && entity ? GetK8sEntityToAggregateAttribute(entity, dotMetricsEnabled) - : query.aggregateAttribute.key, - tagType: query.aggregateAttribute.type ?? null, + : query.aggregateAttribute?.key || '', + tagType: query.aggregateAttribute?.type ?? null, }, { queryKey: [searchParams], @@ -144,7 +144,7 @@ export const useFetchKeysAndValues = ( { searchText: searchKey, dataSource: query.dataSource, - filters: query.filters, + filters: query.filters || { items: [], op: 'AND' }, }, { queryKey: [suggestionsParams], @@ -221,7 +221,8 @@ export const useFetchKeysAndValues = ( dataSource: query.dataSource, aggregateAttribute: GetK8sEntityToAggregateAttribute(entity, dotMetricsEnabled) || - query.aggregateAttribute.key, + query.aggregateAttribute?.key || + '', attributeKey: filterAttributeKey?.key ?? tagKey, filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY, @@ -242,9 +243,9 @@ export const useFetchKeysAndValues = ( payload = response.payload?.data; } else { const response = await getAttributesValues({ - aggregateOperator: query.aggregateOperator, + aggregateOperator: query.aggregateOperator || '', dataSource: query.dataSource, - aggregateAttribute: query.aggregateAttribute.key, + aggregateAttribute: query.aggregateAttribute?.key || '', attributeKey: filterAttributeKey?.key ?? tagKey, filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY, diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts index efef00022a0f..7b24cb78829a 100644 --- a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -1,6 +1,12 @@ +import { + convertAggregationToExpression, + convertFiltersToExpressionWithExistingQuery, + convertHavingToExpression, +} from 'components/QueryBuilderV2/utils'; import { QueryParams } from 'constants/query'; import useUrlQuery from 'hooks/useUrlQuery'; import { useMemo } from 'react'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; export const useGetCompositeQueryParam = (): Query | null => { @@ -18,6 +24,42 @@ export const useGetCompositeQueryParam = (): Query | null => { parsedCompositeQuery = JSON.parse( decodeURIComponent(compositeQuery.replace(/\+/g, ' ')), ); + + // Convert old format to new format for each query in builder.queryData + if (parsedCompositeQuery?.builder?.queryData) { + parsedCompositeQuery.builder.queryData = parsedCompositeQuery.builder.queryData.map( + (query) => { + const existingExpression = query.filter?.expression || ''; + const convertedQuery = { ...query }; + + const convertedFilter = convertFiltersToExpressionWithExistingQuery( + query.filters || { items: [], op: 'AND' }, + existingExpression, + ); + convertedQuery.filter = convertedFilter.filter; + convertedQuery.filters = convertedFilter.filters; + + // Convert having if needed + if (Array.isArray(query.having)) { + const convertedHaving = convertHavingToExpression(query.having); + convertedQuery.having = convertedHaving; + } + + // Convert aggregation if needed + if (!query.aggregations && query.aggregateOperator) { + const convertedAggregation = convertAggregationToExpression( + query.aggregateOperator, + query.aggregateAttribute as BaseAutocompleteData, + query.dataSource, + query.timeAggregation, + query.spaceAggregation, + ) as any; // Type assertion to handle union type + convertedQuery.aggregations = convertedAggregation; + } + return convertedQuery; + }, + ); + } } catch (e) { parsedCompositeQuery = null; } diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index a81e7ccfc73b..b7d6f494439d 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -1,3 +1,4 @@ +import { ENTITY_VERSION_V5 } from 'constants/app'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; @@ -58,10 +59,10 @@ export const useGetExplorerQueryRange = ( query: requestData || initialQueriesMap.metrics, params, }, - version, + // version, + ENTITY_VERSION_V5, { ...options, - retry: false, queryKey: [ key, selectedTimeInterval ?? globalSelectedInterval, diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts index 9a3950d042e3..39c25b9a8578 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -1,3 +1,4 @@ +import { isAxiosError } from 'axios'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { updateStepInterval } from 'container/GridCardLayout/utils'; @@ -6,16 +7,25 @@ import { GetQueryResultsProps, } from 'lib/dashboard/getQueryResults'; import getStartEndRangeTime from 'lib/getStartEndRangeTime'; +import { useErrorModal } from 'providers/ErrorModalProvider'; import { useMemo } from 'react'; import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; import { SuccessResponse } from 'types/api'; +import APIError from 'types/api/error'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { DataSource } from 'types/common/queryBuilder'; +type UseGetQueryRangeOptions = UseQueryOptions< + SuccessResponse, + APIError | Error +> & { + showErrorModal?: boolean; +}; + type UseGetQueryRange = ( requestData: GetQueryResultsProps, version: string, - options?: UseQueryOptions, Error>, + options?: UseGetQueryRangeOptions, headers?: Record, ) => UseQueryResult, Error>; @@ -25,13 +35,14 @@ export const useGetQueryRange: UseGetQueryRange = ( options, headers, ) => { + const { showErrorModal: showErrorModalFn } = useErrorModal(); const newRequestData: GetQueryResultsProps = useMemo(() => { const firstQueryData = requestData.query.builder?.queryData[0]; const isListWithSingleTimestampOrder = requestData.graphType === PANEL_TYPES.LIST && firstQueryData?.orderBy?.length === 1 && // exclude list with id filter (i.e. context logs) - !firstQueryData?.filters.items.some((filter) => filter.key?.key === 'id') && + !firstQueryData?.filters?.items.some((filter) => filter.key?.key === 'id') && firstQueryData?.orderBy[0].columnName === 'timestamp'; const modifiedRequestData = { @@ -102,10 +113,38 @@ export const useGetQueryRange: UseGetQueryRange = ( return requestData; }, [requestData]); - return useQuery, Error>({ + const retry = useMemo(() => { + if (options?.retry !== undefined) { + return options.retry; + } + return (failureCount: number, error: Error): boolean => { + let status: number | undefined; + + if (error instanceof APIError) { + status = error.getHttpStatusCode(); + } else if (isAxiosError(error)) { + status = error.response?.status; + } + + if (status && status >= 400 && status < 500) { + return false; + } + + return failureCount < 3; + }; + }, [options?.retry]); + + return useQuery, APIError | Error>({ queryFn: async ({ signal }) => GetMetricQueryRange(modifiedRequestData, version, signal, headers), ...options, + retry, + onError: (error) => { + if (options?.showErrorModal !== false) { + showErrorModalFn(error as APIError); + } + options?.onError?.(error); + }, queryKey, }); }; diff --git a/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts index 5e5ec70e397e..beee9618ee5a 100644 --- a/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetWidgetQueryRange.ts @@ -41,7 +41,6 @@ export const useGetWidgetQueryRange = ( version, { enabled: !!stagedQuery, - retry: false, queryKey: [ REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, diff --git a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts index 234e614d305a..5ab5bfd20971 100644 --- a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts @@ -1,4 +1,5 @@ -import { ENTITY_VERSION_V4 } from 'constants/app'; +/* eslint-disable sonarjs/cognitive-complexity */ +import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app'; import { LEGEND } from 'constants/global'; import { ATTRIBUTE_TYPES, @@ -23,7 +24,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; -import { isEmpty } from 'lodash-es'; +import { isEmpty, isEqual } from 'lodash-es'; import { useCallback, useEffect, useState } from 'react'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { @@ -31,9 +32,15 @@ import { IBuilderQuery, QueryFunctionProps, } from 'types/api/queryBuilder/queryBuilderData'; +import { + MetricAggregation, + SpaceAggregation, + TimeAggregation, +} from 'types/api/v5/queryRange'; import { HandleChangeFormulaData, HandleChangeQueryData, + HandleChangeQueryDataV5, UseQueryOperations, } from 'types/common/operations.types'; import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder'; @@ -110,7 +117,7 @@ export const useQueryOperations: UseQueryOperations = ({ const handleChangeOperator = useCallback( (value: string): void => { const aggregateDataType: BaseAutocompleteData['dataType'] = - query.aggregateAttribute.dataType; + query.aggregateAttribute?.dataType; const typeOfValue = findDataTypeOfOperator(value); @@ -118,10 +125,19 @@ export const useQueryOperations: UseQueryOperations = ({ (aggregateDataType === 'string' || aggregateDataType === 'bool') && typeOfValue === 'number'; + // since this is only relevant for metrics, we can use the first aggregation + const metricAggregation = query.aggregations?.[0] as MetricAggregation; + const newQuery: IBuilderQuery = { ...query, aggregateOperator: value, timeAggregation: value, + aggregations: [ + { + ...metricAggregation, + timeAggregation: value as TimeAggregation, + }, + ], having: [], limit: null, ...(shouldResetAggregateAttribute @@ -139,6 +155,16 @@ export const useQueryOperations: UseQueryOperations = ({ const newQuery: IBuilderQuery = { ...query, spaceAggregation: value, + aggregations: [ + { + ...query.aggregations?.[0], + spaceAggregation: value as SpaceAggregation, + metricName: (query.aggregations?.[0] as MetricAggregation).metricName, + temporality: (query.aggregations?.[0] as MetricAggregation).temporality, + timeAggregation: (query.aggregations?.[0] as MetricAggregation) + .timeAggregation, + }, + ], }; handleSetQueryData(index, newQuery); @@ -150,7 +176,7 @@ export const useQueryOperations: UseQueryOperations = ({ (aggregateAttribute: BaseAutocompleteData): any => { // operators for unknown metric const isUnknownMetric = - isEmpty(aggregateAttribute.type) && !isEmpty(aggregateAttribute.key); + isEmpty(aggregateAttribute?.type) && !isEmpty(aggregateAttribute?.key); const newOperators = isUnknownMetric ? metricsUnknownTimeAggregateOperatorOptions @@ -158,10 +184,10 @@ export const useQueryOperations: UseQueryOperations = ({ dataSource: DataSource.METRICS, panelType: panelType || PANEL_TYPES.TIME_SERIES, aggregateAttributeType: - (aggregateAttribute.type as ATTRIBUTE_TYPES) || ATTRIBUTE_TYPES.GAUGE, + (aggregateAttribute?.type as ATTRIBUTE_TYPES) || ATTRIBUTE_TYPES.GAUGE, }); - switch (aggregateAttribute.type) { + switch (aggregateAttribute?.type) { case ATTRIBUTE_TYPES.SUM: setSpaceAggregationOptions(metricsSumSpaceAggregateOperatorOptions); break; @@ -198,12 +224,14 @@ export const useQueryOperations: UseQueryOperations = ({ newQuery.dataSource === DataSource.METRICS && entityVersion === ENTITY_VERSION_V4 ) { - handleMetricAggregateAtributeTypes(newQuery.aggregateAttribute); + if (newQuery.aggregateAttribute) { + handleMetricAggregateAtributeTypes(newQuery.aggregateAttribute); + } - if (newQuery.aggregateAttribute.type === ATTRIBUTE_TYPES.SUM) { + if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.SUM) { newQuery.aggregateOperator = MetricAggregateOperator.RATE; newQuery.timeAggregation = MetricAggregateOperator.RATE; - } else if (newQuery.aggregateAttribute.type === ATTRIBUTE_TYPES.GAUGE) { + } else if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.GAUGE) { newQuery.aggregateOperator = MetricAggregateOperator.AVG; newQuery.timeAggregation = MetricAggregateOperator.AVG; } else { @@ -215,8 +243,8 @@ export const useQueryOperations: UseQueryOperations = ({ // Handled query with unknown metric to avoid 400 and 500 errors // With metric value typed and not available then - time - 'avg', space - 'avg' // If not typed - time - 'rate', space - 'sum', op - 'count' - if (isEmpty(newQuery.aggregateAttribute.type)) { - if (!isEmpty(newQuery.aggregateAttribute.key)) { + if (isEmpty(newQuery.aggregateAttribute?.type)) { + if (!isEmpty(newQuery.aggregateAttribute?.key)) { newQuery.aggregateOperator = MetricAggregateOperator.AVG; newQuery.timeAggregation = MetricAggregateOperator.AVG; newQuery.spaceAggregation = MetricAggregateOperator.AVG; @@ -228,6 +256,72 @@ export const useQueryOperations: UseQueryOperations = ({ } } + if ( + newQuery.dataSource === DataSource.METRICS && + entityVersion === ENTITY_VERSION_V5 + ) { + if (newQuery.aggregateAttribute) { + handleMetricAggregateAtributeTypes(newQuery.aggregateAttribute); + } + + if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.SUM) { + newQuery.aggregations = [ + { + timeAggregation: MetricAggregateOperator.RATE, + metricName: newQuery.aggregateAttribute?.key || '', + temporality: '', + spaceAggregation: '', + }, + ]; + } else if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.GAUGE) { + newQuery.aggregations = [ + { + timeAggregation: MetricAggregateOperator.AVG, + metricName: newQuery.aggregateAttribute?.key || '', + temporality: '', + spaceAggregation: '', + }, + ]; + } else { + newQuery.aggregations = [ + { + timeAggregation: '', + metricName: newQuery.aggregateAttribute?.key || '', + temporality: '', + spaceAggregation: '', + }, + ]; + } + + newQuery.aggregateOperator = ''; + newQuery.spaceAggregation = ''; + + // Handled query with unknown metric to avoid 400 and 500 errors + // With metric value typed and not available then - time - 'avg', space - 'avg' + // If not typed - time - 'rate', space - 'sum', op - 'count' + if (isEmpty(newQuery.aggregateAttribute?.type)) { + if (!isEmpty(newQuery.aggregateAttribute?.key)) { + newQuery.aggregations = [ + { + timeAggregation: MetricAggregateOperator.AVG, + metricName: newQuery.aggregateAttribute?.key || '', + temporality: '', + spaceAggregation: MetricAggregateOperator.AVG, + }, + ]; + } else { + newQuery.aggregations = [ + { + timeAggregation: MetricAggregateOperator.COUNT, + metricName: newQuery.aggregateAttribute?.key || '', + temporality: '', + spaceAggregation: MetricAggregateOperator.SUM, + }, + ]; + } + } + } + handleSetQueryData(index, newQuery); }, [ @@ -255,7 +349,7 @@ export const useQueryOperations: UseQueryOperations = ({ }); const entries = Object.entries( - initialQueryBuilderFormValuesMap.metrics, + initialQueryBuilderFormValuesMap[nextSource], ).filter(([key]) => key !== 'queryName' && key !== 'expression'); const initCopyResult = Object.fromEntries(entries); @@ -292,9 +386,11 @@ export const useQueryOperations: UseQueryOperations = ({ index, ]); - const handleChangeQueryData: HandleChangeQueryData = useCallback( - (key, value) => { - const newQuery: IBuilderQuery = { + const handleChangeQueryData: + | HandleChangeQueryData + | HandleChangeQueryDataV5 = useCallback( + (key: string, value: any) => { + const newQuery = { ...query, [key]: key === LEGEND && typeof value === 'string' @@ -358,13 +454,23 @@ export const useQueryOperations: UseQueryOperations = ({ panelType: panelType || PANEL_TYPES.TIME_SERIES, }); - if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return; - - setOperators(initialOperators); + if ( + !operators || + operators.length === 0 || + !isEqual(operators, initialOperators) + ) { + setOperators(initialOperators); + } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataSource, initialDataSource, panelType, operators, entityVersion]); + }, [ + dataSource, + initialDataSource, + panelType, + entityVersion, + query, + handleMetricAggregateAtributeTypes, + ]); useEffect(() => { const additionalFilters = getNewListOfAdditionalFilters(dataSource, true); diff --git a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts index c79da91b3bb6..7fcda4d37269 100644 --- a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts +++ b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts @@ -5,24 +5,32 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { useGetCompositeQueryParam } from './useGetCompositeQueryParam'; import { useQueryBuilder } from './useQueryBuilder'; -export type UseShareBuilderUrlParams = { defaultValue: Query }; +export type UseShareBuilderUrlParams = { + defaultValue: Query; + /** Force reset the query regardless of URL state */ + forceReset?: boolean; +}; -export const useShareBuilderUrl = (defaultQuery: Query): void => { +export const useShareBuilderUrl = ({ + defaultValue, + forceReset = false, +}: UseShareBuilderUrlParams): void => { const { resetQuery, redirectWithQueryBuilderData } = useQueryBuilder(); const urlQuery = useUrlQuery(); const compositeQuery = useGetCompositeQueryParam(); useEffect(() => { - if (!compositeQuery) { - resetQuery(defaultQuery); - redirectWithQueryBuilderData(defaultQuery); + if (!compositeQuery || forceReset) { + resetQuery(defaultValue); + redirectWithQueryBuilderData(defaultValue); } }, [ - defaultQuery, + defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery, resetQuery, + forceReset, ]); }; diff --git a/frontend/src/hooks/queryBuilder/useStepInterval.ts b/frontend/src/hooks/queryBuilder/useStepInterval.ts index 62f8a0d7c198..49bbe4fb67ed 100644 --- a/frontend/src/hooks/queryBuilder/useStepInterval.ts +++ b/frontend/src/hooks/queryBuilder/useStepInterval.ts @@ -31,7 +31,7 @@ export const updateStepInterval = ( queryData: query?.builder?.queryData?.map((item) => ({ ...item, - stepInterval: getStepInterval(item.stepInterval), + stepInterval: getStepInterval(item?.stepInterval ?? 60), })) || [], }, }; diff --git a/frontend/src/hooks/queryBuilder/useTag.ts b/frontend/src/hooks/queryBuilder/useTag.ts index 419aaaedc9b6..cb968e276767 100644 --- a/frontend/src/hooks/queryBuilder/useTag.ts +++ b/frontend/src/hooks/queryBuilder/useTag.ts @@ -55,9 +55,10 @@ export const useTag = ( setSearchKey: (value: string) => void, whereClauseConfig?: WhereClauseConfig, ): IUseTag => { - const initTagsData = useMemo(() => queryFilterTags(query?.filters), [ - query?.filters, - ]); + const initTagsData = useMemo( + () => queryFilterTags(query?.filters || { items: [], op: 'AND' }), + [query?.filters], + ); const [tags, setTags] = useState(initTagsData); diff --git a/frontend/src/hooks/querySuggestions/useGetQueryKeySuggestions.ts b/frontend/src/hooks/querySuggestions/useGetQueryKeySuggestions.ts new file mode 100644 index 000000000000..ce50e38bcb61 --- /dev/null +++ b/frontend/src/hooks/querySuggestions/useGetQueryKeySuggestions.ts @@ -0,0 +1,66 @@ +import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions'; +import { AxiosError, AxiosResponse } from 'axios'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { + QueryKeyRequestProps, + QueryKeySuggestionsResponseProps, +} from 'types/api/querySuggestions/types'; + +type UseGetQueryKeySuggestions = ( + requestData: QueryKeyRequestProps, + options?: UseQueryOptions< + AxiosResponse, + AxiosError + >, +) => UseQueryResult< + AxiosResponse, + AxiosError +>; + +export const useGetQueryKeySuggestions: UseGetQueryKeySuggestions = ( + { + signal, + searchText, + fieldContext, + fieldDataType, + metricName, + }: QueryKeyRequestProps, + options?: UseQueryOptions< + AxiosResponse, + AxiosError + >, +) => { + const queryKey = useMemo(() => { + if (options?.queryKey && Array.isArray(options.queryKey)) { + return ['queryKeySuggestions', ...options.queryKey]; + } + return [ + 'queryKeySuggestions', + signal, + searchText, + metricName, + fieldContext, + fieldDataType, + ]; + }, [ + options?.queryKey, + signal, + searchText, + metricName, + fieldContext, + fieldDataType, + ]); + return useQuery, AxiosError>({ + queryKey, + queryFn: () => + getKeySuggestions({ + signal, + searchText, + metricName, + fieldContext, + fieldDataType, + }), + ...options, + }); +}; diff --git a/frontend/src/hooks/querySuggestions/useGetQueryKeyValueSuggestions.ts b/frontend/src/hooks/querySuggestions/useGetQueryKeyValueSuggestions.ts new file mode 100644 index 000000000000..587cb6d90963 --- /dev/null +++ b/frontend/src/hooks/querySuggestions/useGetQueryKeyValueSuggestions.ts @@ -0,0 +1,22 @@ +import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion'; +import { AxiosError, AxiosResponse } from 'axios'; +import { useQuery, UseQueryResult } from 'react-query'; +import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types'; + +export const useGetQueryKeyValueSuggestions = ({ + key, + signal, + searchText, +}: { + key: string; + signal: 'traces' | 'logs' | 'metrics'; + searchText?: string; +}): UseQueryResult< + AxiosResponse, + AxiosError +> => + useQuery, AxiosError>({ + queryKey: ['queryKeyValueSuggestions', key, signal, searchText], + queryFn: () => + getValueSuggestions({ signal, key, searchText: searchText || '' }), + }); diff --git a/frontend/src/hooks/useGetQueryLabels.ts b/frontend/src/hooks/useGetQueryLabels.ts new file mode 100644 index 000000000000..ea7181f1e3c7 --- /dev/null +++ b/frontend/src/hooks/useGetQueryLabels.ts @@ -0,0 +1,29 @@ +import { getQueryLabelWithAggregation } from 'components/QueryBuilderV2/utils'; +import { useMemo } from 'react'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { EQueryType } from 'types/common/dashboard'; + +export const useGetQueryLabels = ( + currentQuery: Query, +): { label: string; value: string }[] => + useMemo(() => { + if (currentQuery?.queryType === EQueryType.QUERY_BUILDER) { + const queryLabels = getQueryLabelWithAggregation( + currentQuery?.builder?.queryData || [], + ); + const formulaLabels = currentQuery?.builder?.queryFormulas?.map( + (formula) => ({ + label: formula.queryName, + value: formula.queryName, + }), + ); + return [...queryLabels, ...formulaLabels]; + } + if (currentQuery?.queryType === EQueryType.CLICKHOUSE) { + return currentQuery?.clickhouse_sql?.map((q) => ({ + label: q.name, + value: q.name, + })); + } + return currentQuery?.promql?.map((q) => ({ label: q.name, value: q.name })); + }, [currentQuery]); diff --git a/frontend/src/hooks/useLogsData.ts b/frontend/src/hooks/useLogsData.ts index 86a3f0703458..33335de1cd23 100644 --- a/frontend/src/hooks/useLogsData.ts +++ b/frontend/src/hooks/useLogsData.ts @@ -178,7 +178,7 @@ export const useLogsData = ({ if (!stagedQuery) return; const newRequestData = getRequestData(stagedQuery, { - filters, + filters: filters || { items: [], op: 'AND' }, page: page + 1, log: orderByTimestamp ? lastLog : null, pageSize: nextPageSize, diff --git a/frontend/src/lib/dashboard/getQueryResults.ts b/frontend/src/lib/dashboard/getQueryResults.ts index 30c6e611dc4d..e8b9ded5d76c 100644 --- a/frontend/src/lib/dashboard/getQueryResults.ts +++ b/frontend/src/lib/dashboard/getQueryResults.ts @@ -3,6 +3,12 @@ // @ts-nocheck import { getMetricsQueryRange } from 'api/metrics/getQueryRange'; +import { + convertV5ResponseToLegacy, + getQueryRangeV5, + prepareQueryRangePayloadV5, +} from 'api/v5/v5'; +import { ENTITY_VERSION_V5 } from 'constants/app'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; @@ -16,8 +22,91 @@ import { isEmpty } from 'lodash-es'; import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; import { prepareQueryRangePayload } from './prepareQueryRangePayload'; +import { QueryData } from 'types/api/widgets/getQuery'; +import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5'; + +/** + * Validates if metric name is available for METRICS data source + */ +function validateMetricNameForMetricsDataSource(query: Query): boolean { + if (query.queryType !== 'builder') { + return true; // Non-builder queries don't need this validation + } + + const { queryData } = query.builder; + + // Check if any METRICS data source queries exist + const metricsQueries = queryData.filter( + (queryItem) => queryItem.dataSource === DataSource.METRICS, + ); + + // If no METRICS queries, validation passes + if (metricsQueries.length === 0) { + return true; + } + + // Check if ALL METRICS queries are missing metric names + const allMetricsQueriesMissingNames = metricsQueries.every((queryItem) => { + const metricName = + queryItem.aggregations?.[0]?.metricName || queryItem.aggregateAttribute?.key; + return !metricName || metricName.trim() === ''; + }); + + // Return false only if ALL METRICS queries are missing metric names + return !allMetricsQueriesMissingNames; +} + +/** + * Helper function to get the data source for a specific query + */ +const getQueryDataSource = ( + queryData: QueryData, + payloadQuery: Query, +): DataSource | null => { + const queryItem = payloadQuery.builder?.queryData.find( + (query) => query.queryName === queryData.queryName, + ); + return queryItem?.dataSource || null; +}; + +export const getLegend = ( + queryData: QueryData, + payloadQuery: Query, + labelName: string, +) => { + const aggregationPerQuery = payloadQuery?.builder?.queryData.reduce( + (acc, query) => { + if (query.queryName === queryData.queryName) { + acc[query.queryName] = createAggregation(query); + } + return acc; + }, + {}, + ); + + const metaData = queryData?.metaData; + const aggregation = + aggregationPerQuery?.[metaData?.queryName]?.[metaData?.index]; + + const aggregationName = aggregation?.alias || aggregation?.expression || ''; + + // Check if there's only one total query (queryData + queryFormulas) + const totalQueries = + (payloadQuery?.builder?.queryData?.length || 0) + + (payloadQuery?.builder?.queryFormulas?.length || 0); + const showSingleAggregationName = + totalQueries === 1 && labelName === metaData?.queryName; + + if (aggregationName) { + return showSingleAggregationName + ? aggregationName + : `${aggregationName}-${labelName}`; + } + return labelName || metaData?.queryName; +}; export async function GetMetricQueryRange( props: GetQueryResultsProps, @@ -26,15 +115,97 @@ export async function GetMetricQueryRange( headers?: Record, isInfraMonitoring?: boolean, ): Promise> { - const { legendMap, queryPayload } = prepareQueryRangePayload(props); - const response = await getMetricsQueryRange( - queryPayload, - version || 'v3', - signal, - headers, - ); + let legendMap: Record; + let response: + | SuccessResponse + | SuccessResponseV2; - if (response.statusCode >= 400) { + const panelType = props.originalGraphType || props.graphType; + + const finalFormatForWeb = + props.formatForWeb || panelType === PANEL_TYPES.TABLE; + + // Validate metric name for METRICS data source before making the API call + if ( + version === ENTITY_VERSION_V5 && + !validateMetricNameForMetricsDataSource(props.query) + ) { + // Return empty response to avoid 400 error when metric name is missing + return { + statusCode: 200, + error: null, + message: 'Metric name is required for metrics data source', + payload: { + data: { + result: [], + resultType: '', + newResult: { + data: { + result: [], + resultType: '', + }, + }, + }, + }, + params: props, + }; + } + + if (version === ENTITY_VERSION_V5) { + const v5Result = prepareQueryRangePayloadV5(props); + legendMap = v5Result.legendMap; + + // atleast one query should be there to make call to v5 api + if (v5Result.queryPayload.compositeQuery.queries.length === 0) { + return { + statusCode: 200, + error: null, + message: 'At least one query is required', + payload: { + data: { + result: [], + resultType: '', + newResult: { + data: { + result: [], + resultType: '', + }, + }, + }, + }, + params: props, + }; + } + + const v5Response = await getQueryRangeV5( + v5Result.queryPayload, + version, + signal, + headers, + ); + + // Convert V5 response to legacy format for components + response = convertV5ResponseToLegacy( + { + payload: v5Response.data, + params: v5Result.queryPayload, + }, + legendMap, + finalFormatForWeb, + ); + } else { + const legacyResult = prepareQueryRangePayload(props); + legendMap = legacyResult.legendMap; + + response = await getMetricsQueryRange( + legacyResult.queryPayload, + version || 'v3', + signal, + headers, + ); + } + + if (response.statusCode >= 400 && version !== ENTITY_VERSION_V5) { let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`; if (response.body && !isEmpty(response.body)) { error = `${error}, errors: ${response.body}`; @@ -42,7 +213,7 @@ export async function GetMetricQueryRange( throw new Error(error); } - if (props.formatForWeb) { + if (finalFormatForWeb) { return response; } diff --git a/frontend/src/lib/getLabelName.ts b/frontend/src/lib/getLabelName.ts index bfb496742819..0507a7aed51c 100644 --- a/frontend/src/lib/getLabelName.ts +++ b/frontend/src/lib/getLabelName.ts @@ -44,6 +44,8 @@ const getLabelName = ( const result = `${value === undefined ? '' : value}`; if (post.length === 0 && pre.length === 0) { + if (result) return result; + if (query) return query; return result; } return `${result}{${pre}${post}}`; diff --git a/frontend/src/lib/newQueryBuilder/convertNewDataToOld.ts b/frontend/src/lib/newQueryBuilder/convertNewDataToOld.ts index 30610a484665..79fd6722ce99 100644 --- a/frontend/src/lib/newQueryBuilder/convertNewDataToOld.ts +++ b/frontend/src/lib/newQueryBuilder/convertNewDataToOld.ts @@ -29,6 +29,7 @@ export const convertNewDataToOld = ( metric: series.labels, values, queryName: `${item.queryName}`, + metaData: series.metaData, }; oldResult.push(result); @@ -52,6 +53,7 @@ export const convertNewDataToOld = ( metric: series.labels, values, queryName: `${item.queryName}`, + metaData: series?.metaData, }; oldResult.push(result); @@ -75,6 +77,7 @@ export const convertNewDataToOld = ( metric: series.labels, values, queryName: `${item.queryName}`, + metaData: series?.metaData, }; oldResult.push(result); @@ -98,6 +101,7 @@ export const convertNewDataToOld = ( metric: series.labels, values, queryName: `${item.queryName}`, + metaData: series?.metaData, }; oldResult.push(result); diff --git a/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts b/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts index ea9450e47e97..ad8173c93eca 100644 --- a/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts +++ b/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts @@ -57,7 +57,8 @@ export const getPaginationQueryData: SetupPaginationQueryData = ({ const updatedFilters: TagFilter = { ...filters, - items: filters?.items?.filter((item) => item.key?.key !== 'id'), + items: filters?.items?.filter((item) => item.key?.key !== 'id') || [], + op: filters?.op || 'AND', }; const tagFilters: TagFilter = { @@ -82,6 +83,7 @@ export const getPaginationQueryData: SetupPaginationQueryData = ({ ...updatedFilters.items, ] : updatedFilters.items, + op: filters?.op || 'AND', }; const chunkOfQueryData: Partial = { diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/__tests__/mapQueryDataFromApiInputs.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/__tests__/mapQueryDataFromApiInputs.ts index 00d054e9bf62..92c61222e84a 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/__tests__/mapQueryDataFromApiInputs.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/__tests__/mapQueryDataFromApiInputs.ts @@ -465,6 +465,16 @@ export const defaultOutput = { key: 'system_disk_operations', type: 'Sum', }, + aggregations: [ + { + metricName: '', + reduceTo: 'avg', + spaceAggregation: 'sum', + temporality: '', + timeAggregation: 'count', + }, + ], + filter: { expression: '' }, aggregateOperator: 'rate', dataSource: 'metrics', disabled: false, diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts index 52019b13b6c0..a498b6e4a673 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery.ts @@ -7,6 +7,7 @@ import { Query, } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; +import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope'; import { mapQueryDataToApi } from './mapQueryDataToApi'; @@ -17,6 +18,7 @@ const defaultCompositeQuery: ICompositeMetricQuery = { chQueries: {}, promQueries: {}, unit: undefined, + queries: [], }; const buildBuilderQuery = ( @@ -94,7 +96,8 @@ export const mapCompositeQueryFromQuery = ( const functionToBuildQuery = queryTypeMethodMapping[query.queryType]; if (functionToBuildQuery && typeof functionToBuildQuery === 'function') { - return functionToBuildQuery(query, panelType); + const compositeQuery = functionToBuildQuery(query, panelType); + return compositeQueryToQueryEnvelope(compositeQuery); } } @@ -105,5 +108,6 @@ export const mapCompositeQueryFromQuery = ( chQueries: {}, promQueries: {}, unit: undefined, + queries: [], }; }; diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts index d95127b9692b..ab892740bf12 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts @@ -1,11 +1,77 @@ import { initialQueryState } from 'constants/queryBuilder'; import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; -import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { + IBuilderFormula, + IBuilderQuery, + IClickHouseQuery, + IPromQLQuery, + Query, +} from 'types/api/queryBuilder/queryBuilderData'; +import { + BuilderQuery, + ClickHouseQuery, + PromQuery, + QueryBuilderFormula, +} from 'types/api/v5/queryRange'; +import { + convertBuilderQueryToIBuilderQuery, + convertQueryBuilderFormulaToIBuilderFormula, +} from 'utils/convertNewToOldQueryBuilder'; import { v4 as uuid } from 'uuid'; import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataModel'; -export const mapQueryDataFromApi = ( +const mapQueryFromV5 = ( + compositeQuery: ICompositeMetricQuery, + query?: Query, +): Query => { + const builderQueries: Record = {}; + const promQueries: IPromQLQuery[] = []; + const clickhouseQueries: IClickHouseQuery[] = []; + + compositeQuery.queries?.forEach((q) => { + const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery; + if (q.type === 'builder_query') { + if (spec.name) { + builderQueries[spec.name] = convertBuilderQueryToIBuilderQuery( + spec as BuilderQuery, + ); + } + } else if (q.type === 'builder_formula') { + if (spec.name) { + builderQueries[spec.name] = convertQueryBuilderFormulaToIBuilderFormula( + (spec as unknown) as QueryBuilderFormula, + ); + } + } else if (q.type === 'promql') { + const promSpec = spec as PromQuery; + promQueries.push({ + name: promSpec.name, + query: promSpec.query || '', + legend: promSpec.legend || '', + disabled: promSpec.disabled || false, + }); + } else if (q.type === 'clickhouse_sql') { + const chSpec = spec as ClickHouseQuery; + clickhouseQueries.push({ + name: chSpec.name, + query: chSpec.query, + legend: chSpec.legend || '', + disabled: chSpec.disabled || false, + }); + } + }); + return { + builder: transformQueryBuilderDataModel(builderQueries, query?.builder), + promql: promQueries, + clickhouse_sql: clickhouseQueries, + queryType: compositeQuery.queryType, + id: uuid(), + unit: compositeQuery.unit, + }; +}; + +const mapQueryFromV3 = ( compositeQuery: ICompositeMetricQuery, query?: Query, ): Query => { @@ -18,25 +84,35 @@ export const mapQueryDataFromApi = ( const promql = compositeQuery.promQueries ? Object.keys(compositeQuery.promQueries).map((key) => ({ - ...compositeQuery.promQueries[key], + ...compositeQuery.promQueries?.[key], name: key, })) : initialQueryState.promql; const clickhouseSql = compositeQuery.chQueries ? Object.keys(compositeQuery.chQueries).map((key) => ({ - ...compositeQuery.chQueries[key], + ...compositeQuery.chQueries?.[key], name: key, - query: compositeQuery.chQueries[key].query, + query: compositeQuery.chQueries?.[key]?.query || '', })) : initialQueryState.clickhouse_sql; return { builder, - promql, - clickhouse_sql: clickhouseSql, + promql: promql as IPromQLQuery[], + clickhouse_sql: clickhouseSql as IClickHouseQuery[], queryType: compositeQuery.queryType, id: uuid(), unit: compositeQuery.unit, }; }; + +export const mapQueryDataFromApi = ( + compositeQuery: ICompositeMetricQuery, + query?: Query, +): Query => { + if (compositeQuery.queries && compositeQuery.queries.length > 0) { + return mapQueryFromV5(compositeQuery, query); + } + return mapQueryFromV3(compositeQuery, query); +}; diff --git a/frontend/src/lib/query/createTableColumnsFromQuery.ts b/frontend/src/lib/query/createTableColumnsFromQuery.ts index ff5ee7a9ad82..f7e8173d27fa 100644 --- a/frontend/src/lib/query/createTableColumnsFromQuery.ts +++ b/frontend/src/lib/query/createTableColumnsFromQuery.ts @@ -41,6 +41,7 @@ export type DynamicColumn = { title: string; data: (string | number)[]; type: 'field' | 'operator' | 'formula'; + id?: string; }; type DynamicColumns = DynamicColumn[]; @@ -93,7 +94,10 @@ const getQueryByName = ( ); } if (query.queryType === EQueryType.QUERY_BUILDER) { - const queryArray = query.builder[type]; + const queryArray = (query.builder[type] || []) as ( + | IBuilderQuery + | IBuilderFormula + )[]; const defaultValue = type === 'queryData' ? initialQueryBuilderFormValues @@ -119,6 +123,7 @@ const addLabels = ( query: IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery, label: string, dynamicColumns: DynamicColumns, + columnId?: string, ): void => { if (isValueExist('dataIndex', label, dynamicColumns)) return; @@ -129,6 +134,7 @@ const addLabels = ( title: label, data: [], type: 'field', + id: columnId, }; dynamicColumns.push(fieldObj); @@ -139,6 +145,7 @@ const addOperatorFormulaColumns = ( dynamicColumns: DynamicColumns, queryType: EQueryType, customLabel?: string, + columnId?: string, // eslint-disable-next-line sonarjs/cognitive-complexity ): void => { if (isFormula(get(query, 'queryName', ''))) { @@ -156,6 +163,7 @@ const addOperatorFormulaColumns = ( title: customLabel || formulaLabel, data: [], type: 'formula', + id: columnId, }; dynamicColumns.push(formulaColumn); @@ -166,8 +174,8 @@ const addOperatorFormulaColumns = ( if (queryType === EQueryType.QUERY_BUILDER) { const currentQueryData = query as IBuilderQuery; let operatorLabel = `${currentQueryData.aggregateOperator}`; - if (currentQueryData.aggregateAttribute.key) { - operatorLabel += `(${currentQueryData.aggregateAttribute.key})`; + if (currentQueryData.aggregateAttribute?.key) { + operatorLabel += `(${currentQueryData.aggregateAttribute?.key})`; } if (currentQueryData.legend) { @@ -177,10 +185,11 @@ const addOperatorFormulaColumns = ( const operatorColumn: DynamicColumn = { query, field: currentQueryData.queryName, - dataIndex: currentQueryData.queryName, + dataIndex: customLabel || currentQueryData.queryName, title: customLabel || operatorLabel, data: [], type: 'operator', + id: columnId, }; dynamicColumns.push(operatorColumn); @@ -221,6 +230,7 @@ const addOperatorFormulaColumns = ( title: customLabel || operatorLabel, data: [], type: 'operator', + id: columnId, }; dynamicColumns.push(operatorColumn); @@ -258,17 +268,79 @@ const transformColumnTitles = ( return item; }); +const processTableColumns = ( + table: NonNullable, + currentStagedQuery: + | IBuilderQuery + | IBuilderFormula + | IClickHouseQuery + | IPromQLQuery, + dynamicColumns: DynamicColumns, + queryType: EQueryType, +): void => { + table.columns.forEach((column) => { + if (column.isValueColumn) { + // For value columns, add as operator/formula column + addOperatorFormulaColumns( + currentStagedQuery, + dynamicColumns, + queryType, + column.name, + column.id, + ); + } else { + // For non-value columns, add as field/label column + addLabels(currentStagedQuery, column.name, dynamicColumns, column.id); + } + }); +}; + +const processSeriesColumns = ( + series: NonNullable, + currentStagedQuery: + | IBuilderQuery + | IBuilderFormula + | IClickHouseQuery + | IPromQLQuery, + dynamicColumns: DynamicColumns, + queryType: EQueryType, + currentQuery: QueryDataV3, +): void => { + const isValuesColumnExist = series.some((item) => item.values.length > 0); + const isEveryValuesExist = series.every((item) => item.values.length > 0); + + if (isValuesColumnExist) { + addOperatorFormulaColumns( + currentStagedQuery, + dynamicColumns, + queryType, + isEveryValuesExist ? undefined : get(currentStagedQuery, 'queryName', ''), + ); + } + + series.forEach((seria) => { + seria.labelsArray?.forEach((lab) => { + Object.keys(lab).forEach((label) => { + if (label === currentQuery?.queryName) return; + + addLabels(currentStagedQuery, label, dynamicColumns); + }); + }); + }); +}; + const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => { const dynamicColumns: DynamicColumns = []; queryTableData.forEach((currentQuery) => { - const { series, queryName, list } = currentQuery; + const { series, queryName, list, table } = currentQuery; const currentStagedQuery = getQueryByName( query, queryName, isFormula(queryName) ? 'queryFormulas' : 'queryData', ); + if (list) { list.forEach((listItem) => { Object.keys(listItem.data).forEach((label) => { @@ -277,28 +349,23 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => { }); } + if (table) { + processTableColumns( + table, + currentStagedQuery, + dynamicColumns, + query.queryType, + ); + } + if (series) { - const isValuesColumnExist = series.some((item) => item.values.length > 0); - const isEveryValuesExist = series.every((item) => item.values.length > 0); - - if (isValuesColumnExist) { - addOperatorFormulaColumns( - currentStagedQuery, - dynamicColumns, - query.queryType, - isEveryValuesExist ? undefined : get(currentStagedQuery, 'queryName', ''), - ); - } - - series.forEach((seria) => { - seria.labelsArray?.forEach((lab) => { - Object.keys(lab).forEach((label) => { - if (label === currentQuery?.queryName) return; - - addLabels(currentStagedQuery, label, dynamicColumns); - }); - }); - }); + processSeriesColumns( + series, + currentStagedQuery, + dynamicColumns, + query.queryType, + currentQuery, + ); } }); @@ -474,6 +541,59 @@ const fillDataFromList = ( }); }; +const processTableRowValue = (value: any, column: DynamicColumn): void => { + if (value !== null && value !== undefined && value !== '') { + if (isObject(value)) { + column.data.push(JSON.stringify(value)); + } else if (typeof value === 'number' || !isNaN(Number(value))) { + column.data.push(Number(value)); + } else { + column.data.push(value.toString()); + } + } else { + column.data.push('N/A'); + } +}; + +const fillDataFromTable = ( + currentQuery: QueryDataV3, + columns: DynamicColumns, +): void => { + const { table } = currentQuery; + + if (!table || !table.rows) return; + + table.rows.forEach((row) => { + const unusedColumnsKeys = new Set( + columns.map((item) => item.id || item.title), + ); + + columns.forEach((column) => { + const rowData = row.data; + const columnField = column.id || column.title || column.field; + + if (Object.prototype.hasOwnProperty.call(rowData, columnField)) { + const value = rowData[columnField]; + processTableRowValue(value, column); + unusedColumnsKeys.delete(columnField); + } else { + column.data.push('N/A'); + unusedColumnsKeys.delete(columnField); + } + }); + + // Fill any remaining unused columns with N/A + unusedColumnsKeys.forEach((key) => { + const unusedCol = columns.find( + (item) => item.id === key || item.title === key, + ); + if (unusedCol) { + unusedCol.data.push('N/A'); + } + }); + }); +}; + const fillColumnsData: FillColumnData = (queryTableData, cols) => { const fields = cols.filter((item) => item.type === 'field'); const operators = cols.filter((item) => item.type === 'operator'); @@ -497,6 +617,8 @@ const fillColumnsData: FillColumnData = (queryTableData, cols) => { fillDataFromList(listItem, resultColumns); }); } + + fillDataFromTable(currentQuery, resultColumns); }); const rowsLength = resultColumns.length > 0 ? resultColumns[0].data.length : 0; diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts index 53ebac8bd4ea..65ca33e1cd7a 100644 --- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts +++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts @@ -14,6 +14,7 @@ import { calculateEnhancedLegendConfig, } from 'container/PanelWrapper/enhancedLegend'; import { Dimensions } from 'hooks/useDimensions'; +import { getLegend } from 'lib/dashboard/getQueryResults'; import { convertValue } from 'lib/getConvertedValue'; import getLabelName from 'lib/getLabelName'; import { cloneDeep, isUndefined } from 'lodash-es'; @@ -70,6 +71,7 @@ export interface GetUPlotChartOptions { enhancedLegend?: boolean; legendPosition?: LegendPosition; enableZoom?: boolean; + query?: Query; } /** the function converts series A , series B , series C to @@ -198,6 +200,7 @@ export const getUPlotChartOptions = ({ enhancedLegend = true, legendPosition = LegendPosition.BOTTOM, enableZoom, + query, }: GetUPlotChartOptions): uPlot.Options => { const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale); @@ -212,11 +215,17 @@ export const getUPlotChartOptions = ({ // Calculate dynamic legend configuration based on panel dimensions and series count const seriesCount = (apiResponse?.data?.result || []).length; + const seriesLabels = enhancedLegend ? (apiResponse?.data?.result || []).map((item) => - getLabelName(item.metric || {}, item.queryName || '', item.legend || ''), + getLegend( + item, + query || currentQuery, + getLabelName(item.metric || {}, item.queryName || '', item.legend || ''), + ), ) : []; + const legendConfig = enhancedLegend ? calculateEnhancedLegendConfig( dimensions, @@ -332,6 +341,7 @@ export const getUPlotChartOptions = ({ timezone, colorMapping, customTooltipElement, + query: query || currentQuery, }), onClickPlugin({ onClick: onClickHandler, @@ -681,7 +691,7 @@ export const getUPlotChartOptions = ({ widgetMetaData: apiResponse?.data?.result || [], graphsVisibilityStates, panelType, - currentQuery, + currentQuery: query || currentQuery, stackBarChart, hiddenGraph, isDarkMode, diff --git a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts index 9625e3ff21a5..731fbbd0d51e 100644 --- a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts +++ b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts @@ -3,9 +3,11 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { themeColors } from 'constants/theme'; import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; +import { getLegend } from 'lib/dashboard/getQueryResults'; import getLabelName from 'lib/getLabelName'; import { get } from 'lodash-es'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { placement } from '../placement'; import { generateColor } from '../utils/generateColor'; @@ -49,6 +51,7 @@ const generateTooltipContent = ( stackBarChart?: boolean, timezone?: string, colorMapping?: Record, + query?: Query, // eslint-disable-next-line sonarjs/cognitive-complexity ): HTMLElement => { const container = document.createElement('div'); @@ -92,9 +95,16 @@ const generateTooltipContent = ( const value = getTooltipBaseValue(data, index, idx, stackBarChart); const dataIngested = quantity[idx]; - const label = isMergedSeries - ? '' - : getLabelName(metric, queryName || '', legend || ''); + const baseLabelName = getLabelName(metric, queryName || '', legend || ''); + + let label = ''; + if (isMergedSeries) { + label = ''; + } else if (query) { + label = getLegend(seriesList[index - 1], query, baseLabelName); + } else { + label = baseLabelName; + } let color = colorMapping?.[label] || @@ -234,6 +244,7 @@ type ToolTipPluginProps = { customTooltipElement?: HTMLDivElement; timezone?: string; colorMapping?: Record; + query?: Query; }; const tooltipPlugin = ({ @@ -247,6 +258,7 @@ const tooltipPlugin = ({ customTooltipElement, timezone, colorMapping, + query, }: // eslint-disable-next-line sonarjs/cognitive-complexity ToolTipPluginProps): any => { let over: HTMLElement; @@ -315,6 +327,7 @@ ToolTipPluginProps): any => { stackBarChart, timezone, colorMapping, + query, ); if (customTooltipElement) { content.appendChild(customTooltipElement); diff --git a/frontend/src/lib/uPlotLib/utils/getSeriesData.ts b/frontend/src/lib/uPlotLib/utils/getSeriesData.ts index 2c72acb6d6f1..bd02ecdbc3c6 100644 --- a/frontend/src/lib/uPlotLib/utils/getSeriesData.ts +++ b/frontend/src/lib/uPlotLib/utils/getSeriesData.ts @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/cognitive-complexity */ import { PANEL_TYPES } from 'constants/queryBuilder'; import { themeColors } from 'constants/theme'; +import { getLegend } from 'lib/dashboard/getQueryResults'; import getLabelName from 'lib/getLabelName'; import { isUndefined } from 'lodash-es'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -35,6 +36,7 @@ const getSeries = ({ hiddenGraph, isDarkMode, colorMapping, + currentQuery, }: GetSeriesProps): uPlot.Options['series'] => { const configurations: uPlot.Series[] = [ { label: 'Timestamp', stroke: 'purple' }, @@ -47,12 +49,16 @@ const getSeries = ({ for (let i = 0; i < seriesList?.length; i += 1) { const { metric = {}, queryName = '', legend = '' } = widgetMetaData[i] || {}; - const label = getLabelName( + const baseLabelName = getLabelName( metric, queryName || '', // query legend || '', ); + const label = currentQuery + ? getLegend(widgetMetaData[i], currentQuery, baseLabelName) + : baseLabelName; + const color = colorMapping?.[label] || generateColor( @@ -60,8 +66,8 @@ const getSeries = ({ isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, ); - const pointSize = seriesList[i].values.length > 1 ? 5 : 10; - const showPoints = !(seriesList[i].values.length > 1); + const pointSize = seriesList[i]?.values?.length > 1 ? 5 : 10; + const showPoints = !(seriesList[i]?.values?.length > 1); const seriesObj: any = { paths, diff --git a/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts b/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts index a247559ef80e..9ca3b41fe7ca 100644 --- a/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts +++ b/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts @@ -32,7 +32,7 @@ function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any { // Fill missing timestamps with null values processedData.forEach((entry: { values: (number | null)[][] }) => { const existingTimestamps = new Set( - (entry.values ?? []).map((value) => value[0]), + (entry?.values ?? []).map((value) => value[0]), ); const missingTimestamps = Array.from(allTimestampsSet).filter( @@ -42,21 +42,21 @@ function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any { missingTimestamps.forEach((timestamp) => { const value = null; - entry.values.push([timestamp, value]); + entry?.values?.push([timestamp, value]); }); - entry.values.forEach((v) => { + entry?.values?.forEach((v) => { // eslint-disable-next-line no-param-reassign v[1] = normalizePlotValue(v[1]); }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - entry.values.sort((a, b) => a[0] - b[0]); + entry?.values?.sort((a, b) => a[0] - b[0]); }); return processedData.map((entry: { values: [number, string][] }) => - entry.values.map((value) => value[1]), + entry?.values?.map((value) => value[1]), ); } diff --git a/frontend/src/mocks-server/__mockdata__/query_range.ts b/frontend/src/mocks-server/__mockdata__/query_range.ts index ef55283a968c..c127fcb9d9cb 100644 --- a/frontend/src/mocks-server/__mockdata__/query_range.ts +++ b/frontend/src/mocks-server/__mockdata__/query_range.ts @@ -207,6 +207,155 @@ export const queryRangeForTableView = { }, }; +export const logsresponse = { + status: 'success', + data: { + type: 'raw', + meta: { + rowsScanned: 32880, + bytesScanned: 41962053, + durationMs: 1121, + }, + data: { + results: [ + { + queryName: 'A', + nextCursor: 'MTc1MzkyMzM4OTc0OA==', + rows: [ + { + timestamp: '2025-07-31T00:56:48.40583808Z', + data: { + attributes_bool: { + otelTraceSampled: true, + }, + attributes_number: { + 'code.lineno': 47, + }, + attributes_string: { + 'code.filepath': '/usr/src/app/demo_service.py', + 'code.function': 'ProcessRequest', + otelServiceName: 'demo-service', + otelSpanID: 'a1b2c3d4e5f67890', + otelTraceID: '12345678abcdef', + }, + body: + "Processing request for items:['ITEM001', 'ITEM002', 'ITEM003', 'ITEM004', 'ITEM005']", + id: 'demo-log-id-001', + resources_string: {}, + scope_name: 'opentelemetry.sdk._logs._internal', + scope_string: {}, + scope_version: '', + severity_number: 9, + severity_text: 'INFO', + span_id: 'a1b2c3d4e5f67890', + timestamp: 1753923408405838080, + trace_flags: 1, + trace_id: '1234567890abcdef1234567890abcdef', + }, + }, + { + timestamp: '2025-07-31T00:56:48.404301056Z', + data: { + attributes_bool: { + otelTraceSampled: true, + }, + attributes_number: { + 'code.lineno': 47, + }, + attributes_string: { + 'code.filepath': '/usr/src/app/demo_service.py', + 'code.function': 'ProcessRequest', + otelServiceName: 'demo-service', + otelSpanID: 'b2c3d4e5f678901a', + otelTraceID: 'abcdef1234567890', + }, + body: + "Processing request for items:['ITEM006', 'ITEM007', 'ITEM008', 'ITEM009', 'ITEM010']", + id: 'demo-log-id-002', + resources_string: {}, + scope_name: 'opeinternal', + scope_string: {}, + scope_version: '', + severity_number: 9, + severity_text: 'INFO', + span_id: 'b2c3d4e5f678901a', + timestamp: 1753923408404301056, + trace_flags: 1, + trace_id: 'abcdef12234567890', + }, + }, + ], + }, + ], + warnings: [], + }, + }, +}; + +export const queryRangeForTableViewV5 = { + payload: { + data: { + resultType: 'scalar', + result: [ + { + queryName: 'A', + legend: 'A', + series: null, + list: null, + table: { + columns: [ + { + name: 'count()', + queryName: 'A', + isValueColumn: true, + id: 'A.count()', + }, + ], + rows: [ + { + data: { + 'A.count()': 400599, + }, + }, + ], + }, + }, + ], + }, + }, + params: { + schemaVersion: 'v1', + start: 1753777929000, + end: 1753779729000, + requestType: 'scalar', + compositeQuery: { + queries: [ + { + type: 'builder_query', + spec: { + name: 'A', + signal: 'traces', + disabled: false, + having: { + expression: '', + }, + aggregations: [ + { + expression: 'count()', + }, + ], + }, + }, + ], + }, + formatOptions: { + formatTableResultForUI: true, + fillGaps: false, + }, + variables: {}, + }, +}; + export const queryRangeForTraceView = { status: 'success', data: { diff --git a/frontend/src/pages/HomePage/HomePage.tsx b/frontend/src/pages/HomePage/HomePage.tsx index e3c1ac623f28..2d2e9bb1c773 100644 --- a/frontend/src/pages/HomePage/HomePage.tsx +++ b/frontend/src/pages/HomePage/HomePage.tsx @@ -1,4 +1,4 @@ -import Home from 'container/Home'; +import Home from 'container/Home/Home'; function HomePage(): JSX.Element { return ; diff --git a/frontend/src/pages/LiveLogs/index.tsx b/frontend/src/pages/LiveLogs/index.tsx index 0a11b33764bb..d354cfceca5f 100644 --- a/frontend/src/pages/LiveLogs/index.tsx +++ b/frontend/src/pages/LiveLogs/index.tsx @@ -9,7 +9,7 @@ import { useEffect } from 'react'; import { DataSource } from 'types/common/queryBuilder'; function LiveLogs(): JSX.Element { - useShareBuilderUrl(liveLogsCompositeQuery); + useShareBuilderUrl({ defaultValue: liveLogsCompositeQuery }); const { handleSetConfig } = useQueryBuilder(); useEffect(() => { diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx index d6dd4c19e771..6a931e55952d 100644 --- a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx +++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { initialQueriesMap, initialQueryBuilderFormValues, @@ -10,6 +11,7 @@ import { server } from 'mocks-server/server'; import { rest } from 'msw'; import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider'; import { QueryBuilderContext } from 'providers/QueryBuilder'; +import { MemoryRouter } from 'react-router-dom-v5-compat'; // https://virtuoso.dev/mocking-in-tests/ import { VirtuosoMockContext } from 'react-virtuoso'; import { fireEvent, render, waitFor } from 'tests/test-utils'; @@ -108,10 +110,17 @@ describe('Logs Explorer Tests', () => { queryByText, getByTestId, queryByTestId, + container, } = render( - - - , + + + + + , ); // check the presence of frequency chart content @@ -130,13 +139,15 @@ describe('Logs Explorer Tests', () => { const clickhouseView = queryByTestId('clickhouse-view'); expect(clickhouseView).not.toBeInTheDocument(); - // check the presence of List View / Time Series View / Table View - const listView = getByTestId('logs-list-view'); - const timeSeriesView = getByTestId('time-series-view'); - const tableView = getByTestId('table-view'); - expect(listView).toBeInTheDocument(); - expect(timeSeriesView).toBeInTheDocument(); - expect(tableView).toBeInTheDocument(); + // check the presence of List View / Time Series View / Table View using class names + const listViewTab = container.querySelector( + '.list-view-tab.explorer-view-option', + ); + const timeSeriesViewTab = container.querySelector('.timeseries-view-tab'); + const tableViewTab = container.querySelector('.table-view-tab'); + expect(listViewTab).toBeInTheDocument(); + expect(timeSeriesViewTab).toBeInTheDocument(); + expect(tableViewTab).toBeInTheDocument(); // // check the presence of old logs explorer CTA - TODO: add this once we have the header updated // const oldLogsCTA = getByText('Switch to Old Logs Explorer'); @@ -148,13 +159,19 @@ describe('Logs Explorer Tests', () => { // mocking the query range API to return the logs logsQueryServerRequest(); const { queryByText, queryByTestId } = render( - - - - - , + + + + + + + , ); // check for loading state to be not present @@ -177,74 +194,80 @@ describe('Logs Explorer Tests', () => { // mocking the query range API to return the logs logsQueryServerRequest(); const { queryAllByText } = render( - false, - currentQuery: { - ...initialQueriesMap.metrics, - builder: { - ...initialQueriesMap.metrics.builder, - queryData: [ - initialQueryBuilderFormValues, - initialQueryBuilderFormValues, - ], - }, - }, - setSupersetQuery: jest.fn(), - supersetQuery: initialQueriesMap.metrics, - stagedQuery: initialQueriesMap.metrics, - initialDataSource: null, - panelType: PANEL_TYPES.TIME_SERIES, - isEnabledQuery: false, - lastUsedQuery: 0, - setLastUsedQuery: noop, - handleSetQueryData: noop, - handleSetFormulaData: noop, - handleSetQueryItemData: noop, - handleSetConfig: noop, - removeQueryBuilderEntityByIndex: noop, - removeQueryTypeItemByIndex: noop, - addNewBuilderQuery: noop, - cloneQuery: noop, - addNewFormula: noop, - addNewQueryItem: noop, - redirectWithQueryBuilderData: noop, - handleRunQuery: noop, - resetQuery: noop, - updateAllQueriesOperators: (): Query => initialQueriesMap.metrics, - updateQueriesData: (): Query => initialQueriesMap.metrics, - initQueryBuilderData: noop, - handleOnUnitsChange: noop, - isStagedQueryUpdated: (): boolean => false, - }} + - - - - - - , + false, + currentQuery: { + ...initialQueriesMap.metrics, + builder: { + ...initialQueriesMap.metrics.builder, + queryData: [ + initialQueryBuilderFormValues, + initialQueryBuilderFormValues, + ], + }, + }, + setSupersetQuery: jest.fn(), + supersetQuery: initialQueriesMap.metrics, + stagedQuery: initialQueriesMap.metrics, + initialDataSource: null, + panelType: PANEL_TYPES.TIME_SERIES, + isEnabledQuery: false, + lastUsedQuery: 0, + setLastUsedQuery: noop, + handleSetQueryData: noop, + handleSetFormulaData: noop, + handleSetQueryItemData: noop, + handleSetConfig: noop, + removeQueryBuilderEntityByIndex: noop, + removeQueryTypeItemByIndex: noop, + addNewBuilderQuery: noop, + cloneQuery: noop, + addNewFormula: noop, + addNewQueryItem: noop, + redirectWithQueryBuilderData: noop, + handleRunQuery: noop, + resetQuery: noop, + updateAllQueriesOperators: (): Query => initialQueriesMap.metrics, + updateQueriesData: (): Query => initialQueriesMap.metrics, + initQueryBuilderData: noop, + handleOnUnitsChange: noop, + isStagedQueryUpdated: (): boolean => false, + }} + > + + + + + + + , ); const queries = queryAllByText( - 'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options', + "Enter your filter query (e.g., status = 'error' AND service = 'frontend')", ); - expect(queries.length).toBe(2); - - const legendFormats = queryAllByText('Legend Format'); - expect(legendFormats.length).toBe(2); - - const aggrInterval = queryAllByText('AGGREGATION INTERVAL'); - expect(aggrInterval.length).toBe(2); + expect(queries.length).toBe(1); }); test('frequency chart visibility and switch toggle', async () => { const { getByRole, queryByText } = render( - - - , + + + + + , ); // check the presence of Frequency Chart diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 8d5a972cc169..7c1ce541e499 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -3,13 +3,16 @@ import './LogsExplorer.styles.scss'; import * as Sentry from '@sentry/react'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageApi from 'api/browser/localstorage/set'; +import { TelemetryFieldKey } from 'api/v5/v5'; import cx from 'classnames'; import ExplorerCard from 'components/ExplorerCard/ExplorerCard'; import QuickFilters from 'components/QuickFilters/QuickFilters'; import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import { LOCALSTORAGE } from 'constants/localStorage'; +import { QueryParams } from 'constants/query'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import LogExplorerQuerySection from 'container/LogExplorerQuerySection'; -import LogsExplorerViews from 'container/LogsExplorerViews'; +import LogsExplorerViewsContainer from 'container/LogsExplorerViews'; import { defaultLogsSelectedColumns, defaultOptionsQuery, @@ -19,22 +22,33 @@ import { OptionsQuery } from 'container/OptionsMenu/types'; import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions'; import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions'; import Toolbar from 'container/Toolbar/Toolbar'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; +import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { isEqual, isNull } from 'lodash-es'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { useSearchParams } from 'react-router-dom-v5-compat'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; +import { + getExplorerViewForPanelType, + getExplorerViewFromUrl, +} from 'utils/explorerUtils'; -import { WrapperStyled } from './styles'; -import { SELECTED_VIEWS } from './utils'; +import { ExplorerViews } from './utils'; function LogsExplorer(): JSX.Element { - const [showFrequencyChart, setShowFrequencyChart] = useState(true); - const [selectedView, setSelectedView] = useState( - SELECTED_VIEWS.SEARCH, + const [searchParams] = useSearchParams(); + + // Get panel type from URL + const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + const [selectedView, setSelectedView] = useState(() => + getExplorerViewFromUrl(searchParams, panelTypesFromUrl), ); const { preferences, loading: preferencesLoading } = usePreferenceContext(); @@ -48,7 +62,32 @@ function LogsExplorer(): JSX.Element { return true; }); - const { handleRunQuery, currentQuery } = useQueryBuilder(); + // Update selected view when panel type from URL changes + useEffect(() => { + if (panelTypesFromUrl) { + const newView = getExplorerViewForPanelType(panelTypesFromUrl); + if (newView && newView !== selectedView) { + setSelectedView(newView); + } + } + }, [panelTypesFromUrl, selectedView]); + + // Update URL when selectedView changes (without triggering re-renders) + useEffect(() => { + const url = new URL(window.location.href); + url.searchParams.set(QueryParams.selectedExplorerView, selectedView); + window.history.replaceState({}, '', url.toString()); + }, [selectedView]); + + const { + handleRunQuery, + handleSetConfig, + updateAllQueriesOperators, + currentQuery, + updateQueriesData, + } = useQueryBuilder(); + + const { handleExplorerTabChange } = useHandleExplorerTabChange(); const listQueryKeyRef = useRef(); @@ -56,13 +95,83 @@ function LogsExplorer(): JSX.Element { const [isLoadingQueries, setIsLoadingQueries] = useState(false); - const handleToggleShowFrequencyChart = (): void => { - setShowFrequencyChart(!showFrequencyChart); - }; + const [shouldReset, setShouldReset] = useState(false); - const handleChangeSelectedView = (view: SELECTED_VIEWS): void => { - setSelectedView(view); - }; + const [defaultQuery, setDefaultQuery] = useState(() => + updateAllQueriesOperators( + initialQueriesMap.logs, + PANEL_TYPES.LIST, + DataSource.LOGS, + ), + ); + + const handleChangeSelectedView = useCallback( + (view: ExplorerViews): void => { + if (selectedView === ExplorerViews.LIST) { + handleSetConfig(PANEL_TYPES.LIST, DataSource.LOGS); + } + + if (view === ExplorerViews.LIST) { + if ( + selectedView !== ExplorerViews.LIST && + currentQuery?.builder?.queryData?.[0] + ) { + const filterToRetain = currentQuery.builder.queryData[0].filter; + + const newDefaultQuery = updateAllQueriesOperators( + initialQueriesMap.logs, + PANEL_TYPES.LIST, + DataSource.LOGS, + ); + + const newListQuery = updateQueriesData( + newDefaultQuery, + 'queryData', + (item, index) => { + if (index === 0) { + return { ...item, filter: filterToRetain }; + } + return item; + }, + ); + setDefaultQuery(newListQuery); + } + setShouldReset(true); + } + + setSelectedView(view); + handleExplorerTabChange( + view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view, + ); + }, + [ + handleSetConfig, + handleExplorerTabChange, + selectedView, + currentQuery, + updateAllQueriesOperators, + updateQueriesData, + setSelectedView, + ], + ); + + useShareBuilderUrl({ + defaultValue: defaultQuery, + forceReset: shouldReset, + }); + + useEffect(() => { + if (shouldReset) { + setShouldReset(false); + setDefaultQuery( + updateAllQueriesOperators( + initialQueriesMap.logs, + PANEL_TYPES.LIST, + DataSource.LOGS, + ), + ); + } + }, [shouldReset, updateAllQueriesOperators]); const handleFilterVisibilityChange = (): void => { setLocalStorageApi( @@ -72,19 +181,6 @@ function LogsExplorer(): JSX.Element { setShowFilters((prev) => !prev); }; - // Switch to query builder view if there are more than 1 queries - useEffect(() => { - if (currentQuery.builder.queryData.length > 1) { - handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); - } - if ( - currentQuery.builder.queryData.length === 1 && - currentQuery.builder.queryData?.[0]?.groupBy?.length > 0 - ) { - handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); - } - }, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]); - const { redirectWithQuery: redirectWithOptionsData, } = useUrlQueryData(URL_OPTIONS, defaultOptionsQuery); @@ -104,11 +200,11 @@ function LogsExplorer(): JSX.Element { // Check if the columns have the required columns (timestamp, body) const hasRequiredColumns = useCallback( - (columns?: Array<{ key: string }> | null): boolean => { + (columns?: TelemetryFieldKey[] | null): boolean => { if (!columns?.length) return false; - const hasTimestamp = columns.some((col) => col.key === 'timestamp'); - const hasBody = columns.some((col) => col.key === 'body'); + const hasTimestamp = columns.some((col) => col.name === 'timestamp'); + const hasBody = columns.some((col) => col.name === 'body'); return hasTimestamp && hasBody; }, @@ -117,7 +213,7 @@ function LogsExplorer(): JSX.Element { // Merge the columns with the required columns (timestamp, body) if missing const mergeWithRequiredColumns = useCallback( - (columns: BaseAutocompleteData[]): BaseAutocompleteData[] => [ + (columns: TelemetryFieldKey[]): TelemetryFieldKey[] => [ // Add required columns (timestamp, body) if missing ...(!hasRequiredColumns(columns) ? defaultLogsSelectedColumns : []), ...columns, @@ -195,42 +291,44 @@ function LogsExplorer(): JSX.Element { preferencesLoading, ]); - const isMultipleQueries = useMemo( - () => - currentQuery.builder.queryData?.length > 1 || - currentQuery.builder.queryFormulas?.length > 0, - [currentQuery], - ); - - const isGroupByPresent = useMemo( - () => - currentQuery.builder.queryData?.length === 1 && - currentQuery.builder.queryData?.[0]?.groupBy?.length > 0, - [currentQuery.builder.queryData], - ); - const toolbarViews = useMemo( () => ({ - search: { - name: 'search', - label: 'Search', - disabled: isMultipleQueries || isGroupByPresent, + list: { + name: 'list', + label: 'List', show: true, + key: 'list', }, - queryBuilder: { - name: 'query-builder', - label: 'Query Builder', + timeseries: { + name: 'timeseries', + label: 'Timeseries', disabled: false, show: true, + key: 'timeseries', + }, + trace: { + name: 'trace', + label: 'Trace', + disabled: false, + show: false, + key: 'trace', + }, + table: { + name: 'table', + label: 'Table', + disabled: false, + show: true, + key: 'table', }, clickhouse: { name: 'clickhouse', label: 'Clickhouse', disabled: false, show: false, + key: 'clickhouse', }, }), - [isGroupByPresent, isMultipleQueries], + [], ); return ( @@ -256,39 +354,33 @@ function LogsExplorer(): JSX.Element { items={toolbarViews} selectedView={selectedView} onChangeSelectedView={handleChangeSelectedView} - onToggleHistrogramVisibility={handleToggleShowFrequencyChart} - showFrequencyChart={showFrequencyChart} /> } rightActions={ handleRunQuery(true, true)} listQueryKeyRef={listQueryKeyRef} chartQueryKeyRef={chartQueryKeyRef} isLoadingQueries={isLoadingQueries} /> } - showOldCTA /> - -
-
- - - -
-
- -
+
+
+ + +
- +
+ +
+
diff --git a/frontend/src/pages/LogsExplorer/utils.tsx b/frontend/src/pages/LogsExplorer/utils.tsx index f140f495b4ba..7bc4e46d3d80 100644 --- a/frontend/src/pages/LogsExplorer/utils.tsx +++ b/frontend/src/pages/LogsExplorer/utils.tsx @@ -17,9 +17,11 @@ export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({ }); // eslint-disable-next-line @typescript-eslint/naming-convention -export enum SELECTED_VIEWS { - SEARCH = 'search', - QUERY_BUILDER = 'query-builder', +export enum ExplorerViews { + LIST = 'list', + TIMESERIES = 'timeseries', + TRACE = 'trace', + TABLE = 'table', CLICKHOUSE = 'clickhouse', } diff --git a/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx b/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx index 284466528295..07be942964ad 100644 --- a/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx +++ b/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx @@ -29,7 +29,7 @@ function MetricsExplorerPage(): JSX.Element { [updateAllQueriesOperators], ); - useShareBuilderUrl(defaultQuery); + useShareBuilderUrl({ defaultValue: defaultQuery }); return (
diff --git a/frontend/src/pages/TracesExplorer/Filter/Filter.styles.scss b/frontend/src/pages/TracesExplorer/Filter/Filter.styles.scss index 497ac75ba86d..d7050f82a1a9 100644 --- a/frontend/src/pages/TracesExplorer/Filter/Filter.styles.scss +++ b/frontend/src/pages/TracesExplorer/Filter/Filter.styles.scss @@ -8,11 +8,11 @@ .ant-collapse-header-text { color: var(--bg-vanilla-400); font-family: Inter; - font-size: 14px; + font-size: 12px; font-style: normal; font-weight: 400; - line-height: 18px; - letter-spacing: -0.07px; + line-height: 16px; + letter-spacing: -0.065px; text-transform: capitalize; } @@ -24,11 +24,11 @@ .ant-input-group-addon { color: var(--bg-vanilla-400); font-family: 'Space Mono', monospace; - font-size: 12px; + font-size: 11px; font-style: normal; font-weight: 400; line-height: 16px; - letter-spacing: 0.48px; + letter-spacing: 0.44px; padding: 0 6px; } @@ -37,11 +37,11 @@ color: var(--bg-vanilla-400); font-family: 'Space Mono', monospace; - font-size: 12px; + font-size: 11px; font-style: normal; font-weight: 400; line-height: 16px; - letter-spacing: 0.48px; + letter-spacing: 0.44px; } } } @@ -54,7 +54,8 @@ } .filter-header { - padding: 16px 8px 16px 12px; + padding: 4px 8px; + .filter-title { display: flex; gap: 6px; diff --git a/frontend/src/pages/TracesExplorer/Filter/Filter.tsx b/frontend/src/pages/TracesExplorer/Filter/Filter.tsx index 932300e2ab60..8df0fdd573da 100644 --- a/frontend/src/pages/TracesExplorer/Filter/Filter.tsx +++ b/frontend/src/pages/TracesExplorer/Filter/Filter.tsx @@ -183,10 +183,12 @@ export function Filter(props: FilterProps): JSX.Element { ...data, filters: { ...data.filters, - items: data.filters?.items?.map((item) => ({ - ...item, - id: '', - })), + items: + data.filters?.items?.map((item) => ({ + ...item, + id: '', + })) || [], + op: data.filters?.op || 'AND', }, })); return clonedQuery; @@ -204,11 +206,12 @@ export function Filter(props: FilterProps): JSX.Element { ...item.filters, items: props?.resetAll ? [] - : (unionTagFilterItems(item.filters?.items, preparePostData()) + : (unionTagFilterItems(item.filters?.items || [], preparePostData()) .map((item) => item.key?.key === props?.clearByType ? undefined : item, ) .filter((i) => i) as TagFilterItem[]), + op: item.filters?.op || 'AND', }, })), }, diff --git a/frontend/src/pages/TracesExplorer/Filter/filterUtils.ts b/frontend/src/pages/TracesExplorer/Filter/filterUtils.ts index b04d6383420a..273ad6c4e6ea 100644 --- a/frontend/src/pages/TracesExplorer/Filter/filterUtils.ts +++ b/frontend/src/pages/TracesExplorer/Filter/filterUtils.ts @@ -12,18 +12,28 @@ import { DataSource } from 'types/common/queryBuilder'; export const AllTraceFilterKeyValue: Record = { durationNanoMin: 'Duration', durationNano: 'Duration', + duration_nano: 'Duration', durationNanoMax: 'Duration', 'deployment.environment': 'Environment', hasError: 'Status', + has_error: 'Status', serviceName: 'Service Name', + 'service.name': 'service.name', name: 'Operation / Name', rpcMethod: 'RPC Method', + 'rpc.method': 'RPC Method', responseStatusCode: 'Status Code', + response_status_code: 'Status Code', httpHost: 'HTTP Host', + http_host: 'HTTP Host', httpMethod: 'HTTP Method', + http_method: 'HTTP Method', httpRoute: 'HTTP Route', + 'http.route': 'HTTP Route', httpUrl: 'HTTP URL', + 'http.url': 'HTTP URL', traceID: 'Trace ID', + trace_id: 'Trace ID', } as const; export type AllTraceFilterKeys = keyof typeof AllTraceFilterKeyValue; diff --git a/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss b/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss index 91affeef4069..94b67940ced5 100644 --- a/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss +++ b/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss @@ -1,8 +1,4 @@ .trace-explorer-header { - display: flex; - justify-content: space-between; - align-items: center; - .trace-explorer-run-query { display: flex; flex-direction: row-reverse; @@ -26,17 +22,55 @@ } .traces-explorer-views { + padding: 8px; + padding-bottom: 60px; + margin-bottom: 24px; + .ant-tabs-tabpane { padding: 0 8px; } } +.qb-search-view-container { + padding: 8px; + + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .ant-select-selector { + border-radius: 2px; + border: 1px solid var(--bg-slate-400) !important; + background: var(--bg-ink-300) !important; + height: 34px !important; + box-sizing: border-box !important; + } +} + +.trace-explorer-list-view { + flex: 1; +} + +.trace-explorer-traces-view { + flex: 1; +} + +.trace-explorer-table-view { + flex: 1; +} + +.trace-explorer-time-series-view { + flex: 1; +} + .trace-explorer-page { display: flex; .filter { width: 260px; - height: 100vh; + height: 100%; + min-height: 100vh; border-right: 0px; border: 1px solid var(--bg-slate-400); @@ -50,11 +84,10 @@ .trace-explorer { width: 100%; - border-left: 1px solid var(--bg-slate-400); background: var(--bg-ink-500); > .ant-card-body { - padding: 8px 8px; + padding: 0; } border-color: var(--bg-slate-400); diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index e20f8bb3802d..971249bedb88 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -4,7 +4,6 @@ import { ENVIRONMENT } from 'constants/env'; import { initialQueriesMap, initialQueryBuilderFormValues, - PANEL_TYPES, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam'; @@ -12,11 +11,13 @@ import { quickFiltersListResponse } from 'mocks-server/__mockdata__/customQuickF import { queryRangeForListView, queryRangeForTableView, + queryRangeForTableViewV5, queryRangeForTraceView, } from 'mocks-server/__mockdata__/query_range'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; import { QueryBuilderContext } from 'providers/QueryBuilder'; +import { MemoryRouter } from 'react-router-dom-v5-compat'; import { act, cleanup, @@ -26,7 +27,7 @@ import { waitFor, within, } from 'tests/test-utils'; -import { QueryRangePayload } from 'types/api/metrics/getQueryRange'; +import { QueryRangePayloadV5 } from 'types/api/v5/queryRange'; import TracesExplorer from '..'; import { Filter } from '../Filter/Filter'; @@ -43,6 +44,35 @@ import { redirectWithQueryBuilderData, } from './testUtils'; +const currentTestUrl = + '/traces-explorer/?panelType=list&selectedExplorerView=list'; + +jest.mock('react-router-dom-v5-compat', () => ({ + ...jest.requireActual('react-router-dom-v5-compat'), + useSearchParams: jest.fn(() => { + const searchParams = new URLSearchParams(); + + // Parse the current test URL + const url = new URL(currentTestUrl, 'http://localhost'); + const panelType = url.searchParams.get('panelType') || 'list'; + const selectedExplorerView = + url.searchParams.get('selectedExplorerView') || 'list'; + + searchParams.set('panelType', panelType); + searchParams.set('selectedExplorerView', selectedExplorerView); + + return [searchParams, jest.fn()]; + }), +})); + +// Mock useGetPanelTypesQueryParam to return the correct panel type +jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({ + useGetPanelTypesQueryParam: jest.fn(() => { + const url = new URL(currentTestUrl, 'http://localhost'); + return url.searchParams.get('panelType') || 'list'; + }), +})); + const historyPush = jest.fn(); const BASE_URL = ENVIRONMENT.baseURL; @@ -50,8 +80,16 @@ const FILTER_SERVICE_NAME = 'Service Name'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useLocation: (): { pathname: string } => ({ + useLocation: (): { + pathname: string; + search: string; + hash: string; + state: any; + } => ({ pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/`, + search: '', + hash: '', + state: null, }), useHistory: (): any => ({ ...jest.requireActual('react-router-dom').useHistory(), @@ -128,15 +166,49 @@ jest.mock('hooks/useSafeNavigate', () => ({ }), })); +const checkFilterValues = ( + getByText: (text: string) => HTMLElement, + getAllByText: (text: string) => HTMLElement[], +): void => { + Object.values(AllTraceFilterKeyValue).forEach((filter) => { + try { + expect(getByText(filter)).toBeInTheDocument(); + } catch (error) { + // If getByText fails, try getAllByText + expect(getAllByText(filter)[0]).toBeInTheDocument(); + } + }); +}; + +const renderWithTracesExplorerRouter = ( + component: React.ReactNode, + initialEntries: string[] = [ + '/traces-explorer/?panelType=list&selectedExplorerView=list', + ], +): ReturnType => + render( + + + {component} + + , + ); + describe('TracesExplorer - Filters', () => { // Initial filter panel rendering // Test the initial state like which filters section are opened, default state of duration slider, etc. it('should render the Trace filter', async () => { - const { getByText, getByTestId } = render(); + const { getByText, getAllByText, getByTestId } = render( + + + , + ); - Object.values(AllTraceFilterKeyValue).forEach((filter) => { - expect(getByText(filter)).toBeInTheDocument(); - }); + checkFilterValues(getByText, getAllByText); // Check default state of duration slider const minDuration = getByTestId('min-input') as HTMLInputElement; @@ -175,7 +247,11 @@ describe('TracesExplorer - Filters', () => { // test the filter panel actions like opening and closing the sections, etc. it('filter panel actions', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + , + ); // Check if the section is closed checkIfSectionIsNotOpen(getByTestId, 'name'); @@ -199,7 +275,7 @@ describe('TracesExplorer - Filters', () => { }); it('checking filters should update the query', async () => { - const { getByText } = render( + const { getByText } = renderWithTracesExplorerRouter( { .spyOn(compositeQueryHook, 'useGetCompositeQueryParam') .mockReturnValue(compositeQuery); - const { findByText, getByTestId } = render(); + const { findByText, getByTestId } = renderWithTracesExplorerRouter( + , + ); // check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer expect(await findByText('demo-app')).toBeInTheDocument(); @@ -295,11 +373,11 @@ describe('TracesExplorer - Filters', () => { }, }); - const { getByText } = render(); + const { getByText, getAllByText } = renderWithTracesExplorerRouter( + , + ); - Object.values(AllTraceFilterKeyValue).forEach((filter) => { - expect(getByText(filter)).toBeInTheDocument(); - }); + checkFilterValues(getByText, getAllByText); }); it('test edge cases of undefined filters - items', async () => { @@ -320,15 +398,15 @@ describe('TracesExplorer - Filters', () => { }, }); - const { getByText } = render(); + const { getByText, getAllByText } = renderWithTracesExplorerRouter( + , + ); - Object.values(AllTraceFilterKeyValue).forEach((filter) => { - expect(getByText(filter)).toBeInTheDocument(); - }); + checkFilterValues(getByText, getAllByText); }); it('should clear filter on clear & reset button click', async () => { - const { getByText, getByTestId } = render( + const { getByText, getByTestId } = renderWithTracesExplorerRouter( ({ })), })); -let capturedPayload: QueryRangePayload; +let capturedPayload: QueryRangePayloadV5; describe('TracesExplorer - ', () => { const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/traces`; @@ -461,6 +539,11 @@ describe('TracesExplorer - ', () => { res(ctx.status(200), ctx.json(quickFiltersListResponse)), ), ); + server.use( + rest.post(`${BASE_URL}/api/v5/query_range`, (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTableView)), + ), + ); }; beforeEach(() => { @@ -476,21 +559,18 @@ describe('TracesExplorer - ', () => { cleanup(); }); - it('trace explorer - list view', async () => { + it.skip('trace explorer - list view', async () => { server.use( - rest.post(`${BASE_URL}/api/v4/query_range`, (req, res, ctx) => + rest.post(`${BASE_URL}/api/v5/query_range`, (req, res, ctx) => res(ctx.status(200), ctx.json(queryRangeForListView)), ), ); - const { getByText } = render( - - - , - ); + const { getByText } = renderWithTracesExplorerRouter(); await screen.findByText(FILTER_SERVICE_NAME); - expect(await screen.findByText('Timestamp')).toBeInTheDocument(); + + await screen.findByText('demo-app'); expect(getByText('options_menu.options')).toBeInTheDocument(); // test if pagination is there @@ -499,77 +579,60 @@ describe('TracesExplorer - ', () => { // column interaction is covered in E2E tests as its a complex interaction }); + it('should not add id to orderBy when dataSource is traces', async () => { server.use( - rest.post(`${BASE_URL}/api/v4/query_range`, async (req, res, ctx) => { + rest.post(`${BASE_URL}/api/v5/query_range`, async (req, res, ctx) => { const payload = await req.json(); capturedPayload = payload; return res(ctx.status(200), ctx.json(queryRangeForTableView)); }), ); - render( - - - , - ); + renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=list&selectedExplorerView=list', + ]); await waitFor(() => { expect(capturedPayload).toBeDefined(); }); - expect(capturedPayload.compositeQuery.builderQueries?.A.orderBy).toEqual([ - { columnName: 'timestamp', order: 'desc' }, - ]); + expect( + (capturedPayload.compositeQuery.queries[0].spec as any).order, + ).toEqual([{ key: { name: 'timestamp' }, direction: 'desc' }]); }); - it('trace explorer - table view', async () => { + it.skip('trace explorer - table view', async () => { server.use( - rest.post(`${BASE_URL}/api/v4/query_range`, (req, res, ctx) => - res(ctx.status(200), ctx.json(queryRangeForTableView)), + rest.post(`${BASE_URL}/api/v5/query_range`, (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTableViewV5)), ), ); - render( - - - , - ); - expect(await screen.findByText('count')).toBeInTheDocument(); - expect(screen.getByText('87798.00')).toBeInTheDocument(); + renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=table&selectedExplorerView=table', + ]); + + // Wait for the data to load and check for actual table data + await screen.findByText('401310'); + expect(screen.getByText('401310')).toBeInTheDocument(); }); - it('trace explorer - trace view', async () => { + // skipping since we dont have trace view with new query builder for the time being + + it.skip('trace explorer - trace view', async () => { server.use( - rest.post(`${BASE_URL}/api/v4/query_range`, (req, res, ctx) => + rest.post(`${BASE_URL}/api/v5/query_range`, (req, res, ctx) => res(ctx.status(200), ctx.json(queryRangeForTraceView)), ), ); - const { getByText, getAllByText } = render( - - - , - ); + + const { + getByText, + getAllByText, + } = renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=trace&selectedExplorerView=trace', + ]); expect(await screen.findByText('Root Service Name')).toBeInTheDocument(); @@ -594,60 +657,49 @@ describe('TracesExplorer - ', () => { 'http://localhost/trace/5765b60ba7cc4ddafe8bdaa9c1b4b246', ); }); + it('trace explorer - trace view should only send order by timestamp in the query', async () => { - let capturedPayload: QueryRangePayload; + let capturedPayload: QueryRangePayloadV5; const orderBy = [ { columnName: 'id', order: 'desc' }, { columnName: 'serviceName', order: 'desc' }, ]; - const defaultOrderBy = [{ columnName: 'timestamp', order: 'desc' }]; + const defaultOrderBy = [ + { + key: { name: 'timestamp' }, + direction: 'desc', + }, + ]; server.use( - rest.post(`${BASE_URL}/api/v4/query_range`, async (req, res, ctx) => { + rest.post(`${BASE_URL}/api/v5/query_range`, async (req, res, ctx) => { const payload = await req.json(); capturedPayload = payload; return res(ctx.status(200), ctx.json(queryRangeForTraceView)); }), ); - render( - - - , - ); + + renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=trace&selectedExplorerView=trace', + ]); await waitFor(() => { expect(capturedPayload).toBeDefined(); - expect(capturedPayload?.compositeQuery?.builderQueries?.A.orderBy).toEqual( - defaultOrderBy, - ); expect( - capturedPayload?.compositeQuery?.builderQueries?.A.orderBy, + (capturedPayload?.compositeQuery?.queries[0].spec as any).order, + ).toEqual(defaultOrderBy); + expect( + (capturedPayload?.compositeQuery?.queries[0].spec as any).order, ).not.toEqual(orderBy); }); }); it('test for explorer options', async () => { - const { getByText, getByTestId } = render( - - - , - ); + const { + getByText, + getByTestId, + } = renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=list&selectedExplorerView=list', + ]); // assert explorer options - action btns [ @@ -675,11 +727,9 @@ describe('TracesExplorer - ', () => { }); it('select a view options - assert and save this view', async () => { - const { container } = render( - - - , - ); + const { container } = renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=list&selectedExplorerView=list', + ]); await screen.findByText(FILTER_SERVICE_NAME); await act(async () => { fireEvent.mouseDown( @@ -724,11 +774,9 @@ describe('TracesExplorer - ', () => { }); it('create a dashboard btn assert', async () => { - const { getByText } = render( - - - , - ); + const { getByText } = renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=list&selectedExplorerView=list', + ]); await screen.findByText(FILTER_SERVICE_NAME); const createDashboardBtn = getByText('Add to Dashboard'); @@ -752,11 +800,9 @@ describe('TracesExplorer - ', () => { }); it('create an alert btn assert', async () => { - const { getByText } = render( - - - , - ); + const { getByText } = renderWithTracesExplorerRouter(, [ + '/traces-explorer/?panelType=list&selectedExplorerView=list', + ]); await screen.findByText(FILTER_SERVICE_NAME); const createAlertBtn = getByText('Create an Alert'); diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index ec7f3d0b7964..c8029146633c 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -1,8 +1,7 @@ import './TracesExplorer.styles.scss'; -import { FilterOutlined } from '@ant-design/icons'; import * as Sentry from '@sentry/react'; -import { Button, Card, Tabs, Tooltip } from 'antd'; +import { Card } from 'antd'; import logEvent from 'api/common/logEvent'; import cx from 'classnames'; import ExplorerCard from 'components/ExplorerCard/ExplorerCard'; @@ -10,14 +9,20 @@ import QuickFilters from 'components/QuickFilters/QuickFilters'; import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import { LOCALSTORAGE } from 'constants/localStorage'; import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes'; +import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper'; import ExportPanel from 'container/ExportPanel'; import { useOptionsMenu } from 'container/OptionsMenu'; +import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions'; import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions'; -import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2'; +import TimeSeriesView from 'container/TimeSeriesView'; +import Toolbar from 'container/Toolbar/Toolbar'; +import ListView from 'container/TracesExplorer/ListView'; import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs'; import QuerySection from 'container/TracesExplorer/QuerySection'; +import TableView from 'container/TracesExplorer/TableView'; +import TracesView from 'container/TracesExplorer/TracesView'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; @@ -25,16 +30,19 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { cloneDeep, isEmpty, set } from 'lodash-es'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; +import { ExplorerViews } from 'pages/LogsExplorer/utils'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom-v5-compat'; import { Dashboard } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; +import { + getExplorerViewForPanelType, + getExplorerViewFromUrl, +} from 'utils/explorerUtils'; import { v4 } from 'uuid'; -import { ActionsWrapper, Container } from './styles'; -import { getTabsItems } from './utils'; - function TracesExplorer(): JSX.Element { const { currentQuery, @@ -42,6 +50,8 @@ function TracesExplorer(): JSX.Element { updateAllQueriesOperators, handleRunQuery, stagedQuery, + handleSetConfig, + updateQueriesData, } = useQueryBuilder(); const { options } = useOptionsMenu({ @@ -53,12 +63,95 @@ function TracesExplorer(): JSX.Element { }, }); - const currentPanelType = useGetPanelTypesQueryParam(); + const [searchParams, setSearchParams] = useSearchParams(); + + // Get panel type from URL + const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + const [selectedView, setSelectedView] = useState(() => + getExplorerViewFromUrl(searchParams, panelTypesFromUrl), + ); const { handleExplorerTabChange } = useHandleExplorerTabChange(); const { safeNavigate } = useSafeNavigate(); - const currentTab = panelType || PANEL_TYPES.LIST; + // Update selected view when panel type from URL changes + useEffect(() => { + if (panelTypesFromUrl) { + const newView = getExplorerViewForPanelType(panelTypesFromUrl); + if (newView && newView !== selectedView) { + setSelectedView(newView); + } + } + }, [panelTypesFromUrl, selectedView]); + + // Update URL when selectedView changes + useEffect(() => { + setSearchParams((prev: URLSearchParams) => { + prev.set(QueryParams.selectedExplorerView, selectedView); + return prev; + }); + }, [selectedView, setSearchParams]); + + const [shouldReset, setShouldReset] = useState(false); + + const [defaultQuery, setDefaultQuery] = useState(() => + updateAllQueriesOperators( + initialQueriesMap.traces, + PANEL_TYPES.LIST, + DataSource.TRACES, + ), + ); + + const handleChangeSelectedView = useCallback( + (view: ExplorerViews): void => { + if (selectedView === ExplorerViews.LIST) { + handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES); + } + + if (view === ExplorerViews.LIST) { + if ( + selectedView !== ExplorerViews.LIST && + currentQuery?.builder?.queryData?.[0] + ) { + const filterToRetain = currentQuery.builder.queryData[0].filter; + + const newDefaultQuery = updateAllQueriesOperators( + initialQueriesMap.traces, + PANEL_TYPES.LIST, + DataSource.TRACES, + ); + + const newListQuery = updateQueriesData( + newDefaultQuery, + 'queryData', + (item, index) => { + if (index === 0) { + return { ...item, filter: filterToRetain }; + } + return item; + }, + ); + setDefaultQuery(newListQuery); + } + setShouldReset(true); + } + + setSelectedView(view); + handleExplorerTabChange( + view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view, + ); + }, + [ + handleSetConfig, + handleExplorerTabChange, + selectedView, + currentQuery, + updateAllQueriesOperators, + updateQueriesData, + setSelectedView, + ], + ); const listQuery = useMemo(() => { if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null; @@ -66,48 +159,6 @@ function TracesExplorer(): JSX.Element { return stagedQuery.builder.queryData.find((item) => !item.disabled) || null; }, [stagedQuery]); - const isMultipleQueries = useMemo( - () => - currentQuery.builder.queryData.length > 1 || - currentQuery.builder.queryFormulas.length > 0, - [currentQuery], - ); - - const isGroupByExist = useMemo(() => { - const groupByCount: number = currentQuery.builder.queryData.reduce( - (acc, query) => acc + (query?.groupBy?.length || 0), - 0, - ); - - return groupByCount > 0; - }, [currentQuery]); - - const defaultQuery = useMemo(() => { - const query = updateAllQueriesOperators( - initialQueriesMap.traces, - PANEL_TYPES.LIST, - DataSource.TRACES, - ); - - return { - ...query, - builder: { - ...query.builder, - queryData: [ - { - ...query.builder.queryData[0], - orderBy: [{ columnName: 'timestamp', order: 'desc' }], - }, - ], - }, - }; - }, [updateAllQueriesOperators]); - - const tabsItems = getTabsItems({ - isListViewDisabled: isMultipleQueries || isGroupByExist, - isFilterApplied: !isEmpty(listQuery?.filters.items), - }); - const exportDefaultQuery = useMemo( () => updateAllQueriesOperators( @@ -163,26 +214,24 @@ function TracesExplorer(): JSX.Element { [exportDefaultQuery, panelType, safeNavigate, getUpdatedQueryForExport], ); - useShareBuilderUrl(defaultQuery); + useShareBuilderUrl({ defaultValue: defaultQuery, forceReset: shouldReset }); useEffect(() => { - const shouldChangeView = isMultipleQueries || isGroupByExist; - - if ( - (currentTab === PANEL_TYPES.LIST || currentTab === PANEL_TYPES.TRACE) && - shouldChangeView - ) { - handleExplorerTabChange(currentPanelType || PANEL_TYPES.TIME_SERIES); + if (shouldReset) { + setShouldReset(false); + setDefaultQuery( + updateAllQueriesOperators( + initialQueriesMap.traces, + PANEL_TYPES.LIST, + DataSource.TRACES, + ), + ); } - }, [ - currentTab, - isMultipleQueries, - isGroupByExist, - handleExplorerTabChange, - currentPanelType, - ]); + }, [shouldReset, updateAllQueriesOperators]); + const [isOpen, setOpen] = useState(true); const logEventCalledRef = useRef(false); + useEffect(() => { if (!logEventCalledRef.current) { logEvent('Traces Explorer: Page visited', {}); @@ -190,6 +239,50 @@ function TracesExplorer(): JSX.Element { } }, []); + const toolbarViews = useMemo( + () => ({ + list: { + name: 'list', + label: 'List', + show: true, + key: 'list', + }, + timeseries: { + name: 'timeseries', + label: 'Timeseries', + disabled: false, + show: true, + key: 'timeseries', + }, + trace: { + name: 'trace', + label: 'Trace', + disabled: false, + show: true, + key: 'trace', + }, + table: { + name: 'table', + label: 'Table', + disabled: false, + show: true, + key: 'table', + }, + clickhouse: { + name: 'clickhouse', + label: 'Clickhouse', + disabled: false, + show: false, + key: 'clickhouse', + }, + }), + [], + ); + + const isFilterApplied = useMemo(() => !isEmpty(listQuery?.filters?.items), [ + listQuery, + ]); + return ( }>
@@ -203,51 +296,80 @@ function TracesExplorer(): JSX.Element { }} /> - -
- {!isOpen && ( - - - - )} -
- - -
+
+ setOpen(!isOpen)} + items={toolbarViews} + selectedView={selectedView} + onChangeSelectedView={handleChangeSelectedView} + /> + } + rightActions={ + handleRunQuery(true, true)} + /> + } + />
- +
+ +
- - - - +
+
+ +
+ + {selectedView === ExplorerViews.LIST && ( +
+ +
+ )} + + {selectedView === ExplorerViews.TRACE && ( +
+ +
+ )} + + {selectedView === ExplorerViews.TIMESERIES && ( +
+ +
+ )} + + {selectedView === ExplorerViews.TABLE && ( +
+ +
+ )} +
- -
- +
); diff --git a/frontend/src/parser/FilterQuery.interp b/frontend/src/parser/FilterQuery.interp new file mode 100644 index 000000000000..83105ce119b2 --- /dev/null +++ b/frontend/src/parser/FilterQuery.interp @@ -0,0 +1,90 @@ +token literal names: +null +'(' +')' +'[' +']' +',' +null +'!=' +'<>' +'<' +'<=' +'>' +'>=' +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null + +token symbolic names: +null +LPAREN +RPAREN +LBRACK +RBRACK +COMMA +EQUALS +NOT_EQUALS +NEQ +LT +LE +GT +GE +LIKE +ILIKE +BETWEEN +EXISTS +REGEXP +CONTAINS +IN +NOT +AND +OR +HAS +HASANY +HASALL +BOOL +NUMBER +QUOTED_TEXT +KEY +WS +FREETEXT + +rule names: +query +expression +orExpression +andExpression +unaryExpression +primary +comparison +inClause +notInClause +valueList +fullText +functionCall +functionParamList +functionParam +array +value +key + + +atn: +[4, 1, 31, 219, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 5, 2, 43, 8, 2, 10, 2, 12, 2, 46, 9, 2, 1, 3, 1, 3, 1, 3, 1, 3, 5, 3, 52, 8, 3, 10, 3, 12, 3, 55, 9, 3, 1, 4, 3, 4, 58, 8, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 71, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 150, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 164, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 181, 8, 8, 1, 9, 1, 9, 1, 9, 5, 9, 186, 8, 9, 10, 9, 12, 9, 189, 9, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 5, 12, 201, 8, 12, 10, 12, 12, 12, 204, 9, 12, 1, 13, 1, 13, 1, 13, 3, 13, 209, 8, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 0, 0, 17, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 0, 5, 1, 0, 7, 8, 1, 0, 13, 14, 2, 0, 28, 28, 31, 31, 1, 0, 23, 25, 1, 0, 26, 29, 235, 0, 34, 1, 0, 0, 0, 2, 37, 1, 0, 0, 0, 4, 39, 1, 0, 0, 0, 6, 47, 1, 0, 0, 0, 8, 57, 1, 0, 0, 0, 10, 70, 1, 0, 0, 0, 12, 149, 1, 0, 0, 0, 14, 163, 1, 0, 0, 0, 16, 180, 1, 0, 0, 0, 18, 182, 1, 0, 0, 0, 20, 190, 1, 0, 0, 0, 22, 192, 1, 0, 0, 0, 24, 197, 1, 0, 0, 0, 26, 208, 1, 0, 0, 0, 28, 210, 1, 0, 0, 0, 30, 214, 1, 0, 0, 0, 32, 216, 1, 0, 0, 0, 34, 35, 3, 2, 1, 0, 35, 36, 5, 0, 0, 1, 36, 1, 1, 0, 0, 0, 37, 38, 3, 4, 2, 0, 38, 3, 1, 0, 0, 0, 39, 44, 3, 6, 3, 0, 40, 41, 5, 22, 0, 0, 41, 43, 3, 6, 3, 0, 42, 40, 1, 0, 0, 0, 43, 46, 1, 0, 0, 0, 44, 42, 1, 0, 0, 0, 44, 45, 1, 0, 0, 0, 45, 5, 1, 0, 0, 0, 46, 44, 1, 0, 0, 0, 47, 53, 3, 8, 4, 0, 48, 49, 5, 21, 0, 0, 49, 52, 3, 8, 4, 0, 50, 52, 3, 8, 4, 0, 51, 48, 1, 0, 0, 0, 51, 50, 1, 0, 0, 0, 52, 55, 1, 0, 0, 0, 53, 51, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 7, 1, 0, 0, 0, 55, 53, 1, 0, 0, 0, 56, 58, 5, 20, 0, 0, 57, 56, 1, 0, 0, 0, 57, 58, 1, 0, 0, 0, 58, 59, 1, 0, 0, 0, 59, 60, 3, 10, 5, 0, 60, 9, 1, 0, 0, 0, 61, 62, 5, 1, 0, 0, 62, 63, 3, 4, 2, 0, 63, 64, 5, 2, 0, 0, 64, 71, 1, 0, 0, 0, 65, 71, 3, 12, 6, 0, 66, 71, 3, 22, 11, 0, 67, 71, 3, 20, 10, 0, 68, 71, 3, 32, 16, 0, 69, 71, 3, 30, 15, 0, 70, 61, 1, 0, 0, 0, 70, 65, 1, 0, 0, 0, 70, 66, 1, 0, 0, 0, 70, 67, 1, 0, 0, 0, 70, 68, 1, 0, 0, 0, 70, 69, 1, 0, 0, 0, 71, 11, 1, 0, 0, 0, 72, 73, 3, 32, 16, 0, 73, 74, 5, 6, 0, 0, 74, 75, 3, 30, 15, 0, 75, 150, 1, 0, 0, 0, 76, 77, 3, 32, 16, 0, 77, 78, 7, 0, 0, 0, 78, 79, 3, 30, 15, 0, 79, 150, 1, 0, 0, 0, 80, 81, 3, 32, 16, 0, 81, 82, 5, 9, 0, 0, 82, 83, 3, 30, 15, 0, 83, 150, 1, 0, 0, 0, 84, 85, 3, 32, 16, 0, 85, 86, 5, 10, 0, 0, 86, 87, 3, 30, 15, 0, 87, 150, 1, 0, 0, 0, 88, 89, 3, 32, 16, 0, 89, 90, 5, 11, 0, 0, 90, 91, 3, 30, 15, 0, 91, 150, 1, 0, 0, 0, 92, 93, 3, 32, 16, 0, 93, 94, 5, 12, 0, 0, 94, 95, 3, 30, 15, 0, 95, 150, 1, 0, 0, 0, 96, 97, 3, 32, 16, 0, 97, 98, 7, 1, 0, 0, 98, 99, 3, 30, 15, 0, 99, 150, 1, 0, 0, 0, 100, 101, 3, 32, 16, 0, 101, 102, 5, 20, 0, 0, 102, 103, 7, 1, 0, 0, 103, 104, 3, 30, 15, 0, 104, 150, 1, 0, 0, 0, 105, 106, 3, 32, 16, 0, 106, 107, 5, 15, 0, 0, 107, 108, 3, 30, 15, 0, 108, 109, 5, 21, 0, 0, 109, 110, 3, 30, 15, 0, 110, 150, 1, 0, 0, 0, 111, 112, 3, 32, 16, 0, 112, 113, 5, 20, 0, 0, 113, 114, 5, 15, 0, 0, 114, 115, 3, 30, 15, 0, 115, 116, 5, 21, 0, 0, 116, 117, 3, 30, 15, 0, 117, 150, 1, 0, 0, 0, 118, 119, 3, 32, 16, 0, 119, 120, 3, 14, 7, 0, 120, 150, 1, 0, 0, 0, 121, 122, 3, 32, 16, 0, 122, 123, 3, 16, 8, 0, 123, 150, 1, 0, 0, 0, 124, 125, 3, 32, 16, 0, 125, 126, 5, 16, 0, 0, 126, 150, 1, 0, 0, 0, 127, 128, 3, 32, 16, 0, 128, 129, 5, 20, 0, 0, 129, 130, 5, 16, 0, 0, 130, 150, 1, 0, 0, 0, 131, 132, 3, 32, 16, 0, 132, 133, 5, 17, 0, 0, 133, 134, 3, 30, 15, 0, 134, 150, 1, 0, 0, 0, 135, 136, 3, 32, 16, 0, 136, 137, 5, 20, 0, 0, 137, 138, 5, 17, 0, 0, 138, 139, 3, 30, 15, 0, 139, 150, 1, 0, 0, 0, 140, 141, 3, 32, 16, 0, 141, 142, 5, 18, 0, 0, 142, 143, 3, 30, 15, 0, 143, 150, 1, 0, 0, 0, 144, 145, 3, 32, 16, 0, 145, 146, 5, 20, 0, 0, 146, 147, 5, 18, 0, 0, 147, 148, 3, 30, 15, 0, 148, 150, 1, 0, 0, 0, 149, 72, 1, 0, 0, 0, 149, 76, 1, 0, 0, 0, 149, 80, 1, 0, 0, 0, 149, 84, 1, 0, 0, 0, 149, 88, 1, 0, 0, 0, 149, 92, 1, 0, 0, 0, 149, 96, 1, 0, 0, 0, 149, 100, 1, 0, 0, 0, 149, 105, 1, 0, 0, 0, 149, 111, 1, 0, 0, 0, 149, 118, 1, 0, 0, 0, 149, 121, 1, 0, 0, 0, 149, 124, 1, 0, 0, 0, 149, 127, 1, 0, 0, 0, 149, 131, 1, 0, 0, 0, 149, 135, 1, 0, 0, 0, 149, 140, 1, 0, 0, 0, 149, 144, 1, 0, 0, 0, 150, 13, 1, 0, 0, 0, 151, 152, 5, 19, 0, 0, 152, 153, 5, 1, 0, 0, 153, 154, 3, 18, 9, 0, 154, 155, 5, 2, 0, 0, 155, 164, 1, 0, 0, 0, 156, 157, 5, 19, 0, 0, 157, 158, 5, 3, 0, 0, 158, 159, 3, 18, 9, 0, 159, 160, 5, 4, 0, 0, 160, 164, 1, 0, 0, 0, 161, 162, 5, 19, 0, 0, 162, 164, 3, 30, 15, 0, 163, 151, 1, 0, 0, 0, 163, 156, 1, 0, 0, 0, 163, 161, 1, 0, 0, 0, 164, 15, 1, 0, 0, 0, 165, 166, 5, 20, 0, 0, 166, 167, 5, 19, 0, 0, 167, 168, 5, 1, 0, 0, 168, 169, 3, 18, 9, 0, 169, 170, 5, 2, 0, 0, 170, 181, 1, 0, 0, 0, 171, 172, 5, 20, 0, 0, 172, 173, 5, 19, 0, 0, 173, 174, 5, 3, 0, 0, 174, 175, 3, 18, 9, 0, 175, 176, 5, 4, 0, 0, 176, 181, 1, 0, 0, 0, 177, 178, 5, 20, 0, 0, 178, 179, 5, 19, 0, 0, 179, 181, 3, 30, 15, 0, 180, 165, 1, 0, 0, 0, 180, 171, 1, 0, 0, 0, 180, 177, 1, 0, 0, 0, 181, 17, 1, 0, 0, 0, 182, 187, 3, 30, 15, 0, 183, 184, 5, 5, 0, 0, 184, 186, 3, 30, 15, 0, 185, 183, 1, 0, 0, 0, 186, 189, 1, 0, 0, 0, 187, 185, 1, 0, 0, 0, 187, 188, 1, 0, 0, 0, 188, 19, 1, 0, 0, 0, 189, 187, 1, 0, 0, 0, 190, 191, 7, 2, 0, 0, 191, 21, 1, 0, 0, 0, 192, 193, 7, 3, 0, 0, 193, 194, 5, 1, 0, 0, 194, 195, 3, 24, 12, 0, 195, 196, 5, 2, 0, 0, 196, 23, 1, 0, 0, 0, 197, 202, 3, 26, 13, 0, 198, 199, 5, 5, 0, 0, 199, 201, 3, 26, 13, 0, 200, 198, 1, 0, 0, 0, 201, 204, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 202, 203, 1, 0, 0, 0, 203, 25, 1, 0, 0, 0, 204, 202, 1, 0, 0, 0, 205, 209, 3, 32, 16, 0, 206, 209, 3, 30, 15, 0, 207, 209, 3, 28, 14, 0, 208, 205, 1, 0, 0, 0, 208, 206, 1, 0, 0, 0, 208, 207, 1, 0, 0, 0, 209, 27, 1, 0, 0, 0, 210, 211, 5, 3, 0, 0, 211, 212, 3, 18, 9, 0, 212, 213, 5, 4, 0, 0, 213, 29, 1, 0, 0, 0, 214, 215, 7, 4, 0, 0, 215, 31, 1, 0, 0, 0, 216, 217, 5, 29, 0, 0, 217, 33, 1, 0, 0, 0, 11, 44, 51, 53, 57, 70, 149, 163, 180, 187, 202, 208] \ No newline at end of file diff --git a/frontend/src/parser/FilterQuery.tokens b/frontend/src/parser/FilterQuery.tokens new file mode 100644 index 000000000000..4df881075f0a --- /dev/null +++ b/frontend/src/parser/FilterQuery.tokens @@ -0,0 +1,42 @@ +LPAREN=1 +RPAREN=2 +LBRACK=3 +RBRACK=4 +COMMA=5 +EQUALS=6 +NOT_EQUALS=7 +NEQ=8 +LT=9 +LE=10 +GT=11 +GE=12 +LIKE=13 +ILIKE=14 +BETWEEN=15 +EXISTS=16 +REGEXP=17 +CONTAINS=18 +IN=19 +NOT=20 +AND=21 +OR=22 +HAS=23 +HASANY=24 +HASALL=25 +BOOL=26 +NUMBER=27 +QUOTED_TEXT=28 +KEY=29 +WS=30 +FREETEXT=31 +'('=1 +')'=2 +'['=3 +']'=4 +','=5 +'!='=7 +'<>'=8 +'<'=9 +'<='=10 +'>'=11 +'>='=12 diff --git a/frontend/src/parser/FilterQueryLexer.interp b/frontend/src/parser/FilterQueryLexer.interp new file mode 100644 index 000000000000..3b149e9131c8 --- /dev/null +++ b/frontend/src/parser/FilterQueryLexer.interp @@ -0,0 +1,115 @@ +token literal names: +null +'(' +')' +'[' +']' +',' +null +'!=' +'<>' +'<' +'<=' +'>' +'>=' +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null + +token symbolic names: +null +LPAREN +RPAREN +LBRACK +RBRACK +COMMA +EQUALS +NOT_EQUALS +NEQ +LT +LE +GT +GE +LIKE +ILIKE +BETWEEN +EXISTS +REGEXP +CONTAINS +IN +NOT +AND +OR +HAS +HASANY +HASALL +BOOL +NUMBER +QUOTED_TEXT +KEY +WS +FREETEXT + +rule names: +LPAREN +RPAREN +LBRACK +RBRACK +COMMA +EQUALS +NOT_EQUALS +NEQ +LT +LE +GT +GE +LIKE +ILIKE +BETWEEN +EXISTS +REGEXP +CONTAINS +IN +NOT +AND +OR +HAS +HASANY +HASALL +BOOL +SIGN +NUMBER +QUOTED_TEXT +SEGMENT +EMPTY_BRACKS +OLD_JSON_BRACKS +KEY +WS +DIGIT +FREETEXT + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[4, 0, 31, 303, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 87, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 130, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 147, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 3, 25, 190, 8, 25, 1, 26, 1, 26, 1, 27, 3, 27, 195, 8, 27, 1, 27, 4, 27, 198, 8, 27, 11, 27, 12, 27, 199, 1, 27, 1, 27, 5, 27, 204, 8, 27, 10, 27, 12, 27, 207, 9, 27, 3, 27, 209, 8, 27, 1, 27, 1, 27, 3, 27, 213, 8, 27, 1, 27, 4, 27, 216, 8, 27, 11, 27, 12, 27, 217, 3, 27, 220, 8, 27, 1, 27, 3, 27, 223, 8, 27, 1, 27, 1, 27, 4, 27, 227, 8, 27, 11, 27, 12, 27, 228, 1, 27, 1, 27, 3, 27, 233, 8, 27, 1, 27, 4, 27, 236, 8, 27, 11, 27, 12, 27, 237, 3, 27, 240, 8, 27, 3, 27, 242, 8, 27, 1, 28, 1, 28, 1, 28, 1, 28, 5, 28, 248, 8, 28, 10, 28, 12, 28, 251, 9, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 5, 28, 258, 8, 28, 10, 28, 12, 28, 261, 9, 28, 1, 28, 3, 28, 264, 8, 28, 1, 29, 1, 29, 5, 29, 268, 8, 29, 10, 29, 12, 29, 271, 9, 29, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 5, 32, 285, 8, 32, 10, 32, 12, 32, 288, 9, 32, 1, 33, 4, 33, 291, 8, 33, 11, 33, 12, 33, 292, 1, 33, 1, 33, 1, 34, 1, 34, 1, 35, 4, 35, 300, 8, 35, 11, 35, 12, 35, 301, 0, 0, 36, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 0, 55, 27, 57, 28, 59, 0, 61, 0, 63, 0, 65, 29, 67, 30, 69, 0, 71, 31, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0, 36, 36, 65, 90, 95, 95, 97, 122, 6, 0, 36, 36, 45, 45, 47, 58, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 325, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 1, 73, 1, 0, 0, 0, 3, 75, 1, 0, 0, 0, 5, 77, 1, 0, 0, 0, 7, 79, 1, 0, 0, 0, 9, 81, 1, 0, 0, 0, 11, 86, 1, 0, 0, 0, 13, 88, 1, 0, 0, 0, 15, 91, 1, 0, 0, 0, 17, 94, 1, 0, 0, 0, 19, 96, 1, 0, 0, 0, 21, 99, 1, 0, 0, 0, 23, 101, 1, 0, 0, 0, 25, 104, 1, 0, 0, 0, 27, 109, 1, 0, 0, 0, 29, 115, 1, 0, 0, 0, 31, 123, 1, 0, 0, 0, 33, 131, 1, 0, 0, 0, 35, 138, 1, 0, 0, 0, 37, 148, 1, 0, 0, 0, 39, 151, 1, 0, 0, 0, 41, 155, 1, 0, 0, 0, 43, 159, 1, 0, 0, 0, 45, 162, 1, 0, 0, 0, 47, 166, 1, 0, 0, 0, 49, 173, 1, 0, 0, 0, 51, 189, 1, 0, 0, 0, 53, 191, 1, 0, 0, 0, 55, 241, 1, 0, 0, 0, 57, 263, 1, 0, 0, 0, 59, 265, 1, 0, 0, 0, 61, 272, 1, 0, 0, 0, 63, 275, 1, 0, 0, 0, 65, 279, 1, 0, 0, 0, 67, 290, 1, 0, 0, 0, 69, 296, 1, 0, 0, 0, 71, 299, 1, 0, 0, 0, 73, 74, 5, 40, 0, 0, 74, 2, 1, 0, 0, 0, 75, 76, 5, 41, 0, 0, 76, 4, 1, 0, 0, 0, 77, 78, 5, 91, 0, 0, 78, 6, 1, 0, 0, 0, 79, 80, 5, 93, 0, 0, 80, 8, 1, 0, 0, 0, 81, 82, 5, 44, 0, 0, 82, 10, 1, 0, 0, 0, 83, 87, 5, 61, 0, 0, 84, 85, 5, 61, 0, 0, 85, 87, 5, 61, 0, 0, 86, 83, 1, 0, 0, 0, 86, 84, 1, 0, 0, 0, 87, 12, 1, 0, 0, 0, 88, 89, 5, 33, 0, 0, 89, 90, 5, 61, 0, 0, 90, 14, 1, 0, 0, 0, 91, 92, 5, 60, 0, 0, 92, 93, 5, 62, 0, 0, 93, 16, 1, 0, 0, 0, 94, 95, 5, 60, 0, 0, 95, 18, 1, 0, 0, 0, 96, 97, 5, 60, 0, 0, 97, 98, 5, 61, 0, 0, 98, 20, 1, 0, 0, 0, 99, 100, 5, 62, 0, 0, 100, 22, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 103, 5, 61, 0, 0, 103, 24, 1, 0, 0, 0, 104, 105, 7, 0, 0, 0, 105, 106, 7, 1, 0, 0, 106, 107, 7, 2, 0, 0, 107, 108, 7, 3, 0, 0, 108, 26, 1, 0, 0, 0, 109, 110, 7, 1, 0, 0, 110, 111, 7, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 2, 0, 0, 113, 114, 7, 3, 0, 0, 114, 28, 1, 0, 0, 0, 115, 116, 7, 4, 0, 0, 116, 117, 7, 3, 0, 0, 117, 118, 7, 5, 0, 0, 118, 119, 7, 6, 0, 0, 119, 120, 7, 3, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 7, 0, 0, 122, 30, 1, 0, 0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 8, 0, 0, 125, 126, 7, 1, 0, 0, 126, 127, 7, 9, 0, 0, 127, 129, 7, 5, 0, 0, 128, 130, 7, 9, 0, 0, 129, 128, 1, 0, 0, 0, 129, 130, 1, 0, 0, 0, 130, 32, 1, 0, 0, 0, 131, 132, 7, 10, 0, 0, 132, 133, 7, 3, 0, 0, 133, 134, 7, 11, 0, 0, 134, 135, 7, 3, 0, 0, 135, 136, 7, 8, 0, 0, 136, 137, 7, 12, 0, 0, 137, 34, 1, 0, 0, 0, 138, 139, 7, 13, 0, 0, 139, 140, 7, 14, 0, 0, 140, 141, 7, 7, 0, 0, 141, 142, 7, 5, 0, 0, 142, 143, 7, 15, 0, 0, 143, 144, 7, 1, 0, 0, 144, 146, 7, 7, 0, 0, 145, 147, 7, 9, 0, 0, 146, 145, 1, 0, 0, 0, 146, 147, 1, 0, 0, 0, 147, 36, 1, 0, 0, 0, 148, 149, 7, 1, 0, 0, 149, 150, 7, 7, 0, 0, 150, 38, 1, 0, 0, 0, 151, 152, 7, 7, 0, 0, 152, 153, 7, 14, 0, 0, 153, 154, 7, 5, 0, 0, 154, 40, 1, 0, 0, 0, 155, 156, 7, 15, 0, 0, 156, 157, 7, 7, 0, 0, 157, 158, 7, 16, 0, 0, 158, 42, 1, 0, 0, 0, 159, 160, 7, 14, 0, 0, 160, 161, 7, 10, 0, 0, 161, 44, 1, 0, 0, 0, 162, 163, 7, 17, 0, 0, 163, 164, 7, 15, 0, 0, 164, 165, 7, 9, 0, 0, 165, 46, 1, 0, 0, 0, 166, 167, 7, 17, 0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 15, 0, 0, 170, 171, 7, 7, 0, 0, 171, 172, 7, 18, 0, 0, 172, 48, 1, 0, 0, 0, 173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176, 7, 9, 0, 0, 176, 177, 7, 15, 0, 0, 177, 178, 7, 0, 0, 0, 178, 179, 7, 0, 0, 0, 179, 50, 1, 0, 0, 0, 180, 181, 7, 5, 0, 0, 181, 182, 7, 10, 0, 0, 182, 183, 7, 19, 0, 0, 183, 190, 7, 3, 0, 0, 184, 185, 7, 20, 0, 0, 185, 186, 7, 15, 0, 0, 186, 187, 7, 0, 0, 0, 187, 188, 7, 9, 0, 0, 188, 190, 7, 3, 0, 0, 189, 180, 1, 0, 0, 0, 189, 184, 1, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 21, 0, 0, 192, 54, 1, 0, 0, 0, 193, 195, 3, 53, 26, 0, 194, 193, 1, 0, 0, 0, 194, 195, 1, 0, 0, 0, 195, 197, 1, 0, 0, 0, 196, 198, 3, 69, 34, 0, 197, 196, 1, 0, 0, 0, 198, 199, 1, 0, 0, 0, 199, 197, 1, 0, 0, 0, 199, 200, 1, 0, 0, 0, 200, 208, 1, 0, 0, 0, 201, 205, 5, 46, 0, 0, 202, 204, 3, 69, 34, 0, 203, 202, 1, 0, 0, 0, 204, 207, 1, 0, 0, 0, 205, 203, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 209, 1, 0, 0, 0, 207, 205, 1, 0, 0, 0, 208, 201, 1, 0, 0, 0, 208, 209, 1, 0, 0, 0, 209, 219, 1, 0, 0, 0, 210, 212, 7, 3, 0, 0, 211, 213, 3, 53, 26, 0, 212, 211, 1, 0, 0, 0, 212, 213, 1, 0, 0, 0, 213, 215, 1, 0, 0, 0, 214, 216, 3, 69, 34, 0, 215, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 215, 1, 0, 0, 0, 217, 218, 1, 0, 0, 0, 218, 220, 1, 0, 0, 0, 219, 210, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0, 220, 242, 1, 0, 0, 0, 221, 223, 3, 53, 26, 0, 222, 221, 1, 0, 0, 0, 222, 223, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226, 5, 46, 0, 0, 225, 227, 3, 69, 34, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 226, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0, 230, 232, 7, 3, 0, 0, 231, 233, 3, 53, 26, 0, 232, 231, 1, 0, 0, 0, 232, 233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 69, 34, 0, 235, 234, 1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0, 0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 242, 1, 0, 0, 0, 241, 194, 1, 0, 0, 0, 241, 222, 1, 0, 0, 0, 242, 56, 1, 0, 0, 0, 243, 249, 5, 34, 0, 0, 244, 248, 8, 22, 0, 0, 245, 246, 5, 92, 0, 0, 246, 248, 9, 0, 0, 0, 247, 244, 1, 0, 0, 0, 247, 245, 1, 0, 0, 0, 248, 251, 1, 0, 0, 0, 249, 247, 1, 0, 0, 0, 249, 250, 1, 0, 0, 0, 250, 252, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 252, 264, 5, 34, 0, 0, 253, 259, 5, 39, 0, 0, 254, 258, 8, 23, 0, 0, 255, 256, 5, 92, 0, 0, 256, 258, 9, 0, 0, 0, 257, 254, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 258, 261, 1, 0, 0, 0, 259, 257, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0, 0, 0, 261, 259, 1, 0, 0, 0, 262, 264, 5, 39, 0, 0, 263, 243, 1, 0, 0, 0, 263, 253, 1, 0, 0, 0, 264, 58, 1, 0, 0, 0, 265, 269, 7, 24, 0, 0, 266, 268, 7, 25, 0, 0, 267, 266, 1, 0, 0, 0, 268, 271, 1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 60, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 272, 273, 5, 91, 0, 0, 273, 274, 5, 93, 0, 0, 274, 62, 1, 0, 0, 0, 275, 276, 5, 91, 0, 0, 276, 277, 5, 42, 0, 0, 277, 278, 5, 93, 0, 0, 278, 64, 1, 0, 0, 0, 279, 286, 3, 59, 29, 0, 280, 281, 5, 46, 0, 0, 281, 285, 3, 59, 29, 0, 282, 285, 3, 61, 30, 0, 283, 285, 3, 63, 31, 0, 284, 280, 1, 0, 0, 0, 284, 282, 1, 0, 0, 0, 284, 283, 1, 0, 0, 0, 285, 288, 1, 0, 0, 0, 286, 284, 1, 0, 0, 0, 286, 287, 1, 0, 0, 0, 287, 66, 1, 0, 0, 0, 288, 286, 1, 0, 0, 0, 289, 291, 7, 26, 0, 0, 290, 289, 1, 0, 0, 0, 291, 292, 1, 0, 0, 0, 292, 290, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 6, 33, 0, 0, 295, 68, 1, 0, 0, 0, 296, 297, 7, 27, 0, 0, 297, 70, 1, 0, 0, 0, 298, 300, 8, 28, 0, 0, 299, 298, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 299, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 72, 1, 0, 0, 0, 28, 0, 86, 129, 146, 189, 194, 199, 205, 208, 212, 217, 219, 222, 228, 232, 237, 239, 241, 247, 249, 257, 259, 263, 269, 284, 286, 292, 301, 1, 6, 0, 0] \ No newline at end of file diff --git a/frontend/src/parser/FilterQueryLexer.tokens b/frontend/src/parser/FilterQueryLexer.tokens new file mode 100644 index 000000000000..4df881075f0a --- /dev/null +++ b/frontend/src/parser/FilterQueryLexer.tokens @@ -0,0 +1,42 @@ +LPAREN=1 +RPAREN=2 +LBRACK=3 +RBRACK=4 +COMMA=5 +EQUALS=6 +NOT_EQUALS=7 +NEQ=8 +LT=9 +LE=10 +GT=11 +GE=12 +LIKE=13 +ILIKE=14 +BETWEEN=15 +EXISTS=16 +REGEXP=17 +CONTAINS=18 +IN=19 +NOT=20 +AND=21 +OR=22 +HAS=23 +HASANY=24 +HASALL=25 +BOOL=26 +NUMBER=27 +QUOTED_TEXT=28 +KEY=29 +WS=30 +FREETEXT=31 +'('=1 +')'=2 +'['=3 +']'=4 +','=5 +'!='=7 +'<>'=8 +'<'=9 +'<='=10 +'>'=11 +'>='=12 diff --git a/frontend/src/parser/FilterQueryLexer.ts b/frontend/src/parser/FilterQueryLexer.ts new file mode 100644 index 000000000000..ce26b2ff7ced --- /dev/null +++ b/frontend/src/parser/FilterQueryLexer.ts @@ -0,0 +1,220 @@ +// Generated from FilterQuery.g4 by ANTLR 4.13.1 +// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols +import { + ATN, + ATNDeserializer, + CharStream, + DecisionState, DFA, + Lexer, + LexerATNSimulator, + RuleContext, + PredictionContextCache, + Token +} from "antlr4"; +export default class FilterQueryLexer extends Lexer { + public static readonly LPAREN = 1; + public static readonly RPAREN = 2; + public static readonly LBRACK = 3; + public static readonly RBRACK = 4; + public static readonly COMMA = 5; + public static readonly EQUALS = 6; + public static readonly NOT_EQUALS = 7; + public static readonly NEQ = 8; + public static readonly LT = 9; + public static readonly LE = 10; + public static readonly GT = 11; + public static readonly GE = 12; + public static readonly LIKE = 13; + public static readonly ILIKE = 14; + public static readonly BETWEEN = 15; + public static readonly EXISTS = 16; + public static readonly REGEXP = 17; + public static readonly CONTAINS = 18; + public static readonly IN = 19; + public static readonly NOT = 20; + public static readonly AND = 21; + public static readonly OR = 22; + public static readonly HAS = 23; + public static readonly HASANY = 24; + public static readonly HASALL = 25; + public static readonly BOOL = 26; + public static readonly NUMBER = 27; + public static readonly QUOTED_TEXT = 28; + public static readonly KEY = 29; + public static readonly WS = 30; + public static readonly FREETEXT = 31; + public static readonly EOF = Token.EOF; + + public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ]; + public static readonly literalNames: (string | null)[] = [ null, "'('", + "')'", "'['", + "']'", "','", + null, "'!='", + "'<>'", "'<'", + "'<='", "'>'", + "'>='" ]; + public static readonly symbolicNames: (string | null)[] = [ null, "LPAREN", + "RPAREN", "LBRACK", + "RBRACK", "COMMA", + "EQUALS", "NOT_EQUALS", + "NEQ", "LT", + "LE", "GT", + "GE", "LIKE", + "ILIKE", "BETWEEN", + "EXISTS", "REGEXP", + "CONTAINS", + "IN", "NOT", + "AND", "OR", + "HAS", "HASANY", + "HASALL", "BOOL", + "NUMBER", "QUOTED_TEXT", + "KEY", "WS", + "FREETEXT" ]; + public static readonly modeNames: string[] = [ "DEFAULT_MODE", ]; + + public static readonly ruleNames: string[] = [ + "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS", + "NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS", "REGEXP", + "CONTAINS", "IN", "NOT", "AND", "OR", "HAS", "HASANY", "HASALL", "BOOL", + "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS", + "KEY", "WS", "DIGIT", "FREETEXT", + ]; + + + constructor(input: CharStream) { + super(input); + this._interp = new LexerATNSimulator(this, FilterQueryLexer._ATN, FilterQueryLexer.DecisionsToDFA, new PredictionContextCache()); + } + + public get grammarFileName(): string { return "FilterQuery.g4"; } + + public get literalNames(): (string | null)[] { return FilterQueryLexer.literalNames; } + public get symbolicNames(): (string | null)[] { return FilterQueryLexer.symbolicNames; } + public get ruleNames(): string[] { return FilterQueryLexer.ruleNames; } + + public get serializedATN(): number[] { return FilterQueryLexer._serializedATN; } + + public get channelNames(): string[] { return FilterQueryLexer.channelNames; } + + public get modeNames(): string[] { return FilterQueryLexer.modeNames; } + + public static readonly _serializedATN: number[] = [4,0,31,303,6,-1,2,0, + 7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9, + 7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7, + 16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23, + 2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2, + 31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,1,0,1,0,1,1,1,1,1,2,1,2, + 1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,87,8,5,1,6,1,6,1,6,1,7,1,7,1,7,1,8,1,8, + 1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,13,1,13, + 1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1, + 15,1,15,1,15,1,15,3,15,130,8,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,17, + 1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,147,8,17,1,18,1,18,1,18,1,19,1, + 19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,23, + 1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,25,1, + 25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,3,25,190,8,25,1,26,1,26,1,27,3,27, + 195,8,27,1,27,4,27,198,8,27,11,27,12,27,199,1,27,1,27,5,27,204,8,27,10, + 27,12,27,207,9,27,3,27,209,8,27,1,27,1,27,3,27,213,8,27,1,27,4,27,216,8, + 27,11,27,12,27,217,3,27,220,8,27,1,27,3,27,223,8,27,1,27,1,27,4,27,227, + 8,27,11,27,12,27,228,1,27,1,27,3,27,233,8,27,1,27,4,27,236,8,27,11,27,12, + 27,237,3,27,240,8,27,3,27,242,8,27,1,28,1,28,1,28,1,28,5,28,248,8,28,10, + 28,12,28,251,9,28,1,28,1,28,1,28,1,28,1,28,5,28,258,8,28,10,28,12,28,261, + 9,28,1,28,3,28,264,8,28,1,29,1,29,5,29,268,8,29,10,29,12,29,271,9,29,1, + 30,1,30,1,30,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,5,32,285,8,32, + 10,32,12,32,288,9,32,1,33,4,33,291,8,33,11,33,12,33,292,1,33,1,33,1,34, + 1,34,1,35,4,35,300,8,35,11,35,12,35,301,0,0,36,1,1,3,2,5,3,7,4,9,5,11,6, + 13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37, + 19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,0,55,27,57,28,59,0,61,0, + 63,0,65,29,67,30,69,0,71,31,1,0,29,2,0,76,76,108,108,2,0,73,73,105,105, + 2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,2, + 0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,2, + 0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,0, + 79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,89, + 89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,34, + 92,92,2,0,39,39,92,92,4,0,36,36,65,90,95,95,97,122,6,0,36,36,45,45,47,58, + 65,90,95,95,97,122,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,13,32,34, + 39,41,44,44,60,62,91,91,93,93,325,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0, + 7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0, + 0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29, + 1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0, + 0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51, + 1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,71,1,0,0, + 0,1,73,1,0,0,0,3,75,1,0,0,0,5,77,1,0,0,0,7,79,1,0,0,0,9,81,1,0,0,0,11,86, + 1,0,0,0,13,88,1,0,0,0,15,91,1,0,0,0,17,94,1,0,0,0,19,96,1,0,0,0,21,99,1, + 0,0,0,23,101,1,0,0,0,25,104,1,0,0,0,27,109,1,0,0,0,29,115,1,0,0,0,31,123, + 1,0,0,0,33,131,1,0,0,0,35,138,1,0,0,0,37,148,1,0,0,0,39,151,1,0,0,0,41, + 155,1,0,0,0,43,159,1,0,0,0,45,162,1,0,0,0,47,166,1,0,0,0,49,173,1,0,0,0, + 51,189,1,0,0,0,53,191,1,0,0,0,55,241,1,0,0,0,57,263,1,0,0,0,59,265,1,0, + 0,0,61,272,1,0,0,0,63,275,1,0,0,0,65,279,1,0,0,0,67,290,1,0,0,0,69,296, + 1,0,0,0,71,299,1,0,0,0,73,74,5,40,0,0,74,2,1,0,0,0,75,76,5,41,0,0,76,4, + 1,0,0,0,77,78,5,91,0,0,78,6,1,0,0,0,79,80,5,93,0,0,80,8,1,0,0,0,81,82,5, + 44,0,0,82,10,1,0,0,0,83,87,5,61,0,0,84,85,5,61,0,0,85,87,5,61,0,0,86,83, + 1,0,0,0,86,84,1,0,0,0,87,12,1,0,0,0,88,89,5,33,0,0,89,90,5,61,0,0,90,14, + 1,0,0,0,91,92,5,60,0,0,92,93,5,62,0,0,93,16,1,0,0,0,94,95,5,60,0,0,95,18, + 1,0,0,0,96,97,5,60,0,0,97,98,5,61,0,0,98,20,1,0,0,0,99,100,5,62,0,0,100, + 22,1,0,0,0,101,102,5,62,0,0,102,103,5,61,0,0,103,24,1,0,0,0,104,105,7,0, + 0,0,105,106,7,1,0,0,106,107,7,2,0,0,107,108,7,3,0,0,108,26,1,0,0,0,109, + 110,7,1,0,0,110,111,7,0,0,0,111,112,7,1,0,0,112,113,7,2,0,0,113,114,7,3, + 0,0,114,28,1,0,0,0,115,116,7,4,0,0,116,117,7,3,0,0,117,118,7,5,0,0,118, + 119,7,6,0,0,119,120,7,3,0,0,120,121,7,3,0,0,121,122,7,7,0,0,122,30,1,0, + 0,0,123,124,7,3,0,0,124,125,7,8,0,0,125,126,7,1,0,0,126,127,7,9,0,0,127, + 129,7,5,0,0,128,130,7,9,0,0,129,128,1,0,0,0,129,130,1,0,0,0,130,32,1,0, + 0,0,131,132,7,10,0,0,132,133,7,3,0,0,133,134,7,11,0,0,134,135,7,3,0,0,135, + 136,7,8,0,0,136,137,7,12,0,0,137,34,1,0,0,0,138,139,7,13,0,0,139,140,7, + 14,0,0,140,141,7,7,0,0,141,142,7,5,0,0,142,143,7,15,0,0,143,144,7,1,0,0, + 144,146,7,7,0,0,145,147,7,9,0,0,146,145,1,0,0,0,146,147,1,0,0,0,147,36, + 1,0,0,0,148,149,7,1,0,0,149,150,7,7,0,0,150,38,1,0,0,0,151,152,7,7,0,0, + 152,153,7,14,0,0,153,154,7,5,0,0,154,40,1,0,0,0,155,156,7,15,0,0,156,157, + 7,7,0,0,157,158,7,16,0,0,158,42,1,0,0,0,159,160,7,14,0,0,160,161,7,10,0, + 0,161,44,1,0,0,0,162,163,7,17,0,0,163,164,7,15,0,0,164,165,7,9,0,0,165, + 46,1,0,0,0,166,167,7,17,0,0,167,168,7,15,0,0,168,169,7,9,0,0,169,170,7, + 15,0,0,170,171,7,7,0,0,171,172,7,18,0,0,172,48,1,0,0,0,173,174,7,17,0,0, + 174,175,7,15,0,0,175,176,7,9,0,0,176,177,7,15,0,0,177,178,7,0,0,0,178,179, + 7,0,0,0,179,50,1,0,0,0,180,181,7,5,0,0,181,182,7,10,0,0,182,183,7,19,0, + 0,183,190,7,3,0,0,184,185,7,20,0,0,185,186,7,15,0,0,186,187,7,0,0,0,187, + 188,7,9,0,0,188,190,7,3,0,0,189,180,1,0,0,0,189,184,1,0,0,0,190,52,1,0, + 0,0,191,192,7,21,0,0,192,54,1,0,0,0,193,195,3,53,26,0,194,193,1,0,0,0,194, + 195,1,0,0,0,195,197,1,0,0,0,196,198,3,69,34,0,197,196,1,0,0,0,198,199,1, + 0,0,0,199,197,1,0,0,0,199,200,1,0,0,0,200,208,1,0,0,0,201,205,5,46,0,0, + 202,204,3,69,34,0,203,202,1,0,0,0,204,207,1,0,0,0,205,203,1,0,0,0,205,206, + 1,0,0,0,206,209,1,0,0,0,207,205,1,0,0,0,208,201,1,0,0,0,208,209,1,0,0,0, + 209,219,1,0,0,0,210,212,7,3,0,0,211,213,3,53,26,0,212,211,1,0,0,0,212,213, + 1,0,0,0,213,215,1,0,0,0,214,216,3,69,34,0,215,214,1,0,0,0,216,217,1,0,0, + 0,217,215,1,0,0,0,217,218,1,0,0,0,218,220,1,0,0,0,219,210,1,0,0,0,219,220, + 1,0,0,0,220,242,1,0,0,0,221,223,3,53,26,0,222,221,1,0,0,0,222,223,1,0,0, + 0,223,224,1,0,0,0,224,226,5,46,0,0,225,227,3,69,34,0,226,225,1,0,0,0,227, + 228,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,239,1,0,0,0,230,232,7,3, + 0,0,231,233,3,53,26,0,232,231,1,0,0,0,232,233,1,0,0,0,233,235,1,0,0,0,234, + 236,3,69,34,0,235,234,1,0,0,0,236,237,1,0,0,0,237,235,1,0,0,0,237,238,1, + 0,0,0,238,240,1,0,0,0,239,230,1,0,0,0,239,240,1,0,0,0,240,242,1,0,0,0,241, + 194,1,0,0,0,241,222,1,0,0,0,242,56,1,0,0,0,243,249,5,34,0,0,244,248,8,22, + 0,0,245,246,5,92,0,0,246,248,9,0,0,0,247,244,1,0,0,0,247,245,1,0,0,0,248, + 251,1,0,0,0,249,247,1,0,0,0,249,250,1,0,0,0,250,252,1,0,0,0,251,249,1,0, + 0,0,252,264,5,34,0,0,253,259,5,39,0,0,254,258,8,23,0,0,255,256,5,92,0,0, + 256,258,9,0,0,0,257,254,1,0,0,0,257,255,1,0,0,0,258,261,1,0,0,0,259,257, + 1,0,0,0,259,260,1,0,0,0,260,262,1,0,0,0,261,259,1,0,0,0,262,264,5,39,0, + 0,263,243,1,0,0,0,263,253,1,0,0,0,264,58,1,0,0,0,265,269,7,24,0,0,266,268, + 7,25,0,0,267,266,1,0,0,0,268,271,1,0,0,0,269,267,1,0,0,0,269,270,1,0,0, + 0,270,60,1,0,0,0,271,269,1,0,0,0,272,273,5,91,0,0,273,274,5,93,0,0,274, + 62,1,0,0,0,275,276,5,91,0,0,276,277,5,42,0,0,277,278,5,93,0,0,278,64,1, + 0,0,0,279,286,3,59,29,0,280,281,5,46,0,0,281,285,3,59,29,0,282,285,3,61, + 30,0,283,285,3,63,31,0,284,280,1,0,0,0,284,282,1,0,0,0,284,283,1,0,0,0, + 285,288,1,0,0,0,286,284,1,0,0,0,286,287,1,0,0,0,287,66,1,0,0,0,288,286, + 1,0,0,0,289,291,7,26,0,0,290,289,1,0,0,0,291,292,1,0,0,0,292,290,1,0,0, + 0,292,293,1,0,0,0,293,294,1,0,0,0,294,295,6,33,0,0,295,68,1,0,0,0,296,297, + 7,27,0,0,297,70,1,0,0,0,298,300,8,28,0,0,299,298,1,0,0,0,300,301,1,0,0, + 0,301,299,1,0,0,0,301,302,1,0,0,0,302,72,1,0,0,0,28,0,86,129,146,189,194, + 199,205,208,212,217,219,222,228,232,237,239,241,247,249,257,259,263,269, + 284,286,292,301,1,6,0,0]; + + private static __ATN: ATN; + public static get _ATN(): ATN { + if (!FilterQueryLexer.__ATN) { + FilterQueryLexer.__ATN = new ATNDeserializer().deserialize(FilterQueryLexer._serializedATN); + } + + return FilterQueryLexer.__ATN; + } + + + static DecisionsToDFA = FilterQueryLexer._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) ); +} \ No newline at end of file diff --git a/frontend/src/parser/FilterQueryListener.ts b/frontend/src/parser/FilterQueryListener.ts new file mode 100644 index 000000000000..8fd9bf9c28f8 --- /dev/null +++ b/frontend/src/parser/FilterQueryListener.ts @@ -0,0 +1,201 @@ +// Generated from FilterQuery.g4 by ANTLR 4.13.1 + +import {ParseTreeListener} from "antlr4"; + + +import { QueryContext } from "./FilterQueryParser"; +import { ExpressionContext } from "./FilterQueryParser"; +import { OrExpressionContext } from "./FilterQueryParser"; +import { AndExpressionContext } from "./FilterQueryParser"; +import { UnaryExpressionContext } from "./FilterQueryParser"; +import { PrimaryContext } from "./FilterQueryParser"; +import { ComparisonContext } from "./FilterQueryParser"; +import { InClauseContext } from "./FilterQueryParser"; +import { NotInClauseContext } from "./FilterQueryParser"; +import { ValueListContext } from "./FilterQueryParser"; +import { FullTextContext } from "./FilterQueryParser"; +import { FunctionCallContext } from "./FilterQueryParser"; +import { FunctionParamListContext } from "./FilterQueryParser"; +import { FunctionParamContext } from "./FilterQueryParser"; +import { ArrayContext } from "./FilterQueryParser"; +import { ValueContext } from "./FilterQueryParser"; +import { KeyContext } from "./FilterQueryParser"; + + +/** + * This interface defines a complete listener for a parse tree produced by + * `FilterQueryParser`. + */ +export default class FilterQueryListener extends ParseTreeListener { + /** + * Enter a parse tree produced by `FilterQueryParser.query`. + * @param ctx the parse tree + */ + enterQuery?: (ctx: QueryContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.query`. + * @param ctx the parse tree + */ + exitQuery?: (ctx: QueryContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.expression`. + * @param ctx the parse tree + */ + enterExpression?: (ctx: ExpressionContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.expression`. + * @param ctx the parse tree + */ + exitExpression?: (ctx: ExpressionContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.orExpression`. + * @param ctx the parse tree + */ + enterOrExpression?: (ctx: OrExpressionContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.orExpression`. + * @param ctx the parse tree + */ + exitOrExpression?: (ctx: OrExpressionContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.andExpression`. + * @param ctx the parse tree + */ + enterAndExpression?: (ctx: AndExpressionContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.andExpression`. + * @param ctx the parse tree + */ + exitAndExpression?: (ctx: AndExpressionContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.unaryExpression`. + * @param ctx the parse tree + */ + enterUnaryExpression?: (ctx: UnaryExpressionContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.unaryExpression`. + * @param ctx the parse tree + */ + exitUnaryExpression?: (ctx: UnaryExpressionContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.primary`. + * @param ctx the parse tree + */ + enterPrimary?: (ctx: PrimaryContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.primary`. + * @param ctx the parse tree + */ + exitPrimary?: (ctx: PrimaryContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.comparison`. + * @param ctx the parse tree + */ + enterComparison?: (ctx: ComparisonContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.comparison`. + * @param ctx the parse tree + */ + exitComparison?: (ctx: ComparisonContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.inClause`. + * @param ctx the parse tree + */ + enterInClause?: (ctx: InClauseContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.inClause`. + * @param ctx the parse tree + */ + exitInClause?: (ctx: InClauseContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.notInClause`. + * @param ctx the parse tree + */ + enterNotInClause?: (ctx: NotInClauseContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.notInClause`. + * @param ctx the parse tree + */ + exitNotInClause?: (ctx: NotInClauseContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.valueList`. + * @param ctx the parse tree + */ + enterValueList?: (ctx: ValueListContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.valueList`. + * @param ctx the parse tree + */ + exitValueList?: (ctx: ValueListContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.fullText`. + * @param ctx the parse tree + */ + enterFullText?: (ctx: FullTextContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.fullText`. + * @param ctx the parse tree + */ + exitFullText?: (ctx: FullTextContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.functionCall`. + * @param ctx the parse tree + */ + enterFunctionCall?: (ctx: FunctionCallContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.functionCall`. + * @param ctx the parse tree + */ + exitFunctionCall?: (ctx: FunctionCallContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.functionParamList`. + * @param ctx the parse tree + */ + enterFunctionParamList?: (ctx: FunctionParamListContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.functionParamList`. + * @param ctx the parse tree + */ + exitFunctionParamList?: (ctx: FunctionParamListContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.functionParam`. + * @param ctx the parse tree + */ + enterFunctionParam?: (ctx: FunctionParamContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.functionParam`. + * @param ctx the parse tree + */ + exitFunctionParam?: (ctx: FunctionParamContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.array`. + * @param ctx the parse tree + */ + enterArray?: (ctx: ArrayContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.array`. + * @param ctx the parse tree + */ + exitArray?: (ctx: ArrayContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.value`. + * @param ctx the parse tree + */ + enterValue?: (ctx: ValueContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.value`. + * @param ctx the parse tree + */ + exitValue?: (ctx: ValueContext) => void; + /** + * Enter a parse tree produced by `FilterQueryParser.key`. + * @param ctx the parse tree + */ + enterKey?: (ctx: KeyContext) => void; + /** + * Exit a parse tree produced by `FilterQueryParser.key`. + * @param ctx the parse tree + */ + exitKey?: (ctx: KeyContext) => void; +} + diff --git a/frontend/src/parser/FilterQueryParser.ts b/frontend/src/parser/FilterQueryParser.ts new file mode 100644 index 000000000000..70d9142bb768 --- /dev/null +++ b/frontend/src/parser/FilterQueryParser.ts @@ -0,0 +1,1870 @@ +// Generated from FilterQuery.g4 by ANTLR 4.13.1 +// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols + +import { + ATN, + ATNDeserializer, DecisionState, DFA, FailedPredicateException, + RecognitionException, NoViableAltException, BailErrorStrategy, + Parser, ParserATNSimulator, + RuleContext, ParserRuleContext, PredictionMode, PredictionContextCache, + TerminalNode, RuleNode, + Token, TokenStream, + Interval, IntervalSet +} from 'antlr4'; +import FilterQueryListener from "./FilterQueryListener.js"; +import FilterQueryVisitor from "./FilterQueryVisitor.js"; + +// for running tests with parameters, TODO: discuss strategy for typed parameters in CI +// eslint-disable-next-line no-unused-vars +type int = number; + +export default class FilterQueryParser extends Parser { + public static readonly LPAREN = 1; + public static readonly RPAREN = 2; + public static readonly LBRACK = 3; + public static readonly RBRACK = 4; + public static readonly COMMA = 5; + public static readonly EQUALS = 6; + public static readonly NOT_EQUALS = 7; + public static readonly NEQ = 8; + public static readonly LT = 9; + public static readonly LE = 10; + public static readonly GT = 11; + public static readonly GE = 12; + public static readonly LIKE = 13; + public static readonly ILIKE = 14; + public static readonly BETWEEN = 15; + public static readonly EXISTS = 16; + public static readonly REGEXP = 17; + public static readonly CONTAINS = 18; + public static readonly IN = 19; + public static readonly NOT = 20; + public static readonly AND = 21; + public static readonly OR = 22; + public static readonly HAS = 23; + public static readonly HASANY = 24; + public static readonly HASALL = 25; + public static readonly BOOL = 26; + public static readonly NUMBER = 27; + public static readonly QUOTED_TEXT = 28; + public static readonly KEY = 29; + public static readonly WS = 30; + public static readonly FREETEXT = 31; + public static readonly EOF = Token.EOF; + public static readonly RULE_query = 0; + public static readonly RULE_expression = 1; + public static readonly RULE_orExpression = 2; + public static readonly RULE_andExpression = 3; + public static readonly RULE_unaryExpression = 4; + public static readonly RULE_primary = 5; + public static readonly RULE_comparison = 6; + public static readonly RULE_inClause = 7; + public static readonly RULE_notInClause = 8; + public static readonly RULE_valueList = 9; + public static readonly RULE_fullText = 10; + public static readonly RULE_functionCall = 11; + public static readonly RULE_functionParamList = 12; + public static readonly RULE_functionParam = 13; + public static readonly RULE_array = 14; + public static readonly RULE_value = 15; + public static readonly RULE_key = 16; + public static readonly literalNames: (string | null)[] = [ null, "'('", + "')'", "'['", + "']'", "','", + null, "'!='", + "'<>'", "'<'", + "'<='", "'>'", + "'>='" ]; + public static readonly symbolicNames: (string | null)[] = [ null, "LPAREN", + "RPAREN", "LBRACK", + "RBRACK", "COMMA", + "EQUALS", "NOT_EQUALS", + "NEQ", "LT", + "LE", "GT", + "GE", "LIKE", + "ILIKE", "BETWEEN", + "EXISTS", "REGEXP", + "CONTAINS", + "IN", "NOT", + "AND", "OR", + "HAS", "HASANY", + "HASALL", "BOOL", + "NUMBER", "QUOTED_TEXT", + "KEY", "WS", + "FREETEXT" ]; + // tslint:disable:no-trailing-whitespace + public static readonly ruleNames: string[] = [ + "query", "expression", "orExpression", "andExpression", "unaryExpression", + "primary", "comparison", "inClause", "notInClause", "valueList", "fullText", + "functionCall", "functionParamList", "functionParam", "array", "value", + "key", + ]; + public get grammarFileName(): string { return "FilterQuery.g4"; } + public get literalNames(): (string | null)[] { return FilterQueryParser.literalNames; } + public get symbolicNames(): (string | null)[] { return FilterQueryParser.symbolicNames; } + public get ruleNames(): string[] { return FilterQueryParser.ruleNames; } + public get serializedATN(): number[] { return FilterQueryParser._serializedATN; } + + protected createFailedPredicateException(predicate?: string, message?: string): FailedPredicateException { + return new FailedPredicateException(this, predicate, message); + } + + constructor(input: TokenStream) { + super(input); + this._interp = new ParserATNSimulator(this, FilterQueryParser._ATN, FilterQueryParser.DecisionsToDFA, new PredictionContextCache()); + } + // @RuleVersion(0) + public query(): QueryContext { + let localctx: QueryContext = new QueryContext(this, this._ctx, this.state); + this.enterRule(localctx, 0, FilterQueryParser.RULE_query); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 34; + this.expression(); + this.state = 35; + this.match(FilterQueryParser.EOF); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public expression(): ExpressionContext { + let localctx: ExpressionContext = new ExpressionContext(this, this._ctx, this.state); + this.enterRule(localctx, 2, FilterQueryParser.RULE_expression); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 37; + this.orExpression(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public orExpression(): OrExpressionContext { + let localctx: OrExpressionContext = new OrExpressionContext(this, this._ctx, this.state); + this.enterRule(localctx, 4, FilterQueryParser.RULE_orExpression); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 39; + this.andExpression(); + this.state = 44; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la===22) { + { + { + this.state = 40; + this.match(FilterQueryParser.OR); + this.state = 41; + this.andExpression(); + } + } + this.state = 46; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public andExpression(): AndExpressionContext { + let localctx: AndExpressionContext = new AndExpressionContext(this, this._ctx, this.state); + this.enterRule(localctx, 6, FilterQueryParser.RULE_andExpression); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 47; + this.unaryExpression(); + this.state = 53; + this._errHandler.sync(this); + _la = this._input.LA(1); + while ((((_la) & ~0x1F) === 0 && ((1 << _la) & 3215982594) !== 0)) { + { + this.state = 51; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case 21: + { + this.state = 48; + this.match(FilterQueryParser.AND); + this.state = 49; + this.unaryExpression(); + } + break; + case 1: + case 20: + case 23: + case 24: + case 25: + case 26: + case 27: + case 28: + case 29: + case 31: + { + this.state = 50; + this.unaryExpression(); + } + break; + default: + throw new NoViableAltException(this); + } + } + this.state = 55; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public unaryExpression(): UnaryExpressionContext { + let localctx: UnaryExpressionContext = new UnaryExpressionContext(this, this._ctx, this.state); + this.enterRule(localctx, 8, FilterQueryParser.RULE_unaryExpression); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 57; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la===20) { + { + this.state = 56; + this.match(FilterQueryParser.NOT); + } + } + + this.state = 59; + this.primary(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public primary(): PrimaryContext { + let localctx: PrimaryContext = new PrimaryContext(this, this._ctx, this.state); + this.enterRule(localctx, 10, FilterQueryParser.RULE_primary); + try { + this.state = 70; + this._errHandler.sync(this); + switch ( this._interp.adaptivePredict(this._input, 4, this._ctx) ) { + case 1: + this.enterOuterAlt(localctx, 1); + { + this.state = 61; + this.match(FilterQueryParser.LPAREN); + this.state = 62; + this.orExpression(); + this.state = 63; + this.match(FilterQueryParser.RPAREN); + } + break; + case 2: + this.enterOuterAlt(localctx, 2); + { + this.state = 65; + this.comparison(); + } + break; + case 3: + this.enterOuterAlt(localctx, 3); + { + this.state = 66; + this.functionCall(); + } + break; + case 4: + this.enterOuterAlt(localctx, 4); + { + this.state = 67; + this.fullText(); + } + break; + case 5: + this.enterOuterAlt(localctx, 5); + { + this.state = 68; + this.key(); + } + break; + case 6: + this.enterOuterAlt(localctx, 6); + { + this.state = 69; + this.value(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public comparison(): ComparisonContext { + let localctx: ComparisonContext = new ComparisonContext(this, this._ctx, this.state); + this.enterRule(localctx, 12, FilterQueryParser.RULE_comparison); + let _la: number; + try { + this.state = 149; + this._errHandler.sync(this); + switch ( this._interp.adaptivePredict(this._input, 5, this._ctx) ) { + case 1: + this.enterOuterAlt(localctx, 1); + { + this.state = 72; + this.key(); + this.state = 73; + this.match(FilterQueryParser.EQUALS); + this.state = 74; + this.value(); + } + break; + case 2: + this.enterOuterAlt(localctx, 2); + { + this.state = 76; + this.key(); + this.state = 77; + _la = this._input.LA(1); + if(!(_la===7 || _la===8)) { + this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 78; + this.value(); + } + break; + case 3: + this.enterOuterAlt(localctx, 3); + { + this.state = 80; + this.key(); + this.state = 81; + this.match(FilterQueryParser.LT); + this.state = 82; + this.value(); + } + break; + case 4: + this.enterOuterAlt(localctx, 4); + { + this.state = 84; + this.key(); + this.state = 85; + this.match(FilterQueryParser.LE); + this.state = 86; + this.value(); + } + break; + case 5: + this.enterOuterAlt(localctx, 5); + { + this.state = 88; + this.key(); + this.state = 89; + this.match(FilterQueryParser.GT); + this.state = 90; + this.value(); + } + break; + case 6: + this.enterOuterAlt(localctx, 6); + { + this.state = 92; + this.key(); + this.state = 93; + this.match(FilterQueryParser.GE); + this.state = 94; + this.value(); + } + break; + case 7: + this.enterOuterAlt(localctx, 7); + { + this.state = 96; + this.key(); + this.state = 97; + _la = this._input.LA(1); + if(!(_la===13 || _la===14)) { + this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 98; + this.value(); + } + break; + case 8: + this.enterOuterAlt(localctx, 8); + { + this.state = 100; + this.key(); + this.state = 101; + this.match(FilterQueryParser.NOT); + this.state = 102; + _la = this._input.LA(1); + if(!(_la===13 || _la===14)) { + this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 103; + this.value(); + } + break; + case 9: + this.enterOuterAlt(localctx, 9); + { + this.state = 105; + this.key(); + this.state = 106; + this.match(FilterQueryParser.BETWEEN); + this.state = 107; + this.value(); + this.state = 108; + this.match(FilterQueryParser.AND); + this.state = 109; + this.value(); + } + break; + case 10: + this.enterOuterAlt(localctx, 10); + { + this.state = 111; + this.key(); + this.state = 112; + this.match(FilterQueryParser.NOT); + this.state = 113; + this.match(FilterQueryParser.BETWEEN); + this.state = 114; + this.value(); + this.state = 115; + this.match(FilterQueryParser.AND); + this.state = 116; + this.value(); + } + break; + case 11: + this.enterOuterAlt(localctx, 11); + { + this.state = 118; + this.key(); + this.state = 119; + this.inClause(); + } + break; + case 12: + this.enterOuterAlt(localctx, 12); + { + this.state = 121; + this.key(); + this.state = 122; + this.notInClause(); + } + break; + case 13: + this.enterOuterAlt(localctx, 13); + { + this.state = 124; + this.key(); + this.state = 125; + this.match(FilterQueryParser.EXISTS); + } + break; + case 14: + this.enterOuterAlt(localctx, 14); + { + this.state = 127; + this.key(); + this.state = 128; + this.match(FilterQueryParser.NOT); + this.state = 129; + this.match(FilterQueryParser.EXISTS); + } + break; + case 15: + this.enterOuterAlt(localctx, 15); + { + this.state = 131; + this.key(); + this.state = 132; + this.match(FilterQueryParser.REGEXP); + this.state = 133; + this.value(); + } + break; + case 16: + this.enterOuterAlt(localctx, 16); + { + this.state = 135; + this.key(); + this.state = 136; + this.match(FilterQueryParser.NOT); + this.state = 137; + this.match(FilterQueryParser.REGEXP); + this.state = 138; + this.value(); + } + break; + case 17: + this.enterOuterAlt(localctx, 17); + { + this.state = 140; + this.key(); + this.state = 141; + this.match(FilterQueryParser.CONTAINS); + this.state = 142; + this.value(); + } + break; + case 18: + this.enterOuterAlt(localctx, 18); + { + this.state = 144; + this.key(); + this.state = 145; + this.match(FilterQueryParser.NOT); + this.state = 146; + this.match(FilterQueryParser.CONTAINS); + this.state = 147; + this.value(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public inClause(): InClauseContext { + let localctx: InClauseContext = new InClauseContext(this, this._ctx, this.state); + this.enterRule(localctx, 14, FilterQueryParser.RULE_inClause); + try { + this.state = 163; + this._errHandler.sync(this); + switch ( this._interp.adaptivePredict(this._input, 6, this._ctx) ) { + case 1: + this.enterOuterAlt(localctx, 1); + { + this.state = 151; + this.match(FilterQueryParser.IN); + this.state = 152; + this.match(FilterQueryParser.LPAREN); + this.state = 153; + this.valueList(); + this.state = 154; + this.match(FilterQueryParser.RPAREN); + } + break; + case 2: + this.enterOuterAlt(localctx, 2); + { + this.state = 156; + this.match(FilterQueryParser.IN); + this.state = 157; + this.match(FilterQueryParser.LBRACK); + this.state = 158; + this.valueList(); + this.state = 159; + this.match(FilterQueryParser.RBRACK); + } + break; + case 3: + this.enterOuterAlt(localctx, 3); + { + this.state = 161; + this.match(FilterQueryParser.IN); + this.state = 162; + this.value(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public notInClause(): NotInClauseContext { + let localctx: NotInClauseContext = new NotInClauseContext(this, this._ctx, this.state); + this.enterRule(localctx, 16, FilterQueryParser.RULE_notInClause); + try { + this.state = 180; + this._errHandler.sync(this); + switch ( this._interp.adaptivePredict(this._input, 7, this._ctx) ) { + case 1: + this.enterOuterAlt(localctx, 1); + { + this.state = 165; + this.match(FilterQueryParser.NOT); + this.state = 166; + this.match(FilterQueryParser.IN); + this.state = 167; + this.match(FilterQueryParser.LPAREN); + this.state = 168; + this.valueList(); + this.state = 169; + this.match(FilterQueryParser.RPAREN); + } + break; + case 2: + this.enterOuterAlt(localctx, 2); + { + this.state = 171; + this.match(FilterQueryParser.NOT); + this.state = 172; + this.match(FilterQueryParser.IN); + this.state = 173; + this.match(FilterQueryParser.LBRACK); + this.state = 174; + this.valueList(); + this.state = 175; + this.match(FilterQueryParser.RBRACK); + } + break; + case 3: + this.enterOuterAlt(localctx, 3); + { + this.state = 177; + this.match(FilterQueryParser.NOT); + this.state = 178; + this.match(FilterQueryParser.IN); + this.state = 179; + this.value(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public valueList(): ValueListContext { + let localctx: ValueListContext = new ValueListContext(this, this._ctx, this.state); + this.enterRule(localctx, 18, FilterQueryParser.RULE_valueList); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 182; + this.value(); + this.state = 187; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la===5) { + { + { + this.state = 183; + this.match(FilterQueryParser.COMMA); + this.state = 184; + this.value(); + } + } + this.state = 189; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public fullText(): FullTextContext { + let localctx: FullTextContext = new FullTextContext(this, this._ctx, this.state); + this.enterRule(localctx, 20, FilterQueryParser.RULE_fullText); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 190; + _la = this._input.LA(1); + if(!(_la===28 || _la===31)) { + this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public functionCall(): FunctionCallContext { + let localctx: FunctionCallContext = new FunctionCallContext(this, this._ctx, this.state); + this.enterRule(localctx, 22, FilterQueryParser.RULE_functionCall); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 192; + _la = this._input.LA(1); + if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 58720256) !== 0))) { + this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 193; + this.match(FilterQueryParser.LPAREN); + this.state = 194; + this.functionParamList(); + this.state = 195; + this.match(FilterQueryParser.RPAREN); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public functionParamList(): FunctionParamListContext { + let localctx: FunctionParamListContext = new FunctionParamListContext(this, this._ctx, this.state); + this.enterRule(localctx, 24, FilterQueryParser.RULE_functionParamList); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 197; + this.functionParam(); + this.state = 202; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la===5) { + { + { + this.state = 198; + this.match(FilterQueryParser.COMMA); + this.state = 199; + this.functionParam(); + } + } + this.state = 204; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public functionParam(): FunctionParamContext { + let localctx: FunctionParamContext = new FunctionParamContext(this, this._ctx, this.state); + this.enterRule(localctx, 26, FilterQueryParser.RULE_functionParam); + try { + this.state = 208; + this._errHandler.sync(this); + switch ( this._interp.adaptivePredict(this._input, 10, this._ctx) ) { + case 1: + this.enterOuterAlt(localctx, 1); + { + this.state = 205; + this.key(); + } + break; + case 2: + this.enterOuterAlt(localctx, 2); + { + this.state = 206; + this.value(); + } + break; + case 3: + this.enterOuterAlt(localctx, 3); + { + this.state = 207; + this.array(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public array(): ArrayContext { + let localctx: ArrayContext = new ArrayContext(this, this._ctx, this.state); + this.enterRule(localctx, 28, FilterQueryParser.RULE_array); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 210; + this.match(FilterQueryParser.LBRACK); + this.state = 211; + this.valueList(); + this.state = 212; + this.match(FilterQueryParser.RBRACK); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public value(): ValueContext { + let localctx: ValueContext = new ValueContext(this, this._ctx, this.state); + this.enterRule(localctx, 30, FilterQueryParser.RULE_value); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 214; + _la = this._input.LA(1); + if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 1006632960) !== 0))) { + this._errHandler.recoverInline(this); + } + else { + this._errHandler.reportMatch(this); + this.consume(); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public key(): KeyContext { + let localctx: KeyContext = new KeyContext(this, this._ctx, this.state); + this.enterRule(localctx, 32, FilterQueryParser.RULE_key); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 216; + this.match(FilterQueryParser.KEY); + } + } + catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return localctx; + } + + public static readonly _serializedATN: number[] = [4,1,31,219,2,0,7,0,2, + 1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2, + 10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,1,0, + 1,0,1,0,1,1,1,1,1,2,1,2,1,2,5,2,43,8,2,10,2,12,2,46,9,2,1,3,1,3,1,3,1,3, + 5,3,52,8,3,10,3,12,3,55,9,3,1,4,3,4,58,8,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5, + 1,5,1,5,1,5,1,5,3,5,71,8,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, + 1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, + 1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, + 1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, + 1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,3,6,150,8,6,1,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,164,8,7,1,8,1,8,1,8,1,8,1,8,1,8, + 1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,3,8,181,8,8,1,9,1,9,1,9,5,9,186,8,9, + 10,9,12,9,189,9,9,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,5,12, + 201,8,12,10,12,12,12,204,9,12,1,13,1,13,1,13,3,13,209,8,13,1,14,1,14,1, + 14,1,14,1,15,1,15,1,16,1,16,1,16,0,0,17,0,2,4,6,8,10,12,14,16,18,20,22, + 24,26,28,30,32,0,5,1,0,7,8,1,0,13,14,2,0,28,28,31,31,1,0,23,25,1,0,26,29, + 235,0,34,1,0,0,0,2,37,1,0,0,0,4,39,1,0,0,0,6,47,1,0,0,0,8,57,1,0,0,0,10, + 70,1,0,0,0,12,149,1,0,0,0,14,163,1,0,0,0,16,180,1,0,0,0,18,182,1,0,0,0, + 20,190,1,0,0,0,22,192,1,0,0,0,24,197,1,0,0,0,26,208,1,0,0,0,28,210,1,0, + 0,0,30,214,1,0,0,0,32,216,1,0,0,0,34,35,3,2,1,0,35,36,5,0,0,1,36,1,1,0, + 0,0,37,38,3,4,2,0,38,3,1,0,0,0,39,44,3,6,3,0,40,41,5,22,0,0,41,43,3,6,3, + 0,42,40,1,0,0,0,43,46,1,0,0,0,44,42,1,0,0,0,44,45,1,0,0,0,45,5,1,0,0,0, + 46,44,1,0,0,0,47,53,3,8,4,0,48,49,5,21,0,0,49,52,3,8,4,0,50,52,3,8,4,0, + 51,48,1,0,0,0,51,50,1,0,0,0,52,55,1,0,0,0,53,51,1,0,0,0,53,54,1,0,0,0,54, + 7,1,0,0,0,55,53,1,0,0,0,56,58,5,20,0,0,57,56,1,0,0,0,57,58,1,0,0,0,58,59, + 1,0,0,0,59,60,3,10,5,0,60,9,1,0,0,0,61,62,5,1,0,0,62,63,3,4,2,0,63,64,5, + 2,0,0,64,71,1,0,0,0,65,71,3,12,6,0,66,71,3,22,11,0,67,71,3,20,10,0,68,71, + 3,32,16,0,69,71,3,30,15,0,70,61,1,0,0,0,70,65,1,0,0,0,70,66,1,0,0,0,70, + 67,1,0,0,0,70,68,1,0,0,0,70,69,1,0,0,0,71,11,1,0,0,0,72,73,3,32,16,0,73, + 74,5,6,0,0,74,75,3,30,15,0,75,150,1,0,0,0,76,77,3,32,16,0,77,78,7,0,0,0, + 78,79,3,30,15,0,79,150,1,0,0,0,80,81,3,32,16,0,81,82,5,9,0,0,82,83,3,30, + 15,0,83,150,1,0,0,0,84,85,3,32,16,0,85,86,5,10,0,0,86,87,3,30,15,0,87,150, + 1,0,0,0,88,89,3,32,16,0,89,90,5,11,0,0,90,91,3,30,15,0,91,150,1,0,0,0,92, + 93,3,32,16,0,93,94,5,12,0,0,94,95,3,30,15,0,95,150,1,0,0,0,96,97,3,32,16, + 0,97,98,7,1,0,0,98,99,3,30,15,0,99,150,1,0,0,0,100,101,3,32,16,0,101,102, + 5,20,0,0,102,103,7,1,0,0,103,104,3,30,15,0,104,150,1,0,0,0,105,106,3,32, + 16,0,106,107,5,15,0,0,107,108,3,30,15,0,108,109,5,21,0,0,109,110,3,30,15, + 0,110,150,1,0,0,0,111,112,3,32,16,0,112,113,5,20,0,0,113,114,5,15,0,0,114, + 115,3,30,15,0,115,116,5,21,0,0,116,117,3,30,15,0,117,150,1,0,0,0,118,119, + 3,32,16,0,119,120,3,14,7,0,120,150,1,0,0,0,121,122,3,32,16,0,122,123,3, + 16,8,0,123,150,1,0,0,0,124,125,3,32,16,0,125,126,5,16,0,0,126,150,1,0,0, + 0,127,128,3,32,16,0,128,129,5,20,0,0,129,130,5,16,0,0,130,150,1,0,0,0,131, + 132,3,32,16,0,132,133,5,17,0,0,133,134,3,30,15,0,134,150,1,0,0,0,135,136, + 3,32,16,0,136,137,5,20,0,0,137,138,5,17,0,0,138,139,3,30,15,0,139,150,1, + 0,0,0,140,141,3,32,16,0,141,142,5,18,0,0,142,143,3,30,15,0,143,150,1,0, + 0,0,144,145,3,32,16,0,145,146,5,20,0,0,146,147,5,18,0,0,147,148,3,30,15, + 0,148,150,1,0,0,0,149,72,1,0,0,0,149,76,1,0,0,0,149,80,1,0,0,0,149,84,1, + 0,0,0,149,88,1,0,0,0,149,92,1,0,0,0,149,96,1,0,0,0,149,100,1,0,0,0,149, + 105,1,0,0,0,149,111,1,0,0,0,149,118,1,0,0,0,149,121,1,0,0,0,149,124,1,0, + 0,0,149,127,1,0,0,0,149,131,1,0,0,0,149,135,1,0,0,0,149,140,1,0,0,0,149, + 144,1,0,0,0,150,13,1,0,0,0,151,152,5,19,0,0,152,153,5,1,0,0,153,154,3,18, + 9,0,154,155,5,2,0,0,155,164,1,0,0,0,156,157,5,19,0,0,157,158,5,3,0,0,158, + 159,3,18,9,0,159,160,5,4,0,0,160,164,1,0,0,0,161,162,5,19,0,0,162,164,3, + 30,15,0,163,151,1,0,0,0,163,156,1,0,0,0,163,161,1,0,0,0,164,15,1,0,0,0, + 165,166,5,20,0,0,166,167,5,19,0,0,167,168,5,1,0,0,168,169,3,18,9,0,169, + 170,5,2,0,0,170,181,1,0,0,0,171,172,5,20,0,0,172,173,5,19,0,0,173,174,5, + 3,0,0,174,175,3,18,9,0,175,176,5,4,0,0,176,181,1,0,0,0,177,178,5,20,0,0, + 178,179,5,19,0,0,179,181,3,30,15,0,180,165,1,0,0,0,180,171,1,0,0,0,180, + 177,1,0,0,0,181,17,1,0,0,0,182,187,3,30,15,0,183,184,5,5,0,0,184,186,3, + 30,15,0,185,183,1,0,0,0,186,189,1,0,0,0,187,185,1,0,0,0,187,188,1,0,0,0, + 188,19,1,0,0,0,189,187,1,0,0,0,190,191,7,2,0,0,191,21,1,0,0,0,192,193,7, + 3,0,0,193,194,5,1,0,0,194,195,3,24,12,0,195,196,5,2,0,0,196,23,1,0,0,0, + 197,202,3,26,13,0,198,199,5,5,0,0,199,201,3,26,13,0,200,198,1,0,0,0,201, + 204,1,0,0,0,202,200,1,0,0,0,202,203,1,0,0,0,203,25,1,0,0,0,204,202,1,0, + 0,0,205,209,3,32,16,0,206,209,3,30,15,0,207,209,3,28,14,0,208,205,1,0,0, + 0,208,206,1,0,0,0,208,207,1,0,0,0,209,27,1,0,0,0,210,211,5,3,0,0,211,212, + 3,18,9,0,212,213,5,4,0,0,213,29,1,0,0,0,214,215,7,4,0,0,215,31,1,0,0,0, + 216,217,5,29,0,0,217,33,1,0,0,0,11,44,51,53,57,70,149,163,180,187,202,208]; + + private static __ATN: ATN; + public static get _ATN(): ATN { + if (!FilterQueryParser.__ATN) { + FilterQueryParser.__ATN = new ATNDeserializer().deserialize(FilterQueryParser._serializedATN); + } + + return FilterQueryParser.__ATN; + } + + + static DecisionsToDFA = FilterQueryParser._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) ); + +} + +export class QueryContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public expression(): ExpressionContext { + return this.getTypedRuleContext(ExpressionContext, 0) as ExpressionContext; + } + public EOF(): TerminalNode { + return this.getToken(FilterQueryParser.EOF, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_query; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterQuery) { + listener.enterQuery(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitQuery) { + listener.exitQuery(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitQuery) { + return visitor.visitQuery(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ExpressionContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public orExpression(): OrExpressionContext { + return this.getTypedRuleContext(OrExpressionContext, 0) as OrExpressionContext; + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_expression; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterExpression) { + listener.enterExpression(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitExpression) { + listener.exitExpression(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitExpression) { + return visitor.visitExpression(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class OrExpressionContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public andExpression_list(): AndExpressionContext[] { + return this.getTypedRuleContexts(AndExpressionContext) as AndExpressionContext[]; + } + public andExpression(i: number): AndExpressionContext { + return this.getTypedRuleContext(AndExpressionContext, i) as AndExpressionContext; + } + public OR_list(): TerminalNode[] { + return this.getTokens(FilterQueryParser.OR); + } + public OR(i: number): TerminalNode { + return this.getToken(FilterQueryParser.OR, i); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_orExpression; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterOrExpression) { + listener.enterOrExpression(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitOrExpression) { + listener.exitOrExpression(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitOrExpression) { + return visitor.visitOrExpression(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class AndExpressionContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public unaryExpression_list(): UnaryExpressionContext[] { + return this.getTypedRuleContexts(UnaryExpressionContext) as UnaryExpressionContext[]; + } + public unaryExpression(i: number): UnaryExpressionContext { + return this.getTypedRuleContext(UnaryExpressionContext, i) as UnaryExpressionContext; + } + public AND_list(): TerminalNode[] { + return this.getTokens(FilterQueryParser.AND); + } + public AND(i: number): TerminalNode { + return this.getToken(FilterQueryParser.AND, i); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_andExpression; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterAndExpression) { + listener.enterAndExpression(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitAndExpression) { + listener.exitAndExpression(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitAndExpression) { + return visitor.visitAndExpression(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class UnaryExpressionContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public primary(): PrimaryContext { + return this.getTypedRuleContext(PrimaryContext, 0) as PrimaryContext; + } + public NOT(): TerminalNode { + return this.getToken(FilterQueryParser.NOT, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_unaryExpression; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterUnaryExpression) { + listener.enterUnaryExpression(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitUnaryExpression) { + listener.exitUnaryExpression(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitUnaryExpression) { + return visitor.visitUnaryExpression(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class PrimaryContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public LPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.LPAREN, 0); + } + public orExpression(): OrExpressionContext { + return this.getTypedRuleContext(OrExpressionContext, 0) as OrExpressionContext; + } + public RPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.RPAREN, 0); + } + public comparison(): ComparisonContext { + return this.getTypedRuleContext(ComparisonContext, 0) as ComparisonContext; + } + public functionCall(): FunctionCallContext { + return this.getTypedRuleContext(FunctionCallContext, 0) as FunctionCallContext; + } + public fullText(): FullTextContext { + return this.getTypedRuleContext(FullTextContext, 0) as FullTextContext; + } + public key(): KeyContext { + return this.getTypedRuleContext(KeyContext, 0) as KeyContext; + } + public value(): ValueContext { + return this.getTypedRuleContext(ValueContext, 0) as ValueContext; + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_primary; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterPrimary) { + listener.enterPrimary(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitPrimary) { + listener.exitPrimary(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitPrimary) { + return visitor.visitPrimary(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ComparisonContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public key(): KeyContext { + return this.getTypedRuleContext(KeyContext, 0) as KeyContext; + } + public EQUALS(): TerminalNode { + return this.getToken(FilterQueryParser.EQUALS, 0); + } + public value_list(): ValueContext[] { + return this.getTypedRuleContexts(ValueContext) as ValueContext[]; + } + public value(i: number): ValueContext { + return this.getTypedRuleContext(ValueContext, i) as ValueContext; + } + public NOT_EQUALS(): TerminalNode { + return this.getToken(FilterQueryParser.NOT_EQUALS, 0); + } + public NEQ(): TerminalNode { + return this.getToken(FilterQueryParser.NEQ, 0); + } + public LT(): TerminalNode { + return this.getToken(FilterQueryParser.LT, 0); + } + public LE(): TerminalNode { + return this.getToken(FilterQueryParser.LE, 0); + } + public GT(): TerminalNode { + return this.getToken(FilterQueryParser.GT, 0); + } + public GE(): TerminalNode { + return this.getToken(FilterQueryParser.GE, 0); + } + public LIKE(): TerminalNode { + return this.getToken(FilterQueryParser.LIKE, 0); + } + public ILIKE(): TerminalNode { + return this.getToken(FilterQueryParser.ILIKE, 0); + } + public NOT(): TerminalNode { + return this.getToken(FilterQueryParser.NOT, 0); + } + public BETWEEN(): TerminalNode { + return this.getToken(FilterQueryParser.BETWEEN, 0); + } + public AND(): TerminalNode { + return this.getToken(FilterQueryParser.AND, 0); + } + public inClause(): InClauseContext { + return this.getTypedRuleContext(InClauseContext, 0) as InClauseContext; + } + public notInClause(): NotInClauseContext { + return this.getTypedRuleContext(NotInClauseContext, 0) as NotInClauseContext; + } + public EXISTS(): TerminalNode { + return this.getToken(FilterQueryParser.EXISTS, 0); + } + public REGEXP(): TerminalNode { + return this.getToken(FilterQueryParser.REGEXP, 0); + } + public CONTAINS(): TerminalNode { + return this.getToken(FilterQueryParser.CONTAINS, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_comparison; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterComparison) { + listener.enterComparison(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitComparison) { + listener.exitComparison(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitComparison) { + return visitor.visitComparison(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class InClauseContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public IN(): TerminalNode { + return this.getToken(FilterQueryParser.IN, 0); + } + public LPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.LPAREN, 0); + } + public valueList(): ValueListContext { + return this.getTypedRuleContext(ValueListContext, 0) as ValueListContext; + } + public RPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.RPAREN, 0); + } + public LBRACK(): TerminalNode { + return this.getToken(FilterQueryParser.LBRACK, 0); + } + public RBRACK(): TerminalNode { + return this.getToken(FilterQueryParser.RBRACK, 0); + } + public value(): ValueContext { + return this.getTypedRuleContext(ValueContext, 0) as ValueContext; + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_inClause; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterInClause) { + listener.enterInClause(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitInClause) { + listener.exitInClause(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitInClause) { + return visitor.visitInClause(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class NotInClauseContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public NOT(): TerminalNode { + return this.getToken(FilterQueryParser.NOT, 0); + } + public IN(): TerminalNode { + return this.getToken(FilterQueryParser.IN, 0); + } + public LPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.LPAREN, 0); + } + public valueList(): ValueListContext { + return this.getTypedRuleContext(ValueListContext, 0) as ValueListContext; + } + public RPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.RPAREN, 0); + } + public LBRACK(): TerminalNode { + return this.getToken(FilterQueryParser.LBRACK, 0); + } + public RBRACK(): TerminalNode { + return this.getToken(FilterQueryParser.RBRACK, 0); + } + public value(): ValueContext { + return this.getTypedRuleContext(ValueContext, 0) as ValueContext; + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_notInClause; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterNotInClause) { + listener.enterNotInClause(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitNotInClause) { + listener.exitNotInClause(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitNotInClause) { + return visitor.visitNotInClause(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ValueListContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public value_list(): ValueContext[] { + return this.getTypedRuleContexts(ValueContext) as ValueContext[]; + } + public value(i: number): ValueContext { + return this.getTypedRuleContext(ValueContext, i) as ValueContext; + } + public COMMA_list(): TerminalNode[] { + return this.getTokens(FilterQueryParser.COMMA); + } + public COMMA(i: number): TerminalNode { + return this.getToken(FilterQueryParser.COMMA, i); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_valueList; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterValueList) { + listener.enterValueList(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitValueList) { + listener.exitValueList(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitValueList) { + return visitor.visitValueList(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class FullTextContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public QUOTED_TEXT(): TerminalNode { + return this.getToken(FilterQueryParser.QUOTED_TEXT, 0); + } + public FREETEXT(): TerminalNode { + return this.getToken(FilterQueryParser.FREETEXT, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_fullText; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterFullText) { + listener.enterFullText(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitFullText) { + listener.exitFullText(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitFullText) { + return visitor.visitFullText(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class FunctionCallContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public LPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.LPAREN, 0); + } + public functionParamList(): FunctionParamListContext { + return this.getTypedRuleContext(FunctionParamListContext, 0) as FunctionParamListContext; + } + public RPAREN(): TerminalNode { + return this.getToken(FilterQueryParser.RPAREN, 0); + } + public HAS(): TerminalNode { + return this.getToken(FilterQueryParser.HAS, 0); + } + public HASANY(): TerminalNode { + return this.getToken(FilterQueryParser.HASANY, 0); + } + public HASALL(): TerminalNode { + return this.getToken(FilterQueryParser.HASALL, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_functionCall; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterFunctionCall) { + listener.enterFunctionCall(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitFunctionCall) { + listener.exitFunctionCall(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitFunctionCall) { + return visitor.visitFunctionCall(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class FunctionParamListContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public functionParam_list(): FunctionParamContext[] { + return this.getTypedRuleContexts(FunctionParamContext) as FunctionParamContext[]; + } + public functionParam(i: number): FunctionParamContext { + return this.getTypedRuleContext(FunctionParamContext, i) as FunctionParamContext; + } + public COMMA_list(): TerminalNode[] { + return this.getTokens(FilterQueryParser.COMMA); + } + public COMMA(i: number): TerminalNode { + return this.getToken(FilterQueryParser.COMMA, i); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_functionParamList; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterFunctionParamList) { + listener.enterFunctionParamList(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitFunctionParamList) { + listener.exitFunctionParamList(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitFunctionParamList) { + return visitor.visitFunctionParamList(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class FunctionParamContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public key(): KeyContext { + return this.getTypedRuleContext(KeyContext, 0) as KeyContext; + } + public value(): ValueContext { + return this.getTypedRuleContext(ValueContext, 0) as ValueContext; + } + public array(): ArrayContext { + return this.getTypedRuleContext(ArrayContext, 0) as ArrayContext; + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_functionParam; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterFunctionParam) { + listener.enterFunctionParam(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitFunctionParam) { + listener.exitFunctionParam(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitFunctionParam) { + return visitor.visitFunctionParam(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ArrayContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public LBRACK(): TerminalNode { + return this.getToken(FilterQueryParser.LBRACK, 0); + } + public valueList(): ValueListContext { + return this.getTypedRuleContext(ValueListContext, 0) as ValueListContext; + } + public RBRACK(): TerminalNode { + return this.getToken(FilterQueryParser.RBRACK, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_array; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterArray) { + listener.enterArray(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitArray) { + listener.exitArray(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitArray) { + return visitor.visitArray(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ValueContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public QUOTED_TEXT(): TerminalNode { + return this.getToken(FilterQueryParser.QUOTED_TEXT, 0); + } + public NUMBER(): TerminalNode { + return this.getToken(FilterQueryParser.NUMBER, 0); + } + public BOOL(): TerminalNode { + return this.getToken(FilterQueryParser.BOOL, 0); + } + public KEY(): TerminalNode { + return this.getToken(FilterQueryParser.KEY, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_value; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterValue) { + listener.enterValue(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitValue) { + listener.exitValue(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitValue) { + return visitor.visitValue(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class KeyContext extends ParserRuleContext { + constructor(parser?: FilterQueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public KEY(): TerminalNode { + return this.getToken(FilterQueryParser.KEY, 0); + } + public get ruleIndex(): number { + return FilterQueryParser.RULE_key; + } + public enterRule(listener: FilterQueryListener): void { + if(listener.enterKey) { + listener.enterKey(this); + } + } + public exitRule(listener: FilterQueryListener): void { + if(listener.exitKey) { + listener.exitKey(this); + } + } + // @Override + public accept(visitor: FilterQueryVisitor): Result { + if (visitor.visitKey) { + return visitor.visitKey(this); + } else { + return visitor.visitChildren(this); + } + } +} diff --git a/frontend/src/parser/FilterQueryVisitor.ts b/frontend/src/parser/FilterQueryVisitor.ts new file mode 100644 index 000000000000..50578ecb7a7f --- /dev/null +++ b/frontend/src/parser/FilterQueryVisitor.ts @@ -0,0 +1,136 @@ +// Generated from FilterQuery.g4 by ANTLR 4.13.1 + +import {ParseTreeVisitor} from 'antlr4'; + + +import { QueryContext } from "./FilterQueryParser"; +import { ExpressionContext } from "./FilterQueryParser"; +import { OrExpressionContext } from "./FilterQueryParser"; +import { AndExpressionContext } from "./FilterQueryParser"; +import { UnaryExpressionContext } from "./FilterQueryParser"; +import { PrimaryContext } from "./FilterQueryParser"; +import { ComparisonContext } from "./FilterQueryParser"; +import { InClauseContext } from "./FilterQueryParser"; +import { NotInClauseContext } from "./FilterQueryParser"; +import { ValueListContext } from "./FilterQueryParser"; +import { FullTextContext } from "./FilterQueryParser"; +import { FunctionCallContext } from "./FilterQueryParser"; +import { FunctionParamListContext } from "./FilterQueryParser"; +import { FunctionParamContext } from "./FilterQueryParser"; +import { ArrayContext } from "./FilterQueryParser"; +import { ValueContext } from "./FilterQueryParser"; +import { KeyContext } from "./FilterQueryParser"; + + +/** + * This interface defines a complete generic visitor for a parse tree produced + * by `FilterQueryParser`. + * + * @param The return type of the visit operation. Use `void` for + * operations with no return type. + */ +export default class FilterQueryVisitor extends ParseTreeVisitor { + /** + * Visit a parse tree produced by `FilterQueryParser.query`. + * @param ctx the parse tree + * @return the visitor result + */ + visitQuery?: (ctx: QueryContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.expression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitExpression?: (ctx: ExpressionContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.orExpression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitOrExpression?: (ctx: OrExpressionContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.andExpression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitAndExpression?: (ctx: AndExpressionContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.unaryExpression`. + * @param ctx the parse tree + * @return the visitor result + */ + visitUnaryExpression?: (ctx: UnaryExpressionContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.primary`. + * @param ctx the parse tree + * @return the visitor result + */ + visitPrimary?: (ctx: PrimaryContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.comparison`. + * @param ctx the parse tree + * @return the visitor result + */ + visitComparison?: (ctx: ComparisonContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.inClause`. + * @param ctx the parse tree + * @return the visitor result + */ + visitInClause?: (ctx: InClauseContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.notInClause`. + * @param ctx the parse tree + * @return the visitor result + */ + visitNotInClause?: (ctx: NotInClauseContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.valueList`. + * @param ctx the parse tree + * @return the visitor result + */ + visitValueList?: (ctx: ValueListContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.fullText`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFullText?: (ctx: FullTextContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.functionCall`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFunctionCall?: (ctx: FunctionCallContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.functionParamList`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFunctionParamList?: (ctx: FunctionParamListContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.functionParam`. + * @param ctx the parse tree + * @return the visitor result + */ + visitFunctionParam?: (ctx: FunctionParamContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.array`. + * @param ctx the parse tree + * @return the visitor result + */ + visitArray?: (ctx: ArrayContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.value`. + * @param ctx the parse tree + * @return the visitor result + */ + visitValue?: (ctx: ValueContext) => Result; + /** + * Visit a parse tree produced by `FilterQueryParser.key`. + * @param ctx the parse tree + * @return the visitor result + */ + visitKey?: (ctx: KeyContext) => Result; +} + diff --git a/frontend/src/parser/analyzeQuery.ts b/frontend/src/parser/analyzeQuery.ts new file mode 100644 index 000000000000..24066d969dae --- /dev/null +++ b/frontend/src/parser/analyzeQuery.ts @@ -0,0 +1,94 @@ +import FilterQueryLexer from './FilterQueryLexer'; +import FilterQueryParser from './FilterQueryParser'; +import { ParseTreeWalker, CharStreams, CommonTokenStream, Token } from 'antlr4'; +import { isOperatorToken } from 'utils/tokenUtils'; +import FilterQueryListener from './FilterQueryListener'; + +import { + KeyContext, + ValueContext, + ComparisonContext, +} from './FilterQueryParser'; +import { IToken } from 'types/antlrQueryTypes'; + +// 👇 Define the token classification +type TokenClassification = 'Key' | 'Value' | 'Operator'; + +interface TokenInfo { + text: string; + startIndex: number; + stopIndex: number; + type: TokenClassification; +} + +// 👇 Custom listener to walk the parse tree +class TypeTrackingListener implements FilterQueryListener { + public tokens: TokenInfo[] = []; + + enterKey(ctx: KeyContext) { + const token = ctx.KEY().symbol; + this.tokens.push({ + text: token.text!, + startIndex: token.start, + stopIndex: token.stop, + type: 'Key', + }); + } + + enterValue(ctx: ValueContext) { + const token = ctx.start; + this.tokens.push({ + text: token.text!, + startIndex: token.start, + stopIndex: token.stop, + type: 'Value', + }); + } + + enterComparison(ctx: ComparisonContext) { + const children = ctx.children || []; + for (const child of children) { + const token = (child as any).symbol; + if (token && isOperatorToken(token.type)) { + this.tokens.push({ + text: token.text!, + startIndex: token.start, + stopIndex: token.stop, + type: 'Operator', + }); + } + } + } + + // Required no-op stubs + enterEveryRule() {} + exitEveryRule() {} + exitKey() {} + exitValue() {} + exitComparison() {} + visitTerminal() {} + visitErrorNode() {} +} + +// 👇 Analyze function +export function analyzeQuery(input: string, lastToken: IToken) { + input = input.trim(); + const chars = CharStreams.fromString(input); + const lexer = new FilterQueryLexer(chars); + const tokens = new CommonTokenStream(lexer); + const parser = new FilterQueryParser(tokens); + + const tree = parser.query(); + + const listener = new TypeTrackingListener(); + ParseTreeWalker.DEFAULT.walk(listener, tree); + + const currentToken = listener.tokens.find( + (token) => + token.text === lastToken.text && + token.startIndex === lastToken.start && + token.stopIndex === lastToken.stop, + ); + + return currentToken; +} diff --git a/frontend/src/periscope.scss b/frontend/src/periscope.scss index 71a23dc14f89..f37fd8e0de94 100644 --- a/frontend/src/periscope.scss +++ b/frontend/src/periscope.scss @@ -22,6 +22,8 @@ &.ghost { box-shadow: none; border: none; + background: transparent; + color: var(--bg-vanilla-400, #c0c1c3); } cursor: pointer; @@ -33,6 +35,12 @@ box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09); } + &.secondary { + border-radius: 3px; + border: 1px solid var(--Slate-300, #242834); + background: var(--Ink-200, #23262e); + } + &:disabled { opacity: 0.5; } @@ -89,10 +97,93 @@ } } +.periscope-input-with-label { + display: flex; + flex-direction: row; + border-radius: 2px 0px 0px 2px; + + .label { + font-size: 12px; + + color: var(--bg-vanilla-400); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; + letter-spacing: 0.56px; + + max-width: 150px; + min-width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0px 8px; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + display: flex; + justify-content: flex-start; + align-items: center; + font-weight: var(--font-weight-light); + } + + .input { + flex: 1; + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + border-right: none; + border-left: none; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + + background: var(--bg-ink-300); + + min-width: 0; + + .ant-select { + border: none; + height: 36px; + } + + .ant-select-selector { + border: none; + } + } + + .close-btn { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + height: 38px; + width: 38px; + } +} + .lightMode { .periscope-btn { border-color: var(--bg-vanilla-300); background: var(--bg-vanilla-100); color: var(--bg-ink-200); } + + .periscope-input-with-label { + .label { + color: var(--bg-ink-500) !important; + + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + + .input { + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + + .close-btn { + border: 1px solid var(--bg-vanilla-300) !important; + background: var(--bg-vanilla-100) !important; + } + } } diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index f95f04a8c254..de261368f566 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -29,7 +29,7 @@ import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields'; -import { cloneDeep, get, isEqual, merge, set } from 'lodash-es'; +import { cloneDeep, get, isEqual, set } from 'lodash-es'; import { createContext, PropsWithChildren, @@ -42,6 +42,7 @@ import { import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; // ** Types import { IBuilderFormula, @@ -141,10 +142,10 @@ export function QueryBuilderProvider({ const isCurrentOperatorAvailableInList = initialOperators .map((operator) => operator.value) - .includes(queryData.aggregateOperator); + .includes(queryData.aggregateOperator || ''); if (!isCurrentOperatorAvailableInList) { - return { ...queryData, aggregateOperator: initialOperators[0].value }; + return { ...queryData, aggregateOperator: initialOperators[0]?.value }; } return queryData; @@ -177,10 +178,10 @@ export function QueryBuilderProvider({ aggregateAttribute: { ...item.aggregateAttribute, id: createIdFromObjectFields( - item.aggregateAttribute, + item.aggregateAttribute as BaseAutocompleteData, baseAutoCompleteIdKeysOrder, ), - }, + } as BaseAutocompleteData, }; return currentElement; @@ -218,7 +219,7 @@ export function QueryBuilderProvider({ ); const initQueryBuilderData = useCallback( - (query: Query, timeUpdated?: boolean): void => { + (query: Query): void => { const { queryType: newQueryType, ...queryState } = prepareQueryBuilderData( query, ); @@ -233,12 +234,10 @@ export function QueryBuilderProvider({ const nextQuery: Query = { ...newQueryState, queryType: type }; setStagedQuery(nextQuery); - setCurrentQuery( - timeUpdated ? merge(currentQuery, newQueryState) : newQueryState, - ); + setCurrentQuery(newQueryState); setQueryType(type); }, - [prepareQueryBuilderData, currentQuery], + [prepareQueryBuilderData], ); const updateAllQueriesOperators = useCallback( @@ -280,7 +279,7 @@ export function QueryBuilderProvider({ aggregateAttribute: { ...aggregateAttribute, id: '', - }, + } as BaseAutocompleteData, timeAggregation, spaceAggregation, functions, @@ -853,18 +852,41 @@ export function QueryBuilderProvider({ ); const handleRunQuery = useCallback( - (shallUpdateStepInterval?: boolean) => { + (shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => { + let currentQueryData = currentQuery; + if (newQBQuery) { + currentQueryData = { + ...currentQueryData, + builder: { + ...currentQueryData.builder, + queryData: currentQueryData.builder.queryData.map((item) => ({ + ...item, + filter: { + ...item.filter, + expression: + item.filter?.expression.trim() === '' + ? '' + : item.filter?.expression ?? '', + }, + filters: { + items: [], + op: 'AND', + }, + })), + }, + }; + } redirectWithQueryBuilderData({ ...{ - ...currentQuery, + ...currentQueryData, ...updateStepInterval( { - builder: currentQuery.builder, - clickhouse_sql: currentQuery.clickhouse_sql, - promql: currentQuery.promql, - id: currentQuery.id, + builder: currentQueryData.builder, + clickhouse_sql: currentQueryData.clickhouse_sql, + promql: currentQueryData.promql, + id: currentQueryData.id, queryType, - unit: currentQuery.unit, + unit: currentQueryData.unit, }, maxTime, minTime, diff --git a/frontend/src/providers/preferences/__tests__/PreferenceContextProvider.test.tsx b/frontend/src/providers/preferences/__tests__/PreferenceContextProvider.test.tsx index b5ced03b5e77..44b05a618dd6 100644 --- a/frontend/src/providers/preferences/__tests__/PreferenceContextProvider.test.tsx +++ b/frontend/src/providers/preferences/__tests__/PreferenceContextProvider.test.tsx @@ -1,12 +1,12 @@ /* eslint-disable sonarjs/no-identical-functions */ import { render, screen } from '@testing-library/react'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { FormattingOptions, PreferenceMode, Preferences, } from 'providers/preferences/types'; import { MemoryRouter, Route, Switch } from 'react-router-dom'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { PreferenceContextProvider, @@ -17,7 +17,7 @@ import { jest.mock('../sync/usePreferenceSync', () => ({ usePreferenceSync: jest.fn().mockReturnValue({ preferences: { - columns: [] as BaseAutocompleteData[], + columns: [] as TelemetryFieldKey[], formatting: { maxLines: 2, format: 'table', diff --git a/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts b/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts index b6ab1b18afe2..6791efcf5d4b 100644 --- a/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts +++ b/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts @@ -150,7 +150,7 @@ describe('logsLoaderConfig', () => { const result = await logsLoaderConfig.default(); expect(result).toEqual({ - columns: defaultLogsSelectedColumns as BaseAutocompleteData[], + columns: defaultLogsSelectedColumns, formatting: { maxLines: 2, format: 'table' as LogViewMode, diff --git a/frontend/src/providers/preferences/__tests__/logsUpdaterConfig.test.ts b/frontend/src/providers/preferences/__tests__/logsUpdaterConfig.test.ts index 6f9c42176b04..5ed12bda4f8d 100644 --- a/frontend/src/providers/preferences/__tests__/logsUpdaterConfig.test.ts +++ b/frontend/src/providers/preferences/__tests__/logsUpdaterConfig.test.ts @@ -1,3 +1,4 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { LOCALSTORAGE } from 'constants/localStorage'; import { LogViewMode } from 'container/LogsTable'; import { defaultOptionsQuery } from 'container/OptionsMenu/constants'; @@ -7,10 +8,7 @@ import { PreferenceMode, Preferences, } from 'providers/preferences/types'; -import { - BaseAutocompleteData, - DataTypes, -} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import getLogsUpdaterConfig from '../configs/logsUpdaterConfig'; @@ -65,11 +63,11 @@ describe('logsUpdaterConfig', () => { setSavedViewPreferences, ); - const newColumns: BaseAutocompleteData[] = [ + const newColumns: TelemetryFieldKey[] = [ { - key: 'new-column', - type: 'tag', - dataType: DataTypes.String, + name: 'new-column', + fieldContext: '', + fieldDataType: DataTypes.String, isColumn: true, }, ]; @@ -114,11 +112,11 @@ describe('logsUpdaterConfig', () => { setSavedViewPreferences, ); - const newColumns: BaseAutocompleteData[] = [ + const newColumns: TelemetryFieldKey[] = [ { - key: 'new-column', - type: 'tag', - dataType: DataTypes.String, + name: 'new-column', + fieldContext: '', + fieldDataType: DataTypes.String, isColumn: true, }, ]; diff --git a/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts b/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts index 230c297e09c1..f0cf4ee8e437 100644 --- a/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts +++ b/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts @@ -4,6 +4,7 @@ import { BaseAutocompleteData, DataTypes, } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TelemetryFieldKey } from 'types/api/v5/queryRange'; import tracesLoaderConfig from '../configs/tracesLoaderConfig'; @@ -125,7 +126,7 @@ describe('tracesLoaderConfig', () => { const result = await tracesLoaderConfig.default(); expect(result).toEqual({ - columns: defaultTraceSelectedColumns as BaseAutocompleteData[], + columns: defaultTraceSelectedColumns as TelemetryFieldKey[], }); }); }); diff --git a/frontend/src/providers/preferences/__tests__/tracesUpdaterConfig.test.ts b/frontend/src/providers/preferences/__tests__/tracesUpdaterConfig.test.ts index 9b421a7c28f8..1b1fafdcee19 100644 --- a/frontend/src/providers/preferences/__tests__/tracesUpdaterConfig.test.ts +++ b/frontend/src/providers/preferences/__tests__/tracesUpdaterConfig.test.ts @@ -1,9 +1,7 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultOptionsQuery } from 'container/OptionsMenu/constants'; -import { - BaseAutocompleteData, - DataTypes, -} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig'; import { PreferenceMode } from '../types'; @@ -34,11 +32,11 @@ describe('tracesUpdaterConfig', () => { const mockSetSavedViewPreferences = jest.fn(); // Test data - const mockColumns: BaseAutocompleteData[] = [ + const mockColumns: TelemetryFieldKey[] = [ { - key: 'test-trace-column', - type: 'tag', - dataType: DataTypes.String, + name: 'test-trace-column', + fieldContext: '', + fieldDataType: DataTypes.String, isColumn: true, }, ]; diff --git a/frontend/src/providers/preferences/__tests__/usePreferenceUpdater.test.tsx b/frontend/src/providers/preferences/__tests__/usePreferenceUpdater.test.tsx index ccbb9b0236bc..757ad2c81823 100644 --- a/frontend/src/providers/preferences/__tests__/usePreferenceUpdater.test.tsx +++ b/frontend/src/providers/preferences/__tests__/usePreferenceUpdater.test.tsx @@ -1,5 +1,6 @@ /* eslint-disable sonarjs/no-identical-functions */ import { renderHook } from '@testing-library/react'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { LogViewMode } from 'container/LogsTable'; import { FontSize } from 'container/OptionsMenu/types'; import { @@ -8,10 +9,7 @@ import { Preferences, } from 'providers/preferences/types'; import { act } from 'react-dom/test-utils'; -import { - BaseAutocompleteData, - DataTypes, -} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; import { usePreferenceUpdater } from '../updater/usePreferenceUpdater'; @@ -81,11 +79,11 @@ describe('usePreferenceUpdater', () => { it('should call the logs updater for updateColumns with logs dataSource', () => { const setReSync = jest.fn(); const setSavedViewPreferences = jest.fn(); - const newColumns: BaseAutocompleteData[] = [ + const newColumns: TelemetryFieldKey[] = [ { - key: 'new-column', - type: 'tag', - dataType: DataTypes.String, + name: 'new-column', + fieldContext: '', + fieldDataType: DataTypes.String, isColumn: true, }, ]; @@ -147,11 +145,11 @@ describe('usePreferenceUpdater', () => { it('should call the traces updater for updateColumns with traces dataSource', () => { const setReSync = jest.fn(); const setSavedViewPreferences = jest.fn(); - const newColumns: BaseAutocompleteData[] = [ + const newColumns: TelemetryFieldKey[] = [ { - key: 'new-trace-column', - type: 'tag', - dataType: DataTypes.String, + name: 'new-trace-column', + fieldContext: '', + fieldDataType: DataTypes.String, isColumn: true, }, ]; @@ -227,9 +225,9 @@ describe('usePreferenceUpdater', () => { act(() => { result.current.updateColumns([ { - key: 'column', - type: 'tag', - dataType: DataTypes.String, + name: 'column', + fieldContext: '', + fieldDataType: DataTypes.String, isColumn: true, }, ]); diff --git a/frontend/src/providers/preferences/configs/logsLoaderConfig.ts b/frontend/src/providers/preferences/configs/logsLoaderConfig.ts index 9b5b8dd4bdb7..4989e0138800 100644 --- a/frontend/src/providers/preferences/configs/logsLoaderConfig.ts +++ b/frontend/src/providers/preferences/configs/logsLoaderConfig.ts @@ -1,5 +1,6 @@ /* eslint-disable no-empty */ import getLocalStorageKey from 'api/browser/localstorage/get'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants'; import { FontSize } from 'container/OptionsMenu/types'; @@ -50,10 +51,10 @@ const logsLoaders = { return { columns: [], formatting: undefined } as any; }, default: async (): Promise<{ - columns: BaseAutocompleteData[]; + columns: TelemetryFieldKey[]; formatting: FormattingOptions; }> => ({ - columns: defaultLogsSelectedColumns as BaseAutocompleteData[], + columns: defaultLogsSelectedColumns, formatting: { maxLines: 2, format: 'table', diff --git a/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts b/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts index b41e5ac13160..6b0478233878 100644 --- a/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts +++ b/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts @@ -1,9 +1,9 @@ import setLocalStorageKey from 'api/browser/localstorage/set'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultOptionsQuery } from 'container/OptionsMenu/constants'; import { FontSize, OptionsQuery } from 'container/OptionsMenu/types'; import { Dispatch, SetStateAction } from 'react'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { FormattingOptions, PreferenceMode, Preferences } from '../types'; @@ -13,10 +13,10 @@ const getLogsUpdaterConfig = ( redirectWithOptionsData: (options: OptionsQuery) => void, setSavedViewPreferences: Dispatch>, ): { - updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void; + updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void; updateFormatting: (newFormatting: FormattingOptions, mode: string) => void; } => ({ - updateColumns: (newColumns: BaseAutocompleteData[], mode: string): void => { + updateColumns: (newColumns: TelemetryFieldKey[], mode: string): void => { if (mode === PreferenceMode.SAVED_VIEW) { setSavedViewPreferences((prev) => { if (!prev) { diff --git a/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts b/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts index cb323b6aecef..69a01158ef0b 100644 --- a/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts +++ b/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts @@ -1,5 +1,6 @@ /* eslint-disable no-empty */ import getLocalStorageKey from 'api/browser/localstorage/get'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; @@ -33,9 +34,9 @@ const tracesLoaders = { return { columns: [] }; }, default: async (): Promise<{ - columns: BaseAutocompleteData[]; + columns: TelemetryFieldKey[]; }> => ({ - columns: defaultTraceSelectedColumns as BaseAutocompleteData[], + columns: defaultTraceSelectedColumns, }), priority: ['local', 'url', 'default'] as const, }; diff --git a/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts b/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts index f08408201c99..a45cf6edcecf 100644 --- a/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts +++ b/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts @@ -1,9 +1,9 @@ import setLocalStorageKey from 'api/browser/localstorage/set'; +import { TelemetryFieldKey } from 'api/v5/v5'; import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultOptionsQuery } from 'container/OptionsMenu/constants'; import { FontSize, OptionsQuery } from 'container/OptionsMenu/types'; import { Dispatch, SetStateAction } from 'react'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { PreferenceMode, Preferences } from '../types'; @@ -12,10 +12,10 @@ const getTracesUpdaterConfig = ( redirectWithOptionsData: (options: OptionsQuery) => void, setSavedViewPreferences: Dispatch>, ): { - updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void; + updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void; updateFormatting: () => void; } => ({ - updateColumns: (newColumns: BaseAutocompleteData[], mode: string): void => { + updateColumns: (newColumns: TelemetryFieldKey[], mode: string): void => { // remove the formatting props if (mode === PreferenceMode.SAVED_VIEW) { setSavedViewPreferences({ diff --git a/frontend/src/providers/preferences/loader/usePreferenceLoader.ts b/frontend/src/providers/preferences/loader/usePreferenceLoader.ts index 09145da6e2b6..31ef33387285 100644 --- a/frontend/src/providers/preferences/loader/usePreferenceLoader.ts +++ b/frontend/src/providers/preferences/loader/usePreferenceLoader.ts @@ -1,7 +1,7 @@ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable no-empty */ +import { TelemetryFieldKey } from 'api/v5/v5'; import { useEffect, useState } from 'react'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; import logsLoaderConfig from '../configs/logsLoaderConfig'; @@ -43,14 +43,14 @@ async function preferencesLoader(config: { // Use the generic loader with specific configs async function logsPreferencesLoader(): Promise<{ - columns: BaseAutocompleteData[]; + columns: TelemetryFieldKey[]; formatting: FormattingOptions; }> { return preferencesLoader(logsLoaderConfig); } async function tracesPreferencesLoader(): Promise<{ - columns: BaseAutocompleteData[]; + columns: TelemetryFieldKey[]; }> { return preferencesLoader(tracesLoaderConfig); } diff --git a/frontend/src/providers/preferences/sync/usePreferenceSync.ts b/frontend/src/providers/preferences/sync/usePreferenceSync.ts index 7cd5202bc0a3..457d3dada182 100644 --- a/frontend/src/providers/preferences/sync/usePreferenceSync.ts +++ b/frontend/src/providers/preferences/sync/usePreferenceSync.ts @@ -1,8 +1,8 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants'; import { defaultSelectedColumns as defaultTracesSelectedColumns } from 'container/TracesExplorer/ListView/configs'; import { useGetAllViews } from 'hooks/saveViews/useGetAllViews'; import { useEffect, useState } from 'react'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; import { usePreferenceLoader } from '../loader/usePreferenceLoader'; @@ -21,7 +21,7 @@ export function usePreferenceSync({ preferences: Preferences | null; loading: boolean; error: Error | null; - updateColumns: (newColumns: BaseAutocompleteData[]) => void; + updateColumns: (newColumns: TelemetryFieldKey[]) => void; updateFormatting: (newFormatting: FormattingOptions) => void; } { const { data: viewsData } = useGetAllViews(dataSource); @@ -37,7 +37,7 @@ export function usePreferenceSync({ )?.extraData; const parsedExtraData = JSON.parse(extraData || '{}'); - let columns: BaseAutocompleteData[] = []; + let columns: TelemetryFieldKey[] = []; let formatting: FormattingOptions | undefined; if (dataSource === DataSource.LOGS) { columns = parsedExtraData?.selectColumns || defaultLogsSelectedColumns; diff --git a/frontend/src/providers/preferences/types/index.ts b/frontend/src/providers/preferences/types/index.ts index 57bd93ca78f9..4c45f80dd7fa 100644 --- a/frontend/src/providers/preferences/types/index.ts +++ b/frontend/src/providers/preferences/types/index.ts @@ -1,6 +1,6 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { LogViewMode } from 'container/LogsTable'; import { FontSize } from 'container/OptionsMenu/types'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; export enum PreferenceMode { @@ -15,7 +15,7 @@ export interface PreferenceContextValue { mode: PreferenceMode; savedViewId?: string; dataSource: DataSource; - updateColumns: (newColumns: BaseAutocompleteData[]) => void; + updateColumns: (newColumns: TelemetryFieldKey[]) => void; updateFormatting: (newFormatting: FormattingOptions) => void; } @@ -27,6 +27,6 @@ export interface FormattingOptions { } export interface Preferences { - columns: BaseAutocompleteData[]; + columns: TelemetryFieldKey[]; formatting?: FormattingOptions; } diff --git a/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts b/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts index ef9d28501290..2489b7e571ce 100644 --- a/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts +++ b/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts @@ -1,3 +1,4 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { defaultOptionsQuery, URL_OPTIONS, @@ -5,7 +6,6 @@ import { import { OptionsQuery } from 'container/OptionsMenu/types'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { Dispatch, SetStateAction } from 'react'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; import getLogsUpdaterConfig from '../configs/logsUpdaterConfig'; @@ -24,7 +24,7 @@ const getUpdaterConfig = ( ): Record< DataSource, { - updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void; + updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void; updateFormatting: (newFormatting: FormattingOptions, mode: string) => void; } > => ({ @@ -53,7 +53,7 @@ export function usePreferenceUpdater({ setReSync: Dispatch>; setSavedViewPreferences: Dispatch>; }): { - updateColumns: (newColumns: BaseAutocompleteData[]) => void; + updateColumns: (newColumns: TelemetryFieldKey[]) => void; updateFormatting: (newFormatting: FormattingOptions) => void; } { const { @@ -66,7 +66,7 @@ export function usePreferenceUpdater({ )[dataSource]; return { - updateColumns: (newColumns: BaseAutocompleteData[]): void => { + updateColumns: (newColumns: TelemetryFieldKey[]): void => { updater.updateColumns(newColumns, mode); setReSync(true); }, diff --git a/frontend/src/types/antlrQueryTypes.ts b/frontend/src/types/antlrQueryTypes.ts new file mode 100644 index 000000000000..a940df566ee5 --- /dev/null +++ b/frontend/src/types/antlrQueryTypes.ts @@ -0,0 +1,72 @@ +export interface IValidationResult { + isValid: boolean; + message: string; + errors: IDetailedError[]; +} + +export interface IToken { + type: number; + text: string; + start: number; + stop: number; + channel?: number; +} + +export interface IQueryPair { + key: string; + operator: string; + value?: string; + valueList?: string[]; + hasNegation?: boolean; + isMultiValue?: boolean; + position: { + keyStart: number; + keyEnd: number; + operatorStart: number; + operatorEnd: number; + valueStart?: number; + valueEnd?: number; + negationStart?: number; + negationEnd?: number; + }; + valuesPosition?: { + start?: number; + end?: number; + }[]; + isComplete: boolean; // true if the pair has all three components +} + +export interface IQueryContext { + tokenType: number; + text: string; + start: number; + stop: number; + currentToken: string; + isInValue: boolean; + isInKey: boolean; + isInNegation: boolean; + isInOperator: boolean; + isInFunction: boolean; + isInConjunction?: boolean; + isInParenthesis?: boolean; + isInBracketList?: boolean; // For multi-value operators like IN where values are in brackets + keyToken?: string; + operatorToken?: string; + valueToken?: string; + queryPairs?: IQueryPair[]; + currentPair?: IQueryPair | null; +} + +export interface IDetailedError { + message: string; + line: number; + column: number; + offendingSymbol?: string; + expectedTokens?: string[]; +} + +export interface ASTNode { + type: string; + value?: string; + children?: ASTNode[]; +} diff --git a/frontend/src/types/api/alerts/compositeQuery.ts b/frontend/src/types/api/alerts/compositeQuery.ts index f2856fbb3dc4..997712776b38 100644 --- a/frontend/src/types/api/alerts/compositeQuery.ts +++ b/frontend/src/types/api/alerts/compositeQuery.ts @@ -7,11 +7,14 @@ import { } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; +import { QueryEnvelope } from '../v5/queryRange'; + export interface ICompositeMetricQuery { - builderQueries: BuilderQueryDataResourse; - promQueries: BuilderPromQLResource; - chQueries: BuilderClickHouseResource; + builderQueries?: BuilderQueryDataResourse; + promQueries?: BuilderPromQLResource; + chQueries?: BuilderClickHouseResource; queryType: EQueryType; panelType: PANEL_TYPES; unit: Query['unit']; + queries?: QueryEnvelope[]; } diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 2115b94e821d..e737070ffbc6 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -7,7 +7,7 @@ import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { IField } from '../logs/fields'; -import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; +import { TelemetryFieldKey } from '../v5/queryRange'; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; export type TVariableQueryType = typeof VariableQueryTypeArr[number]; @@ -115,7 +115,7 @@ export interface IBaseWidget { fillSpans?: boolean; columnUnits?: ColumnUnit; selectedLogFields: IField[] | null; - selectedTracesFields: BaseAutocompleteData[] | null; + selectedTracesFields: TelemetryFieldKey[] | null; isLogScale?: boolean; columnWidths?: Record; legendPosition?: LegendPosition; diff --git a/frontend/src/types/api/error.ts b/frontend/src/types/api/error.ts index 1e5fa8aa940a..de3ddb8baffe 100644 --- a/frontend/src/types/api/error.ts +++ b/frontend/src/types/api/error.ts @@ -21,6 +21,10 @@ class APIError extends Error { getErrorCode(): string { return this.error.error.code; } + + getErrorDetails(): ErrorResponseV2 { + return this.error; + } } export default APIError; diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index d15a41cec96b..6b7b2259efe5 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -1,3 +1,4 @@ +import { TelemetryFieldKey } from 'api/v5/v5'; import { Format } from 'container/NewWidget/RightContainer/types'; import { EQueryType } from 'types/common/dashboard'; import { @@ -6,6 +7,13 @@ import { ReduceOperators, } from 'types/common/queryBuilder'; +import { + Filter, + Having as HavingV5, + LogAggregation, + MetricAggregation, + TraceAggregation, +} from '../v5/queryRange'; import { BaseAutocompleteData } from './queryAutocompleteResponse'; // Type for Formula @@ -57,25 +65,27 @@ export interface QueryFunctionProps { export type IBuilderQuery = { queryName: string; dataSource: DataSource; - aggregateOperator: string; - aggregateAttribute: BaseAutocompleteData; - timeAggregation: string; + aggregateOperator?: string; + aggregateAttribute?: BaseAutocompleteData; + aggregations?: TraceAggregation[] | LogAggregation[] | MetricAggregation[]; + timeAggregation?: string; spaceAggregation?: string; temporality?: string; functions: QueryFunctionProps[]; - filters: TagFilter; + filter?: Filter; + filters?: TagFilter; groupBy: BaseAutocompleteData[]; expression: string; disabled: boolean; - having: Having[]; + having: Having[] | HavingV5; limit: number | null; - stepInterval: number; + stepInterval: number | undefined; orderBy: OrderByPayload[]; - reduceTo: ReduceOperators; + reduceTo?: ReduceOperators; legend: string; pageSize?: number; offset?: number; - selectColumns?: BaseAutocompleteData[]; + selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[]; }; export interface IClickHouseQuery { diff --git a/frontend/src/types/api/querySuggestions/types.ts b/frontend/src/types/api/querySuggestions/types.ts new file mode 100644 index 000000000000..73f5092e7b04 --- /dev/null +++ b/frontend/src/types/api/querySuggestions/types.ts @@ -0,0 +1,47 @@ +import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants'; + +export interface QueryKeyDataSuggestionsProps { + label: string; + type: string; + info?: string; + apply?: string; + detail?: string; + fieldContext?: 'resource' | 'scope' | 'attribute' | 'span'; + fieldDataType?: QUERY_BUILDER_KEY_TYPES; + name: string; + signal: 'traces' | 'logs' | 'metrics'; +} + +export interface QueryKeySuggestionsResponseProps { + status: string; + data: { + complete: boolean; + keys: { + [key: string]: QueryKeyDataSuggestionsProps[]; + }; + }; +} + +export interface QueryKeyRequestProps { + signal: 'traces' | 'logs' | 'metrics'; + searchText: string; + fieldContext?: 'resource' | 'scope' | 'attribute' | 'span'; + fieldDataType?: QUERY_BUILDER_KEY_TYPES; + metricName?: string; +} + +export interface QueryKeyValueSuggestionsProps { + id: string; + name: string; +} + +export interface QueryKeyValueSuggestionsResponseProps { + status: string; + data: QueryKeyValueSuggestionsProps[]; +} + +export interface QueryKeyValueRequestProps { + signal: 'traces' | 'logs' | 'metrics'; + key: string; + searchText: string; +} diff --git a/frontend/src/types/api/v5/queryRange.ts b/frontend/src/types/api/v5/queryRange.ts new file mode 100644 index 000000000000..958a9351ed9a --- /dev/null +++ b/frontend/src/types/api/v5/queryRange.ts @@ -0,0 +1,429 @@ +// ===================== Base Types ===================== + +export type Step = string | number; // Duration string (e.g., "30s") or seconds as number + +export type RequestType = + | 'scalar' + | 'time_series' + | 'trace' + | 'raw' + | 'distribution' + | ''; + +export type QueryType = + | 'builder_query' + | 'builder_formula' + | 'builder_sub_query' + | 'builder_join' + | 'clickhouse_sql' + | 'promql'; + +export type OrderDirection = 'asc' | 'desc'; + +export type JoinType = 'inner' | 'left' | 'right' | 'full' | 'cross'; + +export type SignalType = 'traces' | 'logs' | 'metrics'; + +export type DataType = 'string' | 'number' | 'boolean' | 'array'; + +export type FieldType = + | 'resource' + | 'attribute' + | 'instrumentation_library' + | 'span'; + +export type FieldContext = + | 'metric' + | 'log' + | 'span' + | 'trace' + | 'resource' + | 'scope' + | 'attribute' + | 'event' + | ''; + +export type FieldDataType = + | 'string' + | 'bool' + | 'float64' + | 'int64' + | 'number' + | '[]string' + | '[]float64' + | '[]bool' + | '[]int64' + | '[]number' + | ''; + +export type FunctionName = + | 'cutOffMin' + | 'cutOffMax' + | 'clampMin' + | 'clampMax' + | 'absolute' + | 'runningDiff' + | 'log2' + | 'log10' + | 'cumSum' + | 'ewma3' + | 'ewma5' + | 'ewma7' + | 'median3' + | 'median5' + | 'median7' + | 'timeShift' + | 'anomaly'; + +export type Temporality = 'cumulative' | 'delta' | ''; + +export type MetricType = + | 'gauge' + | 'sum' + | 'histogram' + | 'summary' + | 'exponential_histogram' + | ''; + +export type TimeAggregation = + | 'latest' + | 'sum' + | 'avg' + | 'min' + | 'max' + | 'count' + | 'count_distinct' + | 'rate' + | 'increase' + | ''; + +export type SpaceAggregation = + | 'sum' + | 'avg' + | 'min' + | 'max' + | 'count' + | 'p50' + | 'p75' + | 'p90' + | 'p95' + | 'p99' + | ''; + +export type ColumnType = 'group' | 'aggregation'; + +// ===================== Variable Types ===================== + +export type VariableType = 'query' | 'dynamic' | 'custom' | 'text'; + +export interface VariableItem { + type?: VariableType; + value: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +// ===================== Core Interface Types ===================== + +export interface TelemetryFieldKey { + name: string; + description?: string; + unit?: string; + signal?: SignalType; + fieldContext?: FieldContext; + fieldDataType?: FieldDataType; + materialized?: boolean; + isColumn?: boolean; + isJSON?: boolean; + isIndexed?: boolean; +} + +export interface Filter { + expression: string; +} + +export interface Having { + expression: string; +} + +export type GroupByKey = TelemetryFieldKey; + +export interface OrderBy { + key: TelemetryFieldKey; + direction: OrderDirection; +} + +export interface LimitBy { + keys: string[]; + value: string; +} + +export interface QueryRef { + name: string; +} + +export interface FunctionArg { + name?: string; + value: string | number; +} + +export interface QueryFunction { + name: FunctionName; + args?: FunctionArg[]; +} + +// ===================== Aggregation Types ===================== + +export interface TraceAggregation { + expression: string; + alias?: string; +} + +export interface LogAggregation { + expression: string; + alias?: string; +} + +export interface MetricAggregation { + metricName: string; + temporality: Temporality; + timeAggregation: TimeAggregation; + spaceAggregation: SpaceAggregation; + reduceTo?: string; +} + +export interface SecondaryAggregation { + stepInterval?: Step; + expression: string; + alias?: string; + groupBy?: GroupByKey[]; + order?: OrderBy[]; + limit?: number; + limitBy?: LimitBy; +} + +// ===================== Query Types ===================== + +export interface BaseBuilderQuery { + name?: string; + stepInterval?: Step; + disabled?: boolean; + filter?: Filter; + groupBy?: GroupByKey[]; + order?: OrderBy[]; + selectFields?: TelemetryFieldKey[]; + limit?: number; + limitBy?: LimitBy; + offset?: number; + cursor?: string; + having?: Having; + secondaryAggregations?: SecondaryAggregation[]; + functions?: QueryFunction[]; + legend?: string; +} + +export interface TraceBuilderQuery extends BaseBuilderQuery { + signal: 'traces'; + aggregations?: TraceAggregation[]; +} + +export interface LogBuilderQuery extends BaseBuilderQuery { + signal: 'logs'; + aggregations?: LogAggregation[]; +} + +export interface MetricBuilderQuery extends BaseBuilderQuery { + signal: 'metrics'; + aggregations?: MetricAggregation[]; +} + +export type BuilderQuery = + | TraceBuilderQuery + | LogBuilderQuery + | MetricBuilderQuery; + +export interface QueryBuilderFormula { + name: string; + expression: string; + functions?: QueryFunction[]; + order?: OrderBy[]; + limit?: number; + having?: Having; + legend?: string; +} + +export interface QueryBuilderJoin { + name: string; + disabled?: boolean; + left: QueryRef; + right: QueryRef; + type: JoinType; + on: string; + aggregations?: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any + selectFields?: TelemetryFieldKey[]; + filter?: Filter; + groupBy?: GroupByKey[]; + having?: Having; + order?: OrderBy[]; + limit?: number; + secondaryAggregations?: SecondaryAggregation[]; + functions?: QueryFunction[]; +} + +export interface PromQuery { + name: string; + query: string; + disabled?: boolean; + step?: Step; + stats?: boolean; + legend?: string; +} + +export interface ClickHouseQuery { + name: string; + query: string; + disabled?: boolean; + legend?: string; +} + +// ===================== Query Envelope ===================== + +export interface QueryEnvelope { + type: QueryType; + spec: + | BuilderQuery // Will be same for both builder_query and builder_sub_query + | QueryBuilderFormula + | QueryBuilderJoin + | PromQuery + | ClickHouseQuery; +} + +export interface CompositeQuery { + queries: QueryEnvelope[]; +} + +// ===================== Request Types ===================== + +export interface QueryRangeRequestV5 { + schemaVersion: string; + start: number; // epoch milliseconds + end: number; // epoch milliseconds + requestType: RequestType; + compositeQuery: CompositeQuery; + variables?: Record; + formatOptions?: { + formatTableResultForUI: boolean; + fillGaps?: boolean; + }; +} + +// ===================== Response Types ===================== + +export interface ExecStats { + rowsScanned: number; + bytesScanned: number; + durationMs: number; +} + +export interface Label { + key: TelemetryFieldKey; + value: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface Bucket { + step: number; +} + +export interface TimeSeriesValue { + timestamp: number; // Unix timestamp in milliseconds + value: number; + values?: number[]; // For heatmap type charts + bucket?: Bucket; + partial?: boolean; +} + +export interface TimeSeries { + labels?: Label[]; + values: TimeSeriesValue[]; +} + +export interface AggregationBucket { + index: number; + alias: string; + meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + series: TimeSeries[]; + predictedSeries?: TimeSeries[]; + upperBoundSeries?: TimeSeries[]; + lowerBoundSeries?: TimeSeries[]; + anomalyScores?: TimeSeries[]; +} + +export interface TimeSeriesData { + queryName: string; + aggregations: AggregationBucket[]; +} + +export interface ColumnDescriptor extends TelemetryFieldKey { + queryName: string; + aggregationIndex: number; + columnType: ColumnType; + meta?: { + unit?: string; + }; +} + +export interface ScalarData { + columns: ColumnDescriptor[]; + data: any[][]; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface RawRow { + timestamp: string; // ISO date-time + data: Record; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface RawData { + queryName: string; + nextCursor?: string; + rows: RawRow[]; +} + +export interface DistributionData { + // Structure to be defined based on requirements + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +// Response data structures with results array +export interface TimeSeriesResponseData { + results: TimeSeriesData[]; +} + +export interface ScalarResponseData { + results: ScalarData[]; +} + +export interface RawResponseData { + results: RawData[]; +} + +export interface DistributionResponseData { + results: DistributionData[]; +} + +export type QueryRangeDataV5 = + | TimeSeriesResponseData + | ScalarResponseData + | RawResponseData + | DistributionResponseData; + +export interface QueryRangeResponseV5 { + type: RequestType; + data: QueryRangeDataV5; + meta: ExecStats; +} + +// ===================== Payload Types for API Functions ===================== + +export type QueryRangePayloadV5 = QueryRangeRequestV5; + +export interface MetricRangePayloadV5 { + data: QueryRangeResponseV5; +} diff --git a/frontend/src/types/api/widgets/getQuery.ts b/frontend/src/types/api/widgets/getQuery.ts index c7e4b43d233d..00e8b8612d8c 100644 --- a/frontend/src/types/api/widgets/getQuery.ts +++ b/frontend/src/types/api/widgets/getQuery.ts @@ -30,6 +30,11 @@ export interface QueryData { [key: string]: string; }[]; }; + metaData?: { + alias: string; + index: number; + queryName: string; + }; } export interface SeriesItem { @@ -38,6 +43,18 @@ export interface SeriesItem { }; labelsArray: { [key: string]: string }[]; values: { timestamp: number; value: string }[]; + metaData?: { + alias: string; + index: number; + queryName: string; + }; +} + +export interface Column { + name: string; + queryName: string; + isValueColumn: boolean; + id?: string; } export interface QueryDataV3 { @@ -53,6 +70,14 @@ export interface QueryDataV3 { predictedSeries?: SeriesItem[] | null; anomalyScores?: SeriesItem[] | null; isAnomaly?: boolean; + table?: { + rows: { + data: { + [key: string]: any; + }; + }[]; + columns: Column[]; + }; } export interface Props { diff --git a/frontend/src/types/common/operations.types.ts b/frontend/src/types/common/operations.types.ts index 9421509e9f1d..8e4f426a7dc0 100644 --- a/frontend/src/types/common/operations.types.ts +++ b/frontend/src/types/common/operations.types.ts @@ -6,6 +6,12 @@ import { IBuilderQuery, QueryFunctionProps, } from 'types/api/queryBuilder/queryBuilderData'; +import { + BaseBuilderQuery, + LogBuilderQuery, + MetricBuilderQuery, + TraceBuilderQuery, +} from 'types/api/v5/queryRange'; import { DataSource } from 'types/common/queryBuilder'; import { SelectOption } from './select'; @@ -17,14 +23,23 @@ type UseQueryOperationsParams = Pick & entityVersion: string; }; -export type HandleChangeQueryData = < - Key extends keyof IBuilderQuery, - Value extends IBuilderQuery[Key] +// Generic type that can work with both legacy and V5 query types +export type HandleChangeQueryData = < + Key extends keyof T, + Value extends T[Key] >( key: Key, value: Value, ) => void; +// Legacy version for backward compatibility +export type HandleChangeQueryDataLegacy = HandleChangeQueryData; + +// V5 version for new API +export type HandleChangeQueryDataV5 = HandleChangeQueryData< + BaseBuilderQuery & (TraceBuilderQuery | LogBuilderQuery | MetricBuilderQuery) +>; + export type HandleChangeFormulaData = < Key extends keyof IBuilderFormula, Value extends IBuilderFormula[Key] diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts index 0ff38cc4a0c3..09a6d35d0bd8 100644 --- a/frontend/src/types/common/queryBuilder.ts +++ b/frontend/src/types/common/queryBuilder.ts @@ -227,7 +227,10 @@ export type QueryBuilderContextType = { redirectToUrl?: typeof ROUTES[keyof typeof ROUTES], shallStringify?: boolean, ) => void; - handleRunQuery: (shallUpdateStepInterval?: boolean) => void; + handleRunQuery: ( + shallUpdateStepInterval?: boolean, + newQBQuery?: boolean, + ) => void; resetQuery: (newCurrentQuery?: QueryState) => void; handleOnUnitsChange: (units: Format['id']) => void; updateAllQueriesOperators: ( diff --git a/frontend/src/utils/aggregationConverter.ts b/frontend/src/utils/aggregationConverter.ts new file mode 100644 index 000000000000..a07c09828cf9 --- /dev/null +++ b/frontend/src/utils/aggregationConverter.ts @@ -0,0 +1,139 @@ +import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5'; +import { + BaseAutocompleteData, + DataTypes, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + LogAggregation, + MetricAggregation, + TraceAggregation, +} from 'types/api/v5/queryRange'; +import { DataSource } from 'types/common/queryBuilder'; +import { v4 as uuid } from 'uuid'; + +/** + * Converts QueryV2 aggregations to BaseAutocompleteData format + * for compatibility with existing OrderByFilter component + */ +export function convertAggregationsToBaseAutocompleteData( + aggregations: + | TraceAggregation[] + | LogAggregation[] + | MetricAggregation[] + | undefined, + dataSource: DataSource, + metricName?: string, + spaceAggregation?: string, +): BaseAutocompleteData[] { + // If no aggregations provided, return default based on data source + if (!aggregations || aggregations.length === 0) { + switch (dataSource) { + case DataSource.METRICS: + return [ + { + id: uuid(), + dataType: DataTypes.Float64, + isColumn: false, + type: '', + isJSON: false, + key: `${spaceAggregation || 'avg'}(${metricName || 'metric'})`, + }, + ]; + case DataSource.TRACES: + case DataSource.LOGS: + default: + return [ + { + id: uuid(), + dataType: DataTypes.Float64, + isColumn: false, + type: '', + isJSON: false, + key: 'count()', + }, + ]; + } + } + + return aggregations.map((agg) => { + if ('expression' in agg) { + // TraceAggregation or LogAggregation + const { expression } = agg; + const alias = 'alias' in agg ? agg.alias : ''; + const displayKey = alias || expression; + + return { + id: uuid(), + dataType: DataTypes.Float64, + isColumn: false, + type: '', + isJSON: false, + key: displayKey, + }; + } + // MetricAggregation + const { + metricName: aggMetricName, + spaceAggregation: aggSpaceAggregation, + } = agg; + const displayKey = `${aggSpaceAggregation}(${aggMetricName})`; + + return { + id: uuid(), + dataType: DataTypes.Float64, + isColumn: false, + type: '', + isJSON: false, + key: displayKey, + }; + }); +} + +/** + * Helper function to get aggregation options for OrderByFilter + * This creates BaseAutocompleteData that can be used with the existing OrderByFilter + */ +export function getAggregationOptionsForOrderBy(query: { + aggregations?: TraceAggregation[] | LogAggregation[] | MetricAggregation[]; + dataSource: DataSource; + aggregateAttribute?: { key: string }; + spaceAggregation?: string; +}): BaseAutocompleteData[] { + const { + aggregations, + dataSource, + aggregateAttribute, + spaceAggregation, + } = query; + + return convertAggregationsToBaseAutocompleteData( + aggregations, + dataSource, + aggregateAttribute?.key, + spaceAggregation, + ); +} + +/** + * Enhanced function that uses createAggregation to parse aggregations first + * then converts them to BaseAutocompleteData format for OrderByFilter + */ +export function getParsedAggregationOptionsForOrderBy(query: { + aggregations?: TraceAggregation[] | LogAggregation[] | MetricAggregation[]; + dataSource: DataSource; + aggregateAttribute?: { key: string }; + spaceAggregation?: string; + timeAggregation?: string; + temporality?: string; +}): BaseAutocompleteData[] { + // First, use createAggregation to parse the aggregations + const parsedAggregations = createAggregation(query); + + // Then convert the parsed aggregations to BaseAutocompleteData format + return convertAggregationsToBaseAutocompleteData( + parsedAggregations, + query.dataSource, + query.aggregateAttribute?.key, + query.spaceAggregation, + ); +} diff --git a/frontend/src/utils/antlrQueryUtils.ts b/frontend/src/utils/antlrQueryUtils.ts new file mode 100644 index 000000000000..7e6aafd41369 --- /dev/null +++ b/frontend/src/utils/antlrQueryUtils.ts @@ -0,0 +1,895 @@ +/* eslint-disable sonarjs/no-collapsible-if */ +/* eslint-disable no-continue */ +/* eslint-disable sonarjs/cognitive-complexity */ +import { CharStreams, CommonTokenStream } from 'antlr4'; +import FilterQueryLexer from 'parser/FilterQueryLexer'; +import FilterQueryParser from 'parser/FilterQueryParser'; +import { + IDetailedError, + IQueryContext, + IToken, + IValidationResult, +} from 'types/antlrQueryTypes'; + +// Custom error listener to capture ANTLR errors +class QueryErrorListener { + private errors: IDetailedError[] = []; + + syntaxError( + _recognizer: any, + offendingSymbol: any, + line: number, + column: number, + msg: string, + ): void { + // For unterminated quotes, we only want to show one error + if (this.hasUnterminatedQuoteError() && msg.includes('expecting')) { + return; + } + + const error: IDetailedError = { + message: msg, + line, + column, + offendingSymbol: offendingSymbol?.text || String(offendingSymbol), + }; + + // Extract expected tokens if available + if (msg.includes('expecting')) { + const expectedTokens = msg + .split('expecting')[1] + .trim() + .split(',') + .map((token) => token.trim()); + error.expectedTokens = expectedTokens; + } + + // Check if this is a duplicate error (same location and similar message) + const isDuplicate = this.errors.some( + (e) => + e.line === line && + e.column === column && + this.isSimilarError(e.message, msg), + ); + + if (!isDuplicate) { + this.errors.push(error); + } + } + + private hasUnterminatedQuoteError(): boolean { + return this.errors.some( + (error) => + error.message.includes('unterminated') || + (error.message.includes('missing') && error.message.includes("'")), + ); + } + + private isSimilarError = (msg1: string, msg2: string): boolean => { + // Consider errors similar if they're for the same core issue + const normalize = (msg: string): string => + msg.toLowerCase().replace(/['"`]/g, 'quote').replace(/\s+/g, ' ').trim(); + + return normalize(msg1) === normalize(msg2); + }; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + reportAmbiguity = (): void => {}; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + reportAttemptingFullContext = (): void => {}; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + reportContextSensitivity = (): void => {}; + + getErrors(): IDetailedError[] { + return this.errors; + } + + hasErrors(): boolean { + return this.errors.length > 0; + } + + getFormattedErrors(): string[] { + return this.errors.map((error) => { + const { + offendingSymbol, + expectedTokens, + message: errorMessage, + line, + column, + } = error; + + let message = `Line ${line}:${column} - ${errorMessage}`; + + if (offendingSymbol && offendingSymbol !== 'undefined') { + message += `\n Symbol: '${offendingSymbol}'`; + } + + if (expectedTokens && expectedTokens.length > 0) { + message += `\n Expected: ${expectedTokens.join(', ')}`; + } + + return message; + }); + } +} + +export const validateQuery = (query: string): IValidationResult => { + // Empty query is considered invalid + if (!query.trim()) { + return { + isValid: true, + message: 'Query is empty', + errors: [], + }; + } + + try { + const errorListener = new QueryErrorListener(); + const inputStream = CharStreams.fromString(query); + + // Setup lexer + const lexer = new FilterQueryLexer(inputStream); + lexer.removeErrorListeners(); // Remove default error listeners + lexer.addErrorListener(errorListener); + + // Setup parser + const tokenStream = new CommonTokenStream(lexer); + const parser = new FilterQueryParser(tokenStream); + parser.removeErrorListeners(); // Remove default error listeners + parser.addErrorListener(errorListener); + + // Try parsing + parser.query(); + + // Check if any errors were captured + if (errorListener.hasErrors()) { + return { + isValid: false, + message: 'Query syntax error', + errors: errorListener.getErrors(), + }; + } + + return { + isValid: true, + message: 'Query is valid!', + errors: [], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Invalid query syntax'; + + const detailedError: IDetailedError = { + message: errorMessage, + line: 0, + column: 0, + offendingSymbol: '', + expectedTokens: [], + }; + return { + isValid: false, + message: 'Invalid query syntax', + errors: [detailedError], + }; + } +}; + +// Helper function to find key-operator-value triplets in token stream +export function findKeyOperatorValueTriplet( + allTokens: IToken[], + currentToken: IToken, + isInKey: boolean, + isInOperator: boolean, + isInValue: boolean, +): { keyToken?: string; operatorToken?: string; valueToken?: string } { + // Find current token index in allTokens + let currentTokenIndex = -1; + for (let i = 0; i < allTokens.length; i++) { + if ( + allTokens[i].start === currentToken.start && + allTokens[i].stop === currentToken.stop && + allTokens[i].type === currentToken.type + ) { + currentTokenIndex = i; + break; + } + } + + if (currentTokenIndex === -1) return {}; + + // Initialize result with empty object + const result: { + keyToken?: string; + operatorToken?: string; + valueToken?: string; + } = {}; + + if (isInKey) { + // When in key context, we only know the key + result.keyToken = currentToken.text; + } else if (isInOperator) { + // When in operator context, we know the operator and can find the preceding key + result.operatorToken = currentToken.text; + + // Look backward for key + for (let i = currentTokenIndex - 1; i >= 0; i--) { + const token = allTokens[i]; + // Skip whitespace and other hidden channel tokens + if (token.channel !== 0) continue; + + if (token.type === FilterQueryLexer.KEY) { + result.keyToken = token.text; + break; + } + } + } else if (isInValue) { + // When in value context, we know the value and can find the preceding operator and key + result.valueToken = currentToken.text; + + let foundOperator = false; + + // Look backward for operator and key + for (let i = currentTokenIndex - 1; i >= 0; i--) { + const token = allTokens[i]; + // Skip whitespace and other hidden channel tokens + if (token.channel !== 0) continue; + + // If we haven't found an operator yet, check for operator + if ( + !foundOperator && + [ + FilterQueryLexer.EQUALS, + FilterQueryLexer.NOT_EQUALS, + FilterQueryLexer.NEQ, + FilterQueryLexer.LT, + FilterQueryLexer.LE, + FilterQueryLexer.GT, + FilterQueryLexer.GE, + FilterQueryLexer.LIKE, + // FilterQueryLexer.NOT_LIKE, + FilterQueryLexer.ILIKE, + // FilterQueryLexer.NOT_ILIKE, + FilterQueryLexer.BETWEEN, + FilterQueryLexer.EXISTS, + FilterQueryLexer.REGEXP, + FilterQueryLexer.CONTAINS, + FilterQueryLexer.IN, + FilterQueryLexer.NOT, + ].includes(token.type) + ) { + result.operatorToken = token.text; + foundOperator = true; + } + // If we already found an operator and this is a key, record it + else if (foundOperator && token.type === FilterQueryLexer.KEY) { + result.keyToken = token.text; + break; // We found our triplet + } + } + } + + return result; +} + +export function getQueryContextAtCursor( + query: string, + cursorIndex: number, +): IQueryContext { + try { + // Create input stream and lexer + const input = query || ''; + const chars = CharStreams.fromString(input); + const lexer = new FilterQueryLexer(chars); + + // Create token stream and force token generation + const tokenStream = new CommonTokenStream(lexer); + tokenStream.fill(); + + // Get all tokens including whitespace + const allTokens = tokenStream.tokens as IToken[]; + + // Find exact token at cursor, including whitespace + let exactToken: IToken | null = null; + let previousToken: IToken | null = null; + let nextToken: IToken | null = null; + + // Handle cursor at the very end of input + if (cursorIndex === input.length && allTokens.length > 0) { + const lastRealToken = allTokens + .filter((t) => t.type !== FilterQueryLexer.EOF) + .pop(); + if (lastRealToken) { + exactToken = lastRealToken; + previousToken = + allTokens.filter((t) => t.stop < lastRealToken.start).pop() || null; + } + } else { + // Normal token search + for (let i = 0; i < allTokens.length; i++) { + const token = allTokens[i]; + // Skip EOF token in normal search + if (token.type === FilterQueryLexer.EOF) { + continue; + } + + // Check if cursor is within token bounds (inclusive) + if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) { + exactToken = token; + previousToken = i > 0 ? allTokens[i - 1] : null; + nextToken = i < allTokens.length - 1 ? allTokens[i + 1] : null; + break; + } + } + + // If cursor is between tokens, find surrounding tokens + if (!exactToken) { + for (let i = 0; i < allTokens.length - 1; i++) { + const current = allTokens[i]; + const next = allTokens[i + 1]; + if (current.type === FilterQueryLexer.EOF) { + continue; + } + if (next.type === FilterQueryLexer.EOF) { + continue; + } + + if (current.stop + 1 < cursorIndex && cursorIndex < next.start) { + previousToken = current; + nextToken = next; + break; + } + } + } + } + + // Determine the context based on cursor position and surrounding tokens + let currentToken: IToken | null = null; + + if (exactToken) { + // If cursor is in a non-whitespace token, use that + if (exactToken.channel === 0) { + currentToken = exactToken; + } else { + // If in whitespace, use the previous non-whitespace token + currentToken = previousToken?.channel === 0 ? previousToken : nextToken; + } + } else if (previousToken?.channel === 0) { + // If between tokens, prefer the previous non-whitespace token + currentToken = previousToken; + } else if (nextToken?.channel === 0) { + // Otherwise use the next non-whitespace token + currentToken = nextToken; + } + + // If still no token (empty query or all whitespace), return default context + if (!currentToken) { + // Handle transitions based on spaces and current state + if (query.trim() === '') { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: true, // Default to key context when input is empty + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + }; + } + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInNegation: false, + isInKey: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + }; + } + + // Determine if the current token is a conjunction (AND or OR) + const isInConjunction = [FilterQueryLexer.AND, FilterQueryLexer.OR].includes( + currentToken.type, + ); + + // Determine if the current token is a parenthesis or bracket + const isInParenthesis = [ + FilterQueryLexer.LPAREN, + FilterQueryLexer.RPAREN, + FilterQueryLexer.LBRACK, + FilterQueryLexer.RBRACK, + ].includes(currentToken.type); + + // Determine the context based on the token type + const isInValue = [ + FilterQueryLexer.QUOTED_TEXT, + FilterQueryLexer.NUMBER, + FilterQueryLexer.BOOL, + ].includes(currentToken.type); + + const isInKey = currentToken.type === FilterQueryLexer.KEY; + + const isInNegation = currentToken.type === FilterQueryLexer.NOT; + + const isInOperator = [ + FilterQueryLexer.EQUALS, + FilterQueryLexer.NOT_EQUALS, + FilterQueryLexer.NEQ, + FilterQueryLexer.LT, + FilterQueryLexer.LE, + FilterQueryLexer.GT, + FilterQueryLexer.GE, + FilterQueryLexer.LIKE, + // FilterQueryLexer.NOT_LIKE, + FilterQueryLexer.ILIKE, + // FilterQueryLexer.NOT_ILIKE, + FilterQueryLexer.BETWEEN, + FilterQueryLexer.EXISTS, + FilterQueryLexer.REGEXP, + FilterQueryLexer.CONTAINS, + FilterQueryLexer.IN, + FilterQueryLexer.NOT, + ].includes(currentToken.type); + + const isInFunction = [ + FilterQueryLexer.HAS, + FilterQueryLexer.HASANY, + FilterQueryLexer.HASALL, + // FilterQueryLexer.HASNONE, + ].includes(currentToken.type); + + // Get the context-related tokens (key, operator, value) + const relationTokens = findKeyOperatorValueTriplet( + allTokens, + currentToken, + isInKey, + isInOperator, + isInValue, + ); + + // Handle transitions based on spaces + // When a user adds a space after a token, change the context accordingly + if ( + currentToken && + cursorIndex === currentToken.stop + 2 && + query[currentToken.stop + 1] === ' ' + ) { + // User added a space right after this token + + if (isInKey) { + // After a key + space, we should be in operator context + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: true, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if (isInOperator) { + // After an operator + space, we should be in value context + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: true, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if (isInValue) { + // After a value + space, we should be in conjunction context + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: true, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if (isInConjunction) { + // After a conjunction + space, we should be in key context again + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInNegation: false, + isInKey: true, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if (isInParenthesis) { + // After a parenthesis/bracket + space, determine context based on which bracket + if (currentToken.type === FilterQueryLexer.LPAREN) { + // After an opening parenthesis + space, we should be in key context + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInNegation: false, + isInKey: true, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, + }; + } + + if ( + currentToken.type === FilterQueryLexer.RPAREN || + currentToken.type === FilterQueryLexer.RBRACK + ) { + // After a closing parenthesis/bracket + space, we should be in conjunction context + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInNegation: false, + isInKey: false, + isInOperator: false, + isInFunction: false, + isInConjunction: true, + isInParenthesis: false, + ...relationTokens, + }; + } + + if (currentToken.type === FilterQueryLexer.LBRACK) { + // After an opening bracket + space, we should be in value context (for arrays) + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: true, + isInNegation: false, + isInKey: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, + }; + } + } + } + + // Add logic for context detection that works for both forward and backward navigation + // This handles both cases: when user is typing forward and when they're moving backward + if (previousToken && nextToken) { + // Determine context based on token sequence pattern + + // Key -> Operator -> Value -> Conjunction pattern detection + if (isInKey && nextToken.type === FilterQueryLexer.EQUALS) { + // When cursor is on a key and next token is an operator + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInKey: true, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if (isInNegation && nextToken.type === FilterQueryLexer.NOT) { + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInKey: false, + isInNegation: true, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if ( + isInOperator && + previousToken.type === FilterQueryLexer.KEY && + (nextToken.type === FilterQueryLexer.QUOTED_TEXT || + nextToken.type === FilterQueryLexer.NUMBER || + nextToken.type === FilterQueryLexer.BOOL) + ) { + // When cursor is on an operator between a key and value + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: true, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if ( + isInValue && + previousToken.type !== FilterQueryLexer.AND && + previousToken.type !== FilterQueryLexer.OR && + (nextToken.type === FilterQueryLexer.AND || + nextToken.type === FilterQueryLexer.OR) + ) { + // When cursor is on a value and next token is a conjunction + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: true, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if ( + isInConjunction && + (previousToken.type === FilterQueryLexer.QUOTED_TEXT || + previousToken.type === FilterQueryLexer.NUMBER || + previousToken.type === FilterQueryLexer.BOOL) && + nextToken.type === FilterQueryLexer.KEY + ) { + // When cursor is on a conjunction between a value and a key + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: true, + isInParenthesis: false, + }; + } + } + + // If we're in between tokens (no exact token match), use next token type to determine context + if (!exactToken && nextToken) { + if (nextToken.type === FilterQueryLexer.KEY) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: true, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if (nextToken.type === FilterQueryLexer.NOT) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: false, + isInNegation: true, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if ( + [ + FilterQueryLexer.EQUALS, + FilterQueryLexer.NOT_EQUALS, + FilterQueryLexer.GT, + FilterQueryLexer.LT, + FilterQueryLexer.GE, + FilterQueryLexer.LE, + ].includes(nextToken.type) + ) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: true, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if ( + [ + FilterQueryLexer.QUOTED_TEXT, + FilterQueryLexer.NUMBER, + FilterQueryLexer.BOOL, + ].includes(nextToken.type) + ) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInNegation: false, + isInValue: true, + isInKey: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + if ([FilterQueryLexer.AND, FilterQueryLexer.OR].includes(nextToken.type)) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: true, + isInParenthesis: false, + ...relationTokens, // Include related tokens + }; + } + + // Add case for parentheses and brackets + if ( + [ + FilterQueryLexer.LPAREN, + FilterQueryLexer.RPAREN, + FilterQueryLexer.LBRACK, + FilterQueryLexer.RBRACK, + ].includes(nextToken.type) + ) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: true, + ...relationTokens, // Include related tokens + }; + } + } + + // Fall back to default context detection based on current token + return { + tokenType: currentToken.type, + text: currentToken.text, + start: currentToken.start, + stop: currentToken.stop, + currentToken: currentToken.text, + isInValue, + isInKey, + isInNegation, + isInOperator, + isInFunction, + isInConjunction, + isInParenthesis, + ...relationTokens, // Include related tokens + }; + } catch (error) { + console.error('Error in getQueryContextAtCursor:', error); + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + }; + } +} diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index d4227f406391..726c33a9d698 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -1,6 +1,7 @@ import getLocalStorage from 'api/browser/localstorage/get'; import { FeatureKeys } from 'constants/features'; import { SKIP_ONBOARDING } from 'constants/onboarding'; +import { get } from 'lodash-es'; export const isOnboardingSkipped = (): boolean => getLocalStorage(SKIP_ONBOARDING) === 'true'; @@ -26,3 +27,13 @@ export const FORBID_DOM_PURIFY_TAGS = ['img', 'form']; export const isFeatureKeys = (key: string): key is keyof typeof FeatureKeys => Object.keys(FeatureKeys).includes(key); + +export function isIngestionActive(data: any): boolean { + const table = get(data, 'data.newResult.data.result[0].table'); + if (!table) return false; + + const key = get(table, 'columns[0].id'); + const value = get(table, `rows[0].data["${key}"]`) || '0'; + + return parseInt(value, 10) > 0; +} diff --git a/frontend/src/utils/compositeQueryToQueryEnvelope.ts b/frontend/src/utils/compositeQueryToQueryEnvelope.ts new file mode 100644 index 000000000000..a9060a5f2a20 --- /dev/null +++ b/frontend/src/utils/compositeQueryToQueryEnvelope.ts @@ -0,0 +1,100 @@ +import { + convertBuilderQueriesToV5, + convertClickHouseQueriesToV5, + convertPromQueriesToV5, + mapPanelTypeToRequestType, +} from 'api/v5/queryRange/prepareQueryRangePayloadV5'; +import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; +import { BuilderQueryDataResourse } from 'types/api/queryBuilder/queryBuilderData'; +import { OrderBy, QueryEnvelope } from 'types/api/v5/queryRange'; + +function convertFormulasToV5( + formulas: BuilderQueryDataResourse, +): QueryEnvelope[] { + return Object.entries(formulas).map( + ([queryName, formulaData]): QueryEnvelope => ({ + type: 'builder_formula' as const, + spec: { + name: queryName, + expression: formulaData.expression || '', + disabled: formulaData.disabled, + limit: formulaData.limit ?? undefined, + legend: formulaData.legend, + order: formulaData.orderBy?.map( + (order: any): OrderBy => ({ + key: { + name: order.columnName, + }, + direction: order.order, + }), + ), + }, + }), + ); +} + +export function compositeQueryToQueryEnvelope( + compositeQuery: ICompositeMetricQuery, +): ICompositeMetricQuery { + const { + builderQueries, + promQueries, + chQueries, + panelType, + queryType, + } = compositeQuery; + + const regularQueries: BuilderQueryDataResourse = {}; + const formulaQueries: BuilderQueryDataResourse = {}; + + Object.entries(builderQueries || {}).forEach(([queryName, queryData]) => { + if ('dataSource' in queryData) { + regularQueries[queryName] = queryData; + } else { + formulaQueries[queryName] = queryData; + } + }); + + const requestType = mapPanelTypeToRequestType(panelType); + + const builderQueriesV5 = convertBuilderQueriesToV5( + regularQueries, + requestType, + panelType, + ); + const formulaQueriesV5 = convertFormulasToV5(formulaQueries); + + const promQueriesV5 = convertPromQueriesToV5(promQueries || {}); + const chQueriesV5 = convertClickHouseQueriesToV5(chQueries || {}); + + // Conditionally include queries based on queryType + let queries: QueryEnvelope[] = []; + + switch (queryType) { + case 'builder': + queries = [...builderQueriesV5, ...formulaQueriesV5]; + break; + case 'promql': + queries = [...promQueriesV5]; + break; + case 'clickhouse_sql': + queries = [...chQueriesV5]; + break; + default: + // Fallback to include all queries if queryType is not recognized + queries = [ + ...builderQueriesV5, + ...formulaQueriesV5, + ...promQueriesV5, + ...chQueriesV5, + ]; + } + + return { + ...compositeQuery, + queries, + builderQueries: undefined, + promQueries: undefined, + chQueries: undefined, + }; +} diff --git a/frontend/src/utils/convertNewToOldQueryBuilder.ts b/frontend/src/utils/convertNewToOldQueryBuilder.ts new file mode 100644 index 000000000000..0cf29cc1b464 --- /dev/null +++ b/frontend/src/utils/convertNewToOldQueryBuilder.ts @@ -0,0 +1,72 @@ +import { + IBuilderFormula, + IBuilderQuery, +} from 'types/api/queryBuilder/queryBuilderData'; +import { BuilderQuery, QueryBuilderFormula } from 'types/api/v5/queryRange'; +import { DataSource } from 'types/common/queryBuilder'; + +// Helper functions + +const getDataSourceFromSignal = (signal: string): DataSource => { + switch (signal) { + case 'metrics': + return DataSource.METRICS; + case 'logs': + return DataSource.LOGS; + case 'traces': + return DataSource.TRACES; + default: + return DataSource.METRICS; + } +}; + +/** + * Converts new BuilderQuery to old IBuilderQuery + */ +export const convertBuilderQueryToIBuilderQuery = ( + builderQuery: BuilderQuery, +): IBuilderQuery => { + // Determine data source based on signal + const dataSource = getDataSourceFromSignal(builderQuery.signal); + + const result: IBuilderQuery = ({ + ...builderQuery, + dataSource, + legend: builderQuery.legend, + groupBy: builderQuery.groupBy?.map((group) => ({ + key: group?.name, + dataType: group?.fieldDataType, + type: group?.fieldContext, + isColumn: group?.isColumn ?? true, + isJSON: group?.isJSON || false, + id: `${group?.name}--${group?.fieldDataType}--${group?.fieldContext}--${group?.isColumn}`, + })), + orderBy: builderQuery.order?.map((order) => ({ + columnName: order?.key?.name, + order: order?.direction, + })), + } as unknown) as IBuilderQuery; + + return result; +}; + +/** + * Converts new QueryBuilderFormula to old IBuilderFormula + */ +export const convertQueryBuilderFormulaToIBuilderFormula = ( + formula: QueryBuilderFormula, +): IBuilderFormula => { + const result: IBuilderFormula = ({ + ...formula, + expression: formula.expression, + queryName: formula.name, + legend: formula.legend, + limit: formula.limit || null, + orderBy: formula.order?.map((order) => ({ + columnName: order?.key?.name, + order: order?.direction, + })), + } as unknown) as IBuilderFormula; + + return result; +}; diff --git a/frontend/src/utils/explorerUtils.ts b/frontend/src/utils/explorerUtils.ts new file mode 100644 index 000000000000..a897e2819248 --- /dev/null +++ b/frontend/src/utils/explorerUtils.ts @@ -0,0 +1,45 @@ +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { ExplorerViews } from 'pages/LogsExplorer/utils'; + +// Mapping between panel types and explorer views +export const panelTypeToExplorerView: Record = { + [PANEL_TYPES.LIST]: ExplorerViews.LIST, + [PANEL_TYPES.TIME_SERIES]: ExplorerViews.TIMESERIES, + [PANEL_TYPES.TRACE]: ExplorerViews.TRACE, + [PANEL_TYPES.TABLE]: ExplorerViews.TABLE, + [PANEL_TYPES.VALUE]: ExplorerViews.TIMESERIES, + [PANEL_TYPES.BAR]: ExplorerViews.TIMESERIES, + [PANEL_TYPES.PIE]: ExplorerViews.TIMESERIES, + [PANEL_TYPES.HISTOGRAM]: ExplorerViews.TIMESERIES, + [PANEL_TYPES.EMPTY_WIDGET]: ExplorerViews.LIST, +}; + +/** + * Get the explorer view based on panel type from URL or saved view + * @param searchParams - URL search parameters + * @param panelTypesFromUrl - Panel type extracted from URL + * @returns The appropriate ExplorerViews value + */ +export const getExplorerViewFromUrl = ( + searchParams: URLSearchParams, + panelTypesFromUrl: PANEL_TYPES | null, +): ExplorerViews => { + const savedView = searchParams.get(QueryParams.selectedExplorerView); + if (savedView) { + return savedView as ExplorerViews; + } + + // If no saved view, use panel type from URL to determine the view + const urlPanelType = panelTypesFromUrl || PANEL_TYPES.LIST; + return panelTypeToExplorerView[urlPanelType]; +}; + +/** + * Get the explorer view for a given panel type + * @param panelType - The panel type + * @returns The corresponding ExplorerViews value + */ +export const getExplorerViewForPanelType = ( + panelType: PANEL_TYPES, +): ExplorerViews => panelTypeToExplorerView[panelType]; diff --git a/frontend/src/utils/queryContextUtils.ts b/frontend/src/utils/queryContextUtils.ts new file mode 100644 index 000000000000..330a083f2cc5 --- /dev/null +++ b/frontend/src/utils/queryContextUtils.ts @@ -0,0 +1,1523 @@ +/* eslint-disable */ + +import { CharStreams, CommonTokenStream, Token } from 'antlr4'; +import FilterQueryLexer from 'parser/FilterQueryLexer'; +import { IQueryContext, IQueryPair, IToken } from 'types/antlrQueryTypes'; +import { analyzeQuery } from 'parser/analyzeQuery'; +import { + isBracketToken, + isConjunctionToken, + isFunctionToken, + isKeyToken, + isMultiValueOperator, + isNonValueOperatorToken, + isOperatorToken, + isQueryPairComplete, + isValueToken, +} from './tokenUtils'; +import { NON_VALUE_OPERATORS } from 'constants/antlrQueryConstants'; + +// Function to create a context object +export function createContext( + token: Token, + isInKey: boolean, + isInNegation: boolean, + isInOperator: boolean, + isInValue: boolean, + keyToken?: string, + operatorToken?: string, + valueToken?: string, + queryPairs?: IQueryPair[], + currentPair?: IQueryPair | null, +): IQueryContext { + return { + tokenType: token.type, + text: token.text || '', + start: token.start, + stop: token.stop, + currentToken: token.text || '', + isInKey, + isInNegation, + isInOperator, + isInValue, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + keyToken, + operatorToken, + valueToken, + queryPairs: queryPairs || [], + currentPair, + }; +} + +// Helper to determine token type for context +function determineTokenContext( + token: IToken, + query: string, +): { + isInKey: boolean; + isInNegation: boolean; + isInOperator: boolean; + isInValue: boolean; + isInFunction: boolean; + isInConjunction: boolean; + isInParenthesis: boolean; +} { + let isInKey: boolean = false; + let isInNegation: boolean = false; + let isInOperator: boolean = false; + let isInValue: boolean = false; + let isInFunction: boolean = false; + let isInConjunction: boolean = false; + let isInParenthesis: boolean = false; + + const tokenType = token.type; + const currentTokenContext = analyzeQuery(query, token); + + if (!currentTokenContext) { + // Key context + isInKey = isKeyToken(tokenType); + + // Operator context + isInOperator = isOperatorToken(tokenType); + + // Value context + isInValue = isValueToken(tokenType); + } else { + switch (currentTokenContext.type) { + case 'Operator': + isInOperator = true; + break; + case 'Value': + isInValue = true; + break; + case 'Key': + isInKey = true; + break; + default: + break; + } + } + + // Negation context + isInNegation = tokenType === FilterQueryLexer.NOT; + + // Function context + isInFunction = isFunctionToken(tokenType); + + // Conjunction context + isInConjunction = isConjunctionToken(tokenType); + + // Parenthesis context + isInParenthesis = isBracketToken(tokenType); + + return { + isInKey, + isInNegation, + isInOperator, + isInValue, + isInFunction, + isInConjunction, + isInParenthesis, + }; +} + +export function getCurrentValueIndexAtCursor( + valuesPosition: { + start?: number; + end?: number; + }[], + cursorIndex: number, +): number | null { + if (!valuesPosition || valuesPosition.length === 0) return null; + + // Find the value that contains the cursor index + for (let i = 0; i < valuesPosition.length; i++) { + const start = valuesPosition[i].start; + const end = valuesPosition[i].end; + if ( + start !== undefined && + end !== undefined && + start <= cursorIndex && + cursorIndex <= end + ) { + return i; + } + } + + return null; +} + +// Function to determine token context boundaries more precisely +function determineContextBoundaries( + query: string, + cursorIndex: number, + tokens: IToken[], + queryPairs: IQueryPair[], +): { + keyContext: { start: number; end: number } | null; + operatorContext: { start: number; end: number } | null; + valueContext: { start: number; end: number } | null; + conjunctionContext: { start: number; end: number } | null; + negationContext: { start: number; end: number } | null; + bracketContext: { start: number; end: number; isForList: boolean } | null; +} { + // Find the current query pair based on cursor position + let currentPair: IQueryPair | null = null; + + if (queryPairs.length > 0) { + currentPair = getCurrentQueryPair(queryPairs, query, cursorIndex); + } + + // Check for bracket context first (could be part of an IN operator's value) + let bracketContext: { + start: number; + end: number; + isForList: boolean; + } | null = null; + + // Find bracket tokens that might contain the cursor + const openBrackets: { token: IToken; isForList: boolean }[] = []; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + // Skip tokens on hidden channel + if (token.channel !== 0) continue; + + // Track opening brackets + if ( + token.type === FilterQueryLexer.LBRACK || + token.type === FilterQueryLexer.LPAREN + ) { + // Check if this opening bracket is for a list (used with IN operator) + let isForList = false; + + // Look back to see if this bracket follows an IN operator + if (i > 0) { + for (let j = i - 1; j >= 0; j--) { + const prevToken = tokens[j]; + if (prevToken.channel !== 0) continue; // Skip hidden channel tokens + + if ( + prevToken.type === FilterQueryLexer.IN || + (prevToken.type === FilterQueryLexer.NOT && + j + 1 < tokens.length && + tokens[j + 1].type === FilterQueryLexer.IN) + ) { + isForList = true; + break; + } else if (prevToken.channel === 0 && !isValueToken(prevToken.type)) { + // If we encounter a non-value token that's not IN, stop looking + break; + } + } + } + + openBrackets.push({ token, isForList }); + } + + // If this is a closing bracket, check if cursor is within this bracket pair + else if ( + (token.type === FilterQueryLexer.RBRACK || + token.type === FilterQueryLexer.RPAREN) && + openBrackets.length > 0 + ) { + const matchingOpen = openBrackets.pop(); + + // If cursor is within these brackets and they're for a list + if ( + matchingOpen && + matchingOpen.token.start <= cursorIndex && + cursorIndex <= token.stop + 1 + ) { + bracketContext = { + start: matchingOpen.token.start, + end: token.stop, + isForList: matchingOpen.isForList, + }; + break; + } + + // Check if cursor is right after the closing bracket (with a space) + // We need to handle this case to transition to conjunction context + if ( + matchingOpen && + token.stop + 1 < cursorIndex && + cursorIndex <= token.stop + 2 && + query[token.stop + 1] === ' ' + ) { + // We'll set a special flag to indicate we're after a closing bracket + bracketContext = { + start: matchingOpen.token.start, + end: token.stop, + isForList: matchingOpen.isForList, + }; + break; + } + } + + // If we're at the cursor position and not in a closing bracket check + if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) { + // If cursor is within an opening bracket token + if ( + token.type === FilterQueryLexer.LBRACK || + token.type === FilterQueryLexer.LPAREN + ) { + // Check if this is the start of a list for IN operator + let isForList = false; + + // Look back to see if this bracket follows an IN operator + for (let j = i - 1; j >= 0; j--) { + const prevToken = tokens[j]; + if (prevToken.channel !== 0) continue; // Skip hidden channel tokens + + if ( + prevToken.type === FilterQueryLexer.IN || + (prevToken.type === FilterQueryLexer.NOT && + j + 1 < tokens.length && + tokens[j + 1].type === FilterQueryLexer.IN) + ) { + isForList = true; + break; + } else if (prevToken.channel === 0) { + // If we encounter any token on the default channel, stop looking + break; + } + } + + bracketContext = { + start: token.start, + end: token.stop, + isForList, + }; + break; + } + + // If cursor is within a closing bracket token + if ( + token.type === FilterQueryLexer.RBRACK || + token.type === FilterQueryLexer.RPAREN + ) { + if (openBrackets.length > 0) { + const matchingOpen = openBrackets[openBrackets.length - 1]; + bracketContext = { + start: matchingOpen.token.start, + end: token.stop, + isForList: matchingOpen.isForList, + }; + } else { + bracketContext = { + start: token.start, + end: token.stop, + isForList: false, // We don't know, assume not list + }; + } + break; + } + } + } + + // If we have a current pair, determine context boundaries from it + if (currentPair) { + const { position } = currentPair; + + // Negation context: from negationStart to negationEnd + const negationContext = { + start: position.negationStart ?? 0, + end: position.negationEnd ?? 0, + }; + + // Key context: from keyStart to keyEnd + const keyContext = { + start: position.keyStart, + end: position.keyEnd, + }; + + // Find the operator context start by looking for the first non-space character after keyEnd + let operatorStart = position.keyEnd + 1; + while (operatorStart < query.length && query[operatorStart] === ' ') { + operatorStart++; + } + + // Operator context: from first non-space after key to operatorEnd + const operatorContext = { + start: operatorStart, + end: position.operatorEnd, + }; + + // Find the value context start by looking for the first non-space character after operatorEnd + let valueStart = position.operatorEnd + 1; + while (valueStart < query.length && query[valueStart] === ' ') { + valueStart++; + } + + // Special handling for multi-value operators like IN + const isInOperator = isMultiValueOperator(currentPair.operator); + + // Value context: from first non-space after operator to valueEnd (if exists) + // If this is an IN operator and we're in a bracket context, use that instead + let valueContext = null; + + if (isInOperator && bracketContext && bracketContext.isForList) { + // For IN operator with brackets, the whole bracket content is the value context + valueContext = { + start: bracketContext.start, + end: bracketContext.end, + }; + } else if (position.valueEnd) { + valueContext = { + start: valueStart, + end: position.valueEnd, + }; + } + + // Look for conjunction after value (if value exists) + let conjunctionContext = null; + if (position.valueEnd) { + let conjunctionStart = position.valueEnd + 1; + while (conjunctionStart < query.length && query[conjunctionStart] === ' ') { + conjunctionStart++; + } + + // Check if there's a conjunction token after the value + for (const token of tokens) { + if ( + token.start === conjunctionStart && + (token.type === FilterQueryLexer.AND || token.type === FilterQueryLexer.OR) + ) { + conjunctionContext = { + start: conjunctionStart, + end: token.stop, + }; + break; + } + } + } + + return { + keyContext, + negationContext, + operatorContext, + valueContext, + conjunctionContext, + bracketContext, + }; + } + + // If no current pair but there might be a partial pair under construction, + // try to determine context from tokens directly + const tokenAtCursor = tokens.find( + (token) => + token.channel === 0 && + token.start <= cursorIndex && + cursorIndex <= token.stop + 1, + ); + + if (tokenAtCursor) { + // Check token type to determine context + if (tokenAtCursor.type === FilterQueryLexer.KEY) { + return { + keyContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + negationContext: null, + operatorContext: null, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; + } + + if (tokenAtCursor.type === FilterQueryLexer.NOT) { + return { + keyContext: null, + negationContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + operatorContext: null, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; + } + + if (isOperatorToken(tokenAtCursor.type)) { + return { + keyContext: null, + negationContext: null, + operatorContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; + } + + if (isValueToken(tokenAtCursor.type)) { + return { + keyContext: null, + negationContext: null, + operatorContext: null, + valueContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + conjunctionContext: null, + bracketContext, + }; + } + + if (isConjunctionToken(tokenAtCursor.type)) { + return { + keyContext: null, + negationContext: null, + operatorContext: null, + valueContext: null, + conjunctionContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + bracketContext, + }; + } + } + + // If no current pair, return null for all contexts except possibly bracket context + return { + keyContext: null, + negationContext: null, + operatorContext: null, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; +} + +/** + * Gets the current query context at the cursor position + * This is useful for determining what kind of suggestions to show + * + * The function now includes full query pair information: + * - queryPairs: All key-operator-value triplets in the query + * - currentPair: The pair at or before the current cursor position + * + * This enables more intelligent context-aware suggestions based on + * the current key, operator, and surrounding query structure. + * + * @param query The query string + * @param cursorIndex The position of the cursor in the query + * @returns The query context at the cursor position + */ +export function getQueryContextAtCursor( + query: string, + cursorIndex: number, +): IQueryContext { + try { + // Guard against infinite recursion by checking call stack + const stackTrace = new Error().stack || ''; + const callCount = (stackTrace.match(/getQueryContextAtCursor/g) || []).length; + if (callCount > 3) { + console.warn( + 'Potential infinite recursion detected in getQueryContextAtCursor', + ); + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInKey: true, + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + queryPairs: [], + currentPair: null, + }; + } + + // First check if the cursor is at a token boundary or within a whitespace area + // This is critical for context detection + const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' '; + const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' '; + const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' '; + const isBeforeToken = + cursorIndex < query.length && query[cursorIndex] !== ' '; + + // Check if cursor is right after a token and at the start of a space + // FIXED: Consider the cursor to be at a transition point if it's at the end of a token + // and not yet at a space (this includes being at the end of the query) + const isTransitionPoint = + (isAtSpace && isAfterToken) || + (cursorIndex === query.length && isAfterToken); + + // Create input stream and lexer with query + const input = query || ''; + const chars = CharStreams.fromString(input); + const lexer = new FilterQueryLexer(chars); + + // Create token stream and force token generation + const tokenStream = new CommonTokenStream(lexer); + tokenStream.fill(); + + // Get all tokens including whitespace + const allTokens = tokenStream.tokens as IToken[]; + + // Find exact token at cursor, including whitespace + let exactToken: IToken | null = null; + let previousToken: IToken | null = null; + let nextToken: IToken | null = null; + + // Find the real 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 === FilterQueryLexer.EOF) continue; + + // FIXED: Consider a token to be the lastTokenBeforeCursor if the cursor is + // exactly at the end of the token (including the last character) + if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) { + lastTokenBeforeCursor = token; + } + + // If we found a token that starts after the cursor, we're done searching + if (token.start > cursorIndex) { + break; + } + } + + // Get query pairs information to enhance context + const queryPairs = extractQueryPairs(query); + + // Find the current pair without causing a circular dependency + let currentPair: IQueryPair | null = null; + if (queryPairs.length > 0) { + currentPair = getCurrentQueryPair(queryPairs, query, cursorIndex); + } + + // Determine precise context boundaries + const contextBoundaries = determineContextBoundaries( + query, + cursorIndex, + allTokens, + queryPairs, + ); + + // Check if cursor is within any of the specific context boundaries + // FIXED: Include the case where the cursor is exactly at the end of a boundary + const isInKeyBoundary = + contextBoundaries.keyContext && + ((cursorIndex >= contextBoundaries.keyContext.start && + cursorIndex <= contextBoundaries.keyContext.end) || + cursorIndex === contextBoundaries.keyContext.end + 1); + + const isInNegationBoundary = + contextBoundaries.negationContext && + ((cursorIndex >= contextBoundaries.negationContext.start && + cursorIndex <= contextBoundaries.negationContext.end) || + cursorIndex === contextBoundaries.negationContext.end + 1); + + const isInOperatorBoundary = + contextBoundaries.operatorContext && + ((cursorIndex >= contextBoundaries.operatorContext.start && + cursorIndex <= contextBoundaries.operatorContext.end) || + cursorIndex === contextBoundaries.operatorContext.end + 1); + + const isInValueBoundary = + contextBoundaries.valueContext && + ((cursorIndex >= contextBoundaries.valueContext.start && + cursorIndex <= contextBoundaries.valueContext.end) || + cursorIndex === contextBoundaries.valueContext.end + 1); + + const isInConjunctionBoundary = + contextBoundaries.conjunctionContext && + ((cursorIndex >= contextBoundaries.conjunctionContext.start && + cursorIndex <= contextBoundaries.conjunctionContext.end) || + cursorIndex === contextBoundaries.conjunctionContext.end + 1); + + // Check for bracket list context (used for IN operator values) + const isInBracketListBoundary = + contextBoundaries.bracketContext && + contextBoundaries.bracketContext.isForList && + cursorIndex >= contextBoundaries.bracketContext.start && + cursorIndex <= contextBoundaries.bracketContext.end + 1; + + // Check for general parenthesis context (not for IN operator lists) + const isInParenthesisBoundary = + contextBoundaries.bracketContext && + !contextBoundaries.bracketContext.isForList && + cursorIndex >= contextBoundaries.bracketContext.start && + cursorIndex <= contextBoundaries.bracketContext.end + 1; + + // Check if we're right after a closing bracket for a list (IN operator) + // This helps transition to conjunction context after a multi-value list + const isAfterClosingBracketList = + contextBoundaries.bracketContext && + contextBoundaries.bracketContext.isForList && + cursorIndex === contextBoundaries.bracketContext.end + 2 && + query[contextBoundaries.bracketContext.end + 1] === ' '; + + // If cursor is within a specific context boundary, this takes precedence + if ( + isInKeyBoundary || + isInNegationBoundary || + isInOperatorBoundary || + isInValueBoundary || + isInConjunctionBoundary || + isInBracketListBoundary || + isAfterClosingBracketList + ) { + // Extract information from the current pair (if available) + const keyToken = currentPair?.key || ''; + const operatorToken = currentPair?.operator || ''; + const valueToken = currentPair?.value || ''; + + // Determine if we're in a multi-value operator context + const isForMultiValueOperator = isMultiValueOperator(operatorToken); + + // If we're in a bracket list and it's for a multi-value operator like IN, + // treat it as part of the value context + const finalIsInValue = + isInValueBoundary || (isInBracketListBoundary && isForMultiValueOperator); + + // If we're right after a closing bracket for a list, transition to conjunction context + const finalIsInConjunction = + isInConjunctionBoundary || isAfterClosingBracketList; + + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInKey: isInKeyBoundary || false, + isInNegation: isInNegationBoundary || false, + isInOperator: isInOperatorBoundary || false, + isInValue: finalIsInValue || false, + isInConjunction: finalIsInConjunction || false, + isInFunction: false, + isInParenthesis: isInParenthesisBoundary || false, + isInBracketList: isInBracketListBoundary || false, + keyToken: isInKeyBoundary + ? keyToken + : isInOperatorBoundary || finalIsInValue + ? keyToken + : undefined, + operatorToken: isInOperatorBoundary + ? operatorToken + : finalIsInValue + ? operatorToken + : undefined, + valueToken: finalIsInValue ? valueToken : undefined, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + // Continue with existing token-based logic for cases not covered by context boundaries + // Handle cursor at the very end of input + if (cursorIndex >= input.length && allTokens.length > 0) { + const lastRealToken = allTokens + .filter((t) => t.type !== FilterQueryLexer.EOF) + .pop(); + if (lastRealToken) { + exactToken = lastRealToken; + previousToken = + allTokens.filter((t) => t.stop < lastRealToken.start).pop() || null; + } + } else { + // Normal token search + for (let i = 0; i < allTokens.length; i++) { + const token = allTokens[i]; + // Skip EOF token in normal search + if (token.type === FilterQueryLexer.EOF) { + continue; + } + + // FIXED: Check if cursor is within token bounds (inclusive) or exactly at the end + if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) { + exactToken = token; + previousToken = i > 0 ? allTokens[i - 1] : null; + nextToken = i < allTokens.length - 1 ? allTokens[i + 1] : null; + break; + } + } + + // If cursor is between tokens, find surrounding tokens + if (!exactToken) { + for (let i = 0; i < allTokens.length - 1; i++) { + const current = allTokens[i]; + const next = allTokens[i + 1]; + if ( + current.type === FilterQueryLexer.EOF || + next.type === FilterQueryLexer.EOF + ) { + continue; + } + + if (current.stop + 1 < cursorIndex && cursorIndex < next.start) { + previousToken = current; + nextToken = next; + break; + } + } + } + } + + // If we don't have tokens yet, return default context + if (!previousToken && !nextToken && !exactToken && !lastTokenBeforeCursor) { + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInKey: true, // Default to key context when input is empty + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + queryPairs: queryPairs, // Add all query pairs to the context + currentPair: null, // No current pair when query is empty + }; + } + + // If we have a token and we're at a space after it (transition point), + // then we should progress the context + if ( + lastTokenBeforeCursor && + (isAtSpace || isAfterSpace || isTransitionPoint) + ) { + const lastTokenContext = determineTokenContext(lastTokenBeforeCursor, input); + + // Apply the context progression logic: key → operator → value → conjunction → key + if (lastTokenContext.isInKey) { + // If we just typed a key and then a space, we move to operator context + return { + tokenType: lastTokenBeforeCursor.type, + text: lastTokenBeforeCursor.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: lastTokenBeforeCursor.text, + isInKey: false, + isInNegation: false, + isInOperator: true, // After key + space, should be operator context + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + keyToken: lastTokenBeforeCursor.text, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if (lastTokenContext.isInNegation) { + return { + tokenType: lastTokenBeforeCursor.type, + text: lastTokenBeforeCursor.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: lastTokenBeforeCursor.text, + isInKey: false, + isInNegation: false, + isInOperator: true, // After key + space + NOT, should be operator context + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + keyToken: lastTokenBeforeCursor.text, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if (lastTokenContext.isInOperator) { + // If we just typed an operator and then a space, we move to value context + const keyFromPair = currentPair?.key || ''; + const isNonValueToken = isNonValueOperatorToken(lastTokenBeforeCursor.type); + return { + tokenType: lastTokenBeforeCursor.type, + text: lastTokenBeforeCursor.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: lastTokenBeforeCursor.text, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInValue: !isNonValueToken, // After operator + space, should be value context + isInFunction: false, + isInConjunction: isNonValueToken, + isInParenthesis: false, + isInBracketList: false, + operatorToken: lastTokenBeforeCursor.text, + keyToken: keyFromPair, // Include key from current pair + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if (lastTokenContext.isInValue) { + // If we just typed a value and then a space, we move to conjunction context + const keyFromPair = currentPair?.key || ''; + const operatorFromPair = currentPair?.operator || ''; + return { + tokenType: lastTokenBeforeCursor.type, + text: lastTokenBeforeCursor.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: lastTokenBeforeCursor.text, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: true, // After value + space, should be conjunction context + isInParenthesis: false, + isInBracketList: false, + valueToken: lastTokenBeforeCursor.text, + keyToken: keyFromPair, // Include key from current pair + operatorToken: operatorFromPair, // Include operator from current pair + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if (lastTokenContext.isInConjunction) { + // If we just typed a conjunction and then a space, we move to key context + return { + tokenType: lastTokenBeforeCursor.type, + text: lastTokenBeforeCursor.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: lastTokenBeforeCursor.text, + isInKey: true, // After conjunction + space, should be key context + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if ( + lastTokenContext.isInParenthesis && + lastTokenBeforeCursor.type === FilterQueryLexer.RPAREN + ) { + // If we are after a parenthesis we should enter the conjunction context. + return { + tokenType: lastTokenBeforeCursor.type, + text: lastTokenBeforeCursor.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: lastTokenBeforeCursor.text, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: true, // After RPARAN + space, should be conjunction context + isInParenthesis: false, + isInBracketList: false, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + } + + // FIXED: Consider the case where the cursor is at the end of a token + // with no space yet (user is actively typing) + if (exactToken && cursorIndex === exactToken.stop + 1) { + const tokenContext = determineTokenContext(exactToken, input); + + // When the cursor is at the end of a token, return the current token context + return { + tokenType: exactToken.type, + text: exactToken.text, + start: exactToken.start, + stop: exactToken.stop, + currentToken: exactToken.text, + ...tokenContext, + isInBracketList: false, + keyToken: tokenContext.isInKey + ? exactToken.text + : currentPair?.key || undefined, + operatorToken: tokenContext.isInOperator + ? exactToken.text + : currentPair?.operator || undefined, + valueToken: tokenContext.isInValue + ? exactToken.text + : currentPair?.value || undefined, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + // Regular token-based context detection (when cursor is directly on a token) + if (exactToken?.channel === 0) { + const tokenContext = determineTokenContext(exactToken, input); + + // Get relevant tokens based on current pair + const keyFromPair = currentPair?.key || ''; + const operatorFromPair = currentPair?.operator || ''; + const valueFromPair = currentPair?.value || ''; + + return { + tokenType: exactToken.type, + text: exactToken.text, + start: exactToken.start, + stop: exactToken.stop, + currentToken: exactToken.text, + ...tokenContext, + isInBracketList: false, + keyToken: tokenContext.isInKey + ? exactToken.text + : tokenContext.isInOperator || tokenContext.isInValue + ? keyFromPair + : undefined, + operatorToken: tokenContext.isInOperator + ? exactToken.text + : tokenContext.isInValue + ? operatorFromPair + : undefined, + valueToken: tokenContext.isInValue ? exactToken.text : undefined, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + // If we're between tokens but not after a space, use previous token to determine context + if (previousToken?.channel === 0) { + const prevContext = determineTokenContext(previousToken, input); + + // Get relevant tokens based on current pair + const keyFromPair = currentPair?.key || ''; + const operatorFromPair = currentPair?.operator || ''; + const valueFromPair = currentPair?.value || ''; + + // CRITICAL FIX: Check if the last meaningful token is an operator + // If so, we're always in the value context regardless of spaces + if (prevContext.isInOperator) { + // If previous token is operator, we must be in value context + return { + tokenType: previousToken.type, + text: previousToken.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: previousToken.text, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInValue: true, // Always in value context after operator + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + operatorToken: previousToken.text, + keyToken: keyFromPair, // Include key from current pair + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + // Maintain the strict progression key → operator → value → conjunction → key + if (prevContext.isInKey) { + return { + tokenType: previousToken.type, + text: previousToken.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: previousToken.text, + isInKey: false, + isInNegation: false, + isInOperator: true, // After key, progress to operator context + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + keyToken: previousToken.text, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if (prevContext.isInValue) { + return { + tokenType: previousToken.type, + text: previousToken.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: previousToken.text, + isInKey: false, + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: true, // After value, progress to conjunction context + isInParenthesis: false, + isInBracketList: false, + valueToken: previousToken.text, + keyToken: keyFromPair, // Include key from current pair + operatorToken: operatorFromPair, // Include operator from current pair + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + if (prevContext.isInConjunction) { + return { + tokenType: previousToken.type, + text: previousToken.text, + start: cursorIndex, + stop: cursorIndex, + currentToken: previousToken.text, + isInKey: true, // After conjunction, progress back to key context + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + } + + // Default fallback to key context + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInKey: true, + isInNegation: false, + isInOperator: false, + isInValue: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } catch (error) { + console.error('Error in getQueryContextAtCursor:', error); + return { + tokenType: -1, + text: '', + start: cursorIndex, + stop: cursorIndex, + currentToken: '', + isInValue: false, + isInKey: true, // Default to key context on error + isInNegation: false, + isInOperator: false, + isInFunction: false, + isInConjunction: false, + isInParenthesis: false, + isInBracketList: false, + queryPairs: [], + currentPair: null, + }; + } +} + +/** + * Extracts all key-operator-value triplets from a query string + * This is useful for getting value suggestions based on the current key and operator + * + * @param query The query string to parse + * @returns An array of IQueryPair objects representing the key-operator-value triplets + */ +export function extractQueryPairs(query: string): IQueryPair[] { + try { + // Guard against infinite recursion by checking call stack + const stackTrace = new Error().stack || ''; + const callCount = (stackTrace.match(/extractQueryPairs/g) || []).length; + if (callCount > 3) { + console.warn('Potential infinite recursion detected in extractQueryPairs'); + return []; + } + + // Create input stream and lexer with query query + const input = query || ''; + const chars = CharStreams.fromString(input); + const lexer = new FilterQueryLexer(chars); + + // Create token stream and force token generation + const tokenStream = new CommonTokenStream(lexer); + tokenStream.fill(); + + // Get all tokens including whitespace + const allTokens = tokenStream.tokens as IToken[]; + + const queryPairs: IQueryPair[] = []; + let currentPair: Partial | null = null; + + let iterator = 0; + + // Process tokens to build triplets + while (iterator < allTokens.length) { + const token = allTokens[iterator]; + iterator += 1; + + // Skip EOF and whitespace tokens + if (token.type === FilterQueryLexer.EOF || token.channel !== 0) { + continue; + } + + // If token is a KEY, start a new pair + if ( + token.type === FilterQueryLexer.KEY && + !(currentPair && currentPair.key) + ) { + // If we have an existing incomplete pair, add it to the result + if (currentPair && currentPair.key) { + queryPairs.push({ + key: currentPair.key, + operator: currentPair.operator || '', + value: currentPair.value, + valueList: currentPair.valueList || [], + valuesPosition: currentPair.valuesPosition || [], + hasNegation: currentPair.hasNegation || false, + isMultiValue: currentPair.isMultiValue || false, + position: { + keyStart: currentPair.position?.keyStart || 0, + keyEnd: currentPair.position?.keyEnd || 0, + operatorStart: currentPair.position?.operatorStart || 0, + operatorEnd: currentPair.position?.operatorEnd || 0, + valueStart: currentPair.position?.valueStart, + valueEnd: currentPair.position?.valueEnd, + negationStart: currentPair.position?.negationStart || 0, + negationEnd: currentPair.position?.negationEnd || 0, + }, + isComplete: !!( + currentPair.key && + currentPair.operator && + currentPair.value + ), + } as IQueryPair); + } + + // Start a new pair + currentPair = { + key: token.text, + position: { + keyStart: token.start, + keyEnd: token.stop, + operatorStart: 0, // Initialize with default values + operatorEnd: 0, // Initialize with default values + }, + }; + } + // If NOT token comes set hasNegation to true + else if (token.type === FilterQueryLexer.NOT && currentPair) { + currentPair.hasNegation = true; + + currentPair.position = { + keyStart: currentPair.position?.keyStart || 0, + keyEnd: currentPair.position?.keyEnd || 0, + operatorStart: currentPair.position?.operatorStart || 0, + operatorEnd: currentPair.position?.operatorEnd || 0, + valueStart: currentPair.position?.valueStart, + valueEnd: currentPair.position?.valueEnd, + negationStart: token.start, + negationEnd: token.stop, + }; + } + + // If token is an operator and we have a key, add the operator + else if ( + isOperatorToken(token.type) && + currentPair && + currentPair.key && + !currentPair.operator + ) { + let multiValueStart: number | undefined; + let multiValueEnd: number | undefined; + + if (isMultiValueOperator(token.text)) { + currentPair.isMultiValue = true; + + // Iterate from '[' || '(' till ']' || ')' to get all the values + const valueList: string[] = []; + const valuesPosition: { start: number; end: number }[] = []; + + if ( + [FilterQueryLexer.LPAREN, FilterQueryLexer.LBRACK].includes( + allTokens[iterator].type, + ) + ) { + multiValueStart = allTokens[iterator].start; + iterator += 1; + const closingToken = + allTokens[iterator].type === FilterQueryLexer.LPAREN + ? FilterQueryLexer.RPAREN + : FilterQueryLexer.RBRACK; + + while ( + allTokens[iterator].type !== closingToken && + iterator < allTokens.length + ) { + if (isValueToken(allTokens[iterator].type)) { + valueList.push(allTokens[iterator].text); + valuesPosition.push({ + start: allTokens[iterator].start, + end: allTokens[iterator].stop, + }); + } + iterator += 1; + } + + if (allTokens[iterator].type === closingToken) { + multiValueEnd = allTokens[iterator].stop; + } + } + + currentPair.valuesPosition = valuesPosition; + currentPair.valueList = valueList; + + if (multiValueStart && multiValueEnd) { + currentPair.value = query.substring(multiValueStart, multiValueEnd + 1); + } + } + + currentPair.operator = token.text; + // Ensure we create a valid position object with all required fields + currentPair.position = { + keyStart: currentPair.position?.keyStart || 0, + keyEnd: currentPair.position?.keyEnd || 0, + operatorStart: token.start, + operatorEnd: token.stop, + valueStart: multiValueStart || currentPair.position?.valueStart, + valueEnd: multiValueEnd || currentPair.position?.valueEnd, + negationStart: currentPair.position?.negationStart || 0, + negationEnd: currentPair.position?.negationEnd || 0, + }; + } + // If token is a value and we have a key and operator, add the value + else if ( + isValueToken(token.type) && + currentPair && + currentPair.key && + currentPair.operator && + !NON_VALUE_OPERATORS.includes(currentPair.operator) && + !currentPair.value + ) { + currentPair.value = token.text; + // Ensure we create a valid position object with all required fields + currentPair.position = { + keyStart: currentPair.position?.keyStart || 0, + keyEnd: currentPair.position?.keyEnd || 0, + operatorStart: currentPair.position?.operatorStart || 0, + operatorEnd: currentPair.position?.operatorEnd || 0, + valueStart: token.start, + valueEnd: token.stop, + negationStart: currentPair.position?.negationStart || 0, + negationEnd: currentPair.position?.negationEnd || 0, + }; + } + // If token is a conjunction (AND/OR) or A key, finalize the current pair + else if ( + currentPair && + currentPair.key && + (isConjunctionToken(token.type) || + (token.type === FilterQueryLexer.KEY && isQueryPairComplete(currentPair))) + ) { + queryPairs.push({ + key: currentPair.key, + operator: currentPair.operator || '', + value: currentPair.value, + valueList: currentPair.valueList || [], + valuesPosition: currentPair.valuesPosition || [], + hasNegation: currentPair.hasNegation || false, + isMultiValue: currentPair.isMultiValue || false, + position: { + keyStart: currentPair.position?.keyStart || 0, + keyEnd: currentPair.position?.keyEnd || 0, + operatorStart: currentPair.position?.operatorStart || 0, + operatorEnd: currentPair.position?.operatorEnd || 0, + valueStart: currentPair.position?.valueStart, + valueEnd: currentPair.position?.valueEnd, + negationStart: currentPair.position?.negationStart || 0, + negationEnd: currentPair.position?.negationEnd || 0, + }, + isComplete: !!( + currentPair.key && + currentPair.operator && + currentPair.value + ), + } as IQueryPair); + + // Reset for the next pair + currentPair = null; + + if (token.type === FilterQueryLexer.KEY) { + // If we encounter a new key, start a new pair immediately + currentPair = { + key: token.text, + position: { + keyStart: token.start, + keyEnd: token.stop, + operatorStart: 0, // Initialize with default values + operatorEnd: 0, // Initialize with default values + }, + }; + } + } + } + + // Add the last pair if not already added + if (currentPair && currentPair.key) { + queryPairs.push({ + key: currentPair.key, + operator: currentPair.operator || '', + value: currentPair.value, + valueList: currentPair.valueList || [], + valuesPosition: currentPair.valuesPosition || [], + hasNegation: currentPair.hasNegation || false, + isMultiValue: currentPair.isMultiValue || false, + position: { + keyStart: currentPair.position?.keyStart || 0, + keyEnd: currentPair.position?.keyEnd || 0, + operatorStart: currentPair.position?.operatorStart || 0, + operatorEnd: currentPair.position?.operatorEnd || 0, + valueStart: currentPair.position?.valueStart, + valueEnd: currentPair.position?.valueEnd, + negationStart: currentPair.position?.negationStart || 0, + negationEnd: currentPair.position?.negationEnd || 0, + }, + isComplete: !!( + currentPair.key && + currentPair.operator && + currentPair.value + ), + } as IQueryPair); + } + + return queryPairs; + } catch (error) { + console.error('Error in extractQueryPairs:', error); + return []; + } +} + +function getEndIndexAfterSpaces(pair: IQueryPair, query: string): number { + const { position } = pair; + let pairEnd = position.valueEnd || position.operatorEnd || position.keyEnd; + + // Start from the next index after pairEnd + pairEnd += 1; + while (pairEnd < query.length && query.charAt(pairEnd) === ' ') { + pairEnd += 1; + } + + return pairEnd; +} + +/** + * Gets the current query pair at the cursor position + * This is useful for getting suggestions based on the current context + * The function finds the rightmost complete pair that ends before or at the cursor position + * + * @param queryPairs An array of IQueryPair objects representing the key-operator-value triplets + * @param query The full query string + * @param cursorIndex The position of the cursor in the query + * @returns The query pair at the cursor position, or null if not found + */ +export function getCurrentQueryPair( + queryPairs: IQueryPair[], + query: string, + cursorIndex: number, +): IQueryPair | null { + try { + // If we have pairs, try to find the one at the cursor position + if (queryPairs.length > 0) { + // Look for the rightmost pair whose end position is before or at the cursor + let bestMatch: IQueryPair | null = null; + + for (const pair of queryPairs) { + const { position } = pair; + + // Find the rightmost position of this pair + const pairEnd = + position.valueEnd || position.operatorEnd || position.keyEnd; + + const pairStart = + position.keyStart ?? (position.operatorStart || position.valueStart || 0); + + // If this pair ends at or before the cursor, and it's further right than our previous best match + if ( + ((pairEnd >= cursorIndex && pairStart <= cursorIndex) || + (!pair.isComplete && + pairStart <= cursorIndex && + getEndIndexAfterSpaces(pair, query) >= cursorIndex)) && + (!bestMatch || + pairEnd > + (bestMatch.position.valueEnd || + bestMatch.position.operatorEnd || + bestMatch.position.keyEnd)) + ) { + bestMatch = pair; + } + } + + // If we found a match, return it + if (bestMatch) { + return bestMatch; + } + + // If cursor is at the very beginning, before any pairs, return null + if (cursorIndex === 0) { + return null; + } + + // If no match found and cursor is at the end, return the last pair + if (cursorIndex >= query.length && queryPairs.length > 0) { + return queryPairs[queryPairs.length - 1]; + } + } + + // If no valid pair is found, and we cannot infer one from context, return null + return null; + } catch (error) { + console.error('Error in getCurrentQueryPair:', error); + return null; + } +} + +/** + * Usage example for query context with pairs: + * + * ```typescript + * // Get context at cursor position + * const context = getQueryContextAtCursor(query, cursorPosition); + * + * // Access all query pairs + * const allPairs = context.queryPairs || []; + * console.log(`Query contains ${allPairs.length} key-operator-value triplets`); + * + * // Access the current pair at cursor + * if (context.currentPair) { + * // Use the current triplet to provide relevant suggestions + * const { key, operator, value } = context.currentPair; + * console.log(`Current context: ${key} ${operator} ${value || ''}`); + * + * // Check if this is a complete triplet + * if (context.currentPair.isComplete) { + * // All parts (key, operator, value) are present + * } else { + * // Incomplete - might be missing operator or value + * } + * } else { + * // No current pair, likely at the start of a new condition + * } + * ``` + */ diff --git a/frontend/src/utils/stringUtils.ts b/frontend/src/utils/stringUtils.ts new file mode 100644 index 000000000000..f2e90bd23382 --- /dev/null +++ b/frontend/src/utils/stringUtils.ts @@ -0,0 +1,13 @@ +export function unquote(str: string): string { + if (typeof str !== 'string') return str; + + const trimmed = str.trim(); + const firstChar = trimmed[0]; + const lastChar = trimmed[trimmed.length - 1]; + + if ((firstChar === '"' || firstChar === "'") && firstChar === lastChar) { + return trimmed.slice(1, -1); + } + + return trimmed; +} diff --git a/frontend/src/utils/tokenUtils.ts b/frontend/src/utils/tokenUtils.ts new file mode 100644 index 000000000000..f4819adb2ad1 --- /dev/null +++ b/frontend/src/utils/tokenUtils.ts @@ -0,0 +1,93 @@ +import { NON_VALUE_OPERATORS } from 'constants/antlrQueryConstants'; +import FilterQueryLexer from 'parser/FilterQueryLexer'; +import { IQueryPair } from 'types/antlrQueryTypes'; + +export function isKeyToken(tokenType: number): boolean { + return tokenType === FilterQueryLexer.KEY; +} + +// Helper function to check if a token is an operator +export function isOperatorToken(tokenType: number): boolean { + return [ + FilterQueryLexer.EQUALS, + FilterQueryLexer.NOT_EQUALS, + FilterQueryLexer.NEQ, + FilterQueryLexer.LT, + FilterQueryLexer.LE, + FilterQueryLexer.GT, + FilterQueryLexer.GE, + FilterQueryLexer.LIKE, + FilterQueryLexer.ILIKE, + FilterQueryLexer.BETWEEN, + FilterQueryLexer.EXISTS, + FilterQueryLexer.REGEXP, + FilterQueryLexer.CONTAINS, + FilterQueryLexer.IN, + FilterQueryLexer.NOT, + ].includes(tokenType); +} + +// Helper function to check if a token is an operator which doesn't require a value +export function isNonValueOperatorToken(tokenType: number): boolean { + return [FilterQueryLexer.EXISTS].includes(tokenType); +} + +// Helper function to check if a token is a value +export function isValueToken(tokenType: number): boolean { + return [ + FilterQueryLexer.QUOTED_TEXT, + FilterQueryLexer.NUMBER, + FilterQueryLexer.BOOL, + FilterQueryLexer.KEY, + ].includes(tokenType); +} + +// Helper function to check if a token is a conjunction +export function isConjunctionToken(tokenType: number): boolean { + return [FilterQueryLexer.AND, FilterQueryLexer.OR].includes(tokenType); +} + +// Helper function to check if a token is a bracket +export function isBracketToken(tokenType: number): boolean { + return [ + FilterQueryLexer.LPAREN, + FilterQueryLexer.RPAREN, + FilterQueryLexer.LBRACK, + FilterQueryLexer.RBRACK, + ].includes(tokenType); +} + +// Helper function to check if an operator typically uses bracket values (multi-value operators) +export function isMultiValueOperator(operatorToken?: string): boolean { + if (!operatorToken) return false; + + const upperOp = operatorToken.toUpperCase(); + return upperOp === 'IN'; +} + +export function isFunctionToken(tokenType: number): boolean { + return [ + FilterQueryLexer.HAS, + FilterQueryLexer.HASANY, + FilterQueryLexer.HASALL, + ].includes(tokenType); +} + +export function isWrappedUnderQuotes(token: string): boolean { + if (!token) return false; + const sanitizedToken = token.trim(); + return ( + (sanitizedToken.startsWith('"') && sanitizedToken.endsWith('"')) || + (sanitizedToken.startsWith("'") && sanitizedToken.endsWith("'")) + ); +} + +export function isQueryPairComplete(queryPair: Partial): boolean { + if (!queryPair) return false; + // A complete query pair must have a key, an operator, and a value (or EXISTS operator) + if (queryPair.operator && NON_VALUE_OPERATORS.includes(queryPair.operator)) { + return !!queryPair.key && !!queryPair.operator; + } + // For other operators, we need a value as well + return Boolean(queryPair.key && queryPair.operator && queryPair.value); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 051040298c21..81d975cbbc86 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -31,7 +31,7 @@ ], "types": ["node", "jest"] }, - "exclude": ["node_modules"], + "exclude": ["node_modules", "src/parser/*.ts"], "include": [ "./src", "./src/**/*.ts", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b99801818e7c..f130e4c3554a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2175,10 +2175,52 @@ resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.26.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" - integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/runtime@^7.13.10": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" + integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.14.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.18.6": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.3.1": + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" + integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.7.6": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== dependencies: regenerator-runtime "^0.14.0" @@ -2322,6 +2364,95 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.0.1.tgz#457233b0a18741b7711855044102b82bae7a070b" integrity sha512-URg8UM6lfC9ZYqFipItRSxYJdgpU5d2Z4KnjsJ+rj6tgAmGme7E+PQNCiud8g0HDaZKMovu2qjfa0f5Ge0Vlsg== +"@codemirror/autocomplete@6.18.6", "@codemirror/autocomplete@^6.0.0": + version "6.18.6" + resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb" + integrity sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.17.0" + "@lezer/common" "^1.0.0" + +"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0": + version "6.8.1" + resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.8.1.tgz#639f5559d2f33f2582a2429c58cb0c1b925c7a30" + integrity sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.4.0" + "@codemirror/view" "^6.27.0" + "@lezer/common" "^1.1.0" + +"@codemirror/lang-javascript@6.2.3": + version "6.2.3" + resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz#d705c359dc816afcd3bcdf120a559f83d31d4cda" + integrity sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/language" "^6.6.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.17.0" + "@lezer/common" "^1.0.0" + "@lezer/javascript" "^1.0.0" + +"@codemirror/language@^6.0.0", "@codemirror/language@^6.6.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.0.tgz#5ae90972601497f4575f30811519d720bf7232c9" + integrity sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.23.0" + "@lezer/common" "^1.1.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + style-mod "^4.0.0" + +"@codemirror/lint@^6.0.0": + version "6.8.5" + resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.5.tgz#9edaa808e764e28e07665b015951934c8ec3a418" + integrity sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.35.0" + crelt "^1.0.5" + +"@codemirror/search@^6.0.0": + version "6.5.10" + resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.10.tgz#7367bfc88094d078b91c752bc74140fb565b55ee" + integrity sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0": + version "6.5.2" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6" + integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA== + dependencies: + "@marijn/find-cluster-break" "^1.0.0" + +"@codemirror/theme-one-dark@^6.0.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8" + integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/highlight" "^1.0.0" + +"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0": + version "6.36.6" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.6.tgz#735a6431caed0c2c7d26c645066b02f10e802812" + integrity sha512-uxugGLet+Nzp0Jcit8Hn3LypM8ioMLKTsdf8FRoT3HWvZtb9GhaWMe0Cc15rz90Ljab4YFJiAulmIVB74OY0IQ== + dependencies: + "@codemirror/state" "^6.5.0" + style-mod "^4.1.0" + w3c-keyname "^2.2.4" + "@commitlint/cli@^16.3.0": version "16.3.0" resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-16.3.0.tgz#5689f5c2abbb7880d5ff13329251e5648a784b16" @@ -3156,6 +3287,34 @@ resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd" + integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA== + +"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b" + integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA== + dependencies: + "@lezer/common" "^1.0.0" + +"@lezer/javascript@^1.0.0": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.5.1.tgz#2a424a6ec29f1d4ef3c34cbccc5447e373618ad8" + integrity sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw== + dependencies: + "@lezer/common" "^1.2.0" + "@lezer/highlight" "^1.1.3" + "@lezer/lr" "^1.3.0" + +"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727" + integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA== + dependencies: + "@lezer/common" "^1.0.0" + "@mapbox/jsonlint-lines-primitives@~2.0.2": version "2.0.2" resolved "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz" @@ -3185,6 +3344,11 @@ resolved "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz" integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== +"@marijn/find-cluster-break@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8" + integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== + "@mdx-js/loader@2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-2.3.0.tgz#56a6b07eb0027b6407e953a97c52bd8619601161" @@ -4948,11 +5112,68 @@ "@typescript-eslint/types" "5.59.1" eslint-visitor-keys "^3.3.0" +"@uiw/codemirror-extensions-basic-setup@4.23.10": + version "4.23.10" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.10.tgz#e5d901e860a039ac61d955af26a12866e9dc356c" + integrity sha512-zpbmSeNs3OU/f/Eyd6brFnjsBUYwv2mFjWxlAsIRSwTlW+skIT60rQHFBSfsj/5UVSxSLWVeUYczN7AyXvgTGQ== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/commands" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/search" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + +"@uiw/codemirror-theme-copilot@4.23.11": + version "4.23.11" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-copilot/-/codemirror-theme-copilot-4.23.11.tgz#075a2a6449c62835af2cb7fdef4fe9558bf20f30" + integrity sha512-m6vvsWHbji0s25ly3L35BdjSMys4DL3dzb4wbVtYa7m79heA3h2YNiWFIkOjbyPtoOh4RUGB6tBT8z0J/5PmTA== + dependencies: + "@uiw/codemirror-themes" "4.23.11" + +"@uiw/codemirror-theme-github@4.24.1": + version "4.24.1" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.24.1.tgz#80a6e735ed9bb97d66d7914b951b8243f974634f" + integrity sha512-dl4qFEXINE4TFus7ALMfjFUCl7sWLkqTdaSaln0Vv3s+HVzSMAh5lkEdnH3yPcOOCl5ehYG4zIx8bqEnA2/FYQ== + dependencies: + "@uiw/codemirror-themes" "4.24.1" + +"@uiw/codemirror-themes@4.23.11": + version "4.23.11" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.11.tgz#abd022b9d65c851d72ecbc93169bb5143b9c35b7" + integrity sha512-90joUOau/3E6KNdA5ePr/t8LVBA/426wIsOuwaZohsDM5a5gsYfdMWGYfClnLMkpfHJUDYYMO+b2JPhJf9mzHw== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + +"@uiw/codemirror-themes@4.24.1": + version "4.24.1" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.24.1.tgz#f27075079b5a899d99716d035b58f38d62fffa12" + integrity sha512-hduBbFNiWNW6nYa2/giKQ9YpzhWNw87BGpCjC+cXYMZ7bCD6q5DC6Hw+7z7ZwSzEaOQvV91lmirOjJ8hn9+pkg== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@uiw/copy-to-clipboard@~1.0.12": version "1.0.15" resolved "https://registry.yarnpkg.com/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.15.tgz#959cebbae64df353964647bb5b9d705176b2e613" integrity sha512-1bbGZ3T+SGmA07BoVPK4UCUDcowDN/moctviJGQexfOc9qL8TMLDQPr7mTPvDKhgJkgnlKkAQNFU8PiarIi9sQ== +"@uiw/react-codemirror@4.23.10": + version "4.23.10" + resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.10.tgz#2e34aec4f65f901ed8e9b8a22e28f2177addce69" + integrity sha512-AbN4eVHOL4ckRuIXpZxkzEqL/1ChVA+BSdEnAKjIB68pLQvKsVoYbiFP8zkXkYc4+Fcgq5KbAjvYqdo4ewemKw== + dependencies: + "@babel/runtime" "^7.18.6" + "@codemirror/commands" "^6.1.0" + "@codemirror/state" "^6.1.1" + "@codemirror/theme-one-dark" "^6.0.0" + "@uiw/codemirror-extensions-basic-setup" "4.23.10" + codemirror "^6.0.0" + "@uiw/react-markdown-preview@^4.1.14": version "4.1.15" resolved "https://registry.yarnpkg.com/@uiw/react-markdown-preview/-/react-markdown-preview-4.1.15.tgz#82f7ca4d7dc0e9896856fd795b5aa063d1209d2a" @@ -5585,6 +5806,11 @@ antd@5.11.0: scroll-into-view-if-needed "^3.1.0" throttle-debounce "^5.0.0" +antlr4@4.13.2: + version "4.13.2" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.2.tgz#0d084ad0e32620482a9c3a0e2470c02e72e4006d" + integrity sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" @@ -6787,6 +7013,19 @@ co@^4.6.0: resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== +codemirror@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" + integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/commands" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/search" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" @@ -7112,6 +7351,11 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +crelt@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" + integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -15047,6 +15291,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" @@ -16246,6 +16495,11 @@ style-loader@1.3.0: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-mod@^4.0.0, style-mod@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" + integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== + style-to-object@^0.4.0, style-to-object@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.2.tgz#a8247057111dea8bd3b8a1a66d2d0c9cf9218a54" @@ -17318,6 +17572,11 @@ w3c-hr-time@^1.0.2: dependencies: browser-process-hrtime "^1.0.0" +w3c-keyname@^2.2.4: + version "2.2.8" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" + integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== + w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz" diff --git a/grammar/FilterQuery.g4 b/grammar/FilterQuery.g4 index 204c5fdd3275..4aaa04718dbe 100644 --- a/grammar/FilterQuery.g4 +++ b/grammar/FilterQuery.g4 @@ -57,7 +57,7 @@ comparison | key GE value | key (LIKE | ILIKE) value - | key (NOT_LIKE | NOT_ILIKE) value + | key NOT (LIKE | ILIKE) value | key BETWEEN value AND value | key NOT BETWEEN value AND value @@ -167,9 +167,7 @@ GE : '>=' ; // Operators that are made of multiple keywords LIKE : [Ll][Ii][Kk][Ee] ; -NOT_LIKE : [Nn][Oo][Tt] [ \t]+ [Ll][Ii][Kk][Ee] ; ILIKE : [Ii][Ll][Ii][Kk][Ee] ; -NOT_ILIKE : [Nn][Oo][Tt] [ \t]+ [Ii][Ll][Ii][Kk][Ee] ; BETWEEN : [Bb][Ee][Tt][Ww][Ee][Ee][Nn] ; EXISTS : [Ee][Xx][Ii][Ss][Tt][Ss]? ; REGEXP : [Rr][Ee][Gg][Ee][Xx][Pp] ; diff --git a/pkg/contextlinks/links.go b/pkg/contextlinks/links.go index 7023554958d7..8412b4757c00 100644 --- a/pkg/contextlinks/links.go +++ b/pkg/contextlinks/links.go @@ -14,13 +14,13 @@ import ( func PrepareLinksToTraces(start, end time.Time, filterItems []v3.FilterItem) string { // Traces list view expects time in nanoseconds - tr := v3.URLShareableTimeRange{ + tr := URLShareableTimeRange{ Start: start.UnixNano(), End: end.UnixNano(), PageSize: 100, } - options := v3.URLShareableOptions{ + options := URLShareableOptions{ MaxLines: 2, Format: "list", SelectColumns: tracesV3.TracesListViewDefaultSelectedColumns, @@ -50,11 +50,11 @@ func PrepareLinksToTraces(start, end time.Time, filterItems []v3.FilterItem) str }, } - urlData := v3.URLShareableCompositeQuery{ + urlData := URLShareableCompositeQuery{ QueryType: string(v3.QueryTypeBuilder), - Builder: v3.URLShareableBuilderQuery{ - QueryData: []v3.BuilderQuery{ - builderQuery, + Builder: URLShareableBuilderQuery{ + QueryData: []LinkQuery{ + {BuilderQuery: builderQuery}, }, QueryFormulas: make([]string, 0), }, @@ -72,13 +72,13 @@ func PrepareLinksToTraces(start, end time.Time, filterItems []v3.FilterItem) str func PrepareLinksToLogs(start, end time.Time, filterItems []v3.FilterItem) string { // Logs list view expects time in milliseconds - tr := v3.URLShareableTimeRange{ + tr := URLShareableTimeRange{ Start: start.UnixMilli(), End: end.UnixMilli(), PageSize: 100, } - options := v3.URLShareableOptions{ + options := URLShareableOptions{ MaxLines: 2, Format: "list", SelectColumns: []v3.AttributeKey{}, @@ -108,11 +108,11 @@ func PrepareLinksToLogs(start, end time.Time, filterItems []v3.FilterItem) strin }, } - urlData := v3.URLShareableCompositeQuery{ + urlData := URLShareableCompositeQuery{ QueryType: string(v3.QueryTypeBuilder), - Builder: v3.URLShareableBuilderQuery{ - QueryData: []v3.BuilderQuery{ - builderQuery, + Builder: URLShareableBuilderQuery{ + QueryData: []LinkQuery{ + {BuilderQuery: builderQuery}, }, QueryFormulas: make([]string, 0), }, @@ -220,3 +220,97 @@ func PrepareFilters(labels map[string]string, whereClauseItems []v3.FilterItem, return filterItems } + +func PrepareLinksToTracesV5(start, end time.Time, whereClause string) string { + + // Traces list view expects time in nanoseconds + tr := URLShareableTimeRange{ + Start: start.UnixNano(), + End: end.UnixNano(), + PageSize: 100, + } + + options := URLShareableOptions{} + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + linkQuery := LinkQuery{ + BuilderQuery: v3.BuilderQuery{ + DataSource: v3.DataSourceTraces, + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + AggregateAttribute: v3.AttributeKey{}, + Expression: "A", + Disabled: false, + Having: []v3.Having{}, + StepInterval: 60, + }, + Filter: &FilterExpression{Expression: whereClause}, + } + + urlData := URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: URLShareableBuilderQuery{ + QueryData: []LinkQuery{ + linkQuery, + }, + QueryFormulas: make([]string, 0), + }, + } + + data, _ := json.Marshal(urlData) + compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) + + optionsData, _ := json.Marshal(options) + urlEncodedOptions := url.QueryEscape(string(optionsData)) + + return fmt.Sprintf("compositeQuery=%s&timeRange=%s&startTime=%d&endTime=%d&options=%s", compositeQuery, urlEncodedTimeRange, tr.Start, tr.End, urlEncodedOptions) +} + +func PrepareLinksToLogsV5(start, end time.Time, whereClause string) string { + + // Logs list view expects time in milliseconds + tr := URLShareableTimeRange{ + Start: start.UnixMilli(), + End: end.UnixMilli(), + PageSize: 100, + } + + options := URLShareableOptions{} + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + linkQuery := LinkQuery{ + BuilderQuery: v3.BuilderQuery{ + DataSource: v3.DataSourceLogs, + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + AggregateAttribute: v3.AttributeKey{}, + Expression: "A", + Disabled: false, + Having: []v3.Having{}, + StepInterval: 60, + }, + Filter: &FilterExpression{Expression: whereClause}, + } + + urlData := URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: URLShareableBuilderQuery{ + QueryData: []LinkQuery{ + linkQuery, + }, + QueryFormulas: make([]string, 0), + }, + } + + data, _ := json.Marshal(urlData) + compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) + + optionsData, _ := json.Marshal(options) + urlEncodedOptions := url.QueryEscape(string(optionsData)) + + return fmt.Sprintf("compositeQuery=%s&timeRange=%s&startTime=%d&endTime=%d&options=%s", compositeQuery, urlEncodedTimeRange, tr.Start, tr.End, urlEncodedOptions) +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index ca98dd59ecd7..fd67fd61ca46 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -3,6 +3,7 @@ package app import ( "context" "fmt" + "log/slog" "net" "net/http" _ "net/http/pprof" // http profiler @@ -15,6 +16,7 @@ import ( "github.com/SigNoz/signoz/pkg/licensing/nooplicensing" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/prometheus" + "github.com/SigNoz/signoz/pkg/querier" querierAPI "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/query-service/agentConf" "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader" @@ -91,6 +93,8 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) signoz.TelemetryStore, signoz.Prometheus, signoz.Modules.OrgGetter, + signoz.Querier, + signoz.Instrumentation.Logger(), ) if err != nil { return nil, err @@ -383,6 +387,8 @@ func makeRulesManager( telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, + querier querier.Querier, + logger *slog.Logger, ) (*rules.Manager, error) { // create manager opts managerOpts := &rules.ManagerOptions{ @@ -391,6 +397,8 @@ func makeRulesManager( Context: context.Background(), Logger: zap.L(), Reader: ch, + Querier: querier, + SLogger: logger, Cache: cache, EvalDelay: constants.GetEvalDelay(), SQLStore: sqlstore, diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index f4e56579a371..67bbf65af586 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -12,6 +12,8 @@ import ( "github.com/SigNoz/signoz/pkg/valuer" "github.com/pkg/errors" "go.uber.org/zap" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" ) type DataSource string @@ -510,8 +512,11 @@ type CompositeQuery struct { BuilderQueries map[string]*BuilderQuery `json:"builderQueries,omitempty"` ClickHouseQueries map[string]*ClickHouseQuery `json:"chQueries,omitempty"` PromQueries map[string]*PromQuery `json:"promQueries,omitempty"` - PanelType PanelType `json:"panelType"` - QueryType QueryType `json:"queryType"` + + Queries []qbtypes.QueryEnvelope `json:"queries,omitempty"` + + PanelType PanelType `json:"panelType"` + QueryType QueryType `json:"queryType"` // Unit for the time series data shown in the graph // This is used in alerts to format the value and threshold Unit string `json:"unit,omitempty"` @@ -1457,28 +1462,6 @@ type MetricMetadataResponse struct { Temporality string `json:"temporality"` } -type URLShareableTimeRange struct { - Start int64 `json:"start"` - End int64 `json:"end"` - PageSize int64 `json:"pageSize"` -} - -type URLShareableBuilderQuery struct { - QueryData []BuilderQuery `json:"queryData"` - QueryFormulas []string `json:"queryFormulas"` -} - -type URLShareableCompositeQuery struct { - QueryType string `json:"queryType"` - Builder URLShareableBuilderQuery `json:"builder"` -} - -type URLShareableOptions struct { - MaxLines int `json:"maxLines"` - Format string `json:"format"` - SelectColumns []AttributeKey `json:"selectColumns"` -} - type QBOptions struct { GraphLimitQtype string IsLivetailQuery bool diff --git a/pkg/query-service/rules/base_rule.go b/pkg/query-service/rules/base_rule.go index 093ab10b9640..40952e4a5059 100644 --- a/pkg/query-service/rules/base_rule.go +++ b/pkg/query-service/rules/base_rule.go @@ -3,6 +3,7 @@ package rules import ( "context" "fmt" + "log/slog" "math" "net/url" "sync" @@ -66,7 +67,7 @@ type BaseRule struct { reader interfaces.Reader - logger *zap.Logger + logger *slog.Logger // sendUnmatched sends observed metric values // even if they dont match the rule condition. this is @@ -106,7 +107,7 @@ func WithEvalDelay(dur time.Duration) RuleOption { } } -func WithLogger(logger *zap.Logger) RuleOption { +func WithLogger(logger *slog.Logger) RuleOption { return func(r *BaseRule) { r.logger = logger } @@ -333,7 +334,7 @@ func (r *BaseRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim Limit(1). Scan(ctx, &orgID) if err != nil { - r.logger.Error("failed to get org ids", zap.Error(err)) + r.logger.ErrorContext(ctx, "failed to get org ids", "error", err) return } diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index 3fba5236466e..c9ff9923b295 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "sort" "strings" "sync" @@ -19,6 +20,7 @@ import ( "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/prometheus" + querierV5 "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/query-service/interfaces" "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore" @@ -38,6 +40,8 @@ type PrepareTaskOptions struct { MaintenanceStore ruletypes.MaintenanceStore Logger *zap.Logger Reader interfaces.Reader + Querier querierV5.Querier + SLogger *slog.Logger Cache cache.Cache ManagerOpts *ManagerOptions NotifyFunc NotifyFunc @@ -51,6 +55,8 @@ type PrepareTestRuleOptions struct { MaintenanceStore ruletypes.MaintenanceStore Logger *zap.Logger Reader interfaces.Reader + Querier querierV5.Querier + SLogger *slog.Logger Cache cache.Cache ManagerOpts *ManagerOptions NotifyFunc NotifyFunc @@ -84,6 +90,8 @@ type ManagerOptions struct { Logger *zap.Logger ResendDelay time.Duration Reader interfaces.Reader + Querier querierV5.Querier + SLogger *slog.Logger Cache cache.Cache EvalDelay time.Duration @@ -146,6 +154,8 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) { opts.OrgID, opts.Rule, opts.Reader, + opts.Querier, + opts.SLogger, WithEvalDelay(opts.ManagerOpts.EvalDelay), WithSQLStore(opts.SQLStore), ) @@ -166,7 +176,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) { ruleId, opts.OrgID, opts.Rule, - opts.Logger, + opts.SLogger, opts.Reader, opts.ManagerOpts.Prometheus, WithSQLStore(opts.SQLStore), @@ -392,6 +402,8 @@ func (m *Manager) editTask(_ context.Context, orgID valuer.UUID, rule *ruletypes MaintenanceStore: m.maintenanceStore, Logger: m.logger, Reader: m.reader, + Querier: m.opts.Querier, + SLogger: m.opts.SLogger, Cache: m.cache, ManagerOpts: m.opts, NotifyFunc: m.prepareNotifyFunc(), @@ -583,6 +595,8 @@ func (m *Manager) addTask(_ context.Context, orgID valuer.UUID, rule *ruletypes. MaintenanceStore: m.maintenanceStore, Logger: m.logger, Reader: m.reader, + Querier: m.opts.Querier, + SLogger: m.opts.SLogger, Cache: m.cache, ManagerOpts: m.opts, NotifyFunc: m.prepareNotifyFunc(), diff --git a/pkg/query-service/rules/prom_rule.go b/pkg/query-service/rules/prom_rule.go index 34cf231a5125..90c9d4619c8e 100644 --- a/pkg/query-service/rules/prom_rule.go +++ b/pkg/query-service/rules/prom_rule.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "time" - "go.uber.org/zap" - + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/query-service/formatter" "github.com/SigNoz/signoz/pkg/query-service/interfaces" @@ -20,10 +20,13 @@ import ( "github.com/SigNoz/signoz/pkg/valuer" "github.com/prometheus/prometheus/promql" yaml "gopkg.in/yaml.v2" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" ) type PromRule struct { *BaseRule + version string prometheus prometheus.Prometheus } @@ -31,12 +34,14 @@ func NewPromRule( id string, orgID valuer.UUID, postableRule *ruletypes.PostableRule, - logger *zap.Logger, + logger *slog.Logger, reader interfaces.Reader, prometheus prometheus.Prometheus, opts ...RuleOption, ) (*PromRule, error) { + opts = append(opts, WithLogger(logger)) + baseRule, err := NewBaseRule(id, orgID, postableRule, reader, opts...) if err != nil { return nil, err @@ -44,6 +49,7 @@ func NewPromRule( p := PromRule{ BaseRule: baseRule, + version: postableRule.Version, prometheus: prometheus, } p.logger = logger @@ -54,7 +60,7 @@ func NewPromRule( // can not generate a valid prom QL query return nil, err } - zap.L().Info("creating new prom rule", zap.String("name", p.name), zap.String("query", query)) + logger.Info("creating new prom rule", "rule_name", p.name, "query", query) return &p, nil } @@ -80,6 +86,25 @@ func (r *PromRule) GetSelectedQuery() string { func (r *PromRule) getPqlQuery() (string, error) { + if r.version == "v5" { + if len(r.ruleCondition.CompositeQuery.Queries) > 0 { + selectedQuery := r.GetSelectedQuery() + for _, item := range r.ruleCondition.CompositeQuery.Queries { + switch item.Type { + case qbtypes.QueryTypePromQL: + promQuery, ok := item.Spec.(qbtypes.PromQuery) + if !ok { + return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", item.Spec) + } + if promQuery.Name == selectedQuery { + return promQuery.Query, nil + } + } + } + } + return "", fmt.Errorf("invalid promql rule setup") + } + if r.ruleCondition.CompositeQuery.QueryType == v3.QueryTypePromQL { if len(r.ruleCondition.CompositeQuery.PromQueries) > 0 { selectedQuery := r.GetSelectedQuery() @@ -110,7 +135,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) if err != nil { return nil, err } - zap.L().Info("evaluating promql query", zap.String("name", r.Name()), zap.String("query", q)) + r.logger.InfoContext(ctx, "evaluating promql query", "rule_name", r.Name(), "query", q) res, err := r.RunAlertQuery(ctx, q, start, end, interval) if err != nil { r.SetHealth(ruletypes.HealthBad) @@ -139,7 +164,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) if !shouldAlert { continue } - zap.L().Debug("alerting for series", zap.String("name", r.Name()), zap.Any("series", series)) + r.logger.DebugContext(ctx, "alerting for series", "rule_name", r.Name(), "series", series) threshold := valueFormatter.Format(r.targetVal(), r.Unit()) @@ -161,7 +186,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) result, err := tmpl.Expand() if err != nil { result = fmt.Sprintf("", err) - r.logger.Warn("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData)) + r.logger.WarnContext(ctx, "Expanding alert template failed", "rule_name", r.Name(), "error", err, "data", tmplData) } return result } @@ -207,7 +232,8 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) } } - zap.L().Debug("found alerts for rule", zap.Int("count", len(alerts)), zap.String("name", r.Name())) + r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts)) + // alerts[h] is ready, add or update active list now for h, a := range alerts { // Check whether we already have alerting state for the identifying label set. @@ -229,7 +255,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) for fp, a := range r.Active { labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { - zap.L().Error("error marshaling labels", zap.Error(err), zap.String("name", r.Name())) + r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "rule_name", r.Name()) } if _, ok := resultFPs[fp]; !ok { // If the alert was previously firing, keep it around for a given diff --git a/pkg/query-service/rules/promrule_test.go b/pkg/query-service/rules/promrule_test.go index eed0c7ea1ff5..1ae753634b9e 100644 --- a/pkg/query-service/rules/promrule_test.go +++ b/pkg/query-service/rules/promrule_test.go @@ -4,12 +4,12 @@ import ( "testing" "time" + "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" pql "github.com/prometheus/prometheus/promql" "github.com/stretchr/testify/assert" - "go.uber.org/zap" ) func TestPromRuleShouldAlert(t *testing.T) { @@ -653,12 +653,14 @@ func TestPromRuleShouldAlert(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + for idx, c := range cases { postableRule.RuleCondition.CompareOp = ruletypes.CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target - rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, zap.NewNop(), nil, nil) + rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, nil, nil) if err != nil { assert.NoError(t, err) } diff --git a/pkg/query-service/rules/test_notification.go b/pkg/query-service/rules/test_notification.go index 034df979b12d..f2a6420a4240 100644 --- a/pkg/query-service/rules/test_notification.go +++ b/pkg/query-service/rules/test_notification.go @@ -48,6 +48,8 @@ func defaultTestNotification(opts PrepareTestRuleOptions) (int, *model.ApiError) opts.OrgID, parsedRule, opts.Reader, + opts.Querier, + opts.SLogger, WithSendAlways(), WithSendUnmatched(), WithSQLStore(opts.SQLStore), @@ -65,7 +67,7 @@ func defaultTestNotification(opts PrepareTestRuleOptions) (int, *model.ApiError) alertname, opts.OrgID, parsedRule, - opts.Logger, + opts.SLogger, opts.Reader, opts.ManagerOpts.Prometheus, WithSendAlways(), diff --git a/pkg/query-service/rules/threshold_rule.go b/pkg/query-service/rules/threshold_rule.go index 192de552194f..9ca3850defc4 100644 --- a/pkg/query-service/rules/threshold_rule.go +++ b/pkg/query-service/rules/threshold_rule.go @@ -5,17 +5,19 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "math" + "reflect" "text/template" "time" - "go.uber.org/zap" - "github.com/SigNoz/signoz/pkg/contextlinks" "github.com/SigNoz/signoz/pkg/query-service/common" "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/postprocess" + "github.com/SigNoz/signoz/pkg/transition" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/query-service/app/querier" @@ -33,6 +35,10 @@ import ( tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4" "github.com/SigNoz/signoz/pkg/query-service/formatter" + querierV5 "github.com/SigNoz/signoz/pkg/querier" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + yaml "gopkg.in/yaml.v2" ) @@ -49,12 +55,12 @@ type ThresholdRule struct { // querierV2 is used for alerts created after the introduction of new metrics query builder querierV2 interfaces.Querier + // querierV5 is used for alerts migrated after the introduction of new query builder + querierV5 querierV5.Querier + // used for attribute metadata enrichment for logs and traces logsKeys map[string]v3.AttributeKey spansKeys map[string]v3.AttributeKey - - // internal use - triggerCnt int } func NewThresholdRule( @@ -62,10 +68,14 @@ func NewThresholdRule( orgID valuer.UUID, p *ruletypes.PostableRule, reader interfaces.Reader, + querierV5 querierV5.Querier, + logger *slog.Logger, opts ...RuleOption, ) (*ThresholdRule, error) { - zap.L().Info("creating new ThresholdRule", zap.String("id", id), zap.Any("opts", opts)) + logger.Info("creating new ThresholdRule", "id", id) + + opts = append(opts, WithLogger(logger)) baseRule, err := NewBaseRule(id, orgID, p, reader, opts...) if err != nil { @@ -91,6 +101,7 @@ func NewThresholdRule( t.querier = querier.NewQuerier(querierOption) t.querierV2 = querierV2.NewQuerier(querierOptsV2) + t.querierV5 = querierV5 t.reader = reader return &t, nil } @@ -99,9 +110,11 @@ func (r *ThresholdRule) Type() ruletypes.RuleType { return ruletypes.RuleTypeThreshold } -func (r *ThresholdRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, error) { +func (r *ThresholdRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.QueryRangeParamsV3, error) { - zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.evalWindow.Milliseconds()), zap.Int64("evalDelay", r.evalDelay.Milliseconds())) + r.logger.InfoContext( + ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.evalWindow.Milliseconds(), "eval_delay", r.evalDelay.Milliseconds(), + ) startTs, endTs := r.Timestamps(ts) start, end := startTs.UnixMilli(), endTs.UnixMilli() @@ -182,10 +195,15 @@ func (r *ThresholdRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, }, nil } -func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) string { +func (r *ThresholdRule) prepareLinksToLogs(ctx context.Context, ts time.Time, lbls labels.Labels) string { + + if r.version == "v5" { + return r.prepareLinksToLogsV5(ctx, ts, lbls) + } + selectedQuery := r.GetSelectedQuery() - qr, err := r.prepareQueryRange(ts) + qr, err := r.prepareQueryRange(ctx, ts) if err != nil { return "" } @@ -216,10 +234,15 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str return contextlinks.PrepareLinksToLogs(start, end, filterItems) } -func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) string { +func (r *ThresholdRule) prepareLinksToTraces(ctx context.Context, ts time.Time, lbls labels.Labels) string { + + if r.version == "v5" { + return r.prepareLinksToTracesV5(ctx, ts, lbls) + } + selectedQuery := r.GetSelectedQuery() - qr, err := r.prepareQueryRange(ts) + qr, err := r.prepareQueryRange(ctx, ts) if err != nil { return "" } @@ -250,13 +273,115 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s return contextlinks.PrepareLinksToTraces(start, end, filterItems) } +func (r *ThresholdRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) { + + r.logger.InfoContext( + ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.evalWindow.Milliseconds(), "eval_delay", r.evalDelay.Milliseconds(), + ) + + startTs, endTs := r.Timestamps(ts) + start, end := startTs.UnixMilli(), endTs.UnixMilli() + + req := &qbtypes.QueryRangeRequest{ + Start: uint64(start), + End: uint64(end), + RequestType: qbtypes.RequestTypeTimeSeries, + CompositeQuery: qbtypes.CompositeQuery{ + Queries: make([]qbtypes.QueryEnvelope, 0), + }, + NoCache: true, + } + copy(r.Condition().CompositeQuery.Queries, req.CompositeQuery.Queries) + return req, nil +} + +func (r *ThresholdRule) prepareLinksToLogsV5(ctx context.Context, ts time.Time, lbls labels.Labels) string { + selectedQuery := r.GetSelectedQuery() + + qr, err := r.prepareQueryRangeV5(ctx, ts) + if err != nil { + return "" + } + start := time.UnixMilli(int64(qr.Start)) + end := time.UnixMilli(int64(qr.End)) + + // TODO(srikanthccv): handle formula queries + if selectedQuery < "A" || selectedQuery > "Z" { + return "" + } + + var q qbtypes.QueryBuilderQuery[qbtypes.LogAggregation] + + for _, query := range r.ruleCondition.CompositeQuery.Queries { + if query.Type == qbtypes.QueryTypeBuilder { + switch spec := query.Spec.(type) { + case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]: + q = spec + } + } + } + + if q.Signal != telemetrytypes.SignalLogs { + return "" + } + + filterExpr := "" + if q.Filter != nil && q.Filter.Expression != "" { + filterExpr = q.Filter.Expression + } + + whereClause := contextlinks.PrepareFilterExpression(lbls.Map(), filterExpr, q.GroupBy) + + return contextlinks.PrepareLinksToLogsV5(start, end, whereClause) +} + +func (r *ThresholdRule) prepareLinksToTracesV5(ctx context.Context, ts time.Time, lbls labels.Labels) string { + selectedQuery := r.GetSelectedQuery() + + qr, err := r.prepareQueryRangeV5(ctx, ts) + if err != nil { + return "" + } + start := time.UnixMilli(int64(qr.Start)) + end := time.UnixMilli(int64(qr.End)) + + // TODO(srikanthccv): handle formula queries + if selectedQuery < "A" || selectedQuery > "Z" { + return "" + } + + var q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation] + + for _, query := range r.ruleCondition.CompositeQuery.Queries { + if query.Type == qbtypes.QueryTypeBuilder { + switch spec := query.Spec.(type) { + case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]: + q = spec + } + } + } + + if q.Signal != telemetrytypes.SignalTraces { + return "" + } + + filterExpr := "" + if q.Filter != nil && q.Filter.Expression != "" { + filterExpr = q.Filter.Expression + } + + whereClause := contextlinks.PrepareFilterExpression(lbls.Map(), filterExpr, q.GroupBy) + + return contextlinks.PrepareLinksToTracesV5(start, end, whereClause) +} + func (r *ThresholdRule) GetSelectedQuery() string { return r.ruleCondition.GetSelectedQueryName() } func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) { - params, err := r.prepareQueryRange(ts) + params, err := r.prepareQueryRange(ctx, ts) if err != nil { return nil, err } @@ -310,14 +435,14 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, } if err != nil { - zap.L().Error("failed to get alert query result", zap.String("rule", r.Name()), zap.Error(err), zap.Any("errors", queryErrors)) + r.logger.ErrorContext(ctx, "failed to get alert query range result", "rule_name", r.Name(), "error", err, "query_errors", queryErrors) return nil, fmt.Errorf("internal error while querying") } if params.CompositeQuery.QueryType == v3.QueryTypeBuilder { results, err = postprocess.PostProcessResult(results, params) if err != nil { - zap.L().Error("failed to post process result", zap.String("rule", r.Name()), zap.Error(err)) + r.logger.ErrorContext(ctx, "failed to post process result", "rule_name", r.Name(), "error", err) return nil, fmt.Errorf("internal error while post processing") } } @@ -340,7 +465,81 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, // if the data is missing for `For` duration then we should send alert if r.ruleCondition.AlertOnAbsent && r.lastTimestampWithDatapoints.Add(time.Duration(r.Condition().AbsentFor)*time.Minute).Before(time.Now()) { - zap.L().Info("no data found for rule condition", zap.String("ruleid", r.ID())) + r.logger.InfoContext(ctx, "no data found for rule condition", "rule_id", r.ID()) + lbls := labels.NewBuilder(labels.Labels{}) + if !r.lastTimestampWithDatapoints.IsZero() { + lbls.Set("lastSeen", r.lastTimestampWithDatapoints.Format(constants.AlertTimeFormat)) + } + resultVector = append(resultVector, ruletypes.Sample{ + Metric: lbls.Labels(), + IsMissing: true, + }) + return resultVector, nil + } + + for _, series := range queryResult.Series { + smpl, shouldAlert := r.ShouldAlert(*series) + if shouldAlert { + resultVector = append(resultVector, smpl) + } + } + + return resultVector, nil +} + +func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) { + + params, err := r.prepareQueryRangeV5(ctx, ts) + if err != nil { + return nil, err + } + + var results []*v3.Result + + v5Result, err := r.querierV5.QueryRange(ctx, orgID, params) + + if err != nil { + r.logger.ErrorContext(ctx, "failed to get alert query result", "rule_name", r.Name(), "error", err) + return nil, fmt.Errorf("internal error while querying") + } + + data, ok := v5Result.Data.(struct { + Results []any `json:"results"` + Warnings []string `json:"warnings"` + }) + + if !ok { + return nil, fmt.Errorf("unexpected result from v5 querier") + } + + for _, item := range data.Results { + if tsData, ok := item.(*qbtypes.TimeSeriesData); ok { + results = append(results, transition.ConvertV5TimeSeriesDataToV4Result(tsData)) + } else { + // NOTE: should not happen but just to ensure we don't miss it if it happens for some reason + r.logger.WarnContext(ctx, "expected qbtypes.TimeSeriesData but got", "item_type", reflect.TypeOf(item)) + } + } + + selectedQuery := r.GetSelectedQuery() + + var queryResult *v3.Result + for _, res := range results { + if res.QueryName == selectedQuery { + queryResult = res + break + } + } + + if queryResult != nil && len(queryResult.Series) > 0 { + r.lastTimestampWithDatapoints = time.Now() + } + + var resultVector ruletypes.Vector + + // if the data is missing for `For` duration then we should send alert + if r.ruleCondition.AlertOnAbsent && r.lastTimestampWithDatapoints.Add(time.Duration(r.Condition().AbsentFor)*time.Minute).Before(time.Now()) { + r.logger.InfoContext(ctx, "no data found for rule condition", "rule_id", r.ID()) lbls := labels.NewBuilder(labels.Labels{}) if !r.lastTimestampWithDatapoints.IsZero() { lbls.Set("lastSeen", r.lastTimestampWithDatapoints.Format(constants.AlertTimeFormat)) @@ -367,7 +566,17 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er prevState := r.State() valueFormatter := formatter.FromUnit(r.Unit()) - res, err := r.buildAndRunQuery(ctx, r.orgID, ts) + + var res ruletypes.Vector + var err error + + if r.version == "v5" { + r.logger.InfoContext(ctx, "running v5 query") + res, err = r.buildAndRunQueryV5(ctx, r.orgID, ts) + } else { + r.logger.InfoContext(ctx, "running v4 query") + res, err = r.buildAndRunQuery(ctx, r.orgID, ts) + } if err != nil { return nil, err @@ -387,7 +596,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er value := valueFormatter.Format(smpl.V, r.Unit()) threshold := valueFormatter.Format(r.targetVal(), r.Unit()) - zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold)) + r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold) tmplData := ruletypes.AlertTemplateData(l, value, threshold) // Inject some convenience variables that are easier to remember for users @@ -408,7 +617,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er result, err := tmpl.Expand() if err != nil { result = fmt.Sprintf("", err) - zap.L().Error("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData)) + r.logger.ErrorContext(ctx, "Expanding alert template failed", "error", err, "data", tmplData) } return result } @@ -436,15 +645,15 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er // is used alert grouping, and we want to group alerts with the same // label set, but different timestamps, together. if r.typ == ruletypes.AlertTypeTraces { - link := r.prepareLinksToTraces(ts, smpl.Metric) + link := r.prepareLinksToTraces(ctx, ts, smpl.Metric) if link != "" && r.hostFromSource() != "" { - zap.L().Info("adding traces link to annotations", zap.String("link", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link))) + r.logger.InfoContext(ctx, "adding traces link to annotations", "link", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)) annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)}) } } else if r.typ == ruletypes.AlertTypeLogs { - link := r.prepareLinksToLogs(ts, smpl.Metric) + link := r.prepareLinksToLogs(ctx, ts, smpl.Metric) if link != "" && r.hostFromSource() != "" { - zap.L().Info("adding logs link to annotations", zap.String("link", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link))) + r.logger.InfoContext(ctx, "adding logs link to annotations", "link", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)) annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)}) } } @@ -454,9 +663,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er resultFPs[h] = struct{}{} if _, ok := alerts[h]; ok { - zap.L().Error("the alert query returns duplicate records", zap.String("ruleid", r.ID()), zap.Any("alert", alerts[h])) - err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels") - return nil, err + return nil, fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels") } alerts[h] = &ruletypes.Alert{ @@ -472,7 +679,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er } } - zap.L().Info("number of alerts found", zap.String("name", r.Name()), zap.Int("count", len(alerts))) + r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts)) // alerts[h] is ready, add or update active list now for h, a := range alerts { @@ -495,7 +702,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er for fp, a := range r.Active { labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { - zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels)) + r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "labels", a.Labels) } if _, ok := resultFPs[fp]; !ok { // If the alert was previously firing, keep it around for a given diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index 068edda15fd1..9735fe985238 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -14,6 +14,7 @@ import ( "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader" @@ -24,6 +25,8 @@ import ( "github.com/stretchr/testify/require" cmock "github.com/srikanthccv/ClickHouse-go-mock" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" ) func TestThresholdRuleShouldAlert(t *testing.T) { @@ -52,6 +55,8 @@ func TestThresholdRuleShouldAlert(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + cases := []struct { values v3.Series expectAlert bool @@ -800,7 +805,7 @@ func TestThresholdRuleShouldAlert(t *testing.T) { postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, WithEvalDelay(2*time.Minute)) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) if err != nil { assert.NoError(t, err) } @@ -888,17 +893,119 @@ func TestPrepareLinksToLogs(t *testing.T) { }, } - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, WithEvalDelay(2*time.Minute)) + logger := instrumentationtest.New().Logger() + + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) if err != nil { assert.NoError(t, err) } ts := time.UnixMilli(1705469040000) - link := rule.prepareLinksToLogs(ts, labels.Labels{}) + link := rule.prepareLinksToLogs(context.Background(), ts, labels.Labels{}) assert.Contains(t, link, "&timeRange=%7B%22start%22%3A1705468620000%2C%22end%22%3A1705468920000%2C%22pageSize%22%3A100%7D&startTime=1705468620000&endTime=1705468920000") } +func TestPrepareLinksToLogsV5(t *testing.T) { + postableRule := ruletypes.PostableRule{ + AlertName: "Tricky Condition Tests", + AlertType: ruletypes.AlertTypeLogs, + RuleType: ruletypes.RuleTypeThreshold, + EvalWindow: ruletypes.Duration(5 * time.Minute), + Frequency: ruletypes.Duration(1 * time.Minute), + RuleCondition: &ruletypes.RuleCondition{ + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + Queries: []qbtypes.QueryEnvelope{ + { + Type: qbtypes.QueryTypeBuilder, + Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Name: "A", + StepInterval: qbtypes.Step{Duration: 1 * time.Minute}, + Aggregations: []qbtypes.LogAggregation{ + { + Expression: "count()", + }, + }, + Filter: &qbtypes.Filter{ + Expression: "service.name EXISTS", + }, + Signal: telemetrytypes.SignalLogs, + }, + }, + }, + }, + CompareOp: "4", // Not Equals + MatchType: "1", // Once + Target: &[]float64{0.0}[0], + SelectedQuery: "A", + }, + Version: "v5", + } + + logger := instrumentationtest.New().Logger() + + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) + if err != nil { + assert.NoError(t, err) + } + + ts := time.UnixMilli(1753527163000) + + link := rule.prepareLinksToLogs(context.Background(), ts, labels.Labels{}) + assert.Contains(t, link, "compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522queryName%2522%253A%2522A%2522%252C%2522stepInterval%2522%253A60%252C%2522dataSource%2522%253A%2522logs%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522key%2522%253A%2522%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522type%2522%253A%2522%2522%252C%2522isColumn%2522%253Afalse%252C%2522isJSON%2522%253Afalse%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522limit%2522%253A0%252C%2522offset%2522%253A0%252C%2522pageSize%2522%253A0%252C%2522ShiftBy%2522%253A0%252C%2522IsAnomaly%2522%253Afalse%252C%2522QueriesUsedInFormula%2522%253Anull%252C%2522filter%2522%253A%257B%2522expression%2522%253A%2522service.name%2BEXISTS%2522%257D%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%257D&timeRange=%7B%22start%22%3A1753526700000%2C%22end%22%3A1753527000000%2C%22pageSize%22%3A100%7D&startTime=1753526700000&endTime=1753527000000&options=%7B%22maxLines%22%3A0%2C%22format%22%3A%22%22%2C%22selectColumns%22%3Anull%7D") +} + +func TestPrepareLinksToTracesV5(t *testing.T) { + postableRule := ruletypes.PostableRule{ + AlertName: "Tricky Condition Tests", + AlertType: ruletypes.AlertTypeTraces, + RuleType: ruletypes.RuleTypeThreshold, + EvalWindow: ruletypes.Duration(5 * time.Minute), + Frequency: ruletypes.Duration(1 * time.Minute), + RuleCondition: &ruletypes.RuleCondition{ + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + Queries: []qbtypes.QueryEnvelope{ + { + Type: qbtypes.QueryTypeBuilder, + Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Name: "A", + StepInterval: qbtypes.Step{Duration: 1 * time.Minute}, + Aggregations: []qbtypes.TraceAggregation{ + { + Expression: "count()", + }, + }, + Filter: &qbtypes.Filter{ + Expression: "service.name EXISTS", + }, + Signal: telemetrytypes.SignalTraces, + }, + }, + }, + }, + CompareOp: "4", // Not Equals + MatchType: "1", // Once + Target: &[]float64{0.0}[0], + SelectedQuery: "A", + }, + Version: "v5", + } + + logger := instrumentationtest.New().Logger() + + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) + if err != nil { + assert.NoError(t, err) + } + + ts := time.UnixMilli(1753527163000) + + link := rule.prepareLinksToTraces(context.Background(), ts, labels.Labels{}) + assert.Contains(t, link, "compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522queryName%2522%253A%2522A%2522%252C%2522stepInterval%2522%253A60%252C%2522dataSource%2522%253A%2522traces%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522key%2522%253A%2522%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522type%2522%253A%2522%2522%252C%2522isColumn%2522%253Afalse%252C%2522isJSON%2522%253Afalse%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522limit%2522%253A0%252C%2522offset%2522%253A0%252C%2522pageSize%2522%253A0%252C%2522ShiftBy%2522%253A0%252C%2522IsAnomaly%2522%253Afalse%252C%2522QueriesUsedInFormula%2522%253Anull%252C%2522filter%2522%253A%257B%2522expression%2522%253A%2522service.name%2BEXISTS%2522%257D%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%257D&timeRange=%7B%22start%22%3A1753526700000000000%2C%22end%22%3A1753527000000000000%2C%22pageSize%22%3A100%7D&startTime=1753526700000000000&endTime=1753527000000000000&options=%7B%22maxLines%22%3A0%2C%22format%22%3A%22%22%2C%22selectColumns%22%3Anull%7D") +} + func TestPrepareLinksToTraces(t *testing.T) { postableRule := ruletypes.PostableRule{ AlertName: "Links to traces test", @@ -929,14 +1036,16 @@ func TestPrepareLinksToTraces(t *testing.T) { }, } - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, WithEvalDelay(2*time.Minute)) + logger := instrumentationtest.New().Logger() + + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) if err != nil { assert.NoError(t, err) } ts := time.UnixMilli(1705469040000) - link := rule.prepareLinksToTraces(ts, labels.Labels{}) + link := rule.prepareLinksToTraces(context.Background(), ts, labels.Labels{}) assert.Contains(t, link, "&timeRange=%7B%22start%22%3A1705468620000000000%2C%22end%22%3A1705468920000000000%2C%22pageSize%22%3A100%7D&startTime=1705468620000000000&endTime=1705468920000000000") } @@ -999,12 +1108,14 @@ func TestThresholdRuleLabelNormalization(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + for idx, c := range cases { postableRule.RuleCondition.CompareOp = ruletypes.CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, WithEvalDelay(2*time.Minute)) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) if err != nil { assert.NoError(t, err) } @@ -1055,17 +1166,19 @@ func TestThresholdRuleEvalDelay(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + for idx, c := range cases { - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil) // no eval delay + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger) // no eval delay if err != nil { assert.NoError(t, err) } - params, err := rule.prepareQueryRange(ts) + params, err := rule.prepareQueryRange(context.Background(), ts) assert.NoError(t, err) assert.Equal(t, c.expectedQuery, params.CompositeQuery.ClickHouseQueries["A"].Query, "Test case %d", idx) - secondTimeParams, err := rule.prepareQueryRange(ts) + secondTimeParams, err := rule.prepareQueryRange(context.Background(), ts) assert.NoError(t, err) assert.Equal(t, c.expectedQuery, secondTimeParams.CompositeQuery.ClickHouseQueries["A"].Query, "Test case %d", idx) } @@ -1103,17 +1216,19 @@ func TestThresholdRuleClickHouseTmpl(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + for idx, c := range cases { - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, WithEvalDelay(2*time.Minute)) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger, WithEvalDelay(2*time.Minute)) if err != nil { assert.NoError(t, err) } - params, err := rule.prepareQueryRange(ts) + params, err := rule.prepareQueryRange(context.Background(), ts) assert.NoError(t, err) assert.Equal(t, c.expectedQuery, params.CompositeQuery.ClickHouseQueries["A"].Query, "Test case %d", idx) - secondTimeParams, err := rule.prepareQueryRange(ts) + secondTimeParams, err := rule.prepareQueryRange(context.Background(), ts) assert.NoError(t, err) assert.Equal(t, c.expectedQuery, secondTimeParams.CompositeQuery.ClickHouseQueries["A"].Query, "Test case %d", idx) } @@ -1221,6 +1336,8 @@ func TestThresholdRuleUnitCombinations(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + for idx, c := range cases { rows := cmock.NewRows(cols, c.values) // We are testing the eval logic after the query is run @@ -1243,7 +1360,7 @@ func TestThresholdRuleUnitCombinations(t *testing.T) { readerCache, err := cachetest.New(cache.Config{Provider: "memory", Memory: cache.Memory{TTL: DefaultFrequency}}) require.NoError(t, err) reader := clickhouseReader.NewReaderFromClickhouseConnection(options, nil, telemetryStore, prometheustest.New(instrumentationtest.New().Logger(), prometheus.Config{}), "", time.Duration(time.Second), readerCache) - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader, nil, logger) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ "signoz_calls_total": { v3.Delta: true, @@ -1317,6 +1434,8 @@ func TestThresholdRuleNoData(t *testing.T) { }, } + logger := instrumentationtest.New().Logger() + for idx, c := range cases { rows := cmock.NewRows(cols, c.values) @@ -1339,7 +1458,7 @@ func TestThresholdRuleNoData(t *testing.T) { options := clickhouseReader.NewOptions("", "", "archiveNamespace") reader := clickhouseReader.NewReaderFromClickhouseConnection(options, nil, telemetryStore, prometheustest.New(instrumentationtest.New().Logger(), prometheus.Config{}), "", time.Duration(time.Second), readerCache) - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader, nil, logger) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ "signoz_calls_total": { v3.Delta: true, @@ -1413,6 +1532,8 @@ func TestThresholdRuleTracesLink(t *testing.T) { cols = append(cols, cmock.ColumnType{Name: "attr", Type: "String"}) cols = append(cols, cmock.ColumnType{Name: "timestamp", Type: "String"}) + logger := instrumentationtest.New().Logger() + for idx, c := range testCases { metaRows := cmock.NewRows(metaCols, c.metaValues) telemetryStore.Mock(). @@ -1443,7 +1564,7 @@ func TestThresholdRuleTracesLink(t *testing.T) { options := clickhouseReader.NewOptions("", "", "archiveNamespace") reader := clickhouseReader.NewReaderFromClickhouseConnection(options, nil, telemetryStore, prometheustest.New(instrumentationtest.New().Logger(), prometheus.Config{}), "", time.Duration(time.Second), nil) - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader, nil, logger) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ "signoz_calls_total": { v3.Delta: true, @@ -1527,6 +1648,8 @@ func TestThresholdRuleLogsLink(t *testing.T) { cols = append(cols, cmock.ColumnType{Name: "attr", Type: "String"}) cols = append(cols, cmock.ColumnType{Name: "timestamp", Type: "String"}) + logger := instrumentationtest.New().Logger() + for idx, c := range testCases { attrMetaRows := cmock.NewRows(attrMetaCols, c.attrMetaValues) telemetryStore.Mock(). @@ -1564,7 +1687,7 @@ func TestThresholdRuleLogsLink(t *testing.T) { options := clickhouseReader.NewOptions("", "", "archiveNamespace") reader := clickhouseReader.NewReaderFromClickhouseConnection(options, nil, telemetryStore, prometheustest.New(instrumentationtest.New().Logger(), prometheus.Config{}), "", time.Duration(time.Second), nil) - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader) + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, reader, nil, logger) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ "signoz_calls_total": { v3.Delta: true, @@ -1640,7 +1763,9 @@ func TestThresholdRuleShiftBy(t *testing.T) { }, } - rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil) + logger := instrumentationtest.New().Logger() + + rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, nil, logger) if err != nil { assert.NoError(t, err) } @@ -1650,7 +1775,7 @@ func TestThresholdRuleShiftBy(t *testing.T) { }, } - params, err := rule.prepareQueryRange(time.Now()) + params, err := rule.prepareQueryRange(context.Background(), time.Now()) if err != nil { assert.NoError(t, err) } diff --git a/pkg/query-service/transition/v3_to_v5_req.go b/pkg/query-service/transition/v3_to_v5_req.go deleted file mode 100644 index e4da53e89ec1..000000000000 --- a/pkg/query-service/transition/v3_to_v5_req.go +++ /dev/null @@ -1,683 +0,0 @@ -package transition - -import ( - "fmt" - "strings" - "time" - - "github.com/SigNoz/signoz/pkg/types/metrictypes" - "github.com/SigNoz/signoz/pkg/types/telemetrytypes" - - "github.com/SigNoz/signoz/pkg/query-service/constants" - v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" - "github.com/SigNoz/signoz/pkg/query-service/utils" - - v5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" -) - -func ConvertV3ToV5(params *v3.QueryRangeParamsV3) (*v5.QueryRangeRequest, error) { - v3Params := params.Clone() - - if v3Params == nil || v3Params.CompositeQuery == nil { - return nil, fmt.Errorf("v3 params or composite query is nil") - } - - varItems := map[string]v5.VariableItem{} - - for name, value := range v3Params.Variables { - varItems[name] = v5.VariableItem{ - Type: v5.QueryVariableType, // doesn't matter at the moment - Value: value, - } - } - - v5Request := &v5.QueryRangeRequest{ - SchemaVersion: "v5", - Start: uint64(v3Params.Start), - End: uint64(v3Params.End), - RequestType: convertPanelTypeToRequestType(v3Params.CompositeQuery.PanelType), - Variables: varItems, - CompositeQuery: v5.CompositeQuery{ - Queries: []v5.QueryEnvelope{}, - }, - FormatOptions: &v5.FormatOptions{ - FormatTableResultForUI: v3Params.FormatForWeb, - FillGaps: v3Params.CompositeQuery.FillGaps, - }, - } - - // Convert based on query type - switch v3Params.CompositeQuery.QueryType { - case v3.QueryTypeBuilder: - if err := convertBuilderQueries(v3Params.CompositeQuery.BuilderQueries, &v5Request.CompositeQuery); err != nil { - return nil, err - } - case v3.QueryTypeClickHouseSQL: - if err := convertClickHouseQueries(v3Params.CompositeQuery.ClickHouseQueries, &v5Request.CompositeQuery); err != nil { - return nil, err - } - case v3.QueryTypePromQL: - if err := convertPromQueries(v3Params.CompositeQuery.PromQueries, v3Params.Step, &v5Request.CompositeQuery); err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("unsupported query type: %s", v3Params.CompositeQuery.QueryType) - } - - return v5Request, nil -} - -func convertPanelTypeToRequestType(panelType v3.PanelType) v5.RequestType { - switch panelType { - case v3.PanelTypeValue, v3.PanelTypeTable: - return v5.RequestTypeScalar - case v3.PanelTypeGraph: - return v5.RequestTypeTimeSeries - case v3.PanelTypeList, v3.PanelTypeTrace: - return v5.RequestTypeRaw - default: - return v5.RequestTypeUnknown - } -} - -func convertBuilderQueries(v3Queries map[string]*v3.BuilderQuery, v5Composite *v5.CompositeQuery) error { - for name, query := range v3Queries { - if query == nil { - continue - } - - // Handle formula queries - if query.Expression != "" && query.Expression != name { - v5Envelope := v5.QueryEnvelope{ - Type: v5.QueryTypeFormula, - Spec: v5.QueryBuilderFormula{ - Name: name, - Expression: query.Expression, - Disabled: query.Disabled, - Order: convertOrderBy(query.OrderBy, query), - Limit: int(query.Limit), - Having: convertHaving(query.Having, query), - Functions: convertFunctions(query.Functions), - }, - } - v5Composite.Queries = append(v5Composite.Queries, v5Envelope) - continue - } - - // Regular builder query - envelope, err := convertSingleBuilderQuery(name, query) - if err != nil { - return err - } - v5Composite.Queries = append(v5Composite.Queries, envelope) - } - return nil -} - -func convertSingleBuilderQuery(name string, v3Query *v3.BuilderQuery) (v5.QueryEnvelope, error) { - v5Envelope := v5.QueryEnvelope{ - Type: v5.QueryTypeBuilder, - } - - switch v3Query.DataSource { - case v3.DataSourceTraces: - v5Query := v5.QueryBuilderQuery[v5.TraceAggregation]{ - Name: name, - Signal: telemetrytypes.SignalTraces, - Disabled: v3Query.Disabled, - StepInterval: v5.Step{Duration: time.Duration(v3Query.StepInterval) * time.Second}, - Filter: convertFilter(v3Query.Filters), - GroupBy: convertGroupBy(v3Query.GroupBy), - Order: convertOrderBy(v3Query.OrderBy, v3Query), - Limit: int(v3Query.Limit), - Offset: int(v3Query.Offset), - Having: convertHaving(v3Query.Having, v3Query), - Functions: convertFunctions(v3Query.Functions), - SelectFields: convertSelectColumns(v3Query.SelectColumns), - } - - // Convert trace aggregations - if v3Query.AggregateOperator != v3.AggregateOperatorNoOp { - v5Query.Aggregations = []v5.TraceAggregation{ - { - Expression: buildTraceAggregationExpression(v3Query), - Alias: "", - }, - } - } - - v5Envelope.Spec = v5Query - - case v3.DataSourceLogs: - v5Query := v5.QueryBuilderQuery[v5.LogAggregation]{ - Name: name, - Signal: telemetrytypes.SignalLogs, - Disabled: v3Query.Disabled, - StepInterval: v5.Step{Duration: time.Duration(v3Query.StepInterval) * time.Second}, - Filter: convertFilter(v3Query.Filters), - GroupBy: convertGroupBy(v3Query.GroupBy), - Order: convertOrderBy(v3Query.OrderBy, v3Query), - Limit: int(v3Query.PageSize), - Offset: int(v3Query.Offset), - Having: convertHaving(v3Query.Having, v3Query), - Functions: convertFunctions(v3Query.Functions), - } - - // Convert log aggregations - if v3Query.AggregateOperator != v3.AggregateOperatorNoOp { - v5Query.Aggregations = []v5.LogAggregation{ - { - Expression: buildLogAggregationExpression(v3Query), - Alias: "", - }, - } - } - - v5Envelope.Spec = v5Query - - case v3.DataSourceMetrics: - v5Query := v5.QueryBuilderQuery[v5.MetricAggregation]{ - Name: name, - Signal: telemetrytypes.SignalMetrics, - Disabled: v3Query.Disabled, - StepInterval: v5.Step{Duration: time.Duration(v3Query.StepInterval) * time.Second}, - Filter: convertFilter(v3Query.Filters), - GroupBy: convertGroupBy(v3Query.GroupBy), - Order: convertOrderBy(v3Query.OrderBy, v3Query), - Limit: int(v3Query.Limit), - Offset: int(v3Query.Offset), - Having: convertHaving(v3Query.Having, v3Query), - Functions: convertFunctions(v3Query.Functions), - } - - if v3Query.AggregateAttribute.Key != "" { - v5Query.Aggregations = []v5.MetricAggregation{ - { - MetricName: v3Query.AggregateAttribute.Key, - Temporality: convertTemporality(v3Query.Temporality), - TimeAggregation: convertTimeAggregation(v3Query.TimeAggregation), - SpaceAggregation: convertSpaceAggregation(v3Query.SpaceAggregation), - }, - } - } - - v5Envelope.Spec = v5Query - - default: - return v5Envelope, fmt.Errorf("unsupported data source: %s", v3Query.DataSource) - } - - return v5Envelope, nil -} - -func buildTraceAggregationExpression(v3Query *v3.BuilderQuery) string { - switch v3Query.AggregateOperator { - case v3.AggregateOperatorCount: - if v3Query.AggregateAttribute.Key != "" { - return fmt.Sprintf("count(%s)", v3Query.AggregateAttribute.Key) - } - return "count()" - case v3.AggregateOperatorCountDistinct: - if v3Query.AggregateAttribute.Key != "" { - return fmt.Sprintf("countDistinct(%s)", v3Query.AggregateAttribute.Key) - } - return "countDistinct()" - case v3.AggregateOperatorSum: - return fmt.Sprintf("sum(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorAvg: - return fmt.Sprintf("avg(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorMin: - return fmt.Sprintf("min(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorMax: - return fmt.Sprintf("max(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP05: - return fmt.Sprintf("p05(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP10: - return fmt.Sprintf("p10(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP20: - return fmt.Sprintf("p20(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP25: - return fmt.Sprintf("p25(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP50: - return fmt.Sprintf("p50(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP75: - return fmt.Sprintf("p75(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP90: - return fmt.Sprintf("p90(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP95: - return fmt.Sprintf("p95(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorP99: - return fmt.Sprintf("p99(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorRate: - return "rate()" - case v3.AggregateOperatorRateSum: - return fmt.Sprintf("rate_sum(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorRateAvg: - return fmt.Sprintf("rate_avg(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorRateMin: - return fmt.Sprintf("rate_min(%s)", v3Query.AggregateAttribute.Key) - case v3.AggregateOperatorRateMax: - return fmt.Sprintf("rate_max(%s)", v3Query.AggregateAttribute.Key) - default: - return "count()" - } -} - -func buildLogAggregationExpression(v3Query *v3.BuilderQuery) string { - // Similar to traces - return buildTraceAggregationExpression(v3Query) -} - -func convertFilter(v3Filter *v3.FilterSet) *v5.Filter { - if v3Filter == nil || len(v3Filter.Items) == 0 { - return nil - } - - expressions := []string{} - for _, item := range v3Filter.Items { - expr := buildFilterExpression(item) - if expr != "" { - expressions = append(expressions, expr) - } - } - - if len(expressions) == 0 { - return nil - } - - operator := "AND" - if v3Filter.Operator == "OR" { - operator = "OR" - } - - return &v5.Filter{ - Expression: strings.Join(expressions, fmt.Sprintf(" %s ", operator)), - } -} - -func buildFilterExpression(item v3.FilterItem) string { - key := item.Key.Key - value := item.Value - - switch item.Operator { - case v3.FilterOperatorEqual: - return fmt.Sprintf("%s = %s", key, formatValue(value)) - case v3.FilterOperatorNotEqual: - return fmt.Sprintf("%s != %s", key, formatValue(value)) - case v3.FilterOperatorGreaterThan: - return fmt.Sprintf("%s > %s", key, formatValue(value)) - case v3.FilterOperatorGreaterThanOrEq: - return fmt.Sprintf("%s >= %s", key, formatValue(value)) - case v3.FilterOperatorLessThan: - return fmt.Sprintf("%s < %s", key, formatValue(value)) - case v3.FilterOperatorLessThanOrEq: - return fmt.Sprintf("%s <= %s", key, formatValue(value)) - case v3.FilterOperatorIn: - return fmt.Sprintf("%s IN %s", key, formatValue(value)) - case v3.FilterOperatorNotIn: - return fmt.Sprintf("%s NOT IN %s", key, formatValue(value)) - case v3.FilterOperatorContains: - return fmt.Sprintf("%s LIKE '%%%v%%'", key, value) - case v3.FilterOperatorNotContains: - return fmt.Sprintf("%s NOT LIKE '%%%v%%'", key, value) - case v3.FilterOperatorRegex: - return fmt.Sprintf("%s REGEXP %s", key, formatValue(value)) - case v3.FilterOperatorNotRegex: - return fmt.Sprintf("%s NOT REGEXP %s", key, formatValue(value)) - case v3.FilterOperatorExists: - return fmt.Sprintf("%s EXISTS", key) - case v3.FilterOperatorNotExists: - return fmt.Sprintf("%s NOT EXISTS", key) - default: - return "" - } -} - -func formatValue(value interface{}) string { - return utils.ClickHouseFormattedValue(value) -} - -func convertGroupBy(v3GroupBy []v3.AttributeKey) []v5.GroupByKey { - v5GroupBy := []v5.GroupByKey{} - for _, key := range v3GroupBy { - v5GroupBy = append(v5GroupBy, v5.GroupByKey{ - TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{ - Name: key.Key, - FieldDataType: convertDataType(key.DataType), - FieldContext: convertAttributeType(key.Type), - Materialized: key.IsColumn, - }, - }) - } - return v5GroupBy -} - -func convertOrderBy(v3OrderBy []v3.OrderBy, v3Query *v3.BuilderQuery) []v5.OrderBy { - v5OrderBy := []v5.OrderBy{} - for _, order := range v3OrderBy { - direction := v5.OrderDirectionAsc - if order.Order == v3.DirectionDesc { - direction = v5.OrderDirectionDesc - } - - var orderByName string - if order.ColumnName == "#SIGNOZ_VALUE" { - if v3Query.DataSource == v3.DataSourceLogs || v3Query.DataSource == v3.DataSourceTraces { - orderByName = buildTraceAggregationExpression(v3Query) - } else { - if v3Query.Expression != v3Query.QueryName { - orderByName = v3Query.Expression - } else { - orderByName = fmt.Sprintf("%s(%s)", v3Query.SpaceAggregation, v3Query.AggregateAttribute.Key) - } - } - } else { - orderByName = order.ColumnName - } - - v5OrderBy = append(v5OrderBy, v5.OrderBy{ - Key: v5.OrderByKey{ - TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{ - Name: orderByName, - Materialized: order.IsColumn, - }, - }, - Direction: direction, - }) - } - return v5OrderBy -} - -func convertHaving(v3Having []v3.Having, v3Query *v3.BuilderQuery) *v5.Having { - if len(v3Having) == 0 { - return nil - } - - expressions := []string{} - for _, h := range v3Having { - var expr string - - if v3Query.DataSource == v3.DataSourceLogs || v3Query.DataSource == v3.DataSourceTraces { - h.ColumnName = buildTraceAggregationExpression(v3Query) - } else { - if v3Query.Expression != v3Query.QueryName { - h.ColumnName = v3Query.Expression - } else { - h.ColumnName = fmt.Sprintf("%s(%s)", v3Query.SpaceAggregation, v3Query.AggregateAttribute.Key) - } - } - expr = buildHavingExpression(h) - - if expr != "" { - expressions = append(expressions, expr) - } - } - - if len(expressions) == 0 { - return nil - } - - return &v5.Having{ - Expression: strings.Join(expressions, " AND "), - } -} - -func buildHavingExpression(having v3.Having) string { - - switch having.Operator { - case v3.HavingOperatorEqual: - return fmt.Sprintf("%s = %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorNotEqual: - return fmt.Sprintf("%s != %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorGreaterThan: - return fmt.Sprintf("%s > %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorGreaterThanOrEq: - return fmt.Sprintf("%s >= %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorLessThan: - return fmt.Sprintf("%s < %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorLessThanOrEq: - return fmt.Sprintf("%s <= %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorIn: - return fmt.Sprintf("%s IN %s", having.ColumnName, formatValue(having.Value)) - case v3.HavingOperatorNotIn: - return fmt.Sprintf("%s NOT IN %s", having.ColumnName, formatValue(having.Value)) - default: - return "" - } -} - -func convertFunctions(v3Functions []v3.Function) []v5.Function { - v5Functions := []v5.Function{} - for _, fn := range v3Functions { - v5Fn := v5.Function{ - Name: convertFunctionName(fn.Name), - Args: []v5.FunctionArg{}, - } - - for _, arg := range fn.Args { - v5Fn.Args = append(v5Fn.Args, v5.FunctionArg{ - Value: arg, - }) - } - - for name, value := range fn.NamedArgs { - v5Fn.Args = append(v5Fn.Args, v5.FunctionArg{ - Name: name, - Value: value, - }) - } - - v5Functions = append(v5Functions, v5Fn) - } - return v5Functions -} - -func convertFunctionName(v3Name v3.FunctionName) v5.FunctionName { - switch v3Name { - case v3.FunctionNameCutOffMin: - return v5.FunctionNameCutOffMin - case v3.FunctionNameCutOffMax: - return v5.FunctionNameCutOffMax - case v3.FunctionNameClampMin: - return v5.FunctionNameClampMin - case v3.FunctionNameClampMax: - return v5.FunctionNameClampMax - case v3.FunctionNameAbsolute: - return v5.FunctionNameAbsolute - case v3.FunctionNameRunningDiff: - return v5.FunctionNameRunningDiff - case v3.FunctionNameLog2: - return v5.FunctionNameLog2 - case v3.FunctionNameLog10: - return v5.FunctionNameLog10 - case v3.FunctionNameCumSum: - return v5.FunctionNameCumulativeSum - case v3.FunctionNameEWMA3: - return v5.FunctionNameEWMA3 - case v3.FunctionNameEWMA5: - return v5.FunctionNameEWMA5 - case v3.FunctionNameEWMA7: - return v5.FunctionNameEWMA7 - case v3.FunctionNameMedian3: - return v5.FunctionNameMedian3 - case v3.FunctionNameMedian5: - return v5.FunctionNameMedian5 - case v3.FunctionNameMedian7: - return v5.FunctionNameMedian7 - case v3.FunctionNameTimeShift: - return v5.FunctionNameTimeShift - case v3.FunctionNameAnomaly: - return v5.FunctionNameAnomaly - default: - return v5.FunctionName{} - } -} - -func convertSelectColumns(cols []v3.AttributeKey) []telemetrytypes.TelemetryFieldKey { - fields := []telemetrytypes.TelemetryFieldKey{} - - for _, key := range cols { - newKey := telemetrytypes.TelemetryFieldKey{ - Name: key.Key, - } - - if _, exists := constants.NewStaticFieldsTraces[key.Key]; exists { - fields = append(fields, newKey) - continue - } - - if _, exists := constants.DeprecatedStaticFieldsTraces[key.Key]; exists { - fields = append(fields, newKey) - continue - } - - if _, exists := constants.StaticFieldsLogsV3[key.Key]; exists { - fields = append(fields, newKey) - continue - } - - newKey.FieldDataType = convertDataType(key.DataType) - newKey.FieldContext = convertAttributeType(key.Type) - newKey.Materialized = key.IsColumn - } - return fields -} - -func convertDataType(v3Type v3.AttributeKeyDataType) telemetrytypes.FieldDataType { - switch v3Type { - case v3.AttributeKeyDataTypeString: - return telemetrytypes.FieldDataTypeString - case v3.AttributeKeyDataTypeInt64: - return telemetrytypes.FieldDataTypeInt64 - case v3.AttributeKeyDataTypeFloat64: - return telemetrytypes.FieldDataTypeFloat64 - case v3.AttributeKeyDataTypeBool: - return telemetrytypes.FieldDataTypeBool - case v3.AttributeKeyDataTypeArrayString: - return telemetrytypes.FieldDataTypeArrayString - case v3.AttributeKeyDataTypeArrayInt64: - return telemetrytypes.FieldDataTypeArrayInt64 - case v3.AttributeKeyDataTypeArrayFloat64: - return telemetrytypes.FieldDataTypeArrayFloat64 - case v3.AttributeKeyDataTypeArrayBool: - return telemetrytypes.FieldDataTypeArrayBool - default: - return telemetrytypes.FieldDataTypeUnspecified - } -} - -func convertAttributeType(v3Type v3.AttributeKeyType) telemetrytypes.FieldContext { - switch v3Type { - case v3.AttributeKeyTypeTag: - return telemetrytypes.FieldContextAttribute - case v3.AttributeKeyTypeResource: - return telemetrytypes.FieldContextResource - case v3.AttributeKeyTypeInstrumentationScope: - return telemetrytypes.FieldContextScope - default: - return telemetrytypes.FieldContextUnspecified - } -} - -func convertTemporality(v3Temp v3.Temporality) metrictypes.Temporality { - switch v3Temp { - case v3.Delta: - return metrictypes.Delta - case v3.Cumulative: - return metrictypes.Cumulative - default: - return metrictypes.Unspecified - } -} - -func convertTimeAggregation(v3TimeAgg v3.TimeAggregation) metrictypes.TimeAggregation { - switch v3TimeAgg { - case v3.TimeAggregationAnyLast: - return metrictypes.TimeAggregationLatest - case v3.TimeAggregationSum: - return metrictypes.TimeAggregationSum - case v3.TimeAggregationAvg: - return metrictypes.TimeAggregationAvg - case v3.TimeAggregationMin: - return metrictypes.TimeAggregationMin - case v3.TimeAggregationMax: - return metrictypes.TimeAggregationMax - case v3.TimeAggregationCount: - return metrictypes.TimeAggregationCount - case v3.TimeAggregationCountDistinct: - return metrictypes.TimeAggregationCountDistinct - case v3.TimeAggregationRate: - return metrictypes.TimeAggregationRate - case v3.TimeAggregationIncrease: - return metrictypes.TimeAggregationIncrease - default: - return metrictypes.TimeAggregationUnspecified - } -} - -func convertSpaceAggregation(v3SpaceAgg v3.SpaceAggregation) metrictypes.SpaceAggregation { - switch v3SpaceAgg { - case v3.SpaceAggregationSum: - return metrictypes.SpaceAggregationSum - case v3.SpaceAggregationAvg: - return metrictypes.SpaceAggregationAvg - case v3.SpaceAggregationMin: - return metrictypes.SpaceAggregationMin - case v3.SpaceAggregationMax: - return metrictypes.SpaceAggregationMax - case v3.SpaceAggregationCount: - return metrictypes.SpaceAggregationCount - case v3.SpaceAggregationPercentile50: - return metrictypes.SpaceAggregationPercentile50 - case v3.SpaceAggregationPercentile75: - return metrictypes.SpaceAggregationPercentile75 - case v3.SpaceAggregationPercentile90: - return metrictypes.SpaceAggregationPercentile90 - case v3.SpaceAggregationPercentile95: - return metrictypes.SpaceAggregationPercentile95 - case v3.SpaceAggregationPercentile99: - return metrictypes.SpaceAggregationPercentile99 - default: - return metrictypes.SpaceAggregationUnspecified - } -} - -func convertClickHouseQueries(v3Queries map[string]*v3.ClickHouseQuery, v5Composite *v5.CompositeQuery) error { - for name, query := range v3Queries { - if query == nil { - continue - } - - v5Envelope := v5.QueryEnvelope{ - Type: v5.QueryTypeClickHouseSQL, - Spec: v5.ClickHouseQuery{ - Name: name, - Query: query.Query, - Disabled: query.Disabled, - }, - } - v5Composite.Queries = append(v5Composite.Queries, v5Envelope) - } - return nil -} - -func convertPromQueries(v3Queries map[string]*v3.PromQuery, step int64, v5Composite *v5.CompositeQuery) error { - for name, query := range v3Queries { - if query == nil { - continue - } - - v5Envelope := v5.QueryEnvelope{ - Type: v5.QueryTypePromQL, - Spec: v5.PromQuery{ - Name: name, - Query: query.Query, - Disabled: query.Disabled, - Step: v5.Step{Duration: time.Duration(step) * time.Second}, - Stats: query.Stats != "", - }, - } - v5Composite.Queries = append(v5Composite.Queries, v5Envelope) - } - return nil -} diff --git a/pkg/query-service/transition/v3_to_v5_resp.go b/pkg/query-service/transition/v3_to_v5_resp.go deleted file mode 100644 index 31dd7354e6ef..000000000000 --- a/pkg/query-service/transition/v3_to_v5_resp.go +++ /dev/null @@ -1,442 +0,0 @@ -package transition - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - - v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" - v5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" - "github.com/SigNoz/signoz/pkg/types/telemetrytypes" -) - -func ConvertV3ResponseToV5(v3Response *v3.QueryRangeResponse, requestType v5.RequestType) (*v5.QueryRangeResponse, error) { - if v3Response == nil { - return nil, fmt.Errorf("v3 response is nil") - } - - v5Response := &v5.QueryRangeResponse{ - Type: requestType, - } - - switch requestType { - case v5.RequestTypeTimeSeries: - data, err := convertToTimeSeriesData(v3Response.Result) - if err != nil { - return nil, err - } - v5Response.Data = data - - case v5.RequestTypeScalar: - data, err := convertToScalarData(v3Response.Result) - if err != nil { - return nil, err - } - v5Response.Data = data - - case v5.RequestTypeRaw: - data, err := convertToRawData(v3Response.Result) - if err != nil { - return nil, err - } - v5Response.Data = data - - default: - return nil, fmt.Errorf("unsupported request type: %v", requestType) - } - - return v5Response, nil -} - -func convertToTimeSeriesData(v3Results []*v3.Result) ([]*v5.TimeSeriesData, error) { - v5Data := []*v5.TimeSeriesData{} - - for _, result := range v3Results { - if result == nil { - continue - } - - tsData := &v5.TimeSeriesData{ - QueryName: result.QueryName, - Aggregations: []*v5.AggregationBucket{}, - } - - if len(result.Series) > 0 { - bucket := &v5.AggregationBucket{ - Index: 0, - Alias: "", - Series: convertSeries(result.Series), - } - tsData.Aggregations = append(tsData.Aggregations, bucket) - } - - v5Data = append(v5Data, tsData) - } - - return v5Data, nil -} - -func convertSeries(v3Series []*v3.Series) []*v5.TimeSeries { - v5Series := []*v5.TimeSeries{} - - for _, series := range v3Series { - if series == nil { - continue - } - - v5TimeSeries := &v5.TimeSeries{ - Labels: convertLabels(series.Labels), - Values: convertPoints(series.Points), - } - - v5Series = append(v5Series, v5TimeSeries) - } - - return v5Series -} - -func convertLabels(v3Labels map[string]string) []*v5.Label { - v5Labels := []*v5.Label{} - - keys := make([]string, 0, len(v3Labels)) - for k := range v3Labels { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, key := range keys { - v5Labels = append(v5Labels, &v5.Label{ - Key: telemetrytypes.TelemetryFieldKey{ - Name: key, - }, - Value: v3Labels[key], - }) - } - - return v5Labels -} - -func convertPoints(v3Points []v3.Point) []*v5.TimeSeriesValue { - v5Values := []*v5.TimeSeriesValue{} - - for _, point := range v3Points { - v5Values = append(v5Values, &v5.TimeSeriesValue{ - Timestamp: point.Timestamp, - Value: point.Value, - }) - } - - return v5Values -} - -func convertToScalarData(v3Results []*v3.Result) (*v5.ScalarData, error) { - scalarData := &v5.ScalarData{ - Columns: []*v5.ColumnDescriptor{}, - Data: [][]any{}, - } - - for _, result := range v3Results { - if result == nil || result.Table == nil { - continue - } - - for _, col := range result.Table.Columns { - columnType := v5.ColumnTypeGroup - if col.IsValueColumn { - columnType = v5.ColumnTypeAggregation - } - - scalarData.Columns = append(scalarData.Columns, &v5.ColumnDescriptor{ - TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{ - Name: col.Name, - }, - QueryName: col.QueryName, - AggregationIndex: 0, - Type: columnType, - }) - } - - for _, row := range result.Table.Rows { - rowData := []any{} - for _, col := range result.Table.Columns { - if val, ok := row.Data[col.Name]; ok { - rowData = append(rowData, val) - } else { - rowData = append(rowData, nil) - } - } - scalarData.Data = append(scalarData.Data, rowData) - } - } - - return scalarData, nil -} - -func convertToRawData(v3Results []*v3.Result) ([]*v5.RawData, error) { - v5Data := []*v5.RawData{} - - for _, result := range v3Results { - if result == nil { - continue - } - - rawData := &v5.RawData{ - QueryName: result.QueryName, - NextCursor: "", - Rows: []*v5.RawRow{}, - } - - for _, row := range result.List { - if row == nil { - continue - } - - dataMap := make(map[string]*any) - for k, v := range row.Data { - val := v - dataMap[k] = &val - } - - rawData.Rows = append(rawData.Rows, &v5.RawRow{ - Timestamp: row.Timestamp, - Data: dataMap, - }) - } - - v5Data = append(v5Data, rawData) - } - - return v5Data, nil -} - -func LogV5Response(response *v5.QueryRangeResponse, logger func(string)) { - if response == nil { - logger("Response: nil") - return - } - - logger(fmt.Sprintf("[%s] Meta{rows:%d bytes:%d ms:%d}", - response.Type, response.Meta.RowsScanned, response.Meta.BytesScanned, response.Meta.DurationMS)) - - switch response.Type { - case v5.RequestTypeTimeSeries: - logTimeSeriesDataCompact(response.Data, logger) - case v5.RequestTypeScalar: - logScalarDataCompact(response.Data, logger) - case v5.RequestTypeRaw: - logRawDataCompact(response.Data, logger) - default: - logger(fmt.Sprintf("Unknown response type: %v", response.Type)) - } -} - -func logTimeSeriesDataCompact(data any, logger func(string)) { - tsData, ok := data.([]*v5.TimeSeriesData) - if !ok { - logger("ERROR: Failed to cast data to TimeSeriesData") - return - } - - sort.Slice(tsData, func(i, j int) bool { - return tsData[i].QueryName < tsData[j].QueryName - }) - - for _, ts := range tsData { - allSeries := flattenSeries(ts.Aggregations) - - sort.Slice(allSeries, func(i, j int) bool { - return createLabelSignature(allSeries[i].Labels) < createLabelSignature(allSeries[j].Labels) - }) - - for _, series := range allSeries { - labels := []string{} - for _, label := range series.Labels { - labels = append(labels, fmt.Sprintf("%s:%v", label.Key.Name, label.Value)) - } - labelStr := strings.Join(labels, ",") - - values := make([]*v5.TimeSeriesValue, len(series.Values)) - copy(values, series.Values) - sort.Slice(values, func(i, j int) bool { - return values[i].Timestamp < values[j].Timestamp - }) - - valueStrs := []string{} - for _, val := range values { - relTime := val.Timestamp - if len(values) > 0 && values[0].Timestamp > 0 { - relTime = (val.Timestamp - values[0].Timestamp) / 1000 // Convert to seconds - } - valueStrs = append(valueStrs, fmt.Sprintf("%d:%.2f", relTime, val.Value)) - } - - logger(fmt.Sprintf("%s {%s} [%s]", ts.QueryName, labelStr, strings.Join(valueStrs, " "))) - } - } -} - -func createLabelSignature(labels []*v5.Label) string { - parts := []string{} - for _, label := range labels { - parts = append(parts, fmt.Sprintf("%s=%v", label.Key.Name, label.Value)) - } - sort.Strings(parts) - return strings.Join(parts, ",") -} - -func logScalarDataCompact(data any, logger func(string)) { - scalar, ok := data.(*v5.ScalarData) - if !ok { - logger("ERROR: Failed to cast data to ScalarData") - return - } - - colNames := []string{} - for _, col := range scalar.Columns { - colNames = append(colNames, col.Name) - } - - logger(fmt.Sprintf("SCALAR [%s]", strings.Join(colNames, "|"))) - - for i, row := range scalar.Data { - rowVals := []string{} - for _, val := range row { - rowVals = append(rowVals, fmt.Sprintf("%v", val)) - } - logger(fmt.Sprintf(" %d: [%s]", i, strings.Join(rowVals, "|"))) - } -} - -func flattenSeries(buckets []*v5.AggregationBucket) []*v5.TimeSeries { - var allSeries []*v5.TimeSeries - for _, bucket := range buckets { - allSeries = append(allSeries, bucket.Series...) - } - return allSeries -} - -func logRawDataCompact(data any, logger func(string)) { - rawData, ok := data.([]*v5.RawData) - if !ok { - logger("ERROR: Failed to cast data to RawData") - return - } - - sort.Slice(rawData, func(i, j int) bool { - return rawData[i].QueryName < rawData[j].QueryName - }) - - for _, rd := range rawData { - logger(fmt.Sprintf("RAW %s (rows:%d cursor:%s)", rd.QueryName, len(rd.Rows), rd.NextCursor)) - - rows := make([]*v5.RawRow, len(rd.Rows)) - copy(rows, rd.Rows) - sort.Slice(rows, func(i, j int) bool { - return rows[i].Timestamp.Before(rows[j].Timestamp) - }) - - allFields := make(map[string]bool) - for _, row := range rows { - for k := range row.Data { - allFields[k] = true - } - } - - fieldNames := []string{} - for k := range allFields { - fieldNames = append(fieldNames, k) - } - sort.Strings(fieldNames) - - logger(fmt.Sprintf(" Fields: [%s]", strings.Join(fieldNames, "|"))) - - for i, row := range rows { - vals := []string{} - for _, field := range fieldNames { - if val, exists := row.Data[field]; exists && val != nil { - vals = append(vals, fmt.Sprintf("%v", *val)) - } else { - vals = append(vals, "-") - } - } - tsStr := row.Timestamp.Format("15:04:05") - logger(fmt.Sprintf(" %d@%s: [%s]", i, tsStr, strings.Join(vals, "|"))) - } - } -} - -func LogV5ResponseJSON(response *v5.QueryRangeResponse, logger func(string)) { - sortedResponse := sortV5ResponseForLogging(response) - - jsonBytes, err := json.MarshalIndent(sortedResponse, "", " ") - if err != nil { - logger(fmt.Sprintf("ERROR: Failed to marshal response: %v", err)) - return - } - - logger(string(jsonBytes)) -} - -func sortV5ResponseForLogging(response *v5.QueryRangeResponse) *v5.QueryRangeResponse { - if response == nil { - return nil - } - - responseCopy := &v5.QueryRangeResponse{ - Type: response.Type, - Meta: response.Meta, - } - - switch response.Type { - case v5.RequestTypeTimeSeries: - if tsData, ok := response.Data.([]*v5.TimeSeriesData); ok { - sortedData := make([]*v5.TimeSeriesData, len(tsData)) - for i, ts := range tsData { - sortedData[i] = &v5.TimeSeriesData{ - QueryName: ts.QueryName, - Aggregations: make([]*v5.AggregationBucket, len(ts.Aggregations)), - } - - for j, bucket := range ts.Aggregations { - sortedBucket := &v5.AggregationBucket{ - Index: bucket.Index, - Alias: bucket.Alias, - Series: make([]*v5.TimeSeries, len(bucket.Series)), - } - - for k, series := range bucket.Series { - sortedSeries := &v5.TimeSeries{ - Labels: series.Labels, - Values: make([]*v5.TimeSeriesValue, len(series.Values)), - } - copy(sortedSeries.Values, series.Values) - - sort.Slice(sortedSeries.Values, func(i, j int) bool { - return sortedSeries.Values[i].Timestamp < sortedSeries.Values[j].Timestamp - }) - - sortedBucket.Series[k] = sortedSeries - } - - sort.Slice(sortedBucket.Series, func(i, j int) bool { - return createLabelSignature(sortedBucket.Series[i].Labels) < - createLabelSignature(sortedBucket.Series[j].Labels) - }) - - sortedData[i].Aggregations[j] = sortedBucket - } - } - - sort.Slice(sortedData, func(i, j int) bool { - return sortedData[i].QueryName < sortedData[j].QueryName - }) - - responseCopy.Data = sortedData - } - default: - responseCopy.Data = response.Data - } - - return responseCopy -} diff --git a/pkg/transition/v5_to_v4.go b/pkg/transition/v5_to_v4.go new file mode 100644 index 000000000000..60e13c12c257 --- /dev/null +++ b/pkg/transition/v5_to_v4.go @@ -0,0 +1,102 @@ +package transition + +import ( + "fmt" + + v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" +) + +// ConvertV5TimeSeriesDataToV4Result converts v5 TimeSeriesData to v4 Result +func ConvertV5TimeSeriesDataToV4Result(v5Data *qbtypes.TimeSeriesData) *v3.Result { + if v5Data == nil { + return nil + } + + result := &v3.Result{ + QueryName: v5Data.QueryName, + Series: make([]*v3.Series, 0), + } + + toV4Series := func(ts *qbtypes.TimeSeries) *v3.Series { + series := &v3.Series{ + Labels: make(map[string]string), + LabelsArray: make([]map[string]string, 0), + Points: make([]v3.Point, 0, len(ts.Values)), + } + + for _, label := range ts.Labels { + valueStr := fmt.Sprintf("%v", label.Value) + series.Labels[label.Key.Name] = valueStr + } + + if len(series.Labels) > 0 { + series.LabelsArray = append(series.LabelsArray, series.Labels) + } + + for _, tsValue := range ts.Values { + if tsValue.Partial { + continue + } + + point := v3.Point{ + Timestamp: tsValue.Timestamp, + Value: tsValue.Value, + } + series.Points = append(series.Points, point) + } + return series + } + + for _, aggBucket := range v5Data.Aggregations { + for _, ts := range aggBucket.Series { + result.Series = append(result.Series, toV4Series(ts)) + } + + if len(aggBucket.AnomalyScores) != 0 { + result.AnomalyScores = make([]*v3.Series, 0) + for _, ts := range aggBucket.AnomalyScores { + result.AnomalyScores = append(result.AnomalyScores, toV4Series(ts)) + } + } + + if len(aggBucket.PredictedSeries) != 0 { + result.PredictedSeries = make([]*v3.Series, 0) + for _, ts := range aggBucket.PredictedSeries { + result.PredictedSeries = append(result.PredictedSeries, toV4Series(ts)) + } + } + + if len(aggBucket.LowerBoundSeries) != 0 { + result.LowerBoundSeries = make([]*v3.Series, 0) + for _, ts := range aggBucket.LowerBoundSeries { + result.LowerBoundSeries = append(result.LowerBoundSeries, toV4Series(ts)) + } + } + + if len(aggBucket.UpperBoundSeries) != 0 { + result.UpperBoundSeries = make([]*v3.Series, 0) + for _, ts := range aggBucket.UpperBoundSeries { + result.UpperBoundSeries = append(result.UpperBoundSeries, toV4Series(ts)) + } + } + } + + return result +} + +// ConvertV5TimeSeriesDataSliceToV4Results converts a slice of v5 TimeSeriesData to v4 QueryRangeResponse +func ConvertV5TimeSeriesDataSliceToV4Results(v5DataSlice []*qbtypes.TimeSeriesData) *v3.QueryRangeResponse { + response := &v3.QueryRangeResponse{ + ResultType: "matrix", // Time series data is typically "matrix" type + Result: make([]*v3.Result, 0, len(v5DataSlice)), + } + + for _, v5Data := range v5DataSlice { + if result := ConvertV5TimeSeriesDataToV4Result(v5Data); result != nil { + response.Result = append(response.Result, result) + } + } + + return response +}